Yadiel Abner Rodriguez Jorge (neokuze)andClaude Opus 5.5 19a6ac08d2 Add make std-run, scripts/run.sh and println_demo
The loader gives the payload its connection as stdin/stdout/stderr, so
println! output already reaches the terminal running prospero-deploy.
scripts/run.sh (bash /dev/tcp) also forwards the keyboard to stdin and,
unlike prospero-deploy -i, returns as soon as the payload exits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:04:09 -04:00

ps5-rust

Write PS5 homebrew payloads in Rust: no_std for tiny ELFs, or full std with tokio when you need it.

Unofficial community project, not affiliated with Sony. See Legal.

This project is a Rust layer on top of the PS5 Payload SDK by John Törnblom. The SDK does the hard part: the toolchain wrappers, startup code, linker script, libc and Sony library stubs. All we add is Rust bindings, safe wrappers and a target definition. You need the SDK installed to build anything here.

Tested on a PS5 CFI-1215A, firmware 13.60: hello_notify, hwinfo, http_test, http_server and threads all run, and so do the std payloads hello_std and tokio_server (an async tokio server on a multi-threaded runtime).

What's inside

crates/ps5-sys/        raw bindings, generated by bindgen from the SDK headers
crates/ps5/            safe wrappers (no core::fmt; heap only with `alloc`)
crates/ps5-std-rt/     glue that makes Rust's std (and tokio) run on the PS5
targets/               x86_64-sie-ps5.json (no_std) and x86_64-sie-ps5-std.json (std)
examples/hello_notify  shows a system toast
examples/hwinfo        model + CPU temperature in a toast and klog
examples/http_test     PS5 as client: GET/POST to a server on your PC
examples/http_server   PS5 as server: the PC asks for info or sends toasts
examples/threads       heap + 4 pthreads summing Vecs, shared Arc<Mutex>
examples/pc/           PC side: test_server.py, ps5_client.py (+ .cmd launchers)
std-examples/          std payloads (own workspace, std target):
  hello_std            std::thread, std::sync, std::net, format!
  tokio_server         async HTTP server with tokio on port 9956
  kq_probe             diagnostics for the kqueue/fd calls tokio needs
  fs_probe             std::fs checks in a scratch dir under /data (port 9958)
  https_probe          DNS + HTTPS GET with rustls/ring, verified certs (port 9959)
  err_probe            syscall/std error reporting, writes nothing (port 9960)
  reqwest_probe        reqwest async + blocking over HTTP/HTTPS, redirect, timeout (port 9962)
  stdout_probe         where fds 0-2 go (the loader socket) and println! redirection (port 10000)
  rest_probe           clock gaps + listener across rest mode (port 10001; blocking, see below)
  rest_idle            rest-mode isolation: resident payload with no sockets
  rest_net             rest-mode isolation: non-blocking listener (port 10002)
  rest_block           rest-mode isolation: thread blocked in accept (port 10003)
  rest_kevent          rest-mode isolation: tokio waiting in kevent (port 10004)
  rest_recv            rest-mode: blocked read + read timeout (port 10005)
  udp_probe            std + tokio UdpSocket: loopback, timeout, broadcast, DNS to 1.1.1.1 (port 10006)
  println_demo         println!/eprintln! to the sender terminal + reading stdin

ps5 modules (each one behind a Cargo feature that links the Sony libraries it needs):

Feature Module Gives you
(none) fmt, klog, net, payload log!/print!/line!, kernel log, TCP sockets, payload args/exit, fw_version()
alloc heap, thread global allocator (libc malloc), Vec/Box/Arc, thread::spawn/join, Mutex
notify notify Toast (JSON toast), simple() popup
hw hw model, serial, CPU frequency/temperature, SoC temp
random random hardware RNG
system system user service, launch the web browser
app app app info by pid, install a title directory
http http HTTP(S) client over libSceHttp2
full all of the above

Anything without a safe wrapper is still available as ps5::sys::*: all of the SDK's ps5/*.h (kernel, klog, mdbg, iommu, nid, payload), the Sony functions above, and common POSIX/libc.

