Skip to content

Missing Windows unwind metadata for generated ARM64 and x86-64 functions #342

Description

@reaperhulk

The generated assembly lacks the unwind descriptions needed to recover caller contexts on Windows ARM64 and Windows x86-64. The proofs establish normal execution and register restoration on return, but do not establish correct unwinding from an intermediate instruction.

This is an ABI / stack-unwinding correctness defect. I have not demonstrated an exploit, key disclosure, or attacker-triggered memory corruption. Native Windows validation remains outstanding.

Where the gap enters the TCB

Reviewed revision: 49f67820510f0508d7be78f2f01a678ee92ebb21, the head of #338, based on cf53f72. The affected emission and calling-convention behavior predates #338.

  • The AArch64 target includes Windows and uses extern "C".
  • The x86-64 target includes Windows and uses extern "sysv64".
  • The shared naked-function emitter emits the instruction bodies without unwind directives. The architecture printers do not add them.
  • abiPreserved checks the entry/exit relationship. Recovering saved registers, SP and the return address at intermediate PCs is outside that predicate.

Rust does not infer these descriptions from the instruction text: the Rust Reference's naked-assembly rules require explicit directives for unwinding, and rustc's naked-function lowering emits global assembly whose COFF prefix/suffix contains symbol and section directives, without SEH records.

ARM64: incorrect return PC and SP

Microsoft's ARM64 exception-handling specification, “.pdata records” requires records for stack-manipulating functions. Its leaf exemption is for functions requiring neither local storage nor nonvolatile-register saves. RtlVirtualUnwind2's FunctionEntry documentation specifies that a missing entry causes a leaf/no-frame fallback equivalent to a return.

For example, vg_sha256_update's prologue pushes x30 by 16 bytes and saves x19–x24 in the scratch buffer. It then calls vg_sha256_compress.

Immediately after that call returns, x30 points inside vg_sha256_update, and SP is still 16 bytes below its entry value. Treating this context as a leaf cannot recover its caller.

I executed the generated SHA-256 init/update/finalize bodies in Unicorn 2.1.4 on the 64-byte message bytes(range(64)). Normal execution restored the callee-saved registers and SP, and the digest matched Python's hashlib.sha256. At the instruction immediately following the compression call, the captured state was:

Value Captured
PC and x30 0x10368c4
SP 0x21ffff0
Original return address saved at SP 0x2100000
Original entry SP 0x2200000

The documented fallback would retain the wrong SP and select the internal address as the return PC. That fallback result is derived from Microsoft's specification; I did not execute the Windows unwinder.

Preserving x29, as #338 does, does not describe how to restore the other registers or locate the saved return address.

x86-64: incorrect nonvolatile-register context

Microsoft's x64 exception-handling specification, “struct RUNTIME_FUNCTION” and “Unwind procedure,” requires entries for functions that allocate stack space or call another function. With no entry, it reads RIP from [RSP], increments RSP by eight, and continues.

The immediate failure differs from ARM64: where an x86-64 body leaves RSP unchanged, the fallback can recover the return PC, but still fails to restore nonvolatile registers. For example, vg_chacha20_block saves rbx, rbp, and r12–r15 in scratch memory before repurposing them. A mid-body unwind cannot restore those values without descriptions of their locations. An incorrect RBP can also prevent correct unwinding of a caller that uses it as a frame base.

extern "sysv64" changes the function's calling convention; it does not supply Windows unwind records. The x86-64 assessment is based on source inspection, not a native Windows reproducer.

Additional validation and scope

  • Audited all 119 generated AArch64 functions at the reviewed revision: 16 move SP, 56 contain calls, and none contain unwind directives.
  • Assembled the extracted generated bodies using LLVM 21.1.0 for ARM64 COFF: the resulting object had 250,248 bytes of text and no .pdata or .xdata. This was assembly of the actual generated instruction bodies, not a full rustc crate build.
  • Independently checked control-flow stack depths and transitive call-stack budgets: all balanced, maximum 32 bytes, no documented budget exceeded.
  • The existing ABI strings are non-unwinding, and the verified callees do not throw Rust panics. This report does not claim that an ordinary Rust panic currently crosses these functions. Windows also uses unwind information for stack walking/debugging.
  • ELF/DWARF and Apple unwind behavior need separate assessment; this issue concerns Windows.

Suggested fix

  1. Add Windows-specific emission and, where necessary, verified prologue/epilogue implementations that use save locations representable by Windows unwind codes.
  2. Generate .seh_* directives / .pdata and .xdata from the same structured code and frame/save description used by the model. Account for saved nonvolatile registers, SP changes, return-address storage, and partially executed prologues/epilogues.
  3. Do not fix only the SP adjustment. Many current functions spill nonvolatile registers to argument-provided scratch buffers; standard Windows unwind codes generally describe stack-relative saves. Those implementations need compatible save layouts, or another fully specified and tested unwind mechanism.
  4. Add native Windows ARM64 and x86-64 regression tests that use RtlLookupFunctionEntry and RtlVirtualUnwind/RtlVirtualUnwind2 on captured contexts across prologues, bodies, nested calls and epilogues. Check the reconstructed caller PC, SP and relevant nonvolatile registers. Also inspect the linked binary's unwind coverage.
  5. Until correct Windows support is implemented, an explicit compile-time exclusion with a clear diagnostic is a conservative interim option.

Changes to the target/emitter and any new unwind model are trust changes and should be reviewed explicitly.

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