From c0967216bfe04be0f9f1a9299f56bfd937a89997 Mon Sep 17 00:00:00 2001 From: SibteProf Date: Mon, 12 Oct 2026 18:21:01 +0500 Subject: [PATCH] fix(relinker): recognize SDK memory syscall trap patterns --- core/relinker/CMakeLists.txt | 13 +++ .../relinker/src/analysis/SyscallScanner.cpp | 42 ++++++++- .../relinker/tests/SdkMmanSyscallTests.cpp | 87 +++++++++++++++++++ .../relinker/tests/test_guest_syscall_trap.py | 60 +++++++++---- docs/dev/TechnicalDebt.md | 1 + docs/user/USAGE.md | 2 +- 6 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 core/relinker/relinker/tests/SdkMmanSyscallTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4d6fd565d..0ceaa0a54 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -181,6 +181,19 @@ if(BUILD_TESTING) add_test(NAME call_site_index COMMAND call_site_index_tests) set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(sdk_mman_syscall_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/SdkMmanSyscallTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp) + target_include_directories(sdk_mman_syscall_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include) + add_test(NAME sdk_mman_syscall COMMAND sdk_mman_syscall_tests) + set_tests_properties(sdk_mman_syscall PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index a7e80bcf0..f83eb1eea 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -50,6 +50,45 @@ enum class ForbiddenInstruction { Return, }; +static constexpr std::array SDK_MPROTECT_INLINE{ + 0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB, +}; + +static constexpr std::array SDK_MMAP_INLINE{ + 0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0, +}; + +static constexpr std::array SDK_MMAP_EXEC_INLINE{ + 0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF, +}; + +template +static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::array& pattern, std::size_t beforeSyscall, + const std::vector& matches, FileByteOffset codeOffset) { + if (syscallOffset < beforeSyscall || syscallOffset > limit) return false; + const auto start = syscallOffset - beforeSyscall; + if (pattern.size() > limit - start || !std::equal(pattern.begin(), pattern.end(), code.begin() + static_cast(start))) return false; + const auto instruction = std::lower_bound(matches.begin(), matches.end(), codeOffset + start, + [](const Codegen::InstructionMatch& match, FileByteOffset offset) { return match.Offset < offset; }); + return instruction != matches.end() && instruction->Offset == codeOffset + start; +} + +static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::vector& matches, FileByteOffset codeOffset) { + return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); +} + static ForbiddenInstruction ClassifyForbiddenInstruction( const std::vector& codeSection, const std::size_t opcodeOffset, @@ -158,7 +197,8 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; - if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && IsSdkSyscallStub(codeSection, opcode)) { + if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && + (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, matches, codeSectionOffset))) { trapped.push_back(opcode); std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; continue; diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp new file mode 100644 index 000000000..264cb6057 --- /dev/null +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +struct Fixture { Bytes code; std::size_t syscall; }; + +const std::vector Fixtures = { + {{0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB}, 42}, + {{0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0}, 17}, + {{0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF}, 23}, +}; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error(message); +} + +bool Rejected(const Bytes& code, bool trap, std::size_t size) { + try { + Relinker::MakeSyscallScanner(trap)->ScanCodeSectionForSyscalls(code, 0, size); + return false; + } catch (const std::runtime_error&) { + return true; + } +} + +void CheckFixtures() { + std::size_t rejectedMutations = 0; + for (const auto& fixture : Fixtures) { + Require(Rejected(fixture.code, false, fixture.code.size()), "default mode accepted an SDK inline syscall"); + for (const auto padding : {std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{64}}) { + Bytes code(padding, 0x90); + code.insert(code.end(), fixture.code.begin(), fixture.code.end()); + const auto trapped = Relinker::MakeSyscallScanner(true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); + Require(trapped == std::vector{padding + fixture.syscall}, "SDK inline syscall returned the wrong patch offset"); + } + for (std::size_t length = fixture.syscall + 2; length < fixture.code.size(); ++length) + Require(Rejected(fixture.code, true, length), "SDK matching read its suffix beyond the code section"); + for (std::size_t removed = 1; removed <= fixture.syscall; ++removed) { + const Bytes fragment(fixture.code.begin() + static_cast(removed), fixture.code.end()); + if (Rejected(fragment, false, fragment.size())) + Require(Rejected(fragment, true, fragment.size()), "SDK matching accepted a truncated prefix"); + } + for (std::size_t offset = 0; offset < fixture.code.size(); ++offset) { + if (offset == fixture.syscall || offset == fixture.syscall + 1) continue; + for (unsigned bit = 0; bit < 8; ++bit) { + auto code = fixture.code; + code[offset] ^= static_cast(1u << bit); + if (!Rejected(code, false, code.size())) continue; + Require(Rejected(code, true, code.size()), "SDK matching accepted a changed instruction sequence"); + ++rejectedMutations; + } + } + } + Require(rejectedMutations > 1000, "too few near-match rejection cases ran"); + Bytes embedded = {0x48, 0xB8}; + embedded.insert(embedded.end(), Fixtures.front().code.begin(), Fixtures.front().code.end()); + Require(Rejected(embedded, false, embedded.size()), "embedded fixture lost its syscall instruction"); + Require(Rejected(embedded, true, embedded.size()), "SDK matching accepted a prefix that starts inside an immediate"); + for (const auto& code : std::vector{{0xB8, 0xDD, 0x01, 0x00, 0x00, 0x0F, 0x05, 0xC3}, {0xCD, 0x80}, {0x0F, 0x34}, {0x0F, 0x07}}) + Require(Rejected(code, true, code.size()), "SDK trap mode accepted an unrelated privileged instruction"); + std::cout << "SDK inline syscall checks passed, " << rejectedMutations << " near-match rejections\n"; +} + +} + +int main() { + try { + CheckFixtures(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/test_guest_syscall_trap.py b/core/relinker/relinker/tests/test_guest_syscall_trap.py index 8a94d30f3..6971a52b6 100644 --- a/core/relinker/relinker/tests/test_guest_syscall_trap.py +++ b/core/relinker/relinker/tests/test_guest_syscall_trap.py @@ -4,38 +4,60 @@ import subprocess import sys import tempfile -from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture +from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture, pe_bytes_at, pe_sections GENERIC_STUB = bytes.fromhex( "48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 " "4c 8b 4c 24 08 0f 05 c3") NUMBERED_STUB = bytes.fromhex("48 c7 c0 09 00 00 00 49 89 ca 0f 05 c3") +MPROTECT_INLINE = bytes.fromhex( + "55 48 89 e5 53 50 89 d1 49 89 f0 48 89 fe f6 c1 04 75 31 48 63 d1 " + "b8 4a 00 00 00 48 89 f7 4c 89 c6 45 31 d2 45 31 c0 45 31 c9 " + "0f 05 0f 93 c1 48 89 c3 48 f7 db 84 c9 48 0f 45 d8 48 85 db") +MMAP_INLINE = bytes.fromhex( + "49 63 d6 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 48 89 c1 0f 93 c2 48 f7 d8 84 d2 48 0f 45 c1 48 85 c0") +MMAP_EXEC_INLINE = bytes.fromhex( + "44 89 f0 83 e0 fb 48 63 d0 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 0f 93 c1 49 89 c7 49 f7 df 84 c9 4c 0f 45 f8 4d 85 ff") def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-guest-syscall-trap-") as directory: work = Path(directory) - modules = work / "sce_module" - modules.mkdir() - module = bytearray(guest_fixture(b"\x90" * 6)) - module[0x400:0x400 + len(GENERIC_STUB)] = GENERIC_STUB - module[0x440:0x440 + len(NUMBERED_STUB)] = NUMBERED_STUB - (modules / "sample.prx").write_bytes(module) - source = work / "input.elf" - source.write_bytes(main_fixture()) - output = work / "output.elf" - result = subprocess.run( - [str(relinker), "--trap-sdk-syscall-stubs", str(source), str(output)], - capture_output=True, text=True, timeout=30) - assert result.returncode == 0, (result.stdout, result.stderr) + for windows in (False, True): + case = work / ("windows" if windows else "linux") + modules = case / "sce_module" + modules.mkdir(parents=True) + module = bytearray(guest_fixture(b"\x90" * 6)) + for offset, stub in ((0x400, GENERIC_STUB), (0x440, NUMBERED_STUB), + (0x450, MPROTECT_INLINE), (0x490, MMAP_INLINE), (0x4b8, MMAP_EXEC_INLINE)): + module[offset:offset + len(stub)] = stub + (modules / "sample.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / ("output.exe" if windows else "output.elf") + options = ["--windows"] if windows else [] + result = subprocess.run( + [str(relinker), *options, "--trap-sdk-syscall-stubs", str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (windows, result.stdout, result.stderr) - converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" - image = converted.read_bytes() - loads = elf_loads(image) - assert elf_bytes_at(image, loads, 0x1017, 3) == b"\x0f\x0b\xc3" - assert elf_bytes_at(image, loads, 0x104a, 3) == b"\x0f\x0b\xc3" + converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" + image = converted.read_bytes() + if windows: + sections = pe_sections(image) + base = next(section[1] for section in sections if section[0] == b".elf0") + converted_code = pe_bytes_at(image, sections, base, 0x100) + else: + converted_code = elf_bytes_at(image, elf_loads(image), 0x1000, 0x100) + expected = bytearray(module[0x400:0x500]) + for offset in (0x17, 0x4a, 0x7a, 0xa1, 0xcf): + assert expected[offset:offset + 2] == b"\x0f\x05" + expected[offset:offset + 2] = b"\x0f\x0b" + assert converted_code == expected, (windows, converted_code.hex(), expected.hex()) if __name__ == "__main__": diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 871c8b013..b4f95dd36 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -570,6 +570,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. +- [SDK memory-management syscall traps](../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - [Ordered not-equal, less-than and greater-than compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index a6857acce..8218a9595 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -60,7 +60,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | -| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | +| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error.