From b1badf961732889e56be9cd49415103644473cbd Mon Sep 17 00:00:00 2001 From: MarkLee131 Date: Wed, 29 Apr 2026 20:04:15 +0800 Subject: [PATCH 1/2] uc_context: validate save/restore against content flags and engine Two failure modes used to abort or corrupt the host process: 1. uc_context_alloc() returned a heap allocation with only fv explicitly zeroed; snapshot_level / ramblock_freed / last_block stayed garbage. uc_context_restore() then trusted those fields and tripped the cpu_asidx_from_attrs() assertion in cpu.h on the next emu_start(). 2. uc_context_restore() trusted fv / last_block from the user-visible struct without checking they came from a uc_context_save() on the same engine; cross-engine restores dereferenced live host pointers belonging to a different uc_engine. Three changes here: - uc_context_alloc() switches to g_malloc0() so untouched header fields read 0. - struct uc_context grows a context_content field set by uc_context_save() to mirror uc->context_content, and an engine pointer set only when UC_CTL_CONTEXT_MEMORY is included. uc_context_restore() refuses contexts with context_content == 0 (covers #2319), with arch/mode that doesn't match the destination engine, or with an engine pointer mismatch on a memory restore (covers #2320). Each restore branch is gated on both sides advertising the matching content bit, so a CPU-only context can't be replayed as a memory snapshot. - uc_context_reg_*() refuses memory-only contexts via context_content, since their data buffer holds no CPU state. Adds three regression tests in tests/unit/test_ctl.c: restore-without-save, cross-engine restore (CPU-only portable, memory-mode rejected), and reg-read/write on a memory-only snapshot. Closes part of #1766. Fixes #2319. Fixes #2320. --- include/uc_priv.h | 11 +++++ tests/unit/test_ctl.c | 94 ++++++++++++++++++++++++++++++++++++++++++ uc.c | 96 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/include/uc_priv.h b/include/uc_priv.h index 6a7cda8c58..ea91264f1b 100644 --- a/include/uc_priv.h +++ b/include/uc_priv.h @@ -451,6 +451,17 @@ struct uc_context { bool ramblock_freed; // wheter there was a some ramblock freed RAMBlock *last_block; // The last element of the ramblock list FlatView *fv; // The current flatview of the memory + // Mirrors uc->context_content at uc_context_save() time. 0 means + // the context was allocated but never saved; uc_context_restore() + // refuses such contexts. Also gates the per-bit restore branches + // and the uc_context_reg_*() entry points. + uc_context_content context_content; + // Engine the memory state was captured against. Set only when + // UC_CTL_CONTEXT_MEMORY is included; checked on restore to refuse + // a memory-restore against a different uc_engine. CPU-only + // contexts leave this NULL and stay portable across engines with + // matching arch/mode. + struct uc_struct *engine; char data[0]; // context }; diff --git a/tests/unit/test_ctl.c b/tests/unit/test_ctl.c index 5fe65e57b0..1680c72f36 100644 --- a/tests/unit/test_ctl.c +++ b/tests/unit/test_ctl.c @@ -398,6 +398,94 @@ static void test_noexec(void) OK(uc_close(uc)); } +// Regression test for #2319. uc_context_alloc() followed by +// uc_context_restore() (no save in between) used to abort the host +// process via the cpu_asidx_from_attrs() assertion in cpu.h. +static void test_uc_context_restore_without_save(void) +{ + uc_engine *uc; + uc_context *ctx; + uint8_t code[] = {0x90}; // nop + + OK(uc_open(UC_ARCH_X86, UC_MODE_64, &uc)); + OK(uc_context_alloc(uc, &ctx)); + + uc_assert_err(UC_ERR_ARG, uc_context_restore(uc, ctx)); + + // The engine must still be usable. + OK(uc_mem_map(uc, 0x1000, 0x1000, UC_PROT_ALL)); + OK(uc_mem_write(uc, 0x1000, code, sizeof(code))); + OK(uc_emu_start(uc, 0x1000, 0x1000 + sizeof(code), 0, 1)); + + // Saving then restoring works as before. + OK(uc_context_save(uc, ctx)); + OK(uc_context_restore(uc, ctx)); + + OK(uc_context_free(ctx)); + OK(uc_close(uc)); +} + +// Regression test for #2320. A context whose memory state was captured +// against engine A must not restore into engine B; the fv / last_block +// pointers it carries are live host pointers tied to A's address space. +// CPU-only contexts stay portable across engines with matching arch +// and mode. +static void test_uc_context_restore_cross_engine(void) +{ + uc_engine *uc1, *uc2; + uc_context *ctx_cpu, *ctx_mem; + + // CPU-only: cross-engine restore must succeed (no engine-specific + // state captured). + OK(uc_open(UC_ARCH_X86, UC_MODE_64, &uc1)); + OK(uc_open(UC_ARCH_X86, UC_MODE_64, &uc2)); + OK(uc_context_alloc(uc1, &ctx_cpu)); + OK(uc_context_save(uc1, ctx_cpu)); + OK(uc_context_restore(uc2, ctx_cpu)); + OK(uc_context_free(ctx_cpu)); + + // Memory-mode: cross-engine restore must be refused. + OK(uc_ctl(uc1, UC_CTL_WRITE(UC_CTL_CONTEXT_MODE, 1), + UC_CTL_CONTEXT_CPU | UC_CTL_CONTEXT_MEMORY)); + OK(uc_ctl(uc2, UC_CTL_WRITE(UC_CTL_CONTEXT_MODE, 1), + UC_CTL_CONTEXT_CPU | UC_CTL_CONTEXT_MEMORY)); + OK(uc_mem_map(uc1, 0x1000, 0x1000, UC_PROT_ALL)); + OK(uc_mem_map(uc2, 0x1000, 0x1000, UC_PROT_ALL)); + + OK(uc_context_alloc(uc1, &ctx_mem)); + OK(uc_context_save(uc1, ctx_mem)); + uc_assert_err(UC_ERR_ARG, uc_context_restore(uc2, ctx_mem)); + OK(uc_context_restore(uc1, ctx_mem)); + + OK(uc_context_free(ctx_mem)); + OK(uc_close(uc1)); + OK(uc_close(uc2)); +} + +// uc_context_reg_*() must reject contexts that don't carry CPU state +// (memory-only snapshots), since their data buffer is empty. +static void test_uc_context_reg_rejects_memory_only(void) +{ + uc_engine *uc; + uc_context *ctx; + int regid = UC_X86_REG_RAX; + uint64_t value = 0; + + OK(uc_open(UC_ARCH_X86, UC_MODE_64, &uc)); + OK(uc_ctl(uc, UC_CTL_WRITE(UC_CTL_CONTEXT_MODE, 1), + UC_CTL_CONTEXT_MEMORY)); + OK(uc_mem_map(uc, 0x1000, 0x1000, UC_PROT_ALL)); + + OK(uc_context_alloc(uc, &ctx)); + OK(uc_context_save(uc, ctx)); + + uc_assert_err(UC_ERR_ARG, uc_context_reg_read(ctx, regid, &value)); + uc_assert_err(UC_ERR_ARG, uc_context_reg_write(ctx, regid, &value)); + + OK(uc_context_free(ctx)); + OK(uc_close(uc)); +} + TEST_LIST = { {"test_uc_ctl_mode", test_uc_ctl_mode}, {"test_uc_ctl_page_size", test_uc_ctl_page_size}, @@ -416,4 +504,10 @@ TEST_LIST = { {"test_uc_emu_stop_set_ip", test_uc_emu_stop_set_ip}, {"test_tlb_clear", test_tlb_clear}, {"test_noexec", test_noexec}, + {"test_uc_context_restore_without_save", + test_uc_context_restore_without_save}, + {"test_uc_context_restore_cross_engine", + test_uc_context_restore_cross_engine}, + {"test_uc_context_reg_rejects_memory_only", + test_uc_context_reg_rejects_memory_only}, {NULL, NULL}}; diff --git a/uc.c b/uc.c index 5cd49fd8a2..6661ba8f62 100644 --- a/uc.c +++ b/uc.c @@ -2262,12 +2262,16 @@ uc_err uc_context_alloc(uc_engine *uc, uc_context **context) UC_INIT(uc); - *_context = g_malloc(size); + // Zero-initialise the whole allocation so a stray uc_context_restore() + // before any uc_context_save() sees context_content == 0 and bails out + // with UC_ERR_ARG instead of feeding garbage snapshot_level / fv / + // last_block into the engine and tripping the cpu_asidx_from_attrs() + // assertion in cpu.h. Covers #2319. + *_context = g_malloc0(size); if (*_context) { (*_context)->context_size = size - sizeof(uc_context); (*_context)->arch = uc->arch; (*_context)->mode = uc->mode; - (*_context)->fv = NULL; restore_jit_state(uc); return UC_ERR_OK; } else { @@ -2303,6 +2307,16 @@ uc_err uc_context_save(uc_engine *uc, uc_context *context) UC_INIT(uc); uc_err ret = UC_ERR_OK; + // Tag the context with what we are about to save and which engine + // owns the memory state. uc_context_restore() refuses contexts whose + // context_content is 0 (never saved, #2319), arch/mode mismatches + // the destination engine, or whose engine pointer doesn't match on + // a memory restore (#2320). uc_context_reg_*() refuses memory-only + // snapshots via context_content as well. + context->context_content = uc->context_content; + context->engine = + (uc->context_content & UC_CTL_CONTEXT_MEMORY) ? uc : NULL; + if (uc->context_content & UC_CTL_CONTEXT_MEMORY) { if (!context->fv) { context->fv = g_malloc0(sizeof(*context->fv)); @@ -2458,9 +2472,28 @@ static context_reg_rw_t find_context_reg_rw(uc_arch arch, uc_mode mode) return rw; } +// Refuse register r/w on memory-only contexts: their data buffer +// holds no CPU state, so the call would just shuffle zeros into the +// caller's value (read) or be lost on the next restore (write). +// Untouched contexts (context_content == 0) are left alone, so the +// uc_context_alloc + uc_context_reg_write + uc_context_save pattern +// keeps working. +static inline uc_err uc_context_check_cpu_state(const uc_context *ctx) +{ + if ((ctx->context_content & UC_CTL_CONTEXT_MEMORY) && + !(ctx->context_content & UC_CTL_CONTEXT_CPU)) { + return UC_ERR_ARG; + } + return UC_ERR_OK; +} + UNICORN_EXPORT uc_err uc_context_reg_write(uc_context *ctx, int regid, const void *value) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } int setpc = 0; size_t size = (size_t)-1; return find_context_reg_rw(ctx->arch, ctx->mode) @@ -2470,6 +2503,10 @@ uc_err uc_context_reg_write(uc_context *ctx, int regid, const void *value) UNICORN_EXPORT uc_err uc_context_reg_read(uc_context *ctx, int regid, void *value) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } size_t size = (size_t)-1; return find_context_reg_rw(ctx->arch, ctx->mode) .read(ctx->data, ctx->mode, regid, value, &size); @@ -2479,6 +2516,10 @@ UNICORN_EXPORT uc_err uc_context_reg_write2(uc_context *ctx, int regid, const void *value, size_t *size) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } int setpc = 0; return find_context_reg_rw(ctx->arch, ctx->mode) .write(ctx->data, ctx->mode, regid, value, size, &setpc); @@ -2488,6 +2529,10 @@ UNICORN_EXPORT uc_err uc_context_reg_read2(uc_context *ctx, int regid, void *value, size_t *size) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } return find_context_reg_rw(ctx->arch, ctx->mode) .read(ctx->data, ctx->mode, regid, value, size); } @@ -2496,6 +2541,10 @@ UNICORN_EXPORT uc_err uc_context_reg_write_batch(uc_context *ctx, int const *regs, void *const *vals, int count) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } reg_write_t reg_write = find_context_reg_rw(ctx->arch, ctx->mode).write; void *env = ctx->data; int mode = ctx->mode; @@ -2519,6 +2568,10 @@ UNICORN_EXPORT uc_err uc_context_reg_read_batch(uc_context *ctx, int const *regs, void **vals, int count) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } reg_read_t reg_read = find_context_reg_rw(ctx->arch, ctx->mode).read; void *env = ctx->data; int mode = ctx->mode; @@ -2542,6 +2595,10 @@ uc_err uc_context_reg_write_batch2(uc_context *ctx, int const *regs, const void *const *vals, size_t *sizes, int count) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } reg_write_t reg_write = find_context_reg_rw(ctx->arch, ctx->mode).write; void *env = ctx->data; int mode = ctx->mode; @@ -2564,6 +2621,10 @@ UNICORN_EXPORT uc_err uc_context_reg_read_batch2(uc_context *ctx, int const *regs, void *const *vals, size_t *sizes, int count) { + uc_err err = uc_context_check_cpu_state(ctx); + if (err) { + return err; + } reg_read_t reg_read = find_context_reg_rw(ctx->arch, ctx->mode).read; void *env = ctx->data; int mode = ctx->mode; @@ -2587,7 +2648,32 @@ uc_err uc_context_restore(uc_engine *uc, uc_context *context) UC_INIT(uc); uc_err ret; - if (uc->context_content & UC_CTL_CONTEXT_MEMORY) { + // Refuse never-saved contexts (#2319) and ones whose recorded + // arch/mode does not match the destination engine (#2320). Both + // would otherwise propagate uninitialised or wrong-sized state into + // the engine and trip cpu_asidx_from_attrs() or read garbage host + // pointers out of fv / last_block. + if (context->context_content == 0 || + context->arch != uc->arch || + context->mode != uc->mode) { + restore_jit_state(uc); + return UC_ERR_ARG; + } + + // Each restore branch is gated on both sides advertising the + // matching content bit. Restoring a CPU-only context onto an engine + // that asks for memory restore (or vice versa) is silently a no-op + // for the missing branch, matching the maintainer's intent of + // avoiding "restore a CPU context as a memory context". + if ((uc->context_content & UC_CTL_CONTEXT_MEMORY) && + (context->context_content & UC_CTL_CONTEXT_MEMORY)) { + // The fv / last_block fields the memory restore is about to + // dereference are live host pointers that only make sense on + // the engine that captured them. + if (context->engine != uc) { + restore_jit_state(uc); + return UC_ERR_ARG; + } uc->snapshot_level = context->snapshot_level; if (!uc->flatview_copy(uc, uc->address_space_memory.current_map, context->fv, true)) { @@ -2604,7 +2690,8 @@ uc_err uc_context_restore(uc_engine *uc, uc_context *context) uc->tcg_flush_tlb(uc); } - if (uc->context_content & UC_CTL_CONTEXT_CPU) { + if ((uc->context_content & UC_CTL_CONTEXT_CPU) && + (context->context_content & UC_CTL_CONTEXT_CPU)) { if (!uc->context_restore) { memcpy(uc->cpu->env_ptr, context->data, context->context_size); restore_jit_state(uc); @@ -2615,6 +2702,7 @@ uc_err uc_context_restore(uc_engine *uc, uc_context *context) return ret; } } + restore_jit_state(uc); return UC_ERR_OK; } From b3c75a5e94097fdf7658d34b44f6b783dc13828c Mon Sep 17 00:00:00 2001 From: MarkLee131 Date: Wed, 29 Apr 2026 21:59:03 +0800 Subject: [PATCH 2/2] uc_context: tighten reg_*() check to require CPU content Previously the check only refused contexts with MEMORY set and CPU unset; never-saved contexts (context_content == 0) were left alone on the assumption that an alloc + reg_write + save pattern might exist. uc_context_save() overwrites the data buffer from the engine though, so any reg_write before save would be clobbered, and there is no real caller for that path. Reject anything without UC_CTL_CONTEXT_CPU set, symmetric with uc_context_restore() refusing context_content == 0. Extends the #2319 regression test to assert uc_context_reg_read / uc_context_reg_write also reject a never-saved context. --- tests/unit/test_ctl.c | 9 +++++++++ uc.c | 13 +++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_ctl.c b/tests/unit/test_ctl.c index 1680c72f36..7fef2d50c1 100644 --- a/tests/unit/test_ctl.c +++ b/tests/unit/test_ctl.c @@ -412,6 +412,15 @@ static void test_uc_context_restore_without_save(void) uc_assert_err(UC_ERR_ARG, uc_context_restore(uc, ctx)); + // reg_*() on a never-saved context is also refused, symmetric + // with restore. + { + int regid = UC_X86_REG_RAX; + uint64_t value = 0; + uc_assert_err(UC_ERR_ARG, uc_context_reg_read(ctx, regid, &value)); + uc_assert_err(UC_ERR_ARG, uc_context_reg_write(ctx, regid, &value)); + } + // The engine must still be usable. OK(uc_mem_map(uc, 0x1000, 0x1000, UC_PROT_ALL)); OK(uc_mem_write(uc, 0x1000, code, sizeof(code))); diff --git a/uc.c b/uc.c index 6661ba8f62..1097cf665a 100644 --- a/uc.c +++ b/uc.c @@ -2472,16 +2472,13 @@ static context_reg_rw_t find_context_reg_rw(uc_arch arch, uc_mode mode) return rw; } -// Refuse register r/w on memory-only contexts: their data buffer -// holds no CPU state, so the call would just shuffle zeros into the -// caller's value (read) or be lost on the next restore (write). -// Untouched contexts (context_content == 0) are left alone, so the -// uc_context_alloc + uc_context_reg_write + uc_context_save pattern -// keeps working. +// Refuse register r/w unless the context advertises CPU state. This +// rejects both memory-only snapshots (whose data buffer holds no CPU +// state) and never-saved contexts (context_content == 0), which is +// symmetric with uc_context_restore() refusing context_content == 0. static inline uc_err uc_context_check_cpu_state(const uc_context *ctx) { - if ((ctx->context_content & UC_CTL_CONTEXT_MEMORY) && - !(ctx->context_content & UC_CTL_CONTEXT_CPU)) { + if (!(ctx->context_content & UC_CTL_CONTEXT_CPU)) { return UC_ERR_ARG; } return UC_ERR_OK;