On m68k, once the host has written SR via uc_reg_write(), a subsequent uc_reg_read(UC_M68K_REG_SR) silently clears the guest's condition codes. A conditional branch that has already had its flags set then takes the wrong path.
This is nasty because reading a register is expected to be side-effect free, and nothing reports an error — the guest just branches wrongly.
Tested on Unicorn 2.1.4 (Python bindings), macOS/arm64.
Minimal reproduction
andi.l #16383,d0 with d0 = 0x4000 gives 0, so Z is set and the following bne must NOT be taken (d1 = 1).
from unicorn import Uc, UC_ARCH_M68K, UC_MODE_BIG_ENDIAN
from unicorn.m68k_const import *
# andi.l #16383,d0 ; bne ELSE ; moveq #1,d1 ; bra END ; ELSE: moveq #2,d1 ; END: bra .
CODE = bytes.fromhex("028000003fff") + b"\x66\x04" + b"\x72\x01" + b"\x60\x02" + b"\x72\x02" + b"\x60\xfe"
def run(read_sr, pre_write_sr):
uc = Uc(UC_ARCH_M68K, UC_MODE_BIG_ENDIAN)
uc.mem_map(0x1000, 0x1000); uc.mem_write(0x1000, CODE)
if pre_write_sr:
uc.reg_write(UC_M68K_REG_SR, 0x2000) # host sets SR at some earlier point
uc.reg_write(UC_M68K_REG_D0, 0x4000) # 0x4000 & 16383 == 0 -> Z set
uc.reg_write(UC_M68K_REG_D1, 0)
uc.emu_start(0x1000, 0, count=1) # execute ONLY the andi.l
if read_sr:
uc.reg_read(UC_M68K_REG_SR) # <-- the only difference
uc.emu_start(uc.reg_read(UC_M68K_REG_PC), 0, count=4)
return uc.reg_read(UC_M68K_REG_D1)
for pre in (False, True):
print(f"pre_write_sr={pre!s:<5} no SR read: d1={run(False, pre)} SR read: d1={run(True, pre)}")
Actual
pre_write_sr=False no SR read: d1=1 SR read: d1=1
pre_write_sr=True no SR read: d1=1 SR read: d1=2 <-- flags lost
Expected
d1 == 1 in all four combinations — reading SR should not change guest state.
Notes
- The trigger is specifically host
reg_write(SR) at any earlier point, then reg_read(SR). With no prior host SR write, the read is harmless, which is why this is easy to miss.
- Reading an unrelated register (e.g.
UC_M68K_REG_D0) in the same position is harmless, so it is the SR accessor specifically.
- Looks consistent with QEMU's lazy flag evaluation (
cc_op/cc_dest): the read appears to materialise SR from stale CC state and write back a normalised value with the flags zeroed.
- There is also no way to observe the condition codes at all — neither
reg_read(SR) nor a guest move.w %sr,%dn returns them (both give 0x2000 whether Z is set or clear) — so an embedder cannot save/restore them around an operation either. The only workaround I found is context_save()/context_restore(), which carries the internal CC state but rewinds every architectural register with it.
Why it matters
Anything that seeds SR once (e.g. to enter supervisor state at reset, which is the architectural m68k reset condition) and later inspects SR — a debugger, a tracer, or an exception-delivery path that must check the interrupt mask — corrupts the guest. In my case an interrupt delivered between a compare and its branch sent the firmware down the wrong path intermittently, with no diagnostic.
Related: #2379 (m68k rte never completes).
On m68k, once the host has written SR via
uc_reg_write(), a subsequentuc_reg_read(UC_M68K_REG_SR)silently clears the guest's condition codes. A conditional branch that has already had its flags set then takes the wrong path.This is nasty because reading a register is expected to be side-effect free, and nothing reports an error — the guest just branches wrongly.
Tested on Unicorn 2.1.4 (Python bindings), macOS/arm64.
Minimal reproduction
andi.l #16383,d0withd0 = 0x4000gives 0, so Z is set and the followingbnemust NOT be taken (d1 = 1).Actual
Expected
d1 == 1in all four combinations — reading SR should not change guest state.Notes
reg_write(SR)at any earlier point, thenreg_read(SR). With no prior host SR write, the read is harmless, which is why this is easy to miss.UC_M68K_REG_D0) in the same position is harmless, so it is the SR accessor specifically.cc_op/cc_dest): the read appears to materialise SR from stale CC state and write back a normalised value with the flags zeroed.reg_read(SR)nor a guestmove.w %sr,%dnreturns them (both give0x2000whether Z is set or clear) — so an embedder cannot save/restore them around an operation either. The only workaround I found iscontext_save()/context_restore(), which carries the internal CC state but rewinds every architectural register with it.Why it matters
Anything that seeds SR once (e.g. to enter supervisor state at reset, which is the architectural m68k reset condition) and later inspects SR — a debugger, a tracer, or an exception-delivery path that must check the interrupt mask — corrupts the guest. In my case an interrupt delivered between a compare and its branch sent the firmware down the wrong path intermittently, with no diagnostic.
Related: #2379 (m68k
rtenever completes).