Relapse: how does the PS5 jailbreak up to firmware 13.60 work?

An analysis of the exploit chain, from the browser to the kernel, using the code from the official release.

CONTENTS · 8 SECTIONS

Welcome to the very first post on my personal blog: it's a pleasure to have you here.

I decided to start this adventure and write down the curious ideas that come to mind, to keep track of them and to open a conversation about topics I care about, mostly cybersecurity.

I also did it to reconnect with writing in my own words (these days, who doesn't write their emails with the help of AI?).

Thanks again for your time.

GR


At the end of September 2026, Relapse was published on GitHub: an open source exploit chain that jailbreaks the PlayStation 5 on firmware 7.00 to 13.60. No hardware mods are needed. Everything starts from a web page opened in the console's built-in browser.

In this post we'll look at how Relapse is built, starting from the code of the official release: which stages make up the chain, what each one does and which limits to keep in mind. The goal is to understand what happens behind the scenes.

A little easter egg: the name is a nod to Lapse, a kernel exploit published in 2025 for older firmware.

Overview of the chain

Relapse works in two stages, one after the other, and ends by launching an ELF loader:

  1. Browser stage. It abuses a few bugs in JavaScriptCore, WebKit's JavaScript engine, to get arbitrary read and write access to the browser process memory.
  2. Kernel stage. Starting from that access, it exploits a race condition in the kernel. This gives it read and write access to kernel memory, root privileges and a sandbox escape.
  3. ELF loader. Once the chain completes, a loader listens on port 9021 and lets you run homebrew software.
DIAGRAMmermaid
flowchart TD
    A["Page opened<br/>in the PS5 browser"] --> B{"Supported<br/>firmware?"}
    B -- no --> X["Execution stops<br/>with a message"]
    B -- yes --> C["Stage 1: browser<br/>WebKit / JavaScriptCore"]
    C --> D["Stage 2: kernel<br/>race condition in aio_multi_wait"]
    D --> E["ELF loader<br/>listening on port 9021"]
    E --> F["Homebrew, emulators,<br/>tools, Linux"]

    classDef browser stroke:#e0a030,stroke-width:2px
    classDef kernel stroke:#e0607e,stroke-width:2px
    classDef ok stroke:#3fa66b,stroke-width:2px
    class C browser
    class D kernel
    class E,F ok

The project's README sums up the chain like this:

From the README

Browser stage uses JSC info leaks and a structured clone object pool mismatch to corrupt a typedarray. The kernel stage combines a address leak with an aio_multi_wait uaf race to establish kernel r/w.

In the next sections we'll try to make sense of these lines by breaking them down into simpler steps.

The firmware check

Before doing anything else, the page checks which console and which firmware it's running on. To do that it reads the User-Agent, the string the browser uses to identify itself to websites. On the PS5 the User-Agent also contains the firmware version, which is compared against the list of supported versions:

jssrc/firmware.js
const supportedFirmware = [
  "13.60", "13.42", "13.40", "13.20", "13.00",
  "12.70", "12.60", "12.40", "12.20", "12.02", "12.00",
  "11.60", "11.20", "11.00",
  "10.60", "10.40", "10.20", "10.01", "10.00",
  "9.60", "9.40", "9.20", "9.00",
  "8.60", "8.40", "8.20", "8.00",
  "7.61", "7.60", "7.40", "7.20", "7.01", "7.00",
];

// The PS5 User-Agent contains, for example, "PlayStation 5/13.60"
const firmwareUserAgent = navigator.userAgent;
const firmwareMatch = /PlayStation 5\/(\d+\.\d+)/.exec(firmwareUserAgent);
const firmwareVersion = firmwareMatch ? firmwareMatch[1] : "";

If the console isn't a PS5, or the firmware isn't on the list, execution stops with an explicit message:

js
rejection() {
  if (!firmwareUserAgent.includes("PlayStation 5")) return "PlayStation 5 Required";
  if (!firmwareVersion) return "FW version not found";
  if (!supportedFirmware.includes(firmwareVersion)) return `FW ${firmwareVersion} is not supported`;
  return null;
}

The versions are listed one by one because the locations of internal functions and structures change from one firmware to the next. The offsets/ folder contains one file for each of the 33 supported versions, and the page loads the one for the console in use.

Fact sheet Relapse
Supported firmware 7.00 to 13.60 (33 versions)
Consoles PS5 and, according to rumours mostly on X, the PS5 Pro too
Sony's patch firmware 14.00
Persistence none, it has to be run again after every reboot

Stage 1: the browser

The PS5 browser is based on WebKit, the same engine as Safari, best known to the public as the default browser on macOS and iOS, and JavaScript code is run by JavaScriptCore (JSC). It's a very complex component, optimised for performance, and that's exactly why it offers a large attack surface.

Relapse combines two kinds of bugs:

  • info leaks: flaws that expose the engine's internal information, such as the memory addresses where certain objects live;
  • a bug in structured cloning (structured clone), the mechanism the browser uses to make faithful copies of JavaScript objects. Under certain conditions, during the copy, the engine pairs an object with the wrong one.

To give you an idea, picture a photocopier that, while copying a notebook, swaps two pages. I know it sounds like a forced example, but I couldn't think of another one. Here the "page" obtained by mistake is a typed array, a structure that gives direct access to an area of memory. Once corrupted, the typed array can be made to point anywhere: that's how you get arbitrary read and write in the browser process memory.

An interesting detail is about addresses. JavaScript's Number type is floating point and loses precision above 2⁵³, while the console's memory addresses are 64 bits wide. That's why the code represents them as two 32-bit halves, with a small dedicated class:

jssrc/utils/int64.js (simplified excerpt)
export function int64(low = 0, hi = 0) {
  this.low = low >>> 0;   // low 32 bits
  this.hi  = hi  >>> 0;   // high 32 bits
}

int64.prototype.add32 = function (value) {
  const low = (this.low + value) >>> 0;
  const hi  = (this.hi + (low < this.low ? 1 : 0)) >>> 0; // carry handling
  return new int64(low, hi);
};

It's an ordinary addition with carry, used to work on 64-bit addresses with a numeric type that can't represent them precisely.

The whole sequence is orchestrated by src/site.js:

jssrc/site.js (shortened)
async function run() {
  const rejection = window.firmware.rejection();
  if (rejection) throw new Error(rejection);   // unsupported firmware

  writeLog("Credits: Sonic_Iso, Jordy, ntfargo, ufm42, Dr. Yenyen, ...", "info");
  writeLog(`Firmware: ${window.fw_str}`, "info");

  const primitive = await getPrimitive();      // stage 1: browser
  await import("./relapse_exploit.js");
  await main(primitive);                       // stage 2: kernel
}

A small flex from the developers: once the firmware check passes, the first thing the page prints on screen is the credits of the people who worked on the project.

Stage 2: the kernel

Controlling the browser isn't enough on its own. The browser process is confined in a sandbox and has no access to system resources. To get past that limit you have to compromise the kernel, the core of the PS5 operating system, which is derived from FreeBSD (nostalgia, huh?).

Relapse uses two ingredients:

  1. an address leak, which reveals where some kernel structures sit in memory;
  2. a race condition that causes a use-after-free in aio_multi_wait, a function of the asynchronous I/O subsystem.

A race condition is when the outcome of an operation depends on the order in which two threads run their instructions. A use-after-free, on the other hand, is a vulnerability that is still devastating today, and it mostly comes from the freedom programmers have in managing memory: one thread keeps using an object that another thread has already freed.

DIAGRAMmermaid
sequenceDiagram
    participant A as Thread A
    participant O as Kernel object
    participant B as Thread B
    A->>O: uses the object
    B->>O: frees the object (free)
    Note over O: the memory has been freed,<br/>but A still holds the reference
    A->>O: keeps using the reference
    Note over A,O: use-after-free

When the race succeeds, the kernel stage gets read and write access to kernel memory. From there it reaches root privileges and escapes the sandbox.

It's also the most delicate part of the chain, as the README itself warns:

From the README

The kernel exploit may hang or panic the console, so reboot before trying again if that happens.

By its nature, a race condition doesn't always succeed. The console may freeze or kernel panic: in that case it has to be rebooted before trying again.

The result: the ELF loader on port 9021

If both stages succeed, the chain ends with these messages:

jssrc/main.js (shortened)
if (result.payloads) {
  log("kernel exploit complete", "info");
  log("elfldr is listening on port 9021", "info");
} else {
  log("kernel chain complete: root and sandbox escape are active", "info");
}

elfldr is an ELF loader, a service that listens on port 9021 of the console and receives programs in ELF format, the executable standard on Unix-derived systems like the PS5's. From then on the console can run homebrew software: emulators, tools, even Linux. The release's payloads/ folder contains the two components used by default, the loader and the kernel exploit binary.

What shows up on screen

For whoever runs it, the whole process boils down to a black page where log lines appear one after the other:

DIAGRAMmermaid
sequenceDiagram
    participant U as User
    participant P as Page
    participant K as Console
    U->>P: opens the page in the browser
    P->>P: firmware check
    P-->>U: [+] Credits, Agent, Firmware
    P-->>U: [*] Starting WebKit exploit
    Note over P: if the browser freezes,<br/>reload the page
    P-->>U: [+] ARW ready
    P->>K: kernel stage starts
    Note over K: on a freeze or panic,<br/>reboot and try again
    K-->>U: [+] kernel exploit complete
    K-->>U: [+] elfldr is listening on port 9021

ARW ready stands for arbitrary read/write and means the browser stage succeeded. According to the README, this first stage may take several attempts: if the browser freezes, just reload the page.

Where things stand: patches and later firmware

DIAGRAMmermaid
flowchart LR
    A["7.00 → 13.60<br/>Relapse works"] --> B["14.00<br/>Sony closes the hole"]
    B --> C["14.00 / 14.10<br/>Prolapse: announced,<br/>code not public"]
    C --> D["14.20<br/>according to reports,<br/>this one is closed too"]

    classDef ok stroke:#3fa66b,stroke-width:2px
    classDef closed stroke:#e0607e,stroke-width:2px
    classDef unsure stroke:#e0a030,stroke-width:2px,stroke-dasharray:5 4
    class A ok
    class B,D closed
    class C unsure

When Relapse came out, Sony had already been shipping firmware 14.00 for a few weeks, which closes the vulnerability. There's no official downgrade on the PS5, so anyone who updated can't go back to a vulnerable version.

That creates an obvious trade-off: staying on 13.60 lets you use Relapse, but the newest games already require 14.00. Prolapse was announced for firmware 14.00 and 14.10, but its code isn't public. According to reports, Sony also closed that vulnerability with firmware 14.20, released at the end of October.

Conclusions

Relapse isn't built on a single vulnerability but on a chain of separate problems: a bug in how JavaScriptCore clones objects, a few information leaks and a race condition in the kernel. Taken one by one, they're isolated flaws. Put together, they get past every protection of a console designed to stay locked.

It's a great example of how the security of a device is studied. Each stage of the chain matches a defence that gave way, and hardening even one of them would have been enough to stop the whole thing.

In the end it's just like we say in our field: a chain is only as strong as its weakest link.

See you next time.


Sources

Written by Giovanni Rapa · 29.10.2026#exploit#kernel#ps5

Was it useful, or did I get something wrong?

Write to me: I read everything and fix the post if needed.

Reply by email

Comments

via GitHub Discussions · giscus

Comments live on GitHub Discussions. Loading them connects your browser to giscus.app and GitHub.

You need a GitHub account. Rather not? Reply by email.