Commit Graph
65 Commits
Author SHA1 Message Date
Adrià 8afd6ea478 Merge pull request #7660 from savvaniss/fix/sce-code-gaps
fix(relinker): resume scans after undecodable bytes between PS4 unwind functions
2026-10-20 13:17:15 +02:00
Dean d85b7c759a Merge pull request #2780 from SuarezPM/ai/relinker-hardening
feat(relinker): lower MCOMMIT and RDPRU for Intel with execution tests
2026-10-20 16:22:49 +13:00
Adrià 34b1b77abf Merge pull request #7651 from DavidSamir/fix/relinker-amd-only-branch-targets
fix(relinker): ignore direct branch targets inside an instruction past its prefixes
2026-10-20 02:13:47 +02:00
SuarezPM bf47697ec0 feat(relinker): lower MCOMMIT and RDPRU for Intel with execution tests
MCOMMIT becomes STC plus NOP padding and RDPRU goes through a
runtime-checked stub returning the host TSC for MPERF/APERF while
other selectors yield zero, with converter and execution tests.

docs/dev/debt/relinker.md records the new lowering and its unverified
assumptions.
2026-10-19 19:19:31 -03:00
plokmklo c1866af79b fix(relinker): synthesize PT_GNU_STACK for Linux outputs without one
Outputs whose input carries no PT_GNU_STACK get one appended with
PF_R|PF_W and alignment 16 so the kernel does not fall back to an
executable stack marking.

RequiredProgramHeaderCount and WriteLayout now derive from one shared
header list, so the reserved slot and the written header cannot
disagree at any PhNum boundary, when a free input slot should be
reused, or when the RELRO coverage guard reduces the kept count.
Space exhaustion beyond the 16-bit header limit fails loudly instead
of silently skipping the header.
2026-10-19 07:30:56 +00:00
plokmklo d4cdfe0639 test(relinker): locate Linux stub segments via e_phoff
The program header table moves when the layout needs more slots than
the input provides, so the stub lookup must follow the output e_phoff
instead of assuming the table stays at file offset 64.
2026-10-19 07:30:56 +00:00
Savvanis SpyrosandClaude Opus 5.5 45a2e3dd4a fix(relinker): resume scans after undecodable bytes between PS4 unwind functions
In PS4 executables and modules the bytes between unwind functions are
16-byte slots of function tails and padding, and a tail can begin inside
an instruction, such as a lone REX byte before a 15-byte NOP. The
syscall and call-site scans decode the executable segment linearly, so
such a slot failed the relink with "Cannot decode instruction".

For images with a PT_SCE_DYNLIBDATA segment the scans now receive the
unwind function ranges. Outside every range an undecodable byte is
skipped and decoding resumes at the next byte, and a decoding that would
cross into a range ends at its start, so gap bytes cannot change how a
function is decoded. Inside a range a decode failure still fails the
relink, and images without unwind ranges are scanned as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:45:49 +03:00
David 5c2062cc0a test(relinker): cover branches into converted instructions past their prefixes 2026-10-19 00:10:07 +04:00
Karim Masmoudi 57e52c1f03 fix(relinker): report processor 0 from RDTSCP under --to-rosetta
Rosetta sets RDTSCP's ECX to its constant TSC_AUX, 0xAAAAAAAB, which is
not a processor index. --to-rosetta already makes RDPID return processor
0 for the same reason, so code reading the processor through RDTSCP saw
a different value from code reading it through RDPID.

