Skip to content

Commit cf3418f

Browse files
ldorauCopilot
andcommitted
Add repro for address-space bucketing use-after-free bug
Adds repro_addrspace_bug.c, a standalone reproducer for a critical regression introduced by the address-space support added to memory providers (umfTrackingMemoryProviderCreate in src/provider/provider_tracking.c). Root cause: when an upstream provider does not implement the new (optional) get_address_space op -- the default for every provider written before this feature existed, since umfMemoryProviderGetAddressSpace() falls back to returning UMF_RESULT_ERROR_NOT_SUPPORTED -- the tracking provider assigns it a *unique* address-space bucket keyed by the provider's own pointer, instead of the shared "host default" bucket ({NULL,0,0}) used by providers that correctly report the default host identity (e.g. os_memory, fixed_memory). As a result, two allocations that are genuinely the exact same host memory (identical pointer value) can end up tracked in two disjoint tracker address-space buckets simply because one provider forgot to implement get_address_space. This defeats both: - ambiguity detection: umfPoolByPtr()/umfGetIPCHandle() silently resolve to an arbitrary pool instead of reporting UMF_RESULT_ERROR_AMBIGUOUS, and - use-after-free protection: umfPoolFree() on one pool is allowed to really free() the shared memory while another pool is completely unaware and still believes its pointer is a live, valid allocation. The reproducer builds pool0 on a minimal "legacy" provider (no get_address_space) and pool1 on umfFixedMemoryProviderOps() pinned at the exact same address range, then demonstrates: 1. umfPoolByPtr(ptr0) silently picks an arbitrary pool with no ambiguity error. 2. umfPoolFree(pool0, ptr0) succeeds and really frees the memory while pool1 still tracks ptr1 (== ptr0) as valid. 3. Writing through ptr1 afterward is a genuine heap-use-after-free, confirmed both by AddressSanitizer (heap-use-after-free report when built with -fsanitize=address) and by UMF's own internal check_if_tracker_is_empty consistency assertion firing at process teardown in a plain debug build. Build: gcc -g -O0 -Iinclude -o repro_addrspace_bug repro_addrspace_bug.c \ -Lbuild/lib -lumf -Wl,-rpath,$(pwd)/build/lib LD_LIBRARY_PATH="" ./repro_addrspace_bug Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8b8d016 commit cf3418f

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

repro_addrspace_bug.c

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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, &params);
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

Comments
 (0)