diff --git a/tests/GosCompatTests/GosCompatMemoryTests/Android.bp b/tests/GosCompatTests/GosCompatMemoryTests/Android.bp
new file mode 100644
index 0000000000000..8e60d9a3f0253
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/Android.bp
@@ -0,0 +1,34 @@
+cc_binary {
+ name: "goscompat_smaps_native_runner",
+ srcs: [
+ "native/simple_smaps_parser.cpp",
+ "native/smaps_parser_native_runner.cpp",
+ ],
+ local_include_dirs: [
+ "native",
+ ],
+ cflags: [
+ "-std=c++17",
+ "-Wall",
+ "-Wextra",
+ "-Werror",
+ "-fexceptions",
+ ],
+ stl: "c++_static",
+}
+
+java_test_host {
+ name: "GosCompatMemoryTests",
+ srcs: [
+ "host/src/**/*.java",
+ ":libtombstone_proto-src",
+ ],
+ libs: [
+ "tradefed",
+ ],
+ device_first_data: [
+ ":goscompat_smaps_native_runner",
+ ],
+ test_config: "NativeAndroidTest.xml",
+ test_suites: ["general-tests"],
+}
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/NativeAndroidTest.xml b/tests/GosCompatTests/GosCompatMemoryTests/NativeAndroidTest.xml
new file mode 100644
index 0000000000000..ea32e6253291e
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/NativeAndroidTest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/README.md b/tests/GosCompatTests/GosCompatMemoryTests/README.md
new file mode 100644
index 0000000000000..8e84edfdfaa89
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/README.md
@@ -0,0 +1,12 @@
+# GosCompatMemoryTests
+
+`GosCompatMemoryTests` is a compatibility test for memory allocator usage in apps.
+
+## SmapsNativeHostTest
+
+This tests streams `/proc/self/smaps` (a live walk of the process VMA tree) while allocating
+retained parser objects, and hardened_malloc turns that allocation pattern into many allocator VMAs.
+Because the live `/proc/self/smaps` walk can see those newly-created VMAs, the parser can start
+parsing mappings created by its own parsing work, leading to a loop of allocations and eventually an
+out-of-memory error. The tests will fail when a very high limit on number of VMA records parsed is
+reached.
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/host/src/app/grapheneos/goscompat/memory/nativehost/SmapsNativeHostTest.java b/tests/GosCompatTests/GosCompatMemoryTests/host/src/app/grapheneos/goscompat/memory/nativehost/SmapsNativeHostTest.java
new file mode 100644
index 0000000000000..5515ab5dfe5ea
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/host/src/app/grapheneos/goscompat/memory/nativehost/SmapsNativeHostTest.java
@@ -0,0 +1,447 @@
+package app.grapheneos.goscompat.memory.nativehost;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.server.os.TombstoneProtos;
+import com.android.server.os.TombstoneProtos.Tombstone;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class SmapsNativeHostTest extends BaseHostJUnit4Test {
+ private static final String RUNNER = "/data/local/tmp/goscompat_smaps_native_runner";
+ private static final String TOMBSTONE_DIR = "/data/tombstones";
+ private static final long TOMBSTONE_WAIT_TIMEOUT_MILLIS = 5_000;
+ private static final long TOMBSTONE_WAIT_STEP_MILLIS = 200;
+ private static final int SIGABRT_EXIT_CODE = 128 + 6;
+ private static final String ALLOCATOR_HARDENED_MALLOC = "hardened_malloc";
+ private static final String ALLOCATOR_HARDENED_MALLOC_DISABLED =
+ "hardened_malloc_disabled";
+ private static final String SIMPLE_GUARD_ABORT_PREFIX =
+ "simple_smaps_parser guard abort after element allocation guard";
+ private static final int MAX_MAPPING_SUMMARY_ENTRIES = 12;
+ private static final String TOMBSTONE_COLLECTION_LOG =
+ "goscompat_smaps_tombstone_collection";
+
+ @Rule
+ public final TestLogData mLogs = new TestLogData();
+
+ private String mToken;
+ private String mTombstoneUnavailableMessage;
+
+ @Before
+ public void setUp() throws Exception {
+ mToken = UUID.randomUUID().toString();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mToken == null) {
+ return;
+ }
+ if (mTombstoneUnavailableMessage != null) {
+ return;
+ }
+ List artifacts = listTokenTombstones(null);
+ if (artifacts.isEmpty()) {
+ return;
+ }
+
+ boolean disableAdbRootAfterCleanup = false;
+ try {
+ if (!getDevice().isAdbRoot()) {
+ if (getDevice().enableAdbRoot()) {
+ disableAdbRootAfterCleanup = true;
+ } else {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_root",
+ "unable to enable adb root for tombstone cleanup; cleanup will "
+ + "continue without root\n");
+ }
+ }
+ } catch (Exception e) {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_root",
+ "unable to check or enable adb root for tombstone cleanup; cleanup will "
+ + "continue with the current shell state: "
+ + describeException(e) + "\n");
+ }
+
+ try {
+ for (TombstoneArtifact artifact : artifacts) {
+ deleteTombstoneFileBestEffort(artifact.path);
+ deleteTombstoneFileBestEffort(stripProtoSuffix(artifact.path));
+ }
+ } finally {
+ if (disableAdbRootAfterCleanup) {
+ try {
+ if (!getDevice().disableAdbRoot()) {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_root",
+ "unable to disable adb root after tombstone cleanup\n");
+ }
+ } catch (Exception e) {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_root",
+ "unable to disable adb root after tombstone cleanup: "
+ + describeException(e) + "\n");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void simpleSmapsElementAllocationsWithHardenedMallocDoesNotCrashFromNativeBinary()
+ throws Exception {
+ String testName =
+ "simpleSmapsElementAllocationsWithHardenedMallocDoesNotCrashFromNativeBinary";
+ assertNativeParserDoesNotCrash(ALLOCATOR_HARDENED_MALLOC, testName, false);
+ }
+
+ @Test
+ public void simpleSmapsElementAllocationsWithoutHardenedMallocDoesNotCrashFromNativeBinary()
+ throws Exception {
+ String testName =
+ "simpleSmapsElementAllocationsWithoutHardenedMallocDoesNotCrashFromNativeBinary";
+ // Control: this test should pass.
+ assertNativeParserDoesNotCrash(ALLOCATOR_HARDENED_MALLOC_DISABLED, testName, true);
+ }
+
+ private void assertNativeParserDoesNotCrash(String allocatorState, String testName,
+ boolean disableHardenedMalloc) throws Exception {
+ NativeRun run = runNativeParser(testName, disableHardenedMalloc);
+ List tombstones = waitForTombstones(testName, run.exitCode != 0);
+ reportRunArtifacts(testName, allocatorState, run, tombstones);
+
+ if (run.exitCode != 0) {
+ if (mTombstoneUnavailableMessage == null) {
+ assertFalse("native crash should produce a token-matched tombstone",
+ tombstones.isEmpty());
+ String abortMessage = tombstones.get(0).tombstone.getAbortMessage();
+ assertTrue("expected guard abort in native tombstone, got: " + abortMessage,
+ abortMessage.startsWith(SIMPLE_GUARD_ABORT_PREFIX));
+ assertEquals("expected SIGABRT for guard abort",
+ SIGABRT_EXIT_CODE, run.exitCode);
+ }
+ }
+
+ assertEquals(failureMessage(run, tombstones), 0, run.exitCode);
+ assertTrue("unexpected tombstone from a successful native parser run",
+ tombstones.isEmpty());
+ }
+
+ private NativeRun runNativeParser(String testName, boolean disableHardenedMalloc)
+ throws Exception {
+ String command = "sh -c 'echo $$; "
+ + (disableHardenedMalloc
+ ? "export DISABLE_HARDENED_MALLOC=1; "
+ : "unset DISABLE_HARDENED_MALLOC; ")
+ + "exec " + RUNNER + " " + testName + " " + mToken + "'";
+ CommandResult result = getDevice().executeShellV2Command(command);
+ assertNotNull("native command exit code", result.getExitCode());
+ return new NativeRun(command, result, result.getExitCode());
+ }
+
+ private List waitForTombstones(String testName, boolean expectTombstone)
+ throws Exception {
+ if (mTombstoneUnavailableMessage != null) {
+ return new ArrayList<>();
+ }
+ long deadline = System.currentTimeMillis() + TOMBSTONE_WAIT_TIMEOUT_MILLIS;
+ List tombstones;
+ do {
+ tombstones = listTokenTombstones(testName);
+ if (!expectTombstone || !tombstones.isEmpty()) {
+ return tombstones;
+ }
+ Thread.sleep(TOMBSTONE_WAIT_STEP_MILLIS);
+ } while (System.currentTimeMillis() < deadline);
+ return tombstones;
+ }
+
+ private List listTokenTombstones(String testName) throws Exception {
+ List matches = new ArrayList<>();
+ if (mTombstoneUnavailableMessage != null) {
+ return matches;
+ }
+ String[] tombstones;
+ try {
+ tombstones = getDevice().getChildren(TOMBSTONE_DIR);
+ } catch (Exception e) {
+ recordTombstonesUnavailable(
+ "unable to list " + TOMBSTONE_DIR + ": " + describeException(e));
+ return matches;
+ }
+ if (tombstones == null) {
+ recordTombstonesUnavailable("unable to list " + TOMBSTONE_DIR);
+ return matches;
+ }
+
+ for (String tombstone : tombstones) {
+ if (!tombstone.endsWith(".pb")) {
+ continue;
+ }
+ String path = TOMBSTONE_DIR + "/" + tombstone;
+ TombstoneArtifact artifact;
+ try {
+ artifact = parseTombstone(path);
+ } catch (Exception e) {
+ recordTombstonesUnavailable(
+ "unable to pull or parse " + path + ": " + describeException(e));
+ return matches;
+ }
+ List commandLine = artifact.tombstone.getCommandLineList();
+ if (!contains(commandLine, mToken)) {
+ continue;
+ }
+ if (testName != null && !contains(commandLine, testName)) {
+ continue;
+ }
+ matches.add(artifact);
+ }
+ return matches;
+ }
+
+ private TombstoneArtifact parseTombstone(String path) throws Exception {
+ File file = getDevice().pullFile(path);
+ byte[] proto = Files.readAllBytes(file.toPath());
+ try (InputStream input = new FileInputStream(file)) {
+ return new TombstoneArtifact(path, Tombstone.parseFrom(input), proto);
+ } finally {
+ file.delete();
+ }
+ }
+
+ private void deleteTombstoneFileBestEffort(String path) {
+ try {
+ getDevice().deleteFile(path);
+ if (getDevice().doesFileExist(path)) {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_" + sanitize(path),
+ "tombstone collection succeeded, but cleanup could not delete "
+ + path + "; this does not affect the test result\n");
+ }
+ } catch (Exception e) {
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG + "_cleanup_" + sanitize(path),
+ "tombstone collection succeeded, but cleanup could not delete "
+ + path + "; this does not affect the test result: "
+ + describeException(e) + "\n");
+ }
+ }
+
+ private void reportRunArtifacts(String testName, String allocatorState,
+ NativeRun run, List tombstones) throws Exception {
+ String baseName = "goscompat_smaps_native_" + sanitize(allocatorState) + "_"
+ + sanitize(testName);
+ String commandLog = "command=" + run.command + "\n"
+ + "exitCode=" + run.exitCode + "\n"
+ + tombstoneCollectionStatus()
+ + "stdout:\n" + emptyToMarker(run.result.getStdout())
+ + "\nstderr:\n" + emptyToMarker(run.result.getStderr());
+ addTextLog(baseName + "_command", commandLog);
+
+ for (int i = 0; i < tombstones.size(); i++) {
+ TombstoneArtifact artifact = tombstones.get(i);
+ String tombstoneName = baseName + "_tombstone_" + i;
+ addTextLog(tombstoneName + "_text", formatTombstone(artifact.tombstone));
+ try (ByteArrayInputStreamSource source =
+ new ByteArrayInputStreamSource(artifact.proto)) {
+ mLogs.addTestLog(tombstoneName + "_pb", LogDataType.PB, source);
+ }
+ }
+ }
+
+ private void addTextLog(String name, String text) throws Exception {
+ try (ByteArrayInputStreamSource source =
+ new ByteArrayInputStreamSource(text.getBytes(StandardCharsets.UTF_8))) {
+ mLogs.addTestLog(name, LogDataType.TEXT, source);
+ }
+ }
+
+ private String failureMessage(NativeRun run, List tombstones) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("native parser command should not crash")
+ .append("\ncommand=").append(run.command)
+ .append("\nexitCode=").append(run.exitCode)
+ .append('\n').append(tombstoneCollectionStatus())
+ .append("stdout:\n").append(emptyToMarker(run.result.getStdout()))
+ .append("\nstderr:\n").append(emptyToMarker(run.result.getStderr()));
+ for (TombstoneArtifact artifact : tombstones) {
+ builder.append("\n\n").append(formatTombstone(artifact.tombstone));
+ }
+ return builder.toString();
+ }
+
+ private String tombstoneCollectionStatus() {
+ if (mTombstoneUnavailableMessage == null) {
+ return "nativeTombstones=available\n";
+ }
+ return "nativeTombstones=unavailable: " + mTombstoneUnavailableMessage + "\n";
+ }
+
+ private void recordTombstonesUnavailable(String message) {
+ if (mTombstoneUnavailableMessage != null) {
+ return;
+ }
+ mTombstoneUnavailableMessage = message;
+ addTextLogBestEffort(TOMBSTONE_COLLECTION_LOG, message + "\n");
+ }
+
+ private void addTextLogBestEffort(String name, String text) {
+ System.out.println(name + ": " + text.trim());
+ try {
+ addTextLog(name, text);
+ } catch (Exception e) {
+ System.out.println(name + " log unavailable: " + describeException(e));
+ }
+ }
+
+ private static String formatTombstone(Tombstone tombstone) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("path commandLine=")
+ .append(tombstone.getCommandLineList())
+ .append("\npid=").append(tombstone.getPid())
+ .append(", tid=").append(tombstone.getTid())
+ .append(", uid=").append(tombstone.getUid())
+ .append(", processUptime=").append(tombstone.getProcessUptime())
+ .append("\nsignal ")
+ .append(tombstone.getSignalInfo().getNumber())
+ .append(" (").append(tombstone.getSignalInfo().getName()).append(")")
+ .append(", code ")
+ .append(tombstone.getSignalInfo().getCode())
+ .append(" (").append(tombstone.getSignalInfo().getCodeName()).append(")")
+ .append("\nabort message: ").append(tombstone.getAbortMessage())
+ .append("\nmemory mappings:\n")
+ .append(formatMemoryMappingSummary(tombstone))
+ .append("\nbacktrace:\n");
+
+ TombstoneProtos.Thread thread = tombstone.getThreadsMap().get(tombstone.getTid());
+ if (thread == null) {
+ builder.append(" \n");
+ return builder.toString();
+ }
+ for (int i = 0; i < thread.getCurrentBacktraceList().size(); i++) {
+ TombstoneProtos.BacktraceFrame frame = thread.getCurrentBacktraceList().get(i);
+ builder.append('#').append(i)
+ .append(' ')
+ .append(frame.getFileName())
+ .append(" (")
+ .append(frame.getFunctionName())
+ .append(")\n");
+ }
+ return builder.toString();
+ }
+
+ private static String formatMemoryMappingSummary(Tombstone tombstone) {
+ List mappings = tombstone.getMemoryMappingsList();
+ if (mappings.isEmpty()) {
+ return " \n";
+ }
+
+ Map counts = new HashMap<>();
+ for (TombstoneProtos.MemoryMapping mapping : mappings) {
+ String name = mapping.getMappingName();
+ if (name.isEmpty()) {
+ name = "";
+ }
+ Integer count = counts.get(name);
+ counts.put(name, count == null ? 1 : count + 1);
+ }
+
+ List> entries = new ArrayList<>(counts.entrySet());
+ entries.sort((left, right) -> {
+ int byCount = Integer.compare(right.getValue(), left.getValue());
+ return byCount != 0 ? byCount : left.getKey().compareTo(right.getKey());
+ });
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(" total=").append(mappings.size()).append('\n');
+ int entryCount = Math.min(entries.size(), MAX_MAPPING_SUMMARY_ENTRIES);
+ for (int i = 0; i < entryCount; i++) {
+ Map.Entry entry = entries.get(i);
+ builder.append(" ")
+ .append(entry.getValue())
+ .append(' ')
+ .append(entry.getKey())
+ .append('\n');
+ }
+ if (entries.size() > MAX_MAPPING_SUMMARY_ENTRIES) {
+ builder.append(" ... ")
+ .append(entries.size() - MAX_MAPPING_SUMMARY_ENTRIES)
+ .append(" more mapping names\n");
+ }
+ return builder.toString();
+ }
+
+ private static boolean contains(List values, String needle) {
+ for (String value : values) {
+ if (value.contains(needle)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String stripProtoSuffix(String path) {
+ return path.endsWith(".pb") ? path.substring(0, path.length() - 3) : path;
+ }
+
+ private static String sanitize(String value) {
+ return value.replaceAll("[^A-Za-z0-9_.-]", "_");
+ }
+
+ private static String emptyToMarker(String value) {
+ return value == null || value.isEmpty() ? "\n" : value;
+ }
+
+ private static String describeException(Exception e) {
+ String message = e.getMessage();
+ if (message == null || message.isEmpty()) {
+ return e.getClass().getName();
+ }
+ return e.getClass().getName() + ": " + message;
+ }
+
+ private static final class NativeRun {
+ final String command;
+ final CommandResult result;
+ final int exitCode;
+
+ NativeRun(String command, CommandResult result, int exitCode) {
+ this.command = command;
+ this.result = result;
+ this.exitCode = exitCode;
+ }
+ }
+
+ private static final class TombstoneArtifact {
+ final String path;
+ final Tombstone tombstone;
+ final byte[] proto;
+
+ TombstoneArtifact(String path, Tombstone tombstone, byte[] proto) {
+ this.path = path;
+ this.tombstone = tombstone;
+ this.proto = proto;
+ }
+ }
+}
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.cpp b/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.cpp
new file mode 100644
index 0000000000000..d694870b3eff9
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.cpp
@@ -0,0 +1,253 @@
+#include "simple_smaps_parser.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern "C" void android_set_abort_message(const char* msg);
+
+// Minimal smaps reproducer: allocate and retain one 80-byte C++ object for
+// each smaps line observed while the kernel is streaming /proc/self/smaps.
+namespace simple_smaps_parser {
+namespace {
+
+using smaps_parser::ParseLimits;
+using smaps_parser::ParseResult;
+using smaps_parser::ParseStats;
+
+enum ElementKind : std::uint32_t {
+ kOtherLine = 0,
+ kMappingHeader = 1,
+ kDetailLine = 2,
+};
+
+struct SmapsElement {
+ SmapsElement* next = nullptr;
+ std::uint64_t sequence = 0;
+ std::uint32_t line_length = 0;
+ std::uint32_t kind = kOtherLine;
+ char line_prefix[56] = {};
+};
+
+static_assert(sizeof(SmapsElement) == 80,
+ "smaps element allocation size must stay at 80 bytes");
+
+// Handrolled to make allocations more clear. Only SmapsElements are getting allocated in the
+// parser loop
+struct ElementList {
+ ~ElementList() {
+ while (head != nullptr) {
+ SmapsElement* element = head;
+ head = head->next;
+ delete element;
+ }
+ }
+
+ void append(SmapsElement* element) {
+ if (tail == nullptr) {
+ head = element;
+ } else {
+ tail->next = element;
+ }
+ tail = element;
+ }
+
+ SmapsElement* head = nullptr;
+ SmapsElement* tail = nullptr;
+};
+
+int get_thread_id() {
+ return static_cast(syscall(SYS_gettid));
+}
+
+void set_error(ParseResult* result, const char* message) {
+ if (result->error[0] == '\0') {
+ std::snprintf(result->error, sizeof(result->error), "%s", message);
+ }
+}
+
+void abort_for_limit(ParseStats* stats, const ParseLimits& limits, const char* reason) {
+ const bool accepted_limit_reached =
+ limits.max_accepted_vma_records != 0
+ && stats->accepted_vma_records > limits.max_accepted_vma_records;
+ const bool byte_limit_reached =
+ limits.max_bytes_allocated != 0
+ && stats->bytes_allocated > limits.max_bytes_allocated;
+ if (!accepted_limit_reached && !byte_limit_reached) {
+ return;
+ }
+
+ char message[256];
+ std::snprintf(message, sizeof(message),
+ "simple_smaps_parser guard abort after %s: accepted_vma_records=%llu "
+ "max_accepted_vma_records=%llu bytes_allocated=%llu "
+ "max_bytes_allocated=%llu",
+ reason,
+ static_cast(stats->accepted_vma_records),
+ static_cast(limits.max_accepted_vma_records),
+ static_cast(stats->bytes_allocated),
+ static_cast(limits.max_bytes_allocated));
+ android_set_abort_message(message);
+ std::fprintf(stderr, "%s\n", message);
+ std::fflush(stderr);
+ std::abort();
+}
+
+bool is_hex_digit(char c) {
+ return (c >= '0' && c <= '9')
+ || (c >= 'a' && c <= 'f')
+ || (c >= 'A' && c <= 'F');
+}
+
+bool is_mapping_header(const char* line, std::size_t length) {
+ std::size_t i = 0;
+ while (i < length && is_hex_digit(line[i])) {
+ i++;
+ }
+ if (i == 0 || i >= length || line[i] != '-') {
+ return false;
+ }
+ i++;
+
+ const std::size_t end_start = i;
+ while (i < length && is_hex_digit(line[i])) {
+ i++;
+ }
+ return i > end_start && i < length && (line[i] == ' ' || line[i] == '\t');
+}
+
+bool is_detail_line(const char* line, std::size_t length) {
+ std::size_t i = 0;
+ while (i < length && (line[i] == ' ' || line[i] == '\t')) {
+ i++;
+ }
+ const std::size_t key_start = i;
+ while (i < length && line[i] != ':' && line[i] != ' ' && line[i] != '\t') {
+ i++;
+ }
+ return i > key_start && i < length && line[i] == ':';
+}
+
+std::size_t trim_line_length(const char* line, std::size_t length) {
+ while (length > 0 && (line[length - 1] == '\n' || line[length - 1] == '\r')) {
+ length--;
+ }
+ return length;
+}
+
+SmapsElement* allocate_element(ParseStats* stats, const ParseLimits& limits,
+ const char* line, std::size_t line_length, ElementKind kind,
+ std::uint64_t sequence) {
+ SmapsElement* element = new SmapsElement();
+ element->sequence = sequence;
+ element->line_length = static_cast(
+ std::min(line_length, UINT32_MAX));
+ element->kind = kind;
+ const std::size_t prefix_length =
+ std::min(line_length, sizeof(element->line_prefix));
+ if (prefix_length != 0) {
+ std::memcpy(element->line_prefix, line, prefix_length);
+ }
+
+ stats->bytes_allocated += sizeof(SmapsElement);
+ if (kind == kMappingHeader) {
+ stats->candidate_vma_records++;
+ stats->accepted_vma_records++;
+ } else if (kind == kDetailLine) {
+ stats->detail_keys_inserted++;
+ } else {
+ stats->other_allocations++;
+ }
+
+ // The guard fires only after the triggering allocation has succeeded.
+ abort_for_limit(stats, limits, "element allocation guard");
+ return element;
+}
+
+void consume_line_remainder(FILE* file) {
+ int ch;
+ do {
+ ch = std::fgetc(file);
+ } while (ch != '\n' && ch != EOF);
+}
+
+void run_parser(ParseResult* result, const ParseLimits& limits) {
+ result->worker_tid = get_thread_id();
+
+ FILE* file = std::fopen("/proc/self/smaps", "r");
+ if (file == nullptr) {
+ set_error(result, "failed to open /proc/self/smaps");
+ return;
+ }
+
+ ElementList elements;
+ char line[8192];
+ std::uint64_t sequence = 0;
+ std::size_t current_details_per_vma = 0;
+ bool saw_vma = false;
+
+ while (std::fgets(line, sizeof(line), file) != nullptr) {
+ const std::size_t raw_length = std::strlen(line);
+ if (raw_length == sizeof(line) - 1 && line[raw_length - 1] != '\n') {
+ consume_line_remainder(file);
+ }
+
+ const std::size_t line_length = trim_line_length(line, raw_length);
+ result->stats.lines_read++;
+ result->stats.max_line_length =
+ std::max(result->stats.max_line_length, line_length);
+ if (line_length == 0) {
+ continue;
+ }
+
+ ElementKind kind = kOtherLine;
+ if (is_mapping_header(line, line_length)) {
+ if (saw_vma) {
+ result->stats.max_details_per_vma =
+ std::max(result->stats.max_details_per_vma,
+ current_details_per_vma);
+ }
+ saw_vma = true;
+ current_details_per_vma = 0;
+ kind = kMappingHeader;
+ } else if (is_detail_line(line, line_length)) {
+ result->stats.detail_lines_seen++;
+ current_details_per_vma++;
+ kind = kDetailLine;
+ }
+
+ elements.append(allocate_element(&result->stats, limits, line, line_length,
+ kind, sequence));
+ sequence++;
+ }
+
+ if (saw_vma) {
+ result->stats.max_details_per_vma =
+ std::max(result->stats.max_details_per_vma,
+ current_details_per_vma);
+ }
+
+ std::fclose(file);
+ result->completed = result->stats.accepted_vma_records > 0
+ && result->stats.detail_keys_inserted > 0
+ && result->stats.bytes_allocated > 0;
+ if (!result->completed && result->error[0] == '\0') {
+ set_error(result, "simple smaps parser returned incomplete counters");
+ }
+}
+
+} // namespace
+
+ParseResult run(int caller_tid, const ParseLimits& limits) {
+ ParseResult result;
+ result.caller_tid = caller_tid;
+ run_parser(&result, limits);
+ return result;
+}
+
+} // namespace simple_smaps_parser
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.h b/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.h
new file mode 100644
index 0000000000000..536b71ed6cc43
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/native/simple_smaps_parser.h
@@ -0,0 +1,12 @@
+#ifndef GOSCOMPAT_SIMPLE_SMAPS_PARSER_H
+#define GOSCOMPAT_SIMPLE_SMAPS_PARSER_H
+
+#include "smaps_parser_common.h"
+
+namespace simple_smaps_parser {
+
+smaps_parser::ParseResult run(int caller_tid, const smaps_parser::ParseLimits& limits);
+
+} // namespace simple_smaps_parser
+
+#endif // GOSCOMPAT_SIMPLE_SMAPS_PARSER_H
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_common.h b/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_common.h
new file mode 100644
index 0000000000000..26228751f5278
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_common.h
@@ -0,0 +1,41 @@
+#ifndef GOSCOMPAT_SMAPS_PARSER_COMMON_H
+#define GOSCOMPAT_SMAPS_PARSER_COMMON_H
+
+#include
+#include
+
+namespace smaps_parser {
+
+struct ParseStats {
+ std::uint64_t lines_read = 0;
+ std::uint64_t candidate_vma_records = 0;
+ std::uint64_t accepted_vma_records = 0;
+ std::uint64_t detail_lines_seen = 0;
+ std::uint64_t detail_keys_inserted = 0;
+ std::uint64_t detail_values_updated = 0;
+ std::uint64_t vector_entries_pushed = 0;
+ std::uint64_t vector_buffer_allocations = 0;
+ std::uint64_t string_allocations = 0;
+ std::uint64_t other_allocations = 0;
+ std::uint64_t bytes_allocated = 0;
+ std::size_t max_details_per_vma = 0;
+ std::size_t max_line_length = 0;
+};
+
+struct ParseLimits {
+ std::uint64_t max_accepted_vma_records = 0;
+ std::uint64_t max_bytes_allocated = 0;
+};
+
+struct ParseResult {
+ bool completed = false;
+ int pointer_size = sizeof(void*);
+ int caller_tid = 0;
+ int worker_tid = 0;
+ ParseStats stats;
+ char error[160] = {};
+};
+
+} // namespace smaps_parser
+
+#endif // GOSCOMPAT_SMAPS_PARSER_COMMON_H
diff --git a/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_native_runner.cpp b/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_native_runner.cpp
new file mode 100644
index 0000000000000..6bc34f91f797f
--- /dev/null
+++ b/tests/GosCompatTests/GosCompatMemoryTests/native/smaps_parser_native_runner.cpp
@@ -0,0 +1,107 @@
+#include "simple_smaps_parser.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+constexpr std::uint64_t kMaxAcceptedVmaRecordsBeforeAbort = 16'384;
+constexpr std::uint64_t kMaxBytesAllocatedBeforeAbort = 192ULL * 1024 * 1024;
+constexpr const char* kParserMode = "simple";
+
+int get_thread_id() {
+ return static_cast(syscall(SYS_gettid));
+}
+
+std::string describe_result(const smaps_parser::ParseResult& result) {
+ std::ostringstream out;
+ out << "completed=" << result.completed
+ << ", pointerSize=" << result.pointer_size
+ << ", callerTid=" << result.caller_tid
+ << ", workerTid=" << result.worker_tid
+ << ", linesRead=" << result.stats.lines_read
+ << ", acceptedVmaRecords=" << result.stats.accepted_vma_records
+ << ", candidateVmaRecords=" << result.stats.candidate_vma_records
+ << ", detailLinesSeen=" << result.stats.detail_lines_seen
+ << ", detailKeysInserted=" << result.stats.detail_keys_inserted
+ << ", vectorEntriesPushed=" << result.stats.vector_entries_pushed
+ << ", bytesAllocated=" << result.stats.bytes_allocated
+ << ", error=" << result.error;
+ return out.str();
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ if (argc != 3) {
+ std::cerr << "usage: " << argv[0] << " \n";
+ return 2;
+ }
+
+ const std::string test_name = argv[1];
+ const std::string token = argv[2];
+
+ std::cout << "parserMode=" << kParserMode
+ << " testName=" << test_name
+ << " token=" << token
+ << " pid=" << getpid()
+ << "\n";
+
+ const int caller_tid = get_thread_id();
+ smaps_parser::ParseLimits limits;
+ limits.max_accepted_vma_records = kMaxAcceptedVmaRecordsBeforeAbort;
+ limits.max_bytes_allocated = kMaxBytesAllocatedBeforeAbort;
+
+ smaps_parser::ParseResult result;
+ bool have_result = false;
+ std::thread worker([&] {
+ result = simple_smaps_parser::run(caller_tid, limits);
+ have_result = true;
+ });
+ worker.join();
+
+ const std::string message = describe_result(result);
+ std::cout << message << "\n";
+
+ if (!have_result) {
+ std::cerr << "parser worker did not produce a result\n";
+ return 3;
+ }
+ if (result.error[0] != '\0') {
+ std::cerr << result.error << "\n";
+ return 4;
+ }
+ if (!result.completed) {
+ std::cerr << "parser did not complete\n";
+ return 5;
+ }
+ if (result.caller_tid == result.worker_tid) {
+ std::cerr << "parser did not run on the worker thread\n";
+ return 7;
+ }
+ if (result.stats.accepted_vma_records == 0
+ || result.stats.detail_keys_inserted == 0
+ || result.stats.bytes_allocated == 0) {
+ std::cerr << "parser did not exercise the expected smaps workload\n";
+ return 8;
+ }
+ if (result.stats.candidate_vma_records < result.stats.accepted_vma_records) {
+ std::cerr << "candidate VMA count is smaller than accepted VMA count\n";
+ return 9;
+ }
+ if (result.stats.detail_keys_inserted != result.stats.detail_lines_seen) {
+ std::cerr << "simple parser detail count does not match detail lines seen\n";
+ return 10;
+ }
+ if (result.stats.vector_entries_pushed != 0) {
+ std::cerr << "simple parser unexpectedly used vector entries\n";
+ return 11;
+ }
+
+ return 0;
+}
diff --git a/tests/GosCompatTests/TEST_MAPPING b/tests/GosCompatTests/TEST_MAPPING
index 2e625181ca3c4..13280132bc067 100644
--- a/tests/GosCompatTests/TEST_MAPPING
+++ b/tests/GosCompatTests/TEST_MAPPING
@@ -11,6 +11,10 @@
},
{
"name": "GosCompatWifiScanTimeoutTests"
+ },
+ // adb root not required, only useful to delete tombstones
+ {
+ "name": "GosCompatMemoryTests"
}
]
}