Skip to content

feat(relinker): frame red-zone leaves without a frame pointer by moving their rsp operands - #5640

Closed
EagleBearerYS wants to merge 6 commits into
boykopovar:mainfrom
EagleBearerYS:feat/relinker-frame-frameless-leaves
Closed

EagleBearerYS wants to merge 6 commits into
boykopovar:mainfrom
EagleBearerYS:feat/relinker-frame-frameless-leaves

Conversation

@EagleBearerYS

Copy link
Copy Markdown
Contributor

What

#5052's --frame-red-zone frames leaf functions with a push rbp; mov rbp, rsp prologue. Titles whose red-zone leaves have no frame pointer get no framing at all:

In Village this is a boot crash, the #361 mechanism. A frameless merge leaf (app.exe+0x1469cbb..) keeps its loop state at -0x8 to -0x28(%rsp) and copies 48-byte entries with vmovups. One of its stores faulted on a write-tracked view and was resumed. Afterwards the slots below RSP held zeros and YMM upper halves. The loop stopped short and left zeroed entries, and GameThread later read 0x18 from a null pointer at app.exe+0x69cb959. That happened in 2 of 5 boots.

This builds on #5052. Only the last two commits are this pull request:

  • Leaves without a frame pointer (743ce53):
    • What gets framed. A leaf that starts with a chain of pushes of at least five bytes, and leaves through pop ...; ret exits of at least five bytes, gets the same prologue and exit stubs as feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052's leaves.
    • The operands. Each of its RSP operands is moved by the 128 bytes RSP is lowered by. A negative 8-bit displacement stays 8-bit, and a lea of a slot below RSP moves the same way, so the address it computes is unchanged.
    • What's skipped. A leaf is skipped when an operand can't move in place: one at or above RSP with an 8-bit displacement, or one without a displacement. It is also skipped when it copies RSP into another register (the one-byte, two-byte and VEX forms, such as cmov and shlx), and in every case feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052 skips.
  • Where a leaf's body ends (a8ed810). This is the case oneandonlydean raised on feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052, and it applies here with more force.
    • The problem. A leaf's body ran to the next known function start or int3 padding. A function reached only through a pointer, placed right after a leaf with no int3 between them, was framed as part of the leaf. Its RSP operands were moved and its exits raised RSP, although nothing had lowered it.
    • In Village: that happened once. The function at 0x1693a0 follows the ret of the leaf at 0x1691f0 with no padding, and is never called directly.
    • The fix. The body now ends at 16-byte-aligned code after a ret or ud2, and at the target of a jmp when only padding lies between them (a tail call to the next function). Code past such a point is framed only from a real function start.
    • Why not after every jmp. Aligned code after any other jmp stays in the body, because clang aligns rotated loop headers there. Cutting after every jmp skipped 2,129 of Village's 2,131 framed leaves.
    • What's left, in the debt entry: a pointer-only leaf right after a jmp to elsewhere, such as a loop's back edge, without int3 padding. Village has none (see Tested). If feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052 takes its bounds from the .eh_frame FDEs, as its review asks, this pull request uses them on rebase.

The debt entry and USAGE.md describe the new shape. Inside a moved frame, the CFA rules of the guest's unwind tables are 128 bytes off; the debt entry records it.

#5596, tibzejoker's port of #637, avoids the write fault itself for direct memory mapped at one address. Framing protects a leaf from any exception frame Windows writes below RSP while the leaf runs, whatever memory faulted, so the two cover different cases.

Tested

  • Windows 11, WinLibs GCC 15.2.0 r7, Release, on feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052's head (fab7d20): red_zone_framer.
    • New cases:
      • a leaf with a frame pointer keeps its operands;
      • a leaf that only pushes is framed: its prologue site covers the pushes, its exit site covers the pops and ret, and both its RSP operands are moved;
      • leaves whose RSP operands can't all move, or whose prologue is too short, are skipped with their reasons;
      • a leaf that copies RSP into another register is skipped;
      • a leaf reached without a call, right after another leaf, isn't framed as part of it;
      • a leaf that tail-calls the function after it is skipped;
      • a leaf whose loop header is aligned after a jmp is still framed.
    • With frameless leaves skipped, as on feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052's head, the frameless cases fail ("a leaf that only pushes was not framed").
    • The pointer-only case fails with the first commit and passes with the second.
  • GitHub's Linux and Windows runners, on my fork: red_zone_framer passes 20 of 20 runs. With the first commit's framer, the second commit's case fails (https://github.com/EagleBearerYS/AnyPS5/actions/runs/38498470455).
  • Full Windows and Linux builds and tests on my fork, with this branch merged into main feddf7f:
  • Resident Evil Village (PPSA01556, 01.210.000) on Windows 11 26300, Ryzen 9 5900HS (Zen 3, no AVX-512), RTX 3070 Laptop GPU, on a local branch past the title's other stops (Resident Evil Village (PPSA01556) #5169):
    • The relink with --frame-red-zone:
    • Against the unwind table (304,570 FDEs, read through PT_GNU_EH_FRAME, comparing the framed and unframed executables):
      • every rewritten function is framed at its own FDE start;
      • no function has rewritten bytes without its start being framed, and no rewritten byte lies outside every FDE;
      • before the second commit, 0x1693a0 was the one exception.
    • The boot crash: 2 of 5 boots without framing. With framing, 0 of 12: 10 at the first commit and 2 at the second.
  • python3 tools/check_conventions.py --base fab7d203f (feat(relinker): --frame-red-zone gives red-zone leaf functions a frame on Windows #5052's head): passed.

Checklist

parl0v and others added 3 commits October 13, 2026 18:46
…e on Windows

Windows writes an exception frame directly below RSP, so a fault resumed on
a guest thread overwrites values a System V leaf function keeps in its red
zone. With --frame-red-zone, a leaf function with a
push rbp; mov rbp, rsp; push ... prologue that reads or writes below its
pushes gets two kinds of trampoline: the prologue lowers RSP by 128 bytes
after its pushes, and each pop ...; pop rbp; ret exit raises it back first.

Sites a branch enters, sites shorter than a jump and sites over a --to-intel
stub are skipped; the conversion reports the framed and skipped counts.
… start

A leaf's body ran from its start to the next known start or int3 padding,
so a function reached only through a pointer, placed right after the leaf
without int3 padding, was framed as part of the leaf: its rsp operands were
moved and its exits raised rsp although nothing had lowered it.

The body now also ends at 16-byte-aligned code after a ret or ud2, and at
the target of a jmp when only padding lies between them (a tail call to the
next function). Code past such a point is framed only from a real start.
A leaf whose code continues past it is skipped as branching out of the
function. Aligned code after any other jmp stays in the body: loop headers
are aligned there, and cutting after every jmp skipped 2,129 of 2,131
framed leaves in PPSA01556. With this cut 2,132 are framed, and none of
them has rewritten bytes in another function of the unwind table.
@adriadam10 adriadam10 self-assigned this Oct 14, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the detailed write-up. Two blockers before this can be reviewed:

  • Base PR is closed. Depends on: #5052 was closed unmerged on 2026-10-14, so this branch still carries its whole RedZoneFramer (the diff against main is +659) plus Josip Parlov's commit. Either coordinate with them to reopen/land it, or resubmit as a self-contained PR with the authorship preserved.
  • Motivation has changed. #361 is now fixed at the source by #5596 (merged). On #5052 parl0v measured 0 crashes in 20 boots of PPSA25344 without the flag on main with #5596. Please re-measure the Village boot crash (2 of 5 without framing) on current main with #5596 included, with and without --frame-red-zone. If it no longer reproduces, there is nothing for this opt-in pass to fix.

If it still reproduces, also note that #5052's review asked for function bounds from the .eh_frame FDEs (EhFrameReader) rather than heuristics; the "end of body" rule added in a8ed810 (aligned code after ret/ud2, jmp to padding) is a heuristic that leaves a documented gap, so it should use the FDEs here too.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Re-measured on current main with #5596: Resident Evil Village (PPSA01556, 01.210.000), Windows 11 Home 10.0.26300, RTX 3070 Laptop (driver 617.42), unframed (relinked with main's relinker).

So there's nothing left for this pass to fix in Village, and I'm closing it. Thanks for the review. parl0v's .eh_frame version (parl0v/AnyPS5@3ff74b9) is the starting point if a title needs framing again.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants