Summary
On a single Uc instance, the order in which two adjacent functions are emulated changes the result: running function B and then function A causes A's emulation to run past its until address into B's code, eventually faulting with UC_ERR_FETCH_UNMAPPED. Running A first works correctly.
Environment
- Unicorn
master @ 7c5db941 (built from source, x86-only config), Python bindings, UC_ARCH_X86 / UC_MODE_64, Linux x86-64
- Verified the fault is independent of any local patches by building pristine
master in a clean worktree and loading it via LIBUNICORN_PATH.
Details
func_a and func_b are laid out back to back, so func_a_end == func_b_start. When func_a is emulated, its ret pops a seeded return address equal to func_a_end, which is also the until passed to emu_start, so emulation should stop there.
Emulating func_b first chains its translation blocks. On the subsequent func_a run, func_a's terminal block is already chain-linked to the block at func_a_end (func_b's entry), so the chained jump bypasses the per-block until check. Instead of stopping, control falls through into func_b; its epilogue then returns to a poisoned stack slot (0xDEADBEEF) and faults.
Calling uc.ctl_flush_tb() between the two runs works around it (drops the stale chain), confirming TB chaining as the cause.
Reproducer
A self-contained regress-style reproducer (3 cases: working order, faulting order, and the flush workaround):
https://github.com/retrocpugeek/unicorn/blob/8f5cb0d4/tests/regress/minimal_tb_chain_repro.py
Summary
On a single
Ucinstance, the order in which two adjacent functions are emulated changes the result: running function B and then function A causes A's emulation to run past itsuntiladdress into B's code, eventually faulting withUC_ERR_FETCH_UNMAPPED. Running A first works correctly.Environment
master@7c5db941(built from source, x86-only config), Python bindings,UC_ARCH_X86/UC_MODE_64, Linux x86-64masterin a clean worktree and loading it viaLIBUNICORN_PATH.Details
func_aandfunc_bare laid out back to back, sofunc_a_end == func_b_start. Whenfunc_ais emulated, itsretpops a seeded return address equal tofunc_a_end, which is also theuntilpassed toemu_start, so emulation should stop there.Emulating
func_bfirst chains its translation blocks. On the subsequentfunc_arun,func_a's terminal block is already chain-linked to the block atfunc_a_end(func_b's entry), so the chained jump bypasses the per-blockuntilcheck. Instead of stopping, control falls through intofunc_b; its epilogue then returns to a poisoned stack slot (0xDEADBEEF) and faults.Calling
uc.ctl_flush_tb()between the two runs works around it (drops the stale chain), confirming TB chaining as the cause.Reproducer
A self-contained regress-style reproducer (3 cases: working order, faulting order, and the flush workaround):
https://github.com/retrocpugeek/unicorn/blob/8f5cb0d4/tests/regress/minimal_tb_chain_repro.py