Skip to content

Map AArch64 xsp to Unicorn's SP register - #361

Open
plowsec wants to merge 1 commit into
angr:masterfrom
plowsec:fix/missing-aarch64-sp
Open

Map AArch64 xsp to Unicorn's SP register#361
plowsec wants to merge 1 commit into
angr:masterfrom
plowsec:fix/missing-aarch64-sp

Conversation

@plowsec

@plowsec plowsec commented Aug 3, 2026

Copy link
Copy Markdown

AArch64's stack pointer is missing from the Unicorn register map, so Unicorn silently never runs

Problem

archinfo publishes the table that says which angr register corresponds to which Unicorn register. On AArch64 that table has no entry for the stack pointer. This can be confirmed like this:

import archinfo

aarch64, armel, amd64 = archinfo.ArchAArch64(), archinfo.ArchARMEL(), archinfo.ArchAMD64()
for arch in (aarch64, armel, amd64):
    name = arch.register_names[arch.sp_offset]
    print(f"{arch.name:8} sp is called {name!r:6} -> in uc_regs: {name in arch.uc_regs}")
AARCH64  sp is called 'xsp'  -> in uc_regs: False
ARMEL    sp is called 'sp'   -> in uc_regs: True
AMD64    sp is called 'rsp'  -> in uc_regs: True

Nothing warns about this. The consequence is that whenever angr hands an AArch64 block to the Unicorn CPU emulator, the emulator starts with a stack pointer of zero, faults on the first stack access, and angr quietly falls back to interpreting the blockand then disables Unicorn for a while. Running a stack round trip (sub sp, sp, #16 / str / ldr / add sp, sp, #16) from sp = 0x70000000 with angr.options.UNICORN enabled, at INFO logging:

INFO  angr.state_plugins.unicorn_engine: started emulation at 0x400230 (1000000 steps)
INFO  angr.state_plugins.unicorn_engine: mmap [0xfffffffffffff000, 0xffffffffffffffff] because 20
INFO  angr.state_plugins.unicorn_engine: finished emulation at 0x400230 after 0 steps: STOP_ERROR
INFO  angr.state_plugins.unicorn_engine: Unicorn stepped 0 blocks in 0.002099sec, enabling cooldown
engine that ran the block : IRSB
  pc = 0x400248   sp = 0x70000000   x0 = 42

The mmap [0xfffffffffffff000, …] line demonstrates it: the emulator computed 0 - 16 and tried to write there, so it really did start with sp = 0. STOP_ERROR after 0 steps is angr throwing the attempt away, IRSB is the interpreter that actually produced the answer, and enabling cooldown means Unicorn is skipped for subsequent blocks too.

With the patch, the same run:

INFO  angr.state_plugins.unicorn_engine: mmap [0x70000000, 0x70000fff] because 20
INFO  angr.state_plugins.unicorn_engine: finished emulation at 0x400248 after 1 steps: STOP_STOPPOINT
INFO  angr.state_plugins.unicorn_engine: Unicorn stepped 1 block in 0.002209 sec (452.704155 blocks/sec)
engine that ran the block : Unicorn (STOP_STOPPOINT after 1 steps)
  pc = 0x400248   sp = 0x70000000   x0 = 42

The emulator now maps the real stack at 0x70000000 and completes the block.

Note that the final register values are the same either way, because falling back to the interpreter is correct. What is lost is Unicorn itself: on AArch64 it never does any useful work, silently, no matter what the user asks for. That is the defect being reported here. Whether a zero stack pointer can also produce a wrong result rather than a clean fault depends on whether the faulting address happens to be mapped, but I haven't investigate that.

root cause

Arch._setup_unicorn_mappings builds the map by walking canonical register names
(arch.py:296-301):

            for r in self.register_names.values():

register_names maps a VEX offset to the register's canonical name only; alias names are never consulted. AArch64 names its stack pointer xsp, with sp merely an alias (arch_aarch64.py:124-131), and Unicorn has no UC_ARM64_REG_XSPonly UC_ARM64_REG_SP. The lookup misses, the register is skipped, and the derived vex_to_unicorn_map (arch.py:303-311) is missing VEX offset 264 as well. Every other architecture escapes this by coincidence of naming: 32-bit ARM calls it sp and x86-64 calls it rsp, and both of those names exist in Unicorn.

suggested fix

Add the one missing entry, mapping AArch64's xsp to UC_ARM64_REG_SP.

Verified against Unicorn 2.1.4:

UC_ARM64_REG_SP    = 4
UC_ARM64_REG_XSP   = does not exist
UC_ARM64_REG_WSP   = 5

SP is the 64-bit architectural stack pointer and the correct counterpart of VEX's XSP. WSP is its 32-bit view; mapping to that would silently truncate every stack pointer transferred in or out, which is the mistake worth being explicit about avoiding.

reproducer

repro.zip

python trigger.py

It builds repro.S, executes one block with Unicorn enabled from a known stack pointer, and checks which engine ran and what came back. It also installs angr's AArch64 Unicorn hook itself, so that a separate angr-side bug (will submit separately) does not hide this one. Before the patch the engine reported is IRSB. After it, Unicorn (STOP_STOPPOINT after 1 steps), with sp and x0 surviving.

note

Doing this end to end also needs angr's Unicorn hook dispatch to accept AArch64, which currently raises SimUnicornUnsupport. I will open that separately against angr if I have the time.

  ArchAArch64 had no stack-pointer entry in uc_regs or vex_to_unicorn_map.
  The mapping is built from canonical register names only, and AArch64's
  canonical name is `xsp`, for which Unicorn has no constant, it has
  UC_ARM64_REG_SP. The register was therefore skipped, and AArch64 blocks
  handed to Unicorn ran with a stack pointer of zero.
Comment thread archinfo/arch_aarch64.py
class ArchAArch64(Arch):
def __init__(self, endness=Endness.LE):
super().__init__(endness)
if _unicorn is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this seem like the best place to have put this?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants