Skip to content

uc_context_restore reads back fields from a struct the user owns; three crash classes follow #2320

Description

@MarkLee131

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:

  1. context_size -> heap-buffer-overflow inside the env memcpy
  2. fv -> SEGV inside flatview_copy
  3. 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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions