Files
ps5upload/payload
phantomptrandClaude Opus 5.5 02469c725b cheats: write game memory through its physical pages, all-or-nothing
Every Marvel's Wolverine cheat crashed the console; CheatRunner's worked.
Our engine PT_ATTACHed the game, set the protection byte of the vm_map
entry holding the address to RWX with kernel_mprotect, wrote with PT_IO,
and "restored" it to R-X. Measured on the Pro (FW 13.60): the game's
.text is one 95 MiB entry (0x400000-0x6324000), execute-only (0x04), so
each toggle made the whole executable RWX under a copy-on-write PT_IO
write and left it R-X. kernel_mprotect edits the entry byte directly (no
pmap update, no split); CheatRunner traced two kernel panics to that
call and moved to the page-table walk this commit uses.

- Read/write through kernel_proc_getpaddr + the direct map (mdbg as a
  fallback), inside kernel_rw_lock; read back after every write. No
  ptrace stop, no protection change.
- Apply a cheat as a whole: every location is read and checked against
  its cheat and original bytes before anything is written; on a write
  failure the earlier writes are undone. The old engine wrote whichever
  locations matched, so a jump could land without its code cave.
- Write code caves before the jumps into them when turning on, and the
  reverse when turning off (cheats_plan.c, host-tested on the real
  Wolverine pack).
- Refuse a cheat the parser had to truncate, and a base of 0.
- Toggle state is keyed by the flat index across all of a title's files;
  a toggle in the second file used to mark the first file's mods, which
  the watcher then re-applied at the next launch.

Verified on the Pro with Wolverine running: God Mode, Max Resources/XP
(20 locations), Infinite Item Charges and an MC4 Infinite Health on and
off, 60 s each, game and console stayed up; a clashing MC4 cheat was
refused with nothing written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
2026-10-18 11:27:08 -07:00
..

Payload

The C payload that runs on the PS5. Built with the PS5 Payload SDK (version pinned in ../scripts/ps5-sdk.env), sent to the console's ELF loader, and left resident until reboot or rest mode.

It listens on one port, 9120, for AVA1 (protocol/ava1/SPEC.md): transfers, status, filesystem, mount, app, hardware, package and shell calls all travel over one encrypted session with up to 8 data lanes.

Layout

src/main.c handles startup: credential elevation, runtime ownership, the takeover, starting the AVA1 server, and cleanup. It binds no socket of its own. src/runtime.c holds the management handlers the AVA1 table (src/mgmt_table.def, dispatched by src/mgmt_rpc.c) calls, and the instance lifecycle (ownership record, reap, shutdown). ava1/ is the AVA1 protocol in C (frames, generated codecs in ava1/gen/, Noise handshake, server); never edit ava1/gen/ by hand. src/takeover.c asks an older resident payload to stand down before binding.

The rest of src/ is one module per capability: registration and launch, package install, hardware and sensors, processes, profiles, saves and backup, cheats, remote play, fan curve, notifications, system registry and time, firmware spoofing, and the SDK version changer.

installer/ builds our own standalone install daemon (ps5upload-installer.elf, TCP :9115) used for package installs on newer firmware. It loads as a companion image on the :9021 loader (never evicting the helper), self-escalates, and answers a JSON-lines protocol (hello/install/job/stop); for a staged file it serves the package to Sony's installer over a 127.0.0.1 HTTP URL. The engine sends it automatically when nothing is listening on :9115.

Prior art

The installer daemon is our own implementation. Its design was informed, as behavioural references only (no code or names are carried into the daemon), by cy33hc's ps5-ezremote-dpi, etaHEN and elf-arsenal's DPI payloads, and itsPLK's on-console PKG Manager (the loopback-serving + cacheability rules).

Build

export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
make -C .. payload        # → payload/ps5upload.elf (+ .gz)
make -C .. send-payload PS5_HOST=<ip>

Everything compiles with -Wall -Wextra -Werror.

Testing

Logic that can be separated from the console lives in a header under include/ and gets a host-compiled self-test in tests/, run by make test-payload with the same warning flags.

See tests/*_selftest.c for the full list (hardware guards, ptrace recovery, app.db reading, SDK param rewriting, installer, cheats, wake watchdog and more). AVA1 itself is tested from Rust: cargo test -p ava1-ctest -- --test-threads=1 in engine/ compiles ava1/ on the host and checks it against the Rust side.

Prefer adding to this set over testing on hardware: a host self-test runs in milliseconds and can't wedge a console.

Things that will bite you

  • Never rename() across mounts. It panics the kernel. Guard on st_dev.
  • Nothing large on a thread stack. Threads that need room ask for it explicitly (512 KiB–1 MiB); the default is small. A 256 KiB buffer in an FTP handler once overflowed it and wedged consoles hard enough to need a power cycle — bulk buffers are heap-allocated now.
  • Every blocking wait needs a bound. Unbounded ptrace waits froze consoles; Sony IPC init could hang the DPI daemon. Both are now timed, and a timed-out attempt is never restarted concurrently.
  • JSON keys must be snake_case, and any interpolated string must be escaped. The engine parses with serde: a wrong key silently yields a default value, and one bad byte rejects the entire response.
  • Some Sony libraries simply aren't on the console. libSceSqlite.sprx is absent under every lib path, so dlsym(RTLD_DEFAULT, "sqlite3_*") fails on every firmware — not just some. Degrade honestly instead of blaming the firmware.
  • "The platform doesn't ship it" is not "we can't have it." That SQLite conclusion was right about Sony and wrong about us: a static library is just more of our own .text. The payload now links its own SQLite (content_db.c, built from the amalgamation that scripts/install-ps5-sdk.sh fetches). Two full SQL implementations had been sitting behind that dead dlsym probe long enough to drift apart and hardcode different table names, and a third — in register.c — was a table of function pointers that nothing ever assigned. A branch that can never execute is where bugs go to hide, so prefer deleting it to keeping it as a fallback that never fires.
  • Check what a database says its schema is. content_db.c finds the app table through sqlite_master and PRAGMA table_info rather than naming it, because the two dead implementations disagreed about the name and there was no way to tell which was right.