Skip to content

m68k: reg_read(UC_M68K_REG_SR) silently destroys the guest's condition codes (after any reg_write of SR) #2380

Description

@cwright7101

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions