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>
16 KiB
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 addscrt1.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),corebuilt 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 tostat/lstatgo to FreeBSD 11 versions, matching how it was built;scandir/alphasortare reimplemented;fts_*are stubs that fail withENOSYS, so the SDK's fts, which would hand FreeBSD 11 structs to Rust, never gets linked;syscalls.ois dropped. Its ~230 raw syscall stubs (dup,openat,unlinkat, ... andfstatat) 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 forENOENT). 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.fstatatis the FreeBSD 12 wrapper inps5-std-rtinstead.err_probechecks 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_REALTIMEandCLOCK_MONOTONICjumped 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 inkeventon the same socket survives. Sops5-std-rtmakes blockingaccept,recv,recvfromandrecvmsgwait inkeventfirst (honouringSO_RCVTIMEO) and only then make the call. Non-blocking sockets (tokio) are untouched. Blockingconnect,send/writeandpollaren'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 setPS5_PAYLOAD_SDK, and pointCARGO_TARGET_X86_64_SIE_PS5_LINKERat itsbin/prospero-clang) - Rust nightly with
rust-src(pinned byrust-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 thex86_64-sie-ps5triple, libclang silently drops every function declaration. - The Sony functions without SDK headers (
sceNotificationSend,sceHttp2*, ...) are declared incrates/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_hdrsection. That's why the target setsdefault-uwtable: without unwind tables the data shares a page with code, and the payload crashes when it reachesmain. 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
allocfeature,ps5registers libcmallocas the global allocator; just writeextern crate alloc;. Threads are pthreads from libkernel. There is nostd, so no thread-locals and no unwinding: a panic in any thread aborts the payload. app::install_title_dirchanges the console. Back up first.
Legal
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.hand 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.