mirror of
				https://github.com/GrapheneOS/hardened_malloc.git
				synced 2025-10-26 06:06:33 +01:00 
			
		
		
		
	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`
This commit is contained in:
		
							parent
							
								
									f2994cf004
								
							
						
					
					
						commit
						62166c10cf
					
				
					 5 changed files with 394 additions and 0 deletions
				
			
		
							
								
								
									
										25
									
								
								androidtest/Android.bp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								androidtest/Android.bp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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", | ||||
|     ], | ||||
| } | ||||
							
								
								
									
										13
									
								
								androidtest/AndroidTest.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								androidtest/AndroidTest.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||
							
								
								
									
										16
									
								
								androidtest/memtag/Android.bp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								androidtest/memtag/Android.bp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|     }, | ||||
| } | ||||
							
								
								
									
										204
									
								
								androidtest/memtag/memtag_test.cc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								androidtest/memtag/memtag_test.cc
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| } | ||||
							
								
								
									
										136
									
								
								androidtest/src/grapheneos/hmalloc/MemtagTest.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								androidtest/src/grapheneos/hmalloc/MemtagTest.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -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…
	
	Add table
		
		Reference in a new issue