Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0a8f7f56 authored by John Wu's avatar John Wu
Browse files

[Ravenwood] Make FileInput/OutputStream behave like libcore

Introduce a new ClassVisitor JdkPatchVisitor in HSG to handle all
bytecode patches required to workaround JRE v.s. ART libcore behavior
differences.

Along with the change is the introduction of a new feature "class
remapping". Unlike jarjar or ClassRemapper in the ASM library, this
class remapping only does the following:

1. Change the super class of all direct subclasses of the target class
   to the remapped class.
2. Change all NEW operand of the target class to the remapped class.
3. Redirect all INVOKESPECIAL <init> of the target class to the remapped
   class.

By doing so, we basically force an additional layer of subclass to all
usages of the target class we want to patch. This gives us the ability
to change or add new behavior to classes in places where we cannot
directly modify, e.g. the standard JRE classes like FIS/FOS.

Bug: 397498134
Flag: EXEMPT host side change only
Test: f/b/r/scripts/run-ravenwood-tests.sh
Change-Id: I2c536e36c6954e30f0c5e1a1cbcdca0d7520c60b
parent 67eb4e7a
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -7,7 +7,9 @@
    { "name": "hoststubgen-invoke-test" },
    { "name": "RavenwoodMockitoTest_device" },
    { "name": "RavenwoodBivalentTest_device" },
    { "name": "RavenwoodBivalentTest_device_ravenizer" },

    // Running ravenizer on device side tests is unsupported: b/411506416
    // { "name": "RavenwoodBivalentTest_device_ravenizer" },

    { "name": "RavenwoodBivalentInstTest_nonself_inst" },
    { "name": "RavenwoodBivalentInstTest_self_inst_device" },
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.ravenwood;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * FileInputStream tracking fd ownership, emulating libcore behavior.
 */
public class RavenwoodFileInputStream extends FileInputStream {

    private final boolean mIsFdOwner;

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileInputStream(String name) throws FileNotFoundException {
        super(name);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileInputStream(File file) throws FileNotFoundException {
        super(file);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileInputStream(FileDescriptor fdObj) {
        this(fdObj, false);
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileInputStream(FileDescriptor fdObj, boolean isFdOwner) {
        super(fdObj);
        this.mIsFdOwner = isFdOwner;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws IOException {
        if (mIsFdOwner) {
            // Only close when we are the owner
            super.close();
        }
    }
}
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.ravenwood;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * FileOutputStream tracking fd ownership, emulating libcore behavior.
 */
public class RavenwoodFileOutputStream extends FileOutputStream {

    private final boolean mIsFdOwner;

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(String name) throws FileNotFoundException {
        super(name);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(String name, boolean append) throws FileNotFoundException {
        super(name, append);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(File file) throws FileNotFoundException {
        super(file);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(File file, boolean append) throws FileNotFoundException {
        super(file, append);
        mIsFdOwner = true;
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(FileDescriptor fdObj) {
        this(fdObj, false);
    }

    /**
     * {@inheritDoc}
     */
    public RavenwoodFileOutputStream(FileDescriptor fdObj, boolean isFdOwner) {
        super(fdObj);
        this.mIsFdOwner = isFdOwner;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws IOException {
        if (mIsFdOwner) {
            // Only close when we are the owner
            super.close();
        }
    }
}
+125 −0
Original line number Diff line number Diff line
@@ -13,12 +13,14 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.ravenwoodtest.bivalenttest.ravenizer;
package com.android.ravenwoodtest.bivalenttest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import android.platform.test.ravenwood.RavenwoodRule;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -26,6 +28,9 @@ import android.system.OsConstants;
import org.junit.Test;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.regex.Pattern;

@@ -57,7 +62,64 @@ public class RavenwoodJdkPatchTest {
            Os.close(fd);
            assertEquals(-1, fd.getInt$());
        } finally {
            if (fd.valid()) {
                Os.close(fd);
            }
        }
    }

    private static class SubFileInputStream extends FileInputStream {

        SubFileInputStream(FileDescriptor fdObj) {
            super(fdObj);
        }
    }

    private static class SubFileOutputStream extends FileOutputStream {

        SubFileOutputStream(FileDescriptor fdObj) {
            super(fdObj);
        }
    }

    @Test
    public void testFileInputOutputStream() throws ErrnoException, IOException {
        FileDescriptor fd = Os.open("/dev/urandom", OsConstants.O_RDWR, 0);
        try {
            try (var fis = new FileInputStream(fd)) {
                if (RavenwoodRule.isOnRavenwood()) {
                    assertEquals("com.android.ravenwood.RavenwoodFileInputStream",
                            fis.getClass().getName());
                }
                // It should be the exact same instance, don't use assertEquals here
                assertTrue(fis.getFD() == fd);
            }
            assertTrue(fd.valid());
            try (var fos = new FileOutputStream(fd)) {
                if (RavenwoodRule.isOnRavenwood()) {
                    assertEquals("com.android.ravenwood.RavenwoodFileOutputStream",
                            fos.getClass().getName());
                }
                // It should be the exact same instance, don't use assertEquals here
                assertTrue(fos.getFD() == fd);
            }
            assertTrue(fd.valid());
            new SubFileInputStream(fd).close();
            assertTrue(fd.valid());
            new SubFileOutputStream(fd).close();
            assertTrue(fd.valid());
            new SubFileInputStream(fd) {}.close();
            assertTrue(fd.valid());
            new SubFileOutputStream(fd) {}.close();
        } finally {
            Os.close(fd);
        }

        fd = Os.open("/dev/urandom", OsConstants.O_RDONLY, 0);
        new FileInputStream(fd, true).close();
        assertFalse(fd.valid());

        // For some reason, FileOutputStream(FileDescriptor, boolean) does not exist in any stub
        // JARs during build time. We cannot check whether the constructor patch works in this test.
    }
}
+0 −11
Original line number Diff line number Diff line
@@ -14,14 +14,3 @@ class :sysprops keepclass

# Keep all resource R classes
class :r keepclass

# Support APIs not available in standard JRE
class java.io.FileDescriptor  # no-pta
    method getInt$ @com.android.ravenwood.RavenwoodJdkPatch.getInt$
    method setInt$ @com.android.ravenwood.RavenwoodJdkPatch.setInt$
class java.util.LinkedHashMap  # no-pta
    method eldest @com.android.ravenwood.RavenwoodJdkPatch.eldest

# Always set flag UNICODE_CHARACTER_CLASS when compiling regex
class java.util.regex.Pattern keep
    method compile @com.android.ravenwood.RavenwoodJdkPatch.compilePattern
Loading