The uc_context struct returned by uc_context_alloc is a normal heap allocation handed to the caller. Its public layout (uc_priv.h:438) contains:
size_t context_size — drives a memcpy at uc.c:2585
void *fv — passed straight to flatview_copy at uc.c:2568
void *last_block — assigned to uc->ram_list.last_block at uc.c:2579
Nothing in uc_context_restore checks that any of these were produced by uc_context_save on the same engine. If the bytes get corrupted in place, the next _restore walks the corrupted fields. Mutating any of the three (which is trivial since the caller owns the allocation) produces a different host crash:
context_size -> heap-buffer-overflow inside the env memcpy
fv -> SEGV inside flatview_copy
last_block -> SEGV inside find_ram_offset_last on the next RAM op
I want to flag the framing carefully: the docs do not promise that a context blob is portable across processes, and the struct embeds live host pointers (fv, last_block) that obviously can't survive a serialize/deserialize round trip. So the realistic scenario isn't "attacker ships a malicious snapshot from the network"; it's "the public API hands the user a struct full of live host pointers and trusts those pointers on the way back in." Even an in-process bug that corrupts the heap region holding a uc_context turns into a controllable-pointer crash on the next restore.
This is defense-in-depth, not a CVE-class report. Filing here because the fix is small and uniform across all three.
Repros
All three reproduce on a stock CMake debug build with ASan
(CC=clang CFLAGS="-fsanitize=address,undefined -g -O1" cmake ...).
1. context_size overflow (poc3)
struct fake_ctx_hdr {
size_t context_size; int mode; int arch; int snapshot_level;
unsigned char ramblock_freed; void *last_block; void *fv;
};
uc_engine *uc; uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
uc_context *ctx; uc_context_alloc(uc, &ctx); uc_context_save(uc, ctx);
((struct fake_ctx_hdr *)ctx)->context_size = 0x100000;
uc_context_restore(uc, ctx);
AddressSanitizer: heap-buffer-overflow
READ of size 1048576 ... uc_context_restore uc.c:2585
Smaller mismatches produce ASan "memcpy-param-overlap"; sign-bit-set sizes produce "negative-size-param". Same root cause.
2. fv deref (repro_ctx_fv_hijack)
uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
uc_ctl(uc, UC_CTL_WRITE(UC_CTL_CONTEXT_MODE, 1),
UC_CTL_CONTEXT_CPU | UC_CTL_CONTEXT_MEMORY);
uc_mem_map(uc, 0x1000, 0x1000, UC_PROT_ALL);
uc_context_alloc(uc, &ctx); uc_context_save(uc, ctx);
((struct fake_ctx_hdr *)ctx)->fv = (void *)0xdead0000c0debabeULL;
uc_context_restore(uc, ctx);
AddressSanitizer: SEGV on unknown address 0x...
#0 flatview_copy_x86_64 memory.c:929
#1 uc_context_restore uc.c:2568
3. last_block deref (repro_ctx_lastblock_hijack)
Save first so fv stays valid, then tamper only last_block and trigger any RAM-allocating op:
uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
uc_ctl(uc, UC_CTL_WRITE(UC_CTL_CONTEXT_MODE, 1),
UC_CTL_CONTEXT_CPU | UC_CTL_CONTEXT_MEMORY);
uc_mem_map(uc, 0x1000, 0x1000, UC_PROT_ALL);
uc_context_alloc(uc, &ctx); uc_context_save(uc, ctx);
struct fake_ctx_hdr h; memcpy(&h, ctx, sizeof h);
h.last_block = (void *)0xdeadbeefdeadbeefULL;
memcpy(ctx, &h, sizeof h);
uc_context_restore(uc, ctx); /* returns OK */
uc_mem_map(uc, 0x10000, 0x1000, UC_PROT_ALL); /* SEGV here */
AddressSanitizer: SEGV on unknown address 0x...
#0 find_ram_offset_last exec.c:963
#1 find_ram_offset exec.c:987
#2 ram_block_add exec.c:1057
#3 qemu_ram_alloc_from_ptr_x86_64 exec.c:1136
#4 memory_region_init_ram_x86_64 memory.c:1570
#5 memory_map_x86_64 memory.c:49
#6 uc_mem_map uc.c:1403
Suggested fix
Two complementary changes, the first being the cheapest:
-
Cheap defense in uc_context_save/_restore: write a small header (magic + version + engine arch + engine mode + cpu_context_size) in _save, verify it in _restore, refuse non-matching blobs with UC_ERR_ARG. This catches all three repros above.
-
Stop putting live pointers in the user-visible struct: keep fv and last_block inside uc_engine's side state, keyed by the blob's header sequence number, instead of embedding them in the struct returned to the caller. This removes the class entirely and also gets you closer to a future where contexts could be serialized.
I'd rather sync on the API direction with you before sending a PR for this one. (1) is a one-screen patch I can put up against dev quickly if you'd prefer a minimal change. (2) is a bigger refactor.
The
uc_contextstruct returned byuc_context_allocis a normal heap allocation handed to the caller. Its public layout (uc_priv.h:438) contains:size_t context_size— drives a memcpy at uc.c:2585void *fv— passed straight toflatview_copyat uc.c:2568void *last_block— assigned touc->ram_list.last_blockat uc.c:2579Nothing in
uc_context_restorechecks that any of these were produced byuc_context_saveon the same engine. If the bytes get corrupted in place, the next_restorewalks the corrupted fields. Mutating any of the three (which is trivial since the caller owns the allocation) produces a different host crash:context_size-> heap-buffer-overflow inside the env memcpyfv-> SEGV insideflatview_copylast_block-> SEGV insidefind_ram_offset_laston the next RAM opI want to flag the framing carefully: the docs do not promise that a context blob is portable across processes, and the struct embeds live host pointers (
fv,last_block) that obviously can't survive a serialize/deserialize round trip. So the realistic scenario isn't "attacker ships a malicious snapshot from the network"; it's "the public API hands the user a struct full of live host pointers and trusts those pointers on the way back in." Even an in-process bug that corrupts the heap region holding auc_contextturns into a controllable-pointer crash on the next restore.This is defense-in-depth, not a CVE-class report. Filing here because the fix is small and uniform across all three.
Repros
All three reproduce on a stock CMake debug build with ASan
(
CC=clang CFLAGS="-fsanitize=address,undefined -g -O1" cmake ...).1.
context_sizeoverflow (poc3)Smaller mismatches produce ASan "memcpy-param-overlap"; sign-bit-set sizes produce "negative-size-param". Same root cause.
2.
fvderef (repro_ctx_fv_hijack)3.
last_blockderef (repro_ctx_lastblock_hijack)Save first so
fvstays valid, then tamper onlylast_blockand trigger any RAM-allocating op:Suggested fix
Two complementary changes, the first being the cheapest:
Cheap defense in
uc_context_save/_restore: write a small header (magic + version + engine arch + engine mode + cpu_context_size) in_save, verify it in_restore, refuse non-matching blobs withUC_ERR_ARG. This catches all three repros above.Stop putting live pointers in the user-visible struct: keep
fvandlast_blockinsideuc_engine's side state, keyed by the blob's header sequence number, instead of embedding them in the struct returned to the caller. This removes the class entirely and also gets you closer to a future where contexts could be serialized.I'd rather sync on the API direction with you before sending a PR for this one. (1) is a one-screen patch I can put up against
devquickly if you'd prefer a minimal change. (2) is a bigger refactor.