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" } ] }