Skip to content

Commit 4f88102

Browse files
authored
Merge pull request #155 from struct/tagging_fixes_docs
Update comments, add memory tagging check
2 parents 7248039 + 74a3fe4 commit 4f88102

9 files changed

Lines changed: 71 additions & 13 deletions

File tree

MEMORY_TAGGING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Isolation Alloc Memory Tagging
22

3-
IsoAlloc supports a software only memory tagging model that is conceptually very similar to Chromes [MTECheckedPtr](https://docs.google.com/document/d/1ph7iOorkGqTuETFZp-xvHV4L2rYootuz1ThzAAoGe30/edit?usp=sharing). This technique for pointer protection is inspired by ARM's upcoming Memory Tagging Extension (MTE) due in ARM v8.5-A. ARM MTE is a comprehensive hardware based solution for detecting memory safety issues in release builds of software with very little overhead. ARM MTE uses the Top Byte Ignore (TBI) feature to transparently tag pointers with metadata or a 'tag'. With ARM MTE this tag is mostly transparently checked and removed in hardware. The feature implemented here is conceptually very similar.
3+
IsoAlloc supports a software only memory tagging model that is very similar to Chromes [MTECheckedPtr](https://docs.google.com/document/d/1ph7iOorkGqTuETFZp-xvHV4L2rYootuz1ThzAAoGe30/edit?usp=sharing). This technique for pointer protection is inspired by ARM's upcoming Memory Tagging Extension (MTE) due in ARM v8.5-A. ARM MTE is a comprehensive hardware based solution for detecting memory safety issues in release builds of software with very little overhead. ARM MTE uses the Top Byte Ignore (TBI) feature to transparently tag pointers with metadata or a 'tag'. With ARM MTE this tag is mostly transparently checked and removed in hardware. The feature implemented here in IsoAlloc is conceptually very similar except that tagging and untagging of pointers happens in software.
44

55
Note that this feature is experimental, off by default, and the APIs are subject to change!
66

77
## Overview
88

9-
We can't achieve the granularity provided by ARM MTE in software alone but we can implement a pointer protection mechanism by generating 1 byte of meta data per chunk managed by a private IsoAlloc zone, adding that tag to the pointer, and verifying it before derefencing it. This feature is enabled or disabled with `MEMORY_TAGGING` in the `Makefile`.
9+
We can't achieve the granularity provided by ARM MTE in software alone but we can implement a pointer protection mechanism by generating 1 byte of meta data per chunk managed by a private IsoAlloc zone, adding that tag to the pointer, and verifying it before dereferencing it. This feature is enabled or disabled with `MEMORY_TAGGING` in the `Makefile`.
1010

1111
This 1 byte tag will be added to the LSB of the pointer returned by calling `iso_alloc_from_zone_tagged`. IsoAlloc also provides a C API for tagging and untagging pointers retrieved by `iso_alloc_from_zone`. These functions are `iso_alloc_tag_ptr` and `iso_alloc_untag_ptr`.
1212

@@ -36,8 +36,8 @@ These APIs can also be used in C, but this requires tagging and untagging pointe
3636

3737
* Currently only private zones can make use of memory tagging in IsoAlloc
3838
* All tag data is stored below user pages with a guard page allocated in between
39-
* A single 1 byte tag is generated per chunk in a private zone, this means the memory required to hold tags is larger for private zones holding smaller chunk sizes. For zones holding chunks 1024 byte or larger only a single of page of memory is required for tags as there are only 4096 possible 1024 byte chunks in zone user size of 4mb. The maximum amount of memory needed is for 16 byte chunks which requires 64 pages because there are 262144 possible chunks with a zone user size of 4mb.
40-
* Tags are 1 byte in size and randomly chosen, they are added to the LSB of a pointer (e.g. tag value `0xed`, tagged pointer `0xed0b8066c1a000`, untagged pointer `0xb8066c1a000`)
39+
* A single 1 byte tag is generated per chunk in a private zone, this means the memory required to hold tags is larger for private zones holding smaller chunk sizes. For zones holding chunks 1024 byte or larger only a single of page of memory is required for tags as there are only 4096 possible 1024 byte chunks in 4mb IsoAlloc zone. The maximum amount of memory needed is for 16 byte chunks which requires 64 pages because there are 262144 possible chunks with a 4mb zone.
40+
* Tags are 1 byte in size and randomly chosen, they are added to the LSB of a pointer (e.g. tag value `0xed`, tagged pointer `0xed000b8066c1a000`, untagged pointer `0xb8066c1a000`)
4141
* Tags are refreshed whenever the private zone has reached %25 of 'retirement age' (defined in conf.h as `ZONE_ALLOC_RETIRE`) with 0 current allocations
4242

4343
## Examples

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ tests: clean library_debug_unit_tests
305305
@echo "make tests"
306306
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/rand_freelist.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/rand_freelist $(LDFLAGS)
307307
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/tagged_ptr_test.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/tagged_ptr_test $(LDFLAGS)
308+
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/bad_tag_ptr_test.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/bad_tag_ptr_test $(LDFLAGS)
308309
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/tests.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/tests $(LDFLAGS)
309310
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/uaf.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/uaf $(LDFLAGS)
310311
$(CC) $(CFLAGS) $(EXE_CFLAGS) $(DEBUG_LOG_FLAGS) $(GDB_FLAGS) $(OS_FLAGS) tests/interfaces_test.c $(ISO_ALLOC_PRINTF_SRC) -o $(BUILD_DIR)/interfaces_test $(LDFLAGS)

include/iso_alloc_internal.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ assert(sizeof(size_t) >= 64)
7676
#define FREE_OR_DONTNEED MADV_FREE
7777
#endif
7878

79+
static_assert(SMALLEST_CHUNK_SZ >= 16, "SMALLEST_CHUNK_SZ is too small, must be at least 16");
80+
7981
#if ENABLE_ASAN
8082
#include <sanitizer/asan_interface.h>
8183

@@ -239,6 +241,8 @@ assert(sizeof(size_t) >= 64)
239241
/* Cap our big zones at 4GB of memory */
240242
#define BIG_SZ_MAX 4294967296
241243

244+
#define MIN_BITMAP_IDX 8
245+
242246
#define WASTED_SZ_MULTIPLIER 8
243247
#define WASTED_SZ_MULTIPLIER_SHIFT 3
244248

src/iso_alloc.c

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,8 @@ INTERNAL_HIDDEN iso_alloc_zone_t *_iso_new_zone(size_t size, bool internal, int3
490490
create_guard_page(p + g_page_size + tag_mapping_size);
491491
new_zone->user_pages_start = (p + g_page_size + tag_mapping_size + g_page_size);
492492
uint64_t *_mtp = p + g_page_size;
493-
int64_t tms = tag_mapping_size / sizeof(uint64_t);
493+
/* (>> 3) == sizeof(uint64_t) == 8 */
494+
uint64_t tms = tag_mapping_size >> 3;
494495

495496
/* Generate random tags */
496497
for(uint64_t o = 0; o < tms; o++) {
@@ -587,9 +588,18 @@ INTERNAL_HIDDEN void fill_free_bit_slots(iso_alloc_zone_t *zone) {
587588
* leads to a less predictable free list */
588589
bitmap_index_t bm_idx = 0;
589590

591+
/* Refresh the random seed for wyrand. This only
592+
* requires a single syscall to getrandom() */
593+
_root->seed = rand_uint64();
594+
590595
/* The largest zone->max_bitmap_idx we will ever
591-
* have is 8192 for SMALLEST_CHUNK_SZ (16) */
592-
if(zone->max_bitmap_idx > ALIGNMENT) {
596+
* have is 8192 for SMALLEST_CHUNK_SZ (16). The
597+
* smallest zone->max_bitmap_idx is 1 when chunk
598+
* size is SMALL_SZ_MAX because the bitmap_size
599+
* is only 8 bytes. If our max bitmap index is
600+
* small then it won't provide enough search
601+
* space for a random list to be of value */
602+
if(zone->max_bitmap_idx > MIN_BITMAP_IDX) {
593603
bm_idx = ((uint32_t) us_rand_uint64(&_root->seed) & (zone->max_bitmap_idx - 1));
594604
}
595605

src/iso_alloc_mem_tags.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33

44
#include "iso_alloc_internal.h"
55

6+
/* Returns a tag for a pointer p, which must be untagged
7+
* when passed to this function */
68
INTERNAL_HIDDEN uint8_t _iso_alloc_get_mem_tag(void *p, iso_alloc_zone_t *zone) {
79
#if MEMORY_TAGGING
810
void *user_pages_start = UNMASK_USER_PTR(zone);
911

12+
if(user_pages_start > p || (user_pages_start + ZONE_USER_SIZE) < p) {
13+
LOG_AND_ABORT("Cannot get tag for pointer %p with wrong zone %d %p - %p", p, zone->index, user_pages_start, user_pages_start + ZONE_USER_SIZE);
14+
}
15+
1016
uint8_t *_mtp = (user_pages_start - g_page_size - ROUND_UP_PAGE(zone->chunk_count * MEM_TAG_SIZE));
1117
const uint64_t chunk_offset = (uint64_t)(p - user_pages_start);
1218

tests/bad_tag_ptr_test.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* iso_alloc bad_tag_ptr_test.c
2+
* Copyright 2022 - chris.rohlf@gmail.com */
3+
4+
/* This test should successfully fail with or
5+
* without MEMORY_TAGGING support */
6+
7+
#include <stdio.h>
8+
#include <string.h>
9+
#include "iso_alloc.h"
10+
11+
#define SIZE 256
12+
13+
int main(int argc, char *argv[]) {
14+
iso_alloc_zone_handle *_zone_handle = iso_alloc_new_zone(SIZE);
15+
16+
if(_zone_handle == NULL) {
17+
abort();
18+
}
19+
20+
char buffer[1024];
21+
void *p = &buffer;
22+
23+
/* This should crash because p is not allocated from this zone */
24+
uint8_t tag = iso_alloc_get_mem_tag(p, _zone_handle);
25+
printf("Tag = %x\n", tag);
26+
iso_alloc_destroy_zone(_zone_handle);
27+
28+
#if !MEMORY_TAGGING
29+
return -1;
30+
#endif
31+
32+
return 0;
33+
}

tests/tagged_ptr_test.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* iso_alloc iso_alloc_tagged_ptr_test.c
1+
/* iso_alloc tagged_ptr_test.c
22
* Copyright 2022 - chris.rohlf@gmail.com */
33

44
/* This test should successfully run with or
@@ -7,6 +7,7 @@
77
#include <stdio.h>
88
#include <string.h>
99
#include "iso_alloc.h"
10+
#include "iso_alloc_internal.h"
1011

1112
#define SIZE 256
1213

@@ -18,11 +19,14 @@ int main(int argc, char *argv[]) {
1819
}
1920

2021
void *p = iso_alloc_from_zone_tagged(_zone_handle);
22+
void *up = iso_alloc_untag_ptr(p, _zone_handle);
2123

22-
void *untagged_p = iso_alloc_untag_ptr(p, _zone_handle);
24+
uint8_t tag = ((uintptr_t) p >> 56);
25+
uint8_t itag = iso_alloc_get_mem_tag(up, _zone_handle);
2326

24-
/* This should crash if the pointer wasn't properly untagged */
25-
memset(untagged_p, 0x41, SIZE);
27+
if(tag != itag) {
28+
LOG_AND_ABORT("Tags %d and %d do not match", tag, itag);
29+
}
2630

2731
/* We can pass a tagged or untagged pointer to iso_free_from_zone */
2832
iso_free_from_zone(p, _zone_handle);

tests/tagged_ptr_test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// iso_alloc iso_alloc_tagged_ptr_test.cpp
1+
// iso_alloc tagged_ptr_test.cpp
22
// Copyright 2022 - chris.rohlf@gmail.com
33

44
// This test should successfully run with or

utils/run_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ done
3131

3232
fail_tests=("double_free" "big_double_free" "heap_overflow" "heap_underflow"
3333
"leaks_test" "wild_free" "unaligned_free" "incorrect_chunk_size_multiple"
34-
"big_canary_test" "zero_alloc" "sized_free")
34+
"big_canary_test" "zero_alloc" "sized_free" "bad_tag_ptr_test")
3535

3636
for t in "${fail_tests[@]}"; do
3737
echo -n "Running $t test"

0 commit comments

Comments
 (0)