|
| 1 | +/* |
| 2 | + * Reproducer for Issue #1 (CRITICAL): |
| 3 | + * |
| 4 | + * "Address-space partitioning silently defeats overlap/ambiguity |
| 5 | + * detection when a real host-memory provider does not implement |
| 6 | + * the new (optional) get_address_space op." |
| 7 | + * |
| 8 | + * Root cause (src/provider/provider_tracking.c, umfTrackingMemoryProviderCreate): |
| 9 | + * |
| 10 | + * umf_result_t ret = |
| 11 | + * umfMemoryProviderGetAddressSpace(hUpstream, &address_space_id); |
| 12 | + * if (ret == UMF_RESULT_ERROR_NOT_SUPPORTED) { |
| 13 | + * address_space_id.namespace_token = hUpstream; // <-- BUG |
| 14 | + * address_space_id.context = 0; |
| 15 | + * address_space_id.device = 0; |
| 16 | + * } |
| 17 | + * |
| 18 | + * Any provider that does not implement get_address_space() (the default for |
| 19 | + * every provider written before this feature was added, since the op is |
| 20 | + * optional and defaults to returning UMF_RESULT_ERROR_NOT_SUPPORTED) is |
| 21 | + * placed into a *unique* tracker bucket keyed by its own pointer, instead of |
| 22 | + * the shared "host default" bucket ({NULL,0,0}) used by every other |
| 23 | + * plain-host-memory provider (os_memory, fixed_memory, etc. all correctly |
| 24 | + * implement get_address_space() and return the default identity for host |
| 25 | + * memory). |
| 26 | + * |
| 27 | + * This program builds two pools that both back onto ordinary process heap |
| 28 | + * memory (glibc malloc), i.e. genuinely the *same* address space: |
| 29 | + * |
| 30 | + * - pool0 is backed by a hand-written "legacy" provider that forgets to |
| 31 | + * implement get_address_space() (very common: every third-party/legacy |
| 32 | + * provider not yet updated for this feature behaves this way). |
| 33 | + * - pool1 is backed by umfFixedMemoryProviderOps(), pinned at the exact |
| 34 | + * [ptr0, ptr0+size) range pool0 just allocated. FixedMemoryProvider |
| 35 | + * *does* implement get_address_space() and reports the default host |
| 36 | + * identity. |
| 37 | + * |
| 38 | + * Because pool0's upstream provider does not implement get_address_space(), |
| 39 | + * UMF puts pool0's allocation and pool1's (numerically identical) allocation |
| 40 | + * into two different tracker buckets, even though they are the exact same |
| 41 | + * bytes of real host memory. As a result: |
| 42 | + * |
| 43 | + * 1. umfPoolByPtr() on the shared address silently resolves to an |
| 44 | + * arbitrary pool instead of reporting UMF_RESULT_ERROR_AMBIGUOUS. |
| 45 | + * 2. umfPoolFree(pool0, ptr0) is allowed to actually free() the real |
| 46 | + * backing memory while pool1 is completely unaware and still believes |
| 47 | + * ptr1 (== ptr0) is a live, valid allocation it owns. |
| 48 | + * 3. Any subsequent use of ptr1 through pool1 (or a raw dereference) is a |
| 49 | + * genuine, tool-detectable use-after-free of the same heap chunk pool0 |
| 50 | + * already returned to glibc. |
| 51 | + * |
| 52 | + * Build & run (from repo root, against the build/ dir): |
| 53 | + * |
| 54 | + * gcc -g -O0 -fsanitize=address -Iinclude -o repro_addrspace_bug \ |
| 55 | + * repro_addrspace_bug.c -Lbuild/lib -lumf -Wl,-rpath,build/lib |
| 56 | + * ./repro_addrspace_bug |
| 57 | + * |
| 58 | + * Expected (buggy) output: umfPoolFree(pool0,...) returns SUCCESS, and |
| 59 | + * AddressSanitizer reports a heap-use-after-free when ptr1 is subsequently |
| 60 | + * touched through pool1. |
| 61 | + */ |
| 62 | + |
| 63 | +#include <stdio.h> |
| 64 | +#include <stdlib.h> |
| 65 | +#include <string.h> |
| 66 | + |
| 67 | +#include <umf/memory_pool.h> |
| 68 | +#include <umf/memory_provider.h> |
| 69 | +#include <umf/memory_provider_ops.h> |
| 70 | +#include <umf/pools/pool_proxy.h> |
| 71 | +#include <umf/providers/provider_fixed_memory.h> |
| 72 | + |
| 73 | +/* ---- minimal "legacy" host provider: no get_address_space ---- */ |
| 74 | + |
| 75 | +static umf_result_t legacy_initialize(const void *params, void **provider) { |
| 76 | + (void)params; |
| 77 | + *provider = (void *)1; /* no real state needed */ |
| 78 | + return UMF_RESULT_SUCCESS; |
| 79 | +} |
| 80 | + |
| 81 | +static umf_result_t legacy_finalize(void *provider) { |
| 82 | + (void)provider; |
| 83 | + return UMF_RESULT_SUCCESS; |
| 84 | +} |
| 85 | + |
| 86 | +static umf_result_t legacy_alloc(void *provider, size_t size, size_t alignment, |
| 87 | + void **ptr) { |
| 88 | + (void)provider; |
| 89 | + if (alignment == 0) { |
| 90 | + alignment = sizeof(void *); |
| 91 | + } |
| 92 | + void *p = NULL; |
| 93 | + if (alignment < 4096) { alignment = 4096; } if (posix_memalign(&p, alignment, size) != 0) { |
| 94 | + return UMF_RESULT_ERROR_OUT_OF_HOST_MEMORY; |
| 95 | + } |
| 96 | + *ptr = p; |
| 97 | + return UMF_RESULT_SUCCESS; |
| 98 | +} |
| 99 | + |
| 100 | +static umf_result_t legacy_free(void *provider, void *ptr, size_t size) { |
| 101 | + (void)provider; |
| 102 | + (void)size; |
| 103 | + printf("[legacy provider] real free(%p)\n", ptr); |
| 104 | + free(ptr); |
| 105 | + return UMF_RESULT_SUCCESS; |
| 106 | +} |
| 107 | + |
| 108 | +static umf_result_t legacy_get_last_native_error(void *provider, |
| 109 | + const char **msg, |
| 110 | + int32_t *err) { |
| 111 | + (void)provider; |
| 112 | + *msg = "no error"; |
| 113 | + *err = 0; |
| 114 | + return UMF_RESULT_SUCCESS; |
| 115 | +} |
| 116 | + |
| 117 | +static umf_result_t legacy_get_recommended_page_size(void *provider, |
| 118 | + size_t size, |
| 119 | + size_t *pageSize) { |
| 120 | + (void)provider; |
| 121 | + (void)size; |
| 122 | + *pageSize = 4096; |
| 123 | + return UMF_RESULT_SUCCESS; |
| 124 | +} |
| 125 | + |
| 126 | +static umf_result_t legacy_get_min_page_size(void *provider, const void *ptr, |
| 127 | + size_t *pageSize) { |
| 128 | + (void)provider; |
| 129 | + (void)ptr; |
| 130 | + *pageSize = 4096; |
| 131 | + return UMF_RESULT_SUCCESS; |
| 132 | +} |
| 133 | + |
| 134 | +static umf_result_t legacy_get_cache_line_size(void *provider, size_t *size) { |
| 135 | + (void)provider; |
| 136 | + *size = 64; |
| 137 | + return UMF_RESULT_SUCCESS; |
| 138 | +} |
| 139 | + |
| 140 | +static umf_result_t legacy_get_name(void *provider, const char **name) { |
| 141 | + (void)provider; |
| 142 | + *name = "legacy_no_address_space_provider"; |
| 143 | + return UMF_RESULT_SUCCESS; |
| 144 | +} |
| 145 | +/* NOTE: get_address_space intentionally left unset (NULL) below, exactly |
| 146 | + * like any provider written before this feature existed. */ |
| 147 | + |
| 148 | +static const umf_memory_provider_ops_t LEGACY_PROVIDER_OPS = { |
| 149 | + .version = UMF_PROVIDER_OPS_VERSION_CURRENT, |
| 150 | + .initialize = legacy_initialize, |
| 151 | + .finalize = legacy_finalize, |
| 152 | + .alloc = legacy_alloc, |
| 153 | + .free = legacy_free, |
| 154 | + .get_last_native_error = legacy_get_last_native_error, |
| 155 | + .get_recommended_page_size = legacy_get_recommended_page_size, |
| 156 | + .get_min_page_size = legacy_get_min_page_size, |
| 157 | + .get_cache_line_size = legacy_get_cache_line_size, |
| 158 | + .get_name = legacy_get_name, |
| 159 | + /* .get_address_space = NULL -- not implemented, matches the vast |
| 160 | + * majority of pre-existing providers. */ |
| 161 | +}; |
| 162 | + |
| 163 | +#define CHECK(cond, msg) \ |
| 164 | + do { \ |
| 165 | + if (!(cond)) { \ |
| 166 | + fprintf(stderr, "FAILED: %s (%s:%d)\n", msg, __FILE__, __LINE__); \ |
| 167 | + exit(1); \ |
| 168 | + } \ |
| 169 | + } while (0) |
| 170 | + |
| 171 | +int main(void) { |
| 172 | + setvbuf(stdout, NULL, _IONBF, 0); |
| 173 | + umf_result_t res; |
| 174 | + const size_t size = 4096; /* one page; must be page-aligned for the fixed-memory provider's internal coarse allocator */ |
| 175 | + |
| 176 | + /* --- pool0: backed by the "legacy" provider (no get_address_space) --- */ |
| 177 | + umf_memory_provider_handle_t provider0 = NULL; |
| 178 | + res = umfMemoryProviderCreate(&LEGACY_PROVIDER_OPS, NULL, &provider0); |
| 179 | + CHECK(res == UMF_RESULT_SUCCESS, "create legacy provider0"); |
| 180 | + |
| 181 | + umf_memory_pool_handle_t pool0 = NULL; |
| 182 | + res = umfPoolCreate(umfProxyPoolOps(), provider0, NULL, |
| 183 | + UMF_POOL_CREATE_FLAG_OWN_PROVIDER, &pool0); |
| 184 | + CHECK(res == UMF_RESULT_SUCCESS, "create pool0"); |
| 185 | + |
| 186 | + void *ptr0 = umfPoolMalloc(pool0, size); |
| 187 | + CHECK(ptr0 != NULL, "umfPoolMalloc(pool0)"); |
| 188 | + printf("pool0 (legacy provider, no get_address_space) allocated ptr0=%p\n", |
| 189 | + ptr0); |
| 190 | + |
| 191 | + /* --- pool1: FixedMemoryProvider pinned at the exact same [ptr0,ptr0+size) |
| 192 | + * range. FixedMemoryProvider *does* implement get_address_space() and |
| 193 | + * reports the default host identity {NULL,0,0} -- i.e. real host memory, |
| 194 | + * exactly what pool0's memory also is. --- */ |
| 195 | + umf_fixed_memory_provider_params_handle_t params = NULL; |
| 196 | + res = umfFixedMemoryProviderParamsCreate(ptr0, size, ¶ms); |
| 197 | + CHECK(res == UMF_RESULT_SUCCESS, "create fixed provider params"); |
| 198 | + |
| 199 | + umf_memory_provider_handle_t provider1 = NULL; |
| 200 | + res = umfMemoryProviderCreate(umfFixedMemoryProviderOps(), params, |
| 201 | + &provider1); |
| 202 | + CHECK(res == UMF_RESULT_SUCCESS, "create fixed provider1"); |
| 203 | + umfFixedMemoryProviderParamsDestroy(params); |
| 204 | + |
| 205 | + umf_memory_pool_handle_t pool1 = NULL; |
| 206 | + res = umfPoolCreate(umfProxyPoolOps(), provider1, NULL, |
| 207 | + UMF_POOL_CREATE_FLAG_OWN_PROVIDER, &pool1); |
| 208 | + CHECK(res == UMF_RESULT_SUCCESS, "create pool1"); |
| 209 | + |
| 210 | + void *ptr1 = umfPoolMalloc(pool1, size); |
| 211 | + CHECK(ptr1 != NULL, "umfPoolMalloc(pool1)"); |
| 212 | + printf("pool1 (FixedMemoryProvider, default host address space) " |
| 213 | + "allocated ptr1=%p\n", |
| 214 | + ptr1); |
| 215 | + CHECK(ptr1 == ptr0, |
| 216 | + "ptr1 must alias ptr0 -- both are the same real host memory"); |
| 217 | + |
| 218 | + /* --- Step 1: pointer resolution silently picks an arbitrary pool --- */ |
| 219 | + umf_memory_pool_handle_t found_pool = NULL; |
| 220 | + res = umfPoolByPtr(ptr0, &found_pool); |
| 221 | + printf("umfPoolByPtr(ptr0) -> result=%d, found_pool=%p " |
| 222 | + "(pool0=%p, pool1=%p) -- no ambiguity reported!\n", |
| 223 | + res, (void *)found_pool, (void *)pool0, (void *)pool1); |
| 224 | + |
| 225 | + /* --- Step 2: pool0 is allowed to really free the memory while pool1 |
| 226 | + * still believes ptr1 is a live, valid allocation it owns. --- */ |
| 227 | + printf("\nCalling umfPoolFree(pool0, ptr0) -- pool1 has NOT freed ptr1 " |
| 228 | + "yet and still tracks it as valid...\n"); |
| 229 | + res = umfPoolFree(pool0, ptr0); |
| 230 | + printf("umfPoolFree(pool0, ptr0) -> result=%d (SUCCESS means the real " |
| 231 | + "memory was released even though pool1 still owns ptr1)\n", |
| 232 | + res); |
| 233 | + CHECK(res == UMF_RESULT_SUCCESS, "umfPoolFree(pool0, ptr0) should " |
| 234 | + "'succeed' -- that's the bug"); |
| 235 | + |
| 236 | + /* --- Step 3: pool1 still thinks ptr1 is valid; touching it is a |
| 237 | + * genuine use-after-free of memory pool0 already returned to glibc. --- */ |
| 238 | + found_pool = NULL; |
| 239 | + res = umfPoolByPtr(ptr1, &found_pool); |
| 240 | + printf("umfPoolByPtr(ptr1) -> result=%d, found_pool=%p (pool1=%p) -- " |
| 241 | + "pool1 still believes ptr1 is a live allocation it owns!\n", |
| 242 | + res, (void *)found_pool, (void *)pool1); |
| 243 | + |
| 244 | + printf("\nWriting through ptr1 (already free()'d by pool0) -- " |
| 245 | + "this is a heap-use-after-free:\n"); |
| 246 | + memset(ptr1, 0x41, size); /* <-- AddressSanitizer should flag this */ |
| 247 | + printf("(if you see this without an ASan report, rerun built with " |
| 248 | + "-fsanitize=address to observe the UAF)\n"); |
| 249 | + |
| 250 | + /* cleanup (best effort; pool1 will now double-account for memory |
| 251 | + * pool0 already freed) */ |
| 252 | + umfPoolDestroy(pool1); |
| 253 | + umfPoolDestroy(pool0); |
| 254 | + |
| 255 | + printf("\nDone. Issue #1 reproduced: address-space bucketing based on " |
| 256 | + "an unimplemented get_address_space() silently split what is " |
| 257 | + "really the *same* host memory into two disjoint tracker " |
| 258 | + "buckets, defeating both the ambiguity check and use-after-free " |
| 259 | + "protection.\n"); |
| 260 | + return 0; |
| 261 | +} |
0 commit comments