--to-rosetta now moves RDTSCP into an optional stub that runs the
instruction unchanged and then loads 0 into ECX with a MOV, which
leaves the counter in EDX:EAX and the flags as they were. A site with no
movable instruction after it for the jump is kept, as for RDPID. The
Intel target and a plain relink are unchanged.
2026-10-18 18:27:15 +02:00
Dean 5b7ec4a9bd Merge pull request #7272 from OskarLGS/fix/relinker-decoder-osize-branches
fix(relinker): decode near branches with an operand-size prefix as AM…
2026-10-18 12:56:40 +13:00
Morten HolmandClaude Sonnet 5.5 cf9e5a2567 test(relinker): keep an FS or GS access out of the window of a 256-bit store
Covers a thread-pointer read (FS and GS form) after the store with a movable
instruction before it, and the fallback to the following access when there is
no other neighbour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 00:40:41 +02:00
oskarlgsandClaude Opus 5.5 206aaa56cb fix(relinker): decode near branches with an operand-size prefix as AMD does
66 E8, 66 E9 and 66 0F 80-8F without REX.W take a 16-bit displacement on
AMD processors, the console included. Every near relative branch with that
prefix truncates its target to 16 bits, so the decoder reports no branch
target for it and stubs refuse to move it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TFMtUC4L4wMS4eeEqaNSR
2026-10-17 20:58:29 +00:00
javierUDP d67e7b7389 test(relinker): link the converter tests with a small macOS zero page
The CLZERO case that needs a 32-bit address found no mapping because a
64-bit Mach-O executable reserves 4 GiB as `__PAGEZERO`, not because
Rosetta withholds low memory: the same `mmap` succeeds once the
executable is linked with a smaller zero page. The Apple x86-64 build
links the test that way and the case runs with the other three, so the
skip it had is gone and the Linux mapping failure stays fatal. The arm64
build keeps the 4 GiB zero page, which it requires to start.
2026-10-15 17:10:59 -03:00
javierUDP 6f100d3f61 test(relinker): execute the AMD64-only conversion stubs on macOS
The execution part of the AMD64-only converter tests was compiled for
Linux x86-64 only, so the SSE4A, SHA-1, SHA-256, CLZERO and reciprocal
stubs were never run on a macOS x86-64 build, unlike the ANDN lowering
tests. Rosetta maps no memory below 4 GiB, so the CLZERO case that needs
a 32-bit address is left out when that mapping is unavailable.
2026-10-15 15:40:26 -03:00
Adrià 15a60e1455 Merge pull request #5762 from AlexOfTheForest/fix/rosetta-legacy-compare
fix(relinker): move legacy CMPSS and CMPSD out of line for Rosetta
2026-10-15 20:34:12 +02:00
Adrià 3bb409b603 Merge pull request #5784 from gguerra26/fix/macos-m1-andn
fix(relinker): lower ANDN in Rosetta conversion
2026-10-15 19:25:06 +02:00
Adrià 5c0cc1bba4 Merge pull request #5755 from AlexOfTheForest/fix/decoder-rip-far-indirect
fix(relinker): report RIP-relative operands of far indirect CALL and JMP
2026-10-15 15:58:16 +02:00
gguerra26 9b92c55edf fix(relinker): lower ANDN in Rosetta conversion 2026-10-15 15:00:43 +02:00
Alexej Szpevák ba4b2db238 fix(relinker): move legacy CMPSS and CMPSD out of line for Rosetta
The Rosetta split covered only VEX scalar compares. #5642 measured the
same lost load after legacy CMPSS and CMPSD, and the decoder already
hands them to the matcher with the right length and RIP offset.

Recognise F3 or F2, segment overrides and an optional REX before
0F C2 /r ib, with the same predicate rule as the VEX forms. A 66, 67 or
LOCK prefix, or both F2 and F3, keeps the instruction inline as before.
2026-10-14 11:48:11 +02:00
Alexej Szpevák 8e812f4b73 fix(relinker): report RIP-relative operands of far indirect CALL and JMP
DecodeInstruction returned before reading the ModRM of FF /3 and FF /5,
so a far CALL or JMP through a RIP-relative pointer had no RIP-relative
displacement. A stub that moved such a JMP copied the displacement
without relocating it.

The decoder was compared with iced-x86 (AMD decoding) over 3.99 million
encodings: every opcode of the legacy, 0F, 0F38 and 0F3A maps under 34
prefix combinations and the VEX 0F, 0F38 and 0F3A maps, each with 48
ModRM forms. Apart from this, it agrees on length, RIP-relative offset,
control flow kind and branch target for every valid Zen 2 instruction
except near branches with a 66 prefix, recorded as technical debt, and
kernel-only returns, which it treats as sequential.

Add the comparison's metadata as cases: one RIP-relative encoding per
opcode, map and immediate size, every FF /r form, and the branch forms
with their flow kind and target. The existing cases check lengths only;
a loop or jrcxz decoded as an unconditional jump passed the whole suite.
2026-10-14 11:33:59 +02:00
Leandro Soares 8b400afc5d fix(relinker): move hazard scalar compares out of line for Rosetta 2026-10-14 06:19:58 +00:00
Dean e305bc4e4a Merge pull request #4483 from fable-void/pr/relinker-jump-table-targets
fix(relinker): treat jump table targets as branch targets when moving instructions
2026-10-13 07:28:12 +13:00
Dean f1514adea3 Merge pull request #4060 from coltken95/claude/sleepy-dirac-e44kw4
fix(relinker): decode 3DNow! instructions with their ModRM and opcode…
2026-10-13 02:35:21 +13:00
fablevoid 1541244b78 fix(relinker): treat jump table targets as branch targets when moving instructions
The AMD-only converter moves the instructions next to a short AMD-only
instruction into its stub, and refuses to move one that a branch
enters. It knew only direct jump and call targets, so a switch case
entered through a jump table could land inside the jump written over
the moved instructions.

The converter now also reads relative jump tables: a RIP-relative LEA
whose address lies in a loaded segment starts a table of 32-bit
offsets from that address, read while each target starts an
instruction of the segment being converted. To read them, Convert
takes the program headers and converts their executable load
segments; the relinker passes all headers, and
ElfReader::ReadCodeSegments, now unused, is removed.

The converter test builds a dispatch through such a table whose second
case enters the instruction after a 4-byte 256-bit store, and checks
that the store takes the instruction before it instead. It also checks
that reading stops at an entry that does not start an instruction,
that only a LEA forms a table address, that tables are read only
inside loaded segments, that only executable load segments are
converted, and that a segment past the end of the file fails the
conversion.

PPSA12544: one 256-bit store in Il2cppUserAssemblies moved the
instruction after it, which is case 1 of a switch; it now goes through
an island. The other relinked files are unchanged.

AI-assisted: yes
2026-10-12 13:06:23 +00:00
fablevoid c763a78b67 feat(relinker): split unaligned 256-bit stores for Rosetta
Rosetta 2 ends the process when an unaligned 256-bit store crosses into
a page the store may not write ("rosetta error: unexpectedly need to
EmulateForward on a synchronous exception") instead of raising the
fault. A store inside such a page, and a 128-bit store across one,
fault normally. The macOS write watch keeps watched pages read-only
between collections, so a guest that stores 32 bytes across the start
of a watched page ends the process.

--to-rosetta now lowers VMOVUPS, VMOVUPD and VMOVDQU with a 256-bit
memory destination to a stub that stores the low half with the same
instruction at VEX.L=0 and the high half with VEXTRACTF128 at the
operand plus 16, so each half faults like any other store. RIP-relative
operands are relocated from the instruction's place in the site. A
store whose upper half has no 32-bit displacement fails the conversion.
Aligned stores (VMOVAPS, VMOVAPD, VMOVDQA and the non-temporal forms)
cannot cross a page and are left alone.

Every store is lowered. A 4-byte store has no room for the jump to its
stub, so its stub also takes one neighbouring instruction. The
following one: a return ends the stub, a call pushes its original
return address and jumps, a jump or conditional branch is re-encoded
with a 32-bit displacement, and another 256-bit store is lowered in the
same stub. Otherwise the preceding one, unless it is a call, a jump, a
return or a trap. Register calls, LOOP, JRCXZ and branches with a
prefix that changes them do not move. When neither neighbour can move,
the store becomes a short jump to an island: a run of movable
instructions within reach is moved to its own stub, and the bytes it
frees hold the jump to the store's stub. A store with neither fails the
conversion.

The converter tests compare the halves with clang's encodings for base,
SIB, disp8 overflowing to disp32, disp32, RSP-relative, FS-absolute,
r13-based and RIP-relative operands, and check that aligned stores,
loads, register forms and masked stores are not split. They check the
stub of a short store followed by a return, a call (direct, with an
address-size prefix, and through a RIP-relative pointer), short, near
and RIP-relative jumps, short and near conditional branches and another
store; that a following register call, LOOP, JRCXZ, prefixed branch or
CLWB makes the store take the instruction before it; islands past
instructions already moved, a store that needs its own stub, a branch
target, a register call and the short-jump reach; and that a store with
no room fails.

PPSA12544: 55295 stores split in the executable, 278 of them in the
stub of the store before them; every module converts with none kept.

AI-assisted: yes
2026-10-12 02:48:33 +00:00
Dean 2263cefe25 Merge pull request #3601 from Scramblrrrr/codex/legacy-sse-reciprocals
fix(relinker): lower legacy packed reciprocal register instructions
2026-10-12 15:43:20 +13:00
Claude 36c78ba143 fix(relinker): report the ModRM of 3DNow! instructions in decoded instruction info
DecodeInstruction did not treat 0F 0F as having a ModRM byte, so a
RIP-relative 3DNow! operand was not reported to the analyses and the
--to-intel stub relocation that read HasRipRelativeDisp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb
2026-10-11 22:34:39 +00:00
Claude feb19a844d fix(relinker): decode 3DNow! instructions with their ModRM and opcode suffix
0F 0F was decoded as a two-byte instruction. 3DNow! instructions are
0F 0F ModRM [SIB] [displacement] imm8, the trailing byte selecting the
operation (AMD64 Architecture Programmer's Manual Volume 3, Appendix A,
3DNow! opcodes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZWLdTjf1S57iLgn2rcaFb
2026-10-11 22:21:32 +00:00
MrMha e5c280fed4 fix(codegen): reject rewrites inside instructions 2026-10-11 19:56:00 +04:00
Scramblrrrr 363d17985e fix(relinker): lower legacy packed reciprocal register instructions 2026-10-11 10:58:14 -03:00
ethan f2ef0f9557 fix(relinker): reject x86 instructions exceeding 15 bytes
(cherry picked from commit 7243caa56ed79fab09edd23d92851e2508671775)
2026-10-11 03:02:05 -07:00
mugurc 92b20608e8 feat(relinker): convert for Rosetta with --to-rosetta
Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE, but, unlike an Intel
host, it has neither RDSEED, CLWB nor RDPID. --to-rosetta converts
everything --to-intel does and lowers these three as well:

- RDSEED becomes RDRAND (the /6 form of the same opcode), which also
  returns a random value and sets CF.
- CLWB only writes a cache line back, so it becomes a NOP of the same
  length.
- RDPID becomes a load of 0 through an optional stub: Rosetta's TSC_AUX
  is a constant, not a processor index, so every thread reports
  processor 0. A site with no movable instruction after it for the jump
  is kept.

The flag implies --to-intel and conflicts with --windows. It is
independent of --macos: it only changes what the conversion produces, so
--macos alone leaves these instructions as they are, as on an Intel Mac,
and a title for Rosetta is relinked with --macos --to-rosetta. Intel
output is unchanged: the target is a parameter of the converter, Intel by
default.
2026-10-10 22:46:45 +03:00
Dean 6f262eb0f4 Merge pull request #2568 from oneandonlydean/fix/relinker-decode-xop-2559
fix(relinker): decode XOP instructions
2026-10-10 19:47:50 +13:00
Achraf ReyaniandClaude Opus 5.5 ce8dbc977f fix(relinker): decode XOP instructions
The x86 decoder had no XOP (AMD 8F escape) support and read 8F E8.. as
POP r/m, losing instruction boundaries. Since #2238 roots code addresses
taken by RIP-relative LEA, code analysis reaches CPU-dispatched XOP
paths, such as the vpperm routines in Stremio Plus (PPSA74126), OpenNOW
(PPSA99082) and PPSA99420, and their Windows conversion failed with
"Code analysis: overlapping instruction boundaries". Decode 8F followed
by a map of 8 or more as a three-byte XOP prefix (8F can only be POP r/m
with ModRM.reg 0) with a ModRM operand, an 8-bit immediate in map 8 and
a 32-bit immediate in map 0Ah, in both the length and operand decoders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:27:24 +13:00
jweidenh 25984a728c fix(relinker): count all synthetic Linux program headers 2026-10-10 01:38:07 -04:00
Dean 3aae312a4b Merge pull request #1552 from arvindfroi/test/decoder-length-table
test(relinker): check decoder lengths against an LLVM-generated table
2026-10-10 18:36:17 +13:00
Adrià 9e2ffe0da4 Merge pull request #1457 from drk1rd/fix/relinker-ud1-trap
fix(relinker): decode UD1 as a trap
2026-10-08 16:17:06 +02:00
Arvind FrøilandandClaude Opus 5.5 0858f01429 test(relinker): check decoder lengths against an LLVM-generated table
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:09:18 +09:00
xsvm 22ebf5e608 fix(relinker): classify rewrite reference sites with the instruction decoder
ClassifyInstruction assumed a ModRM byte right after every opcode that
was not a near branch. A length-changing rewrite therefore adjusted
immediates such as MOVABS imm64 that looked like a RIP-relative ModRM,
and missed RIP-relative operands of VEX and 0F 38 / 0F 3A instructions
and the rel8 targets of LOOP and JRCXZ. Take the RIP-relative
displacement and branch target from DecodeInstruction instead.
2026-10-08 18:20:14 +08:00
Suryansh Prajapati 7a3ec9f015 fix(relinker): decode UD1 as a trap
UD1 (0F B9 /r) always raises #UD, like UD2, but DecodeInstruction
reported it as sequential. Reachability analysis then followed the
bytes after it, so an import reachable only through that fallthrough
was kept by the strict unused NID filter. UD1 keeps its ModRM, so a
RIP-relative operand is still reported.
2026-10-08 15:26:54 +05:30
Adrià 73ac9ef7f4 Merge pull request #1218 from hashaam1217/feat/relinker-rip-relative-stubs
feat(relinker): relocate RIP-relative operands into --to-intel stubs
2026-10-08 11:40:17 +02:00
Dean 9e5ade70fb Merge pull request #1381 from overlordxrz-source/fix/relinker-decoder-0f-no-modrm
fix(relinker): decode 0F A8-AA without a ModRM and MOV CR/DR without a memory operand
2026-10-08 22:35:44 +13:00
Dean a753d0de45 Merge pull request #1303 from overlordxrz-source/fix/relinker-evex-grp3-immediate
fix(relinker): do not add the F6/F7 TEST immediate to EVEX instructions
2026-10-08 22:35:39 +13:00
Hashaam ZafarandClaude Opus 5.5 a571a7f100 feat(relinker): relocate RIP-relative operands into --to-intel stubs
SHA-1 and SHA-256 instructions with a [rip+disp32] operand, and
instructions with one that a short site absorbs, now move into the
stub. The stub records each displacement as a relocation relative to
the site, and the Linux, guest and Windows placements resolve it once
the stub address is known. Indirect branches through [rip+disp32]
still fail the relink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:10:38 +02:00
Adrià 2959dc5318 Merge pull request #1168 from oguzhanozfe/fix/decoder-moffs-length
fix(relinker): decode the 64-bit address of MOV moffs instructions
2026-10-08 10:39:59 +02:00
overlordxrz-source e66d78c626 fix(relinker): decode 0F A8-AA without a ModRM and MOV CR/DR without a memory operand
PUSH GS, POP GS and RSM (0F A8, 0F A9, 0F AA) have no ModRM byte, but fell in the 0F A3-AB ModRM range, so the decoder read the next byte as a ModRM and sized its displacement. MOV to and from control and debug registers (0F 20-23) ignore the ModRM mod field and always name a register, but were sized and reported as memory forms, including a RIP-relative disp32 for mod 00 r/m 101.
2026-10-07 21:14:30 -04:00
overlordxrz-source f4ef1cd6ec fix(relinker): do not add the F6/F7 TEST immediate to EVEX instructions
The decoder adds the TEST immediate of opcodes F6 and F7 when ModRM.reg
is 0 or 1. The check excluded VEX and two-byte opcodes but not EVEX, so
an EVEX.0F F6 (VPSADBW) whose ModRM.reg is 0 or 1 was given an extra
imm8 and decoded one byte too long. VPSADBW has no immediate (Intel SDM
Vol. 2B, VPSADBW).
2026-10-07 18:51:05 -04:00
Oguzhan OzdemirandClaude Opus 5.5 e8f73a5531 fix(relinker): decode the 64-bit address of MOV moffs instructions
A0-A3 (MOV AL/eAX/rAX to and from moffs) have no ModRM byte and carry an
8-byte absolute address in 64-bit mode, or 4 bytes with a 0x67 prefix.
The decoder added nothing for the address, so the linear scanner decoded
the address bytes as code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:00:37 +03:00
Oguzhan OzdemirandClaude Opus 5.5 d64c0605ea fix(relinker): let REX.W override 0x66 when sizing decoder immediates
In 64-bit mode REX.W takes precedence over the 0x66 operand-size prefix,
so the immediate of 81, C7, 69, 68, A9, the 05..3D accumulator forms and
the F7 TEST form stays imm32. The decoder only applied this to B8+r and
returned a length two bytes short for the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:51:56 +03:00
DotDebianandClaude Opus 5.5 e11b4b5554 feat(relinker): lower SHA-1 instructions with a memory operand
SHA1RNDS4/NEXTE/MSG1/MSG2 with a memory source made `--to-intel` fail
the whole relink. The stub now spills a scratch xmm, loads the operand
into it with the unaligned load already used for SHA-256 and runs the
register-form lowering. The SHA1RNDS4 immediate is read after the SIB
byte and displacement. RIP-relative operands still fail: the stub runs
at a different address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:02:13 +02:00