diff options
6 files changed, 712 insertions, 7 deletions
diff --git a/core/java/android/util/proto/ProtoFieldFilter.java b/core/java/android/util/proto/ProtoFieldFilter.java new file mode 100644 index 000000000000..c3ae106b68f8 --- /dev/null +++ b/core/java/android/util/proto/ProtoFieldFilter.java @@ -0,0 +1,335 @@ +/* + * 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); + } + +} diff --git a/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java b/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java new file mode 100644 index 000000000000..76d0aaaa4309 --- /dev/null +++ b/core/tests/utiltests/src/android/util/proto/ProtoFieldFilterTest.java @@ -0,0 +1,230 @@ +/* + * 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); + } + +} diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index f8315fe1e55f..383e75bb5122 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -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 diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 1588e0421675..7a5b8660ef7c 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -40,7 +40,9 @@ import android.util.AtomicFile; import android.util.EventLog; import android.util.Slog; import android.util.Xml; +import android.util.proto.ProtoFieldFilter; import android.util.proto.ProtoOutputStream; +import android.util.proto.ProtoParseException; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; @@ -49,10 +51,13 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.am.DropboxRateLimiter; +import com.android.server.os.TombstoneProtos.Tombstone; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; @@ -64,6 +69,7 @@ import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; import java.util.HashMap; import java.util.Iterator; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -392,6 +398,129 @@ public class BootReceiver extends BroadcastReceiver { writeTimestamps(timestamps); } + /** + * Processes a tombstone file and adds it to the DropBox after filtering and applying + * rate limiting. + * Filtering removes memory sections from the tombstone proto to reduce size while preserving + * critical information. The filtered tombstone is then added to DropBox in both proto + * and text formats, with the text format derived from the filtered proto. + * Rate limiting is applied as it is the case with other crash types. + * + * @param ctx Context + * @param tombstone path to the tombstone + * @param processName the name of the process corresponding to the tombstone + * @param tmpFileLock the lock for reading/writing tmp files + */ + public static void filterAndAddTombstoneToDropBox( + Context ctx, File tombstone, String processName, ReentrantLock tmpFileLock) { + final DropBoxManager db = ctx.getSystemService(DropBoxManager.class); + if (db == null) { + Slog.e(TAG, "Can't log tombstone: DropBoxManager not available"); + return; + } + File filteredProto = null; + // Check if we should rate limit and abort early if needed. + DropboxRateLimiter.RateLimitResult rateLimitResult = + sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName); + if (rateLimitResult.shouldRateLimit()) return; + + HashMap<String, Long> timestamps = readTimestamps(); + try { + tmpFileLock.lock(); + Slog.i(TAG, "Filtering tombstone file: " + tombstone.getName()); + // Create a temporary tombstone without memory sections. + filteredProto = createTempTombstoneWithoutMemory(tombstone); + Slog.i(TAG, "Generated tombstone file: " + filteredProto.getName()); + + if (recordFileTimestamp(tombstone, timestamps)) { + // We need to attach the count indicating the number of dropped dropbox entries + // due to rate limiting. Do this by enclosing the proto tombsstone in a + // container proto that has the dropped entry count and the proto tombstone as + // bytes (to avoid the complexity of reading and writing nested protos). + Slog.i(TAG, "Adding tombstone " + filteredProto.getName() + " to dropbox"); + addAugmentedProtoToDropbox(filteredProto, db, rateLimitResult); + } + // Always add the text version of the tombstone to the DropBox, in order to + // match the previous behaviour. + Slog.i(TAG, "Adding text tombstone version of " + filteredProto.getName() + + " to dropbox"); + addTextTombstoneFromProtoToDropbox(filteredProto, db, timestamps, rateLimitResult); + + } catch (IOException | ProtoParseException e) { + Slog.e(TAG, "Failed to log tombstone '" + tombstone.getName() + + "' to DropBox. Error during processing or writing: " + e.getMessage(), e); + } finally { + if (filteredProto != null) { + filteredProto.delete(); + } + tmpFileLock.unlock(); + } + writeTimestamps(timestamps); + } + + /** + * Creates a temporary tombstone file by filtering out memory mapping fields. + * This ensures that the unneeded memory mapping data is removed from the tombstone + * before adding it to Dropbox + * + * @param tombstone the original tombstone file to process + * @return a temporary file containing the filtered tombstone data + * @throws IOException if an I/O error occurs during processing + */ + private static File createTempTombstoneWithoutMemory(File tombstone) throws IOException { + // Process the proto tombstone file and write it to a temporary file + File tombstoneProto = + File.createTempFile(tombstone.getName(), ".pb.tmp", TOMBSTONE_TMP_DIR); + ProtoFieldFilter protoFilter = + new ProtoFieldFilter(fieldNumber -> fieldNumber != (int) Tombstone.MEMORY_MAPPINGS); + + try (FileInputStream fis = new FileInputStream(tombstone); + BufferedInputStream bis = new BufferedInputStream(fis); + FileOutputStream fos = new FileOutputStream(tombstoneProto); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + protoFilter.filter(bis, bos); + return tombstoneProto; + } + } + + private static void addTextTombstoneFromProtoToDropbox(File tombstone, DropBoxManager db, + HashMap<String, Long> timestamps, DropboxRateLimiter.RateLimitResult rateLimitResult) { + File tombstoneTextFile = null; + + try { + tombstoneTextFile = File.createTempFile(tombstone.getName(), + ".pb.txt.tmp", TOMBSTONE_TMP_DIR); + + // Create a ProcessBuilder to execute pbtombstone + ProcessBuilder pb = new ProcessBuilder("/system/bin/pbtombstone", tombstone.getPath()); + pb.redirectOutput(tombstoneTextFile); + Process process = pb.start(); + + // Wait 10 seconds for the process to complete + if (!process.waitFor(10, TimeUnit.SECONDS)) { + Slog.e(TAG, "pbtombstone timed out"); + process.destroyForcibly(); + return; + } + + int exitCode = process.exitValue(); + if (exitCode != 0) { + Slog.e(TAG, "pbtombstone failed with exit code " + exitCode); + } else { + final String headers = getBootHeadersToLogAndUpdate() + + rateLimitResult.createHeader(); + addFileToDropBox(db, timestamps, headers, tombstoneTextFile.getPath(), LOG_SIZE, + TAG_TOMBSTONE); + } + } catch (IOException | InterruptedException e) { + Slog.e(TAG, "Failed to process tombstone with pbtombstone", e); + } finally { + if (tombstoneTextFile != null) { + tombstoneTextFile.delete(); + } + } + } + private static void addAugmentedProtoToDropbox( File tombstone, DropBoxManager db, DropboxRateLimiter.RateLimitResult rateLimitResult) throws IOException { diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index f23d7823be94..33c122964d77 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -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 diff --git a/services/core/java/com/android/server/os/core_os_flags.aconfig b/services/core/java/com/android/server/os/core_os_flags.aconfig index efdc9b8c164f..5e35cf5f02d3 100644 --- a/services/core/java/com/android/server/os/core_os_flags.aconfig +++ b/services/core/java/com/android/server/os/core_os_flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "proto_tombstone" - namespace: "proto_tombstone_ns" + namespace: "stability" description: "Use proto tombstones as source of truth for adding to dropbox" bug: "323857385" } |