The x86_64-sie-ps5 target

Rust has no built-in PS5 target, so targets/x86_64-sie-ps5.json defines one. It uses the same name as the SDK's clang target (-target x86_64-sie-ps5):

  • the CPU is znver2 (the PS5's Zen 2) and the ABI is FreeBSD/SysV;
  • linking goes through the SDK's prospero-clang, which adds crt1.o, the SDK linker script, PIE and the default Sony libraries, so the ELF matches what the C samples produce;
  • panic = abort, no thread-locals (the SDK uses emulated TLS), core built from source.

cargo build produces a payload ELF directly. You don't need a separate link step.

std and tokio

targets/x86_64-sie-ps5-std.json is the same target, but it tells Rust the OS is FreeBSD so build-std compiles the real FreeBSD std against the SDK's libc. The PS5 kernel is FreeBSD 11 with some gaps, and Rust assumes FreeBSD 12, so payloads also depend on ps5-std-rt, which bridges the difference:

Problem on the PS5 What ps5-std-rt does
std links -lexecinfo, -lkvm, -lrt... which the SDK doesn't ship empty stand-in archives
accept4, pthread_setname_np, dl_iterate_phdr missing implements them
struct kevent is the 32-byte FreeBSD 11 layout; libc uses the 64-byte 12 one kevent converts and calls the syscall
fcntl(F_DUPFD_CLOEXEC) fails with EOPNOTSUPP (breaks try_clone and tokio) emulates it with F_DUPFD + F_SETFD
struct stat is the 120-byte FreeBSD 11 one; libc uses the 224-byte 12 one stat/lstat/fstat/fstatat call the FreeBSD 11 syscalls and widen the result
getrandom() doesn't exist (FreeBSD 12+); the getrandom crate (ring, rustls) needs it implements it over arc4random_buf
struct dirent has a 32-bit d_fileno and no d_off own opendir/readdir/closedir/fdopendir/dirfd over getdirentries, widening each entry (buffer sized from st_blksize: 64 KB on /data)
cd std-examples
cargo build --release -p tokio_server   # -> target/x86_64-sie-ps5-std/release/tokio_server
// a std payload: Cargo.toml depends on ps5-std-rt (and tokio, ps5, ...)
use ps5_std_rt as _; // keeps the glue linked

#[tokio::main]
async fn main() { /* ... */ }

std payloads are bigger (about 130 KB for hello_std, 480 KB for tokio_server) and show panics through a panic hook instead of vanishing.

Networking beyond tokio works too: DNS through std (getaddrinfo, e.g. TcpStream::connect(("example.com", 443))) and HTTPS with rustls + ring, certificates checked against webpki-roots (TLS 1.3, checked by https_probe). Crates with C code build with the SDK's compiler: std-examples/.cargo/config.toml sets CC_x86_64-sie-ps5-std and AR_x86_64-sie-ps5-std to prospero-clang/prospero-ar, otherwise cc-rs falls back to the host's gcc.

std::fs works (read/write, metadata, read_dir, copy, rename, canonicalize, remove_dir_all, checked by fs_probe), and so do the libc calls crates reach directly: fstatat, mkstemp/mkdtemp, scandir/alphasort.

Some of that needs a patched libc. The SDK's libc.a defines fstatat itself (a second definition would clash) and contains code compiled for the FreeBSD 11 layouts (mktemp, scandir, fts). So ps5-std-rt's build script makes a copy of the installed SDK's libc.a with llvm-objcopy (no SDK binaries are stored in this repo) and bundles it into its rlib, which the linker reads before the SDK's own:

  • mktemp's calls to stat/lstat go to FreeBSD 11 versions, matching how it was built;
  • scandir/alphasort are reimplemented; fts_* are stubs that fail with ENOSYS, so the SDK's fts, which would hand FreeBSD 11 structs to Rust, never gets linked;
  • syscalls.o is dropped. Its ~230 raw syscall stubs (dup, openat, unlinkat, ... and fstatat) don't check the carry flag, so on failure they return the errno value as if it were a result (e.g. openat "returns" fd 2 for ENOENT). The build script regenerates every one of them, with the SDK's own syscall numbers, plus the FreeBSD error epilogue (-1 + errno); a stub with an unexpected shape stops the build. fstatat is the FreeBSD 12 wrapper in ps5-std-rt instead. err_probe checks the error paths.

For this the std target sets no-default-libraries and lists the Sony libraries itself in late-link-args: prospero-clang would otherwise put the SDK's -lc ahead of everything.

The PS5 kernel doesn't implement every FreeBSD syscall: faccessat, readlinkat and eaccess return ENOSYS. ps5-std-rt emulates the first two with access and readlink when the path is absolute or relative to AT_FDCWD (the usual case); relative to another directory fd they stay ENOSYS, since FreeBSD 11 can't rebuild that path reliably. faccessat(..., AT_EACCESS) also stays ENOSYS: a payload runs with real uid 1 and effective uid 0, so access (which checks the real ids) would answer for the wrong user. Other things err_probe found on the console: /data doesn't support symlinks (EOPNOTSUPP).

clock_nanosleep is missing too (ENOSYS), and it returns its error number directly instead of -1/errno, so std's thread::sleep hit an assertion. ps5-std-rt implements it over nanosleep.

Rest mode

Measured on firmware 13.60 with the rest_* probes:

  • A payload survives rest mode: the process is frozen and carries on after wake. The clocks don't show how long it slept (both CLOCK_REALTIME and CLOCK_MONOTONIC jumped only about 13-17 s, one after the other), so a "woke up" check has to look for a gap in either clock.
  • A thread asleep inside a blocking accept() when the console enters rest powers the console off. A thread asleep in kevent on the same socket survives. So ps5-std-rt makes blocking accept, recv, recvfrom and recvmsg wait in kevent first (honouring SO_RCVTIMEO) and only then make the call. Non-blocking sockets (tokio) are untouched. Blocking connect, send/write and poll aren't covered or tested yet.
  • After wake, sockets fail with Sony's os error 163, and new sockets opened by payloads fail the same way until the console restarts. Plan for a payload that is alive but offline.

Requirements

On Linux or WSL (Debian/Ubuntu, Arch or Fedora), scripts/setup.sh installs all of this: the system packages, Rust nightly, the latest SDK release in /opt/ps5-payload-sdk, and PS5_PAYLOAD_SDK / PS5_HOST in your shell's startup file (bash, zsh or fish). It asks before each step that needs sudo, and it's safe to run again.

bash scripts/setup.sh

What it sets up:

  • Linux or WSL with the PS5 Payload SDK at /opt/ps5-payload-sdk (or set PS5_PAYLOAD_SDK, and point CARGO_TARGET_X86_64_SIE_PS5_LINKER at its bin/prospero-clang)
  • Rust nightly with rust-src (pinned by rust-toolchain.toml)
  • libclang (for bindgen)
  • A PS5 running an ELF loader on port 9021

Build and run the examples

make                                        # builds build/*.elf
make test EXAMPLE=hwinfo PS5_HOST=<ps5-ip>  # build + send to the PS5

make std                                                  # std examples -> build/std/*.elf
make std-test STD_EXAMPLE=tokio_server PS5_HOST=<ps5-ip>  # build + send one
make std-run STD_EXAMPLE=println_demo PS5_HOST=<ps5-ip>   # same + keyboard on stdin

The ELF loader hands the payload the connection it was sent over as stdin/stdout/stderr, so println!/eprintln! output shows up in the terminal that ran make std-test. make std-run (scripts/run.sh) also forwards what you type to the payload's stdin and returns as soon as the payload exits. If that terminal is closed while the payload still prints, the next println! panics (write to a closed socket); use writeln!(std::io::stdout(), ...) and ignore the error where that matters.

Every .elf goes through prospero-strip, which drops the symbol tables (about a quarter of a std payload: reqwest_probe goes from 2.7 MB to 2.0 MB). To skip typing the address, export PS5_HOST=<ps5-ip> once in the shell.

For http_test, start the server on your PC first, then build the payload with your PC's address:

python examples/pc/test_server.py
PS5_TEST_SERVER=http://<pc-ip>:8080 make test EXAMPLE=http_test PS5_HOST=<ps5-ip>

The PS5 shows the PC's greeting in a toast. The PC prints a report with the console's model, CPU temperature and firmware. The server's IP is fixed at build time, so rebuild if your PC's address changes.

For http_server it's the other way round: the PS5 listens on port 9954 and the PC is the client. Send the payload, wait for the "http_server running" toast, then:

python examples/pc/ps5_client.py   # asks for the PS5's IP, then a menu
curl http://<ps5-ip>:9954/info
curl "http://<ps5-ip>:9954/notify?msg=hello"
curl http://<ps5-ip>:9954/quit      # stops the server

On Windows, double-click examples/pc/start_test_server.cmd or examples/pc/start_client_ps5.cmd to launch them.

Use it in your own payload

In a new crate:

# Cargo.toml
[dependencies]
ps5 = { git = "https://github.com/neokuze/ps5-rust", features = ["notify", "hw"] }

[profile.release]
opt-level = "z"
lto = "fat"
codegen-units = 1

Copy targets/x86_64-sie-ps5.json, .cargo/config.toml and rust-toolchain.toml from this repo. They tell cargo how to build for the PS5.

// src/main.rs
#![no_std]
#![no_main]

use ps5::{hw, log, Toast};

#[no_mangle]
pub extern "C" fn main() -> i32 {
    let temp = hw::cpu_temperature().unwrap_or(-1);
    log!("cpu ", temp, "C");
    match Toast::new("Hello from Rust").send() {
        Ok(()) => 0,
        Err(e) => e.0,
    }
}

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}
cargo build --release   # -> target/x86_64-sie-ps5/release/<name>

Notes

  • bindgen parses the SDK headers as x86_64-unknown-freebsd. With the x86_64-sie-ps5 triple, libclang silently drops every function declaration.
  • The Sony functions without SDK headers (sceNotificationSend, sceHttp2*, ...) are declared in crates/ps5-sys/include/sce_extra.h, with signatures copied from the SDK samples. The struct layouts there (app_info_t, notify_request_t) also come from the samples and haven't been checked against firmware.
  • The SDK's linker script keeps code and data on separate pages only through the page-aligned .eh_frame_hdr section. That's why the target sets default-uwtable: without unwind tables the data shares a page with code, and the payload crashes when it reaches main. The target also uses full relro, because that's the only way rustc emits GOT calls (GLOB_DAT) instead of PLT ones, matching the SDK's -fno-plt.
  • With the alloc feature, ps5 registers libc malloc as the global allocator; just write extern crate alloc;. Threads are pthreads from libkernel. There is no std, so no thread-locals and no unwinding: a panic in any thread aborts the payload.
  • app::install_title_dir changes the console. Back up first.

This project is not affiliated with, endorsed by, or sponsored by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc., used here only to say which hardware the code runs on.

It contains no Sony code, SDK, firmware or keys. The bindings are generated from the open-source PS5 Payload SDK's headers, and the Sony system functions are only declared (name and signature) so payloads can call what's already on the console.

This is for homebrew and for running your own software on hardware you own. It doesn't include, and won't help with, anything for running pirated games. Use it at your own risk: the software comes with no warranty (see the license).

Credits

  • John Törnblom: author of the PS5 Payload SDK, the toolchain and samples this project is built on. The signatures in sce_extra.h and the HTTP2 call sequence come from his samples.
  • Aleksei Kulaev: contributor to the SDK's linker wrapper.
  • The ps5-payload-dev community.

License

GPL-3.0-or-later, the same license as the PS5 Payload SDK. See LICENSE.

This isn't optional. The bindings are generated from the SDK's GPLv3 headers, and every payload ELF statically links the SDK's GPLv3 startup and kernel code. If you distribute a payload built with this library, its source must be available under the GPL too.

Languages
Rust 90.3%
Shell 5.4%
C 2.4%
Makefile 1.9%