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

Commit 4450fbbb authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Revert^2 "Filter memory tombstone fields before DropBox logging"" into main

parents eb624654 1145ed54
Loading
Loading
Loading
Loading
+335 −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 android.util.proto;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.Predicate;

/**
 * A utility class that reads raw protobuf data from an InputStream
 * and copies only those fields for which a given predicate returns true.
 *
 * <p>
 * This is a low-level approach that does not fully decode fields
 * (unless necessary to determine lengths). It simply:
 * <ul>
 *   <li>Parses each field's tag (varint for field number & wire type)</li>
 *   <li>If {@code includeFn(fieldNumber) == true}, copies
 *       the tag bytes and the field bytes directly to the output</li>
 *   <li>Otherwise, skips that field in the input</li>
 * </ul>
 * </p>
 *
 * <p>
 * Because we do not re-encode, unknown or unrecognized fields are copied
 * <i>verbatim</i> and remain exactly as in the input (useful for partial
 * parsing or partial transformations).
 * </p>
 *
 * <p>
 * Note: This class only filters based on top-level field numbers. For length-delimited
 * fields (including nested messages), the entire contents are either copied or skipped
 * as a single unit. The class is not capable of nested filtering.
 * </p>
 *
 * @hide
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class ProtoFieldFilter {

    private static final int BUFFER_SIZE_BYTES = 4096;

    private final Predicate<Integer> mFieldPredicate;
    // General-purpose buffer for reading proto fields and their data
    private final byte[] mBuffer;
    // Buffer specifically designated to hold varint values (max 10 bytes in protobuf encoding)
    private final byte[] mVarIntBuffer = new byte[10];

    /**
    * Constructs a ProtoFieldFilter with a predicate that considers depth.
    *
    * @param fieldPredicate A predicate returning true if the given fieldNumber should be
    *                       included in the output.
    * @param bufferSize The size of the internal buffer used for processing proto fields.
    *                   Larger buffers may improve performance when processing large
    *                   length-delimited fields.
    */
    public ProtoFieldFilter(Predicate<Integer> fieldPredicate, int bufferSize) {
        this.mFieldPredicate = fieldPredicate;
        this.mBuffer = new byte[bufferSize];
    }

    /**
    * Constructs a ProtoFieldFilter with a predicate that considers depth and
    * uses a default buffer size.
    *
    * @param fieldPredicate A predicate returning true if the given fieldNumber should be
    *                       included in the output.
    */
    public ProtoFieldFilter(Predicate<Integer> fieldPredicate) {
        this(fieldPredicate, BUFFER_SIZE_BYTES);
    }

    /**
     * Reads raw protobuf data from {@code in} and writes only those fields
     * passing {@code includeFn} to {@code out}. The predicate is given
     * (fieldNumber, wireType) for each encountered field.
     *
     * @param in        The input stream of protobuf data
     * @param out       The output stream to which we write the filtered protobuf
     * @throws IOException If reading or writing fails, or if the protobuf data is corrupted
     */
    public void filter(InputStream in, OutputStream out) throws IOException {
        int tagBytesLength;
        while ((tagBytesLength = readRawVarint(in)) > 0) {
            // Parse the varint loaded in mVarIntBuffer, through readRawVarint
            long tagVal = parseVarint(mVarIntBuffer, tagBytesLength);
            int fieldNumber = (int) (tagVal >>> ProtoStream.FIELD_ID_SHIFT);
            int wireType   = (int) (tagVal & ProtoStream.WIRE_TYPE_MASK);

            if (fieldNumber == 0) {
                break;
            }
            if (mFieldPredicate.test(fieldNumber)) {
                out.write(mVarIntBuffer, 0, tagBytesLength);
                copyFieldData(in, out, wireType);
            } else {
                skipFieldData(in, wireType);
            }
        }
    }

    /**
     * Reads a varint (up to 10 bytes) from the stream as raw bytes
     * and returns it in a byte array. If the stream is at EOF, returns null.
     *
     * @param in The input stream
     * @return the size of the varint bytes moved to mVarIntBuffer
     * @throws IOException If an error occurs, or if we detect a malformed varint
     */
    private int readRawVarint(InputStream in) throws IOException {
        // We attempt to read 1 byte. If none available => null
        int b = in.read();
        if (b < 0) {
            return 0;
        }
        int count = 0;
        mVarIntBuffer[count++] = (byte) b;
        // If the continuation bit is set, we continue
        while ((b & 0x80) != 0) {
            // read next byte
            b = in.read();
            // EOF
            if (b < 0) {
                throw new IOException("Malformed varint: reached EOF mid-varint");
            }
            // max 10 bytes for varint 64
            if (count >= 10) {
                throw new IOException("Malformed varint: too many bytes (max 10)");
            }
            mVarIntBuffer[count++] = (byte) b;
        }
        return count;
    }

    /**
     * Parses a varint from the given raw bytes and returns it as a long.
     *
     * @param rawVarint The bytes representing the varint
     * @param byteLength The number of bytes to read from rawVarint
     * @return The decoded long value
     */
    private static long parseVarint(byte[] rawVarint, int byteLength) throws IOException {
        long result = 0;
        int shift = 0;
        for (int i = 0; i < byteLength; i++) {
            result |= ((rawVarint[i] & 0x7F) << shift);
            shift += 7;
            if (shift > 63) {
                throw new IOException("Malformed varint: exceeds 64 bits");
            }
        }
        return result;
    }

    /**
     * Copies the wire data for a single field from {@code in} to {@code out},
     * assuming we have already read the field's tag.
     *
     * @param in       The input stream (protobuf data)
     * @param out      The output stream
     * @param wireType The wire type (0=varint, 1=fixed64, 2=length-delim, 5=fixed32)
     * @throws IOException if reading/writing fails or data is malformed
     */
    private void copyFieldData(InputStream in, OutputStream out, int wireType)
            throws IOException {
        switch (wireType) {
            case ProtoStream.WIRE_TYPE_VARINT:
                copyVarint(in, out);
                break;
            case ProtoStream.WIRE_TYPE_FIXED64:
                copyFixed(in, out, 8);
                break;
            case ProtoStream.WIRE_TYPE_LENGTH_DELIMITED:
                copyLengthDelimited(in, out);
                break;
            case ProtoStream.WIRE_TYPE_FIXED32:
                copyFixed(in, out, 4);
                break;
            // case WIRE_TYPE_START_GROUP:
                // Not Supported
            // case WIRE_TYPE_END_GROUP:
                // Not Supported
            default:
                // Error or unrecognized wire type
                throw new IOException("Unknown or unsupported wire type: " + wireType);
        }
    }

    /**
     * Skips the wire data for a single field from {@code in},
     * assuming the field's tag was already read.
     */
    private void skipFieldData(InputStream in, int wireType) throws IOException {
        switch (wireType) {
            case ProtoStream.WIRE_TYPE_VARINT:
                skipVarint(in);
                break;
            case ProtoStream.WIRE_TYPE_FIXED64:
                skipBytes(in, 8);
                break;
            case ProtoStream.WIRE_TYPE_LENGTH_DELIMITED:
                skipLengthDelimited(in);
                break;
            case ProtoStream.WIRE_TYPE_FIXED32:
                skipBytes(in, 4);
                break;
             // case WIRE_TYPE_START_GROUP:
                // Not Supported
            // case WIRE_TYPE_END_GROUP:
                // Not Supported
            default:
                throw new IOException("Unknown or unsupported wire type: " + wireType);
        }
    }

    /** Copies a varint (the field's value) from in to out. */
    private static void copyVarint(InputStream in, OutputStream out) throws IOException {
        while (true) {
            int b = in.read();
            if (b < 0) {
                throw new IOException("EOF while copying varint");
            }
            out.write(b);
            if ((b & 0x80) == 0) {
                break;
            }
        }
    }

    /**
     * Copies exactly {@code length} bytes from {@code in} to {@code out}.
     */
    private void copyFixed(InputStream in, OutputStream out,
                int length) throws IOException {
        int toRead = length;
        while (toRead > 0) {
            int chunk = Math.min(toRead, mBuffer.length);
            int readCount = in.read(mBuffer, 0, chunk);
            if (readCount < 0) {
                throw new IOException("EOF while copying fixed" + (length * 8) + " field");
            }
            out.write(mBuffer, 0, readCount);
            toRead -= readCount;
        }
    }

    /** Copies a length-delimited field */
    private void copyLengthDelimited(InputStream in,
                    OutputStream out) throws IOException {
        // 1) read length varint (and copy)
        int lengthVarintLength = readRawVarint(in);
        if (lengthVarintLength <= 0) {
            throw new IOException("EOF reading length for length-delimited field");
        }
        out.write(mVarIntBuffer, 0, lengthVarintLength);

        long lengthVal = parseVarint(mVarIntBuffer, lengthVarintLength);
        if (lengthVal < 0 || lengthVal > Integer.MAX_VALUE) {
            throw new IOException("Invalid length for length-delimited field: " + lengthVal);
        }

        // 2) copy that many bytes
        copyFixed(in, out, (int) lengthVal);
    }

    /** Skips a varint in the input (does not write anything). */
    private static void skipVarint(InputStream in) throws IOException {
        int bytesSkipped = 0;
        while (true) {
            int b = in.read();
            if (b < 0) {
                throw new IOException("EOF while skipping varint");
            }
            if ((b & 0x80) == 0) {
                break;
            }
            bytesSkipped++;
            if (bytesSkipped > 10) {
                throw new IOException("Malformed varint: exceeds maximum length of 10 bytes");
            }
        }
    }

    /** Skips exactly n bytes. */
    private void skipBytes(InputStream in, long n) throws IOException {
        long skipped = in.skip(n);
        // If skip fails, fallback to reading the remaining bytes
        if (skipped < n) {
            long bytesRemaining = n - skipped;

            while (bytesRemaining > 0) {
                int bytesToRead = (int) Math.min(bytesRemaining, mBuffer.length);
                int bytesRead = in.read(mBuffer, 0, bytesToRead);
                if (bytesRemaining < 0) {
                    throw new IOException("EOF while skipping bytes");
                }
                bytesRemaining -= bytesRead;
            }
        }
    }

    /**
     * Skips a length-delimited field.
     * 1) read the length as varint,
     * 2) skip that many bytes
     */
    private void skipLengthDelimited(InputStream in) throws IOException {
        int lengthVarintLength = readRawVarint(in);
        if (lengthVarintLength <= 0) {
            throw new IOException("EOF reading length for length-delimited field");
        }
        long lengthVal = parseVarint(mVarIntBuffer, lengthVarintLength);
        if (lengthVal < 0 || lengthVal > Integer.MAX_VALUE) {
            throw new IOException("Invalid length to skip: " + lengthVal);
        }
        skipBytes(in, lengthVal);
    }

}
+230 −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 android.util.proto;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;


/**
 * Unit tests for {@link android.util.proto.ProtoFieldFilter}.
 *
 *  Build/Install/Run:
 *  atest FrameworksCoreTests:ProtoFieldFilterTest
 *
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ProtoFieldFilterTest {

    private static final class FieldTypes {
        static final long INT64 = ProtoStream.FIELD_TYPE_INT64 | ProtoStream.FIELD_COUNT_SINGLE;
        static final long FIXED64 = ProtoStream.FIELD_TYPE_FIXED64 | ProtoStream.FIELD_COUNT_SINGLE;
        static final long BYTES = ProtoStream.FIELD_TYPE_BYTES | ProtoStream.FIELD_COUNT_SINGLE;
        static final long FIXED32 = ProtoStream.FIELD_TYPE_FIXED32 | ProtoStream.FIELD_COUNT_SINGLE;
        static final long MESSAGE = ProtoStream.FIELD_TYPE_MESSAGE | ProtoStream.FIELD_COUNT_SINGLE;
        static final long INT32 = ProtoStream.FIELD_TYPE_INT32 | ProtoStream.FIELD_COUNT_SINGLE;
    }

    private static ProtoOutputStream createBasicTestProto() {
        ProtoOutputStream out = new ProtoOutputStream();

        out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);
        out.writeFixed64(ProtoStream.makeFieldId(2, FieldTypes.FIXED64), 0x1234567890ABCDEFL);
        out.writeBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES), new byte[]{1, 2, 3, 4, 5});
        out.writeFixed32(ProtoStream.makeFieldId(4, FieldTypes.FIXED32), 0xDEADBEEF);

        return out;
    }

    private static byte[] filterProto(byte[] input, ProtoFieldFilter filter) throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        filter.filter(inputStream, outputStream);
        return outputStream.toByteArray();
    }

    @Test
    public void testNoFieldsFiltered() throws IOException {
        byte[] input = createBasicTestProto().getBytes();
        byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> true));
        assertArrayEquals("No fields should be filtered out", input, output);
    }

    @Test
    public void testAllFieldsFiltered() throws IOException {
        byte[] input = createBasicTestProto().getBytes();
        byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> false));

        assertEquals("All fields should be filtered out", 0, output.length);
    }

    @Test
    public void testSpecificFieldsFiltered() throws IOException {

        ProtoOutputStream out = createBasicTestProto();
        byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(n -> n != 2));

        ProtoInputStream in = new ProtoInputStream(output);
        boolean[] fieldsFound = new boolean[5];

        int fieldNumber;
        while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
            fieldsFound[fieldNumber] = true;
            switch (fieldNumber) {
                case 1:
                    assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
                    break;
                case 2:
                    fail("Field 2 should be filtered out");
                    break;
                case 3:
                    assertArrayEquals(new byte[]{1, 2, 3, 4, 5},
                            in.readBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES)));
                    break;
                case 4:
                    assertEquals(0xDEADBEEF,
                            in.readInt(ProtoStream.makeFieldId(4, FieldTypes.FIXED32)));
                    break;
                default:
                    fail("Unexpected field number: " + fieldNumber);
            }
        }

        assertTrue("Field 1 should be present", fieldsFound[1]);
        assertFalse("Field 2 should be filtered", fieldsFound[2]);
        assertTrue("Field 3 should be present", fieldsFound[3]);
        assertTrue("Field 4 should be present", fieldsFound[4]);
    }

    @Test
    public void testDifferentWireTypes() throws IOException {
        ProtoOutputStream out = new ProtoOutputStream();

        out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);
        out.writeFixed64(ProtoStream.makeFieldId(2, FieldTypes.FIXED64), 0x1234567890ABCDEFL);
        out.writeBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES), new byte[]{10, 20, 30});

        long token = out.start(ProtoStream.makeFieldId(4, FieldTypes.MESSAGE));
        out.writeInt32(ProtoStream.makeFieldId(1, FieldTypes.INT32), 42);
        out.end(token);

        out.writeFixed32(ProtoStream.makeFieldId(5, FieldTypes.FIXED32), 0xDEADBEEF);

        byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(fieldNumber -> true));

        ProtoInputStream in = new ProtoInputStream(output);
        boolean[] fieldsFound = new boolean[6];

        int fieldNumber;
        while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
            fieldsFound[fieldNumber] = true;
            switch (fieldNumber) {
                case 1:
                    assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
                    break;
                case 2:
                    assertEquals(0x1234567890ABCDEFL,
                            in.readLong(ProtoStream.makeFieldId(2, FieldTypes.FIXED64)));
                    break;
                case 3:
                    assertArrayEquals(new byte[]{10, 20, 30},
                            in.readBytes(ProtoStream.makeFieldId(3, FieldTypes.BYTES)));
                    break;
                case 4:
                    token = in.start(ProtoStream.makeFieldId(4, FieldTypes.MESSAGE));
                    assertTrue(in.nextField() == 1);
                    assertEquals(42, in.readInt(ProtoStream.makeFieldId(1, FieldTypes.INT32)));
                    assertTrue(in.nextField() == ProtoInputStream.NO_MORE_FIELDS);
                    in.end(token);
                    break;
                case 5:
                    assertEquals(0xDEADBEEF,
                            in.readInt(ProtoStream.makeFieldId(5, FieldTypes.FIXED32)));
                    break;
                default:
                    fail("Unexpected field number: " + fieldNumber);
            }
        }

        assertTrue("All fields should be present",
                fieldsFound[1] && fieldsFound[2] && fieldsFound[3]
                && fieldsFound[4] && fieldsFound[5]);
    }
    @Test
    public void testNestedMessagesUnfiltered() throws IOException {
        ProtoOutputStream out = new ProtoOutputStream();

        out.writeInt64(ProtoStream.makeFieldId(1, FieldTypes.INT64), 12345L);

        long token = out.start(ProtoStream.makeFieldId(2, FieldTypes.MESSAGE));
        out.writeInt32(ProtoStream.makeFieldId(1, FieldTypes.INT32), 6789);
        out.writeFixed32(ProtoStream.makeFieldId(2, FieldTypes.FIXED32), 0xCAFEBABE);
        out.end(token);

        byte[] output = filterProto(out.getBytes(), new ProtoFieldFilter(n -> n != 2));

        // Verify output
        ProtoInputStream in = new ProtoInputStream(output);
        boolean[] fieldsFound = new boolean[3];

        int fieldNumber;
        while ((fieldNumber = in.nextField()) != ProtoInputStream.NO_MORE_FIELDS) {
            fieldsFound[fieldNumber] = true;
            if (fieldNumber == 1) {
                assertEquals(12345L, in.readLong(ProtoStream.makeFieldId(1, FieldTypes.INT64)));
            } else {
                fail("Unexpected field number: " + fieldNumber);
            }
        }

        assertTrue("Field 1 should be present", fieldsFound[1]);
        assertFalse("Field 2 should be filtered out", fieldsFound[2]);
    }

    @Test
    public void testRepeatedFields() throws IOException {

        ProtoOutputStream out = new ProtoOutputStream();
        long fieldId = ProtoStream.makeFieldId(1,
                ProtoStream.FIELD_TYPE_INT32 | ProtoStream.FIELD_COUNT_REPEATED);

        out.writeRepeatedInt32(fieldId, 100);
        out.writeRepeatedInt32(fieldId, 200);
        out.writeRepeatedInt32(fieldId, 300);

        byte[] input = out.getBytes();

        byte[] output = filterProto(input, new ProtoFieldFilter(fieldNumber -> true));

        assertArrayEquals("Repeated fields should be preserved", input, output);
    }

}
+1 −0
Original line number Diff line number Diff line
@@ -115,6 +115,7 @@ android.util.UtilConfig
android.util.Xml

android.util.proto.EncodedBuffer
android.util.proto.ProtoFieldFilter
android.util.proto.ProtoInputStream
android.util.proto.ProtoOutputStream
android.util.proto.ProtoParseException
+129 −0

File changed.

Preview size limit exceeded, changes collapsed.

+16 −6
Original line number Diff line number Diff line
@@ -137,16 +137,26 @@ public final class NativeTombstoneManager {
            return;
        }

        String processName = "UNKNOWN";
        final boolean isProtoFile = filename.endsWith(".pb");
        File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");

        Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile);
        if (parsedTombstone.isPresent()) {
            processName = parsedTombstone.get().getProcessName();
        // Only process the pb tombstone output, the text version will be generated in
        // BootReceiver.filterAndAddTombstoneToDropBox through pbtombstone
        if (Flags.protoTombstone() && !isProtoFile) {
            return;
        }
        BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock);

        File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb");

        final String processName = handleProtoTombstone(protoPath, isProtoFile)
                .map(TombstoneFile::getProcessName)
                .orElse("UNKNOWN");

        if (Flags.protoTombstone()) {
            BootReceiver.filterAndAddTombstoneToDropBox(mContext, path, processName, mTmpFileLock);
        } else {
            BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile,
                    processName, mTmpFileLock);
        }
        // TODO(b/339371242): An optimizer on WearOS is misbehaving and this member is being garbage
        // collected as it's never referenced inside this class outside of the constructor. But,
        // it's a file watcher, and needs to stay alive to do its job. So, add a cheap check here to
Loading