From 576328b1b4f5e9345bc4fd0099aa6ec3364bcd83 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 28 Oct 2023 17:04:27 +0300 Subject: [PATCH] android: add MTE tests To run them, connect an MTE-enabled device via adb and execute `atest HMallocTest:MemtagTest`. Since these tests are not deterministic (and neither is hardened_malloc itself), it's better to run them multiple times, e.g. `atest --iterations 30 HMallocTest:MemtagTest`. There are also CTS tests that are useful for checking correctness of the Android integration: `atest CtsTaggingHostTestCases` --- androidtest/Android.bp | 25 +++ androidtest/AndroidTest.xml | 13 ++ androidtest/memtag/Android.bp | 16 ++ androidtest/memtag/memtag_test.cc | 204 ++++++++++++++++++ .../src/grapheneos/hmalloc/MemtagTest.java | 136 ++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 androidtest/Android.bp create mode 100644 androidtest/AndroidTest.xml create mode 100644 androidtest/memtag/Android.bp create mode 100644 androidtest/memtag/memtag_test.cc create mode 100644 androidtest/src/grapheneos/hmalloc/MemtagTest.java diff --git a/androidtest/Android.bp b/androidtest/Android.bp new file mode 100644 index 0000000..ae0aa49 --- /dev/null +++ b/androidtest/Android.bp @@ -0,0 +1,25 @@ +java_test_host { + name: "HMallocTest", + srcs: [ + "src/**/*.java", + ], + + libs: [ + "tradefed", + "compatibility-tradefed", + "compatibility-host-util", + ], + + static_libs: [ + "cts-host-utils", + "frameworks-base-hostutils", + ], + + test_suites: [ + "general-tests", + ], + + data_device_bins_64: [ + "memtag_test", + ], +} diff --git a/androidtest/AndroidTest.xml b/androidtest/AndroidTest.xml new file mode 100644 index 0000000..333f1dd --- /dev/null +++ b/androidtest/AndroidTest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/androidtest/memtag/Android.bp b/androidtest/memtag/Android.bp new file mode 100644 index 0000000..14ab691 --- /dev/null +++ b/androidtest/memtag/Android.bp @@ -0,0 +1,16 @@ +cc_test { + name: "memtag_test", + srcs: ["memtag_test.cc"], + cflags: [ + "-Wall", + "-Werror", + "-Wextra", + "-O0", + ], + + compile_multilib: "64", + + sanitize: { + memtag_heap: true, + }, +} diff --git a/androidtest/memtag/memtag_test.cc b/androidtest/memtag/memtag_test.cc new file mode 100644 index 0000000..16c379d --- /dev/null +++ b/androidtest/memtag/memtag_test.cc @@ -0,0 +1,204 @@ +// needed to uncondionally enable assertions +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; + +using u8 = uint8_t; +using uptr = uintptr_t; +using u64 = uint64_t; + +const size_t DEFAULT_ALLOC_SIZE = 8; +const size_t CANARY_SIZE = 8; + +void do_context_switch() { + utsname s; + uname(&s); +} + +u8 get_pointer_tag(void *ptr) { + return (((uptr) ptr) >> 56) & 0xf; +} + +void *untag_pointer(void *ptr) { + const uintptr_t mask = UINTPTR_MAX >> 8; + return (void *) ((uintptr_t) ptr & mask); +} + +void tag_distinctness() { + if (rand() & 1) { + // make allocations in all of used size classes and free half of them + + const int max = 21000; + void *ptrs[max]; + + for (int i = 0; i < max; ++i) { + ptrs[i] = malloc(max); + } + + for (int i = 1; i < max; i += 2) { + free(ptrs[i]); + } + } + + const size_t cnt = 3000; + const size_t iter_cnt = 5; + const size_t alloc_cnt = cnt * iter_cnt; + + const int sizes[] = { 16, 160, 10240, 20480 }; + + for (size_t size_idx = 0; size_idx < sizeof(sizes) / sizeof(int); ++size_idx) { + const size_t full_alloc_size = sizes[size_idx]; + const size_t alloc_size = full_alloc_size - CANARY_SIZE; + + unordered_map map; + map.reserve(alloc_cnt); + + for (size_t iter = 0; iter < iter_cnt; ++iter) { + uptr allocations[cnt]; + + for (size_t i = 0; i < cnt; ++i) { + u8 *p = (u8 *) malloc(alloc_size); + uptr addr = (uptr) untag_pointer(p); + u8 tag = get_pointer_tag(p); + assert(tag >= 1 && tag <= 14); + + // check most recent tags of left and right neighbors + + auto left = map.find(addr - full_alloc_size); + if (left != map.end()) { + assert(left->second != tag); + } + + auto right = map.find(addr + full_alloc_size); + if (right != map.end()) { + assert(right->second != tag); + } + + // check previous tag of this slot + auto prev = map.find(addr); + if (prev != map.end()) { + assert(prev->second != tag); + map.erase(addr); + } + + map.emplace(addr, tag); + + for (size_t j = 0; j < alloc_size; ++j) { + // check that slot is zeroed + assert(p[j] == 0); + // check that slot is readable and writable + p[j]++; + } + + allocations[i] = addr; + // async tag check failures are reported on context switch + do_context_switch(); + } + + for (size_t i = 0; i < cnt; ++i) { + free((void *) allocations[i]); + } + } + } +} + +u8* alloc_default() { + if (rand() & 1) { + int cnt = rand() & 0x3f; + for (int i = 0; i < cnt; ++i) { + (void) malloc(DEFAULT_ALLOC_SIZE); + } + } + return (u8 *) malloc(DEFAULT_ALLOC_SIZE); +} + +volatile u8 u8_var; + +void read_after_free() { + u8 *p = alloc_default(); + free(p); + volatile u8 v = p[0]; + (void) v; +} + +void write_after_free() { + u8 *p = alloc_default(); + free(p); + p[0] = 1; +} + +void underflow_read() { + u8 *p = alloc_default(); + volatile u8 v = p[-1]; + (void) v; +} + +void underflow_write() { + u8 *p = alloc_default(); + p[-1] = 1; +} + +void overflow_read() { + u8 *p = alloc_default(); + volatile u8 v = p[DEFAULT_ALLOC_SIZE + CANARY_SIZE]; + (void) v; +} + +void overflow_write() { + u8 *p = alloc_default(); + p[DEFAULT_ALLOC_SIZE + CANARY_SIZE] = 1; +} + +void untagged_read() { + u8 *p = alloc_default(); + p = (u8 *) untag_pointer(p); + volatile u8 v = p[0]; + (void) v; +} + +void untagged_write() { + u8 *p = alloc_default(); + p = (u8 *) untag_pointer(p); + p[0] = 1; +} + +map> tests = { +#define TEST(s) { #s, s } + TEST(tag_distinctness), + TEST(read_after_free), + TEST(write_after_free), + TEST(overflow_read), + TEST(overflow_write), + TEST(underflow_read), + TEST(underflow_write), + TEST(untagged_read), + TEST(untagged_write), +#undef TEST +}; + +int main(int argc, char **argv) { + setbuf(stdout, NULL); + assert(argc == 2); + + auto test_name = string(argv[1]); + auto test_fn = tests[test_name]; + assert(test_fn != nullptr); + + assert(mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, M_HEAP_TAGGING_LEVEL_ASYNC) == 1); + + test_fn(); + do_context_switch(); + + return 0; +} diff --git a/androidtest/src/grapheneos/hmalloc/MemtagTest.java b/androidtest/src/grapheneos/hmalloc/MemtagTest.java new file mode 100644 index 0000000..5544128 --- /dev/null +++ b/androidtest/src/grapheneos/hmalloc/MemtagTest.java @@ -0,0 +1,136 @@ +package grapheneos.hmalloc; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(DeviceJUnit4ClassRunner.class) +public class MemtagTest extends BaseHostJUnit4Test { + + private static final String TEST_BINARY = "/data/local/tmp/memtag_test"; + + enum Result { + SUCCESS, + // it's expected that the device is configured to use asymm MTE tag checking mode + ASYNC_MTE_ERROR, + SYNC_MTE_ERROR, + } + + private static final int SEGV_EXIT_CODE = 139; + + private void runTest(String name, Result expectedResult) throws DeviceNotAvailableException { + var args = new ArrayList(); + args.add(TEST_BINARY); + args.add(name); + var device = getDevice(); + long deviceDate = device.getDeviceDate(); + String cmdLine = String.join(" ", args); + var result = device.executeShellV2Command(cmdLine); + + int expectedExitCode = expectedResult == Result.SUCCESS ? 0 : SEGV_EXIT_CODE; + + assertEquals("process exit code", expectedExitCode, result.getExitCode().intValue()); + + if (expectedResult == Result.SUCCESS) { + return; + } + + try { + // wait a bit for debuggerd to capture the crash + Thread.sleep(50); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + try (var logcat = device.getLogcatSince(deviceDate)) { + try (var s = logcat.createInputStream()) { + String[] lines = new String(s.readAllBytes()).split("\n"); + boolean foundCmd = false; + String cmd = "Cmdline: " + cmdLine; + String expectedSignalCode = switch (expectedResult) { + case ASYNC_MTE_ERROR -> "SEGV_MTEAERR"; + case SYNC_MTE_ERROR -> "SEGV_MTESERR"; + default -> throw new IllegalStateException(expectedResult.name()); + }; + for (String line : lines) { + if (!foundCmd) { + if (line.contains(cmd)) { + foundCmd = true; + } + continue; + } + + if (line.contains("signal 11 (SIGSEGV), code")) { + if (!line.contains(expectedSignalCode)) { + break; + } else { + return; + } + } + + if (line.contains("backtrace")) { + break; + } + } + + fail("missing " + expectedSignalCode + " crash in logcat"); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + @Test + public void tag_distinctness() throws DeviceNotAvailableException { + runTest("tag_distinctness", Result.SUCCESS); + } + + @Test + public void read_after_free() throws DeviceNotAvailableException { + runTest("read_after_free", Result.SYNC_MTE_ERROR); + } + + @Test + public void write_after_free() throws DeviceNotAvailableException { + runTest("write_after_free", Result.ASYNC_MTE_ERROR); + } + + @Test + public void underflow_read() throws DeviceNotAvailableException { + runTest("underflow_read", Result.SYNC_MTE_ERROR); + } + + @Test + public void underflow_write() throws DeviceNotAvailableException { + runTest("underflow_write", Result.ASYNC_MTE_ERROR); + } + + @Test + public void overflow_read() throws DeviceNotAvailableException { + runTest("overflow_read", Result.SYNC_MTE_ERROR); + } + + @Test + public void overflow_write() throws DeviceNotAvailableException { + runTest("overflow_write", Result.ASYNC_MTE_ERROR); + } + + @Test + public void untagged_read() throws DeviceNotAvailableException { + runTest("untagged_read", Result.SYNC_MTE_ERROR); + } + + @Test + public void untagged_write() throws DeviceNotAvailableException { + runTest("untagged_write", Result.ASYNC_MTE_ERROR); + } +}