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`pull/226/head
parent
5137d2da4d
commit
576328b1b4
|
@ -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",
|
||||
],
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration description="hardened_malloc test">
|
||||
|
||||
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
|
||||
<option name="cleanup" value="true" />
|
||||
<option name="push" value="memtag_test->/data/local/tmp/memtag_test" />
|
||||
</target_preparer>
|
||||
|
||||
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
|
||||
<option name="jar" value="HMallocTest.jar" />
|
||||
</test>
|
||||
|
||||
</configuration>
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
// needed to uncondionally enable assertions
|
||||
#undef NDEBUG
|
||||
#include <assert.h>
|
||||
#include <malloc.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
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<uptr, u8> 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<string, function<void()>> 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;
|
||||
}
|
|
@ -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<String>();
|
||||
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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue