Skip to content

x86 real mode cache invalidation bug #2258

@mrexodia

Description

@mrexodia
#!/usr/bin/env python3
"""
Minimal reproduction case for Unicorn translation cache bug
Simulates MBR relocating itself and loading PBR back to 0x7C00
"""

from unicorn import *
from unicorn.x86_const import *
from capstone import *

print("="*80)
print("Minimal Reproduction: MBR Relocation + PBR Load Bug")
print("="*80)

# Initialize Unicorn and Capstone
uc = Uc(UC_ARCH_X86, UC_MODE_16)
cs_disasm = Cs(CS_ARCH_X86, CS_MODE_16)

# Map memory
uc.mem_map(0, 0x100000)

# Code at 0x7C00 (MBR): nop; int 13h; ljmp 0:0x7C00
# This simulates MBR that will:
# 1. Call INT 13h to "load PBR"  
# 2. Jump back to 0x7C00 to execute the PBR
mbr_code = bytes([
    0x90,                    # nop
    0xCD, 0x13,              # int 0x13
    0xEA, 0x00, 0x7C, 0x00, 0x00  # ljmp 0:0x7c00
])

print(f"\n[*] Initial setup:")
print(f"    Writing MBR code to 0x7C00:")
for i, b in enumerate(mbr_code):
    addr = 0x7C00 + i
    print(f"      0x{addr:04X}: {b:02X}")

uc.mem_write(0x7C00, mbr_code)

# Code at 0x0600 (relocation target): copy the MBR code here
# In real MBR, this would be done by the bootloader copying itself
# For our test, we'll just write it directly
print(f"\n[*] Writing relocated MBR code to 0x0600:")
uc.mem_write(0x0600, mbr_code)
for i, b in enumerate(mbr_code):
    addr = 0x0600 + i
    print(f"      0x{addr:04X}: {b:02X}")

# Set up initial CPU state
uc.reg_write(UC_X86_REG_CS, 0x0000)
uc.reg_write(UC_X86_REG_IP, 0x0600)  # Start at relocated code

print(f"\n[*] Initial CPU state:")
print(f"    CS:IP = 0x0000:0x0600")

# Trace execution
trace = []
int13_called = False

def hook_interrupt(uc, intno, user_data):
    """Handle INT 13h - simulate BIOS disk read loading PBR to 0x7C00"""
    global int13_called
    
    if intno == 0x13:
        print(f"\n[INT 0x13] Called - Simulating BIOS loading PBR to 0x7C00")
        int13_called = True
        
        # Simulate loading a new PBR (just a HLT instruction) to 0x7C00
        pbr_code = bytes([0xF4])  # HLT
        
        print(f"    Writing new PBR code to 0x7C00: F4 (hlt)")
        uc.mem_write(0x7C00, pbr_code)
        
        # Show what's in memory now
        mem = uc.mem_read(0x7C00, 8)
        print(f"    Memory at 0x7C00 after write: {mem.hex(' ')}")
        
        # THIS IS THE BUG: Without cache invalidation, the old translation is still cached!
        print(f"    ⚠️  Translation cache NOT invalidated (demonstrating bug)")
        
        # Note: In the fixed version, we would do:
        # from unicorn.unicorn_const import UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE
        # uc.ctl(UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE, 0x7C00, 0x7C01)

def hook_code(uc, address, size, user_data):
    """Trace each instruction"""
    code_bytes = uc.mem_read(address, min(15, size))
    
    try:
        instr = next(cs_disasm.disasm(code_bytes, address, 1))
        line = f"0x{address:04X}: {code_bytes[:instr.size].hex():16s} {instr.mnemonic:8s} {instr.op_str}"
        trace.append(line)
        print(f"  {line}")
        
        # Stop after reasonable number of instructions
        if len(trace) > 20:
            print("\n[!] Safety limit reached, stopping")
            uc.emu_stop()
            
    except StopIteration:
        line = f"0x{address:04X}: {code_bytes.hex()} <invalid>"
        trace.append(line)
        print(f"  {line}")
        uc.emu_stop()

# Add hooks
uc.hook_add(UC_HOOK_INTR, hook_interrupt)
uc.hook_add(UC_HOOK_CODE, hook_code)

print(f"\n[*] Starting emulation...")
print(f"    Expected behavior:")
print(f"      1. Execute NOP at 0x0600")
print(f"      2. Execute INT 0x13 at 0x0601 (loads PBR to 0x7C00)")
print(f"      3. Execute LJMP to 0x7C00")
print(f"      4. Execute HLT at 0x7C00 (NEW PBR code)")
print(f"\n    Actual execution trace:")

try:
    uc.emu_start(0x0600, 0xFFFFFFFF)
except UcError as e:
    print(f"\n[!] Emulation stopped: {e}")

# Analyze results
print(f"\n" + "="*80)
print("Analysis:")
print("="*80)

# Check final IP
final_ip = uc.reg_read(UC_X86_REG_IP)
print(f"Final IP: 0x{final_ip:04X}")

# Check what instruction was at 0x7C00 when we jumped back
if int13_called:
    print(f"\nWhat happened at 0x7C00 after the LJMP?")
    
    # Find the instruction(s) executed after returning to 0x7C00
    executed_at_7c00 = [line for line in trace if line.startswith("0x7C00:") or line.startswith("0x7c00:")]
    
    if executed_at_7c00:
        print(f"  Instructions executed at 0x7C00:")
        for line in executed_at_7c00:
            print(f"    {line}")
        
        # Check if we executed the NOP (old MBR code) or HLT (new PBR code)
        if "nop" in executed_at_7c00[0].lower():
            print(f"\n❌ BUG REPRODUCED!")
            print(f"   Executed OLD cached code (NOP) instead of new code (HLT)")
            print(f"   The translation cache was not invalidated after mem_write in INT 13h")
            print(f"\n   This is exactly the bug in your bootloader emulator:")
            print(f"   - MBR relocates to 0x0600")
            print(f"   - INT 13h loads PBR to 0x7C00 (overwrites old MBR)")  
            print(f"   - Jump back to 0x7C00 executes CACHED old MBR code")
            print(f"   - Instead of new PBR code!")
        elif "hlt" in executed_at_7c00[0].lower():
            print(f"\n✅ Bug fixed!")
            print(f"   Executed NEW code (HLT) correctly")
            print(f"   Translation cache was properly invalidated")
        else:
            print(f"\n⚠️  Unexpected instruction at 0x7C00")
    else:
        print(f"  ⚠️  No instructions executed at 0x7C00 (emulation stopped early?)")

print(f"\n" + "="*80)
print("Solution:")
print("="*80)
print("After mem_write in the INT 13h handler, add:")
print("")
print("    from unicorn.unicorn_const import UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE")
print("    uc.ctl(UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE, 0x7C00, 0x7C01)")
print("")
print("This invalidates Unicorn's translation cache, forcing it to")
print("re-translate the code at 0x7C00 instead of using stale cache.")
print("="*80)

print(f"\n[*] Now testing WITH the fix...")
print("="*80)

# Reset and test WITH fix
uc_fixed = Uc(UC_ARCH_X86, UC_MODE_16)
cs_disasm_fixed = Cs(CS_ARCH_X86, CS_MODE_16)
uc_fixed.mem_map(0, 0x100000)
uc_fixed.mem_write(0x7C00, mbr_code)
uc_fixed.mem_write(0x0600, mbr_code)
uc_fixed.reg_write(UC_X86_REG_CS, 0x0000)
uc_fixed.reg_write(UC_X86_REG_IP, 0x0600)

trace_fixed = []
int13_called_fixed = False

def hook_interrupt_fixed(uc, intno, user_data):
    """Handle INT 13h WITH cache invalidation"""
    global int13_called_fixed
    
    if intno == 0x13:
        print(f"\n[INT 0x13] Called - Loading PBR with cache invalidation")
        int13_called_fixed = True
        
        pbr_code = bytes([0xF4])  # HLT
        uc.mem_write(0x7C00, pbr_code)
        print(f"    Wrote new PBR code to 0x7C00: F4 (hlt)")
        
        # THE FIX: Invalidate translation cache
        try:
            from unicorn.unicorn_const import UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE
            uc.ctl(UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE, 0x7C00, 0x7C01)
            print(f"    ✓ Translation cache invalidated for 0x7C00")
        except Exception as e:
            print(f"    ⚠️  Could not invalidate cache: {e}")

def hook_code_fixed(uc, address, size, user_data):
    """Trace each instruction"""
    code_bytes = uc.mem_read(address, min(15, size))
    
    try:
        instr = next(cs_disasm_fixed.disasm(code_bytes, address, 1))
        line = f"0x{address:04X}: {code_bytes[:instr.size].hex():16s} {instr.mnemonic:8s} {instr.op_str}"
        trace_fixed.append(line)
        print(f"  {line}")
        
        if len(trace_fixed) > 20:
            uc.emu_stop()
            
    except StopIteration:
        line = f"0x{address:04X}: {code_bytes.hex()} <invalid>"
        trace_fixed.append(line)
        print(f"  {line}")
        uc.emu_stop()

uc_fixed.hook_add(UC_HOOK_INTR, hook_interrupt_fixed)
uc_fixed.hook_add(UC_HOOK_CODE, hook_code_fixed)

print(f"\n    Execution trace WITH fix:")

try:
    uc_fixed.emu_start(0x0600, 0xFFFFFFFF)
except UcError as e:
    print(f"\n[!] Emulation stopped: {e}")

# Analyze fixed results
final_ip_fixed = uc_fixed.reg_read(UC_X86_REG_IP)
print(f"\nFinal IP: 0x{final_ip_fixed:04X}")

executed_at_7c00_fixed = [line for line in trace_fixed if line.startswith("0x7C00:") or line.startswith("0x7c00:")]

if executed_at_7c00_fixed:
    print(f"\nInstructions executed at 0x7C00:")
    for line in executed_at_7c00_fixed:
        print(f"  {line}")
    
    if "hlt" in executed_at_7c00_fixed[0].lower():
        print(f"\n✅ SUCCESS! Bug is fixed!")
        print(f"   Executed NEW code (HLT) correctly after cache invalidation")
    else:
        print(f"\n❌ Still seeing old code - fix didn't work")

print(f"\n" + "="*80)

Output:

================================================================================
Minimal Reproduction: MBR Relocation + PBR Load Bug
================================================================================

[*] Initial setup:
    Writing MBR code to 0x7C00:
      0x7C00: 90
      0x7C01: CD
      0x7C02: 13
      0x7C03: EA
      0x7C04: 00
      0x7C05: 7C
      0x7C06: 00
      0x7C07: 00

[*] Writing relocated MBR code to 0x0600:
      0x0600: 90
      0x0601: CD
      0x0602: 13
      0x0603: EA
      0x0604: 00
      0x0605: 7C
      0x0606: 00
      0x0607: 00

[*] Initial CPU state:
    CS:IP = 0x0000:0x0600

[*] Starting emulation...
    Expected behavior:
      1. Execute NOP at 0x0600
      2. Execute INT 0x13 at 0x0601 (loads PBR to 0x7C00)
      3. Execute LJMP to 0x7C00
      4. Execute HLT at 0x7C00 (NEW PBR code)

    Actual execution trace:
  0x0600: 90               nop
  0x0601: cd13             int      0x13

[INT 0x13] Called - Simulating BIOS loading PBR to 0x7C00
    Writing new PBR code to 0x7C00: F4 (hlt)
    Memory at 0x7C00 after write: f4 cd 13 ea 00 7c 00 00
    ⚠️  Translation cache NOT invalidated (demonstrating bug)
  0x0603: ea007c0000       ljmp     0:0x7c00
  0x7C00: f4               hlt

================================================================================
Analysis:
================================================================================
Final IP: 0x7C01

What happened at 0x7C00 after the LJMP?
  Instructions executed at 0x7C00:
    0x7C00: f4               hlt

✅ Bug fixed!
   Executed NEW code (HLT) correctly
   Translation cache was properly invalidated

================================================================================
Solution:
================================================================================
After mem_write in the INT 13h handler, add:

    from unicorn.unicorn_const import UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE
    uc.ctl(UC_CTL_TB_REMOVE_CACHE, UC_CTL_IO_WRITE, 0x7C00, 0x7C01)

This invalidates Unicorn's translation cache, forcing it to
re-translate the code at 0x7C00 instead of using stale cache.
================================================================================

[*] Now testing WITH the fix...
================================================================================

    Execution trace WITH fix:
  0x0600: 90               nop
  0x0601: cd13             int      0x13

[INT 0x13] Called - Loading PBR with cache invalidation
    Wrote new PBR code to 0x7C00: F4 (hlt)
    ✓ Translation cache invalidated for 0x7C00
  0x0603: ea007c0000       ljmp     0:0x7c00
  0x7C00: f4               hlt

Final IP: 0x7C01

Instructions executed at 0x7C00:
  0x7C00: f4               hlt

✅ SUCCESS! Bug is fixed!
   Executed NEW code (HLT) correctly after cache invalidation

================================================================================

The reproduction was written by Claude, but based on an emulator I wrote for real-mode boot loaders. My workaround is:

    def mem_write(self, address: int, data: bytes):
        self.uc.mem_write(address, data)
        self.uc.ctl_remove_cache(address, address + len(data))

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