A working copy of ns2jb, a software-only
research exploit for the Nintendo Switch 2. It runs two small native functions
inside the console's filesystem service (fs), checks the results, and puts the
original instructions back.
What it is not: custom firmware, a game loader, or a piracy tool. Upstream's own words are "This demo does not install custom firmware or persistence." Nothing survives a console restart, and a restart is how you undo a run.
Read Before you start before touching anything. This is timing-sensitive research code: it can fail, it can crash the app, and a failed run may need a console restart.
- Which download to use
- Before you start
- Check your setup first
- Step 1 — Put the bootstrap into SmileBASIC
- Step 2 — Start the host on your computer
- Step 3 — Drive it from the console
- Step 4 — Read the result
- If it fails
- Optional: held RPC
- Optional: system-code dump
- What is in this repository
- Tools built on this
- Verifying what you downloaded
- Credits
| You have | Use this |
|---|---|
| macOS on Apple silicon | mac2jb v0.4.0 — a GUI that types the bootstrap into the console over a USB cable and sends the commands for you. It bundles this same engine. Read docs/MAC2JB.md first; it needs a kernel extension and SIP disabled. |
| Windows, Linux, or Intel macOS | This repository. You need Node.js and you type the bootstrap yourself (see Step 1). |
Both routes run the same exploit engine. The only difference is who types the 229-line BASIC program into the console.
Why this repository exists. It started as a capture of pepsipu's original release. It now tracks kurumuz's fork by default, because that fork is what fixes the most common "it doesn't work on my console" failure — see Which console firmware do I have?. pepsipu's original tree is still here at tag
pepsipu-95f5525.
- A Nintendo Switch 2.
- System software 23.0.1 and SmileBASIC 4.4.9. Other versions are untested and may simply not work.
- A computer on the same network as the console, running Node.js 22 or
newer (
node --version). - More than 20 MiB free in the SmileBASIC save area.
- A USB keyboard is strongly recommended for entering the bootstrap, or a second person to read it out. It is 229 lines of BASIC.
- Patience. Acquisition is timing-sensitive; upstream's own hardware test passed on the third fresh boot, with the first two attempts stopping at validation checks and cleaning up.
On the console open System Settings → Support/Health & Safety and look at item 3.
- Shows (B) — your console has the microSD Express support update. Use this repository. pepsipu's original release can get through the first stage and then fail starting its native helper on this memory layout.
- Does not show (B) — either tree should work. This repository handles both layouts it has seen.
This is the single most common cause of a run that dies late with a validation error, so check it first.
An update can move the firmware past what this exploit was tested against, and there is no way back. Before testing, block Nintendo's update servers with DNS filtering on the network the console uses:
- System Settings → Internet → Internet Settings → pick the connection → Change Settings → DNS Settings: Manual.
- Americas: primary
207.246.121.77, secondary163.172.141.219. Everywhere else: primary163.172.141.219, secondary207.246.121.77. - Set both IPv6 DNS entries to
::, save, reconnect.
This blocks Nintendo services generally, so the eShop and Nintendo Switch Online will stop working on that connection. It is also not a guarantee — cached addresses or a different network can bypass it. It only protects the versions you have; it does nothing for exploit reliability.
- The exploit writes to live process memory. A wrong or unlucky run can crash SmileBASIC or the console.
- Do not run it on a console you cannot afford to restart, and do not run it while anything you care about is unsaved.
- Enter
REMOVE NSR,&H60000000,&H3FFFFFFFonce per SmileBASIC launch. Doing it twice is how you break the application state. - Restart the console before every attempt, and after every failed one.
Before you spend a console restart on this, run the preflight on your computer:
node doctor.cjsIt checks the five things that go wrong before the exploit is even involved, and
tells you the exact V=START(...) line for your machine:
[ ok ] Node.js version Node.js 24.18.0 on win32/x64
[ ok ] Repository files all 6 required paths are present
[ ok ] File integrity 118/118 files match SHA256SUMS
[warn] Network address several candidates; assuming 192.168.0.4 on Ethernet (also: 172.30.0.1)
[ ok ] Port 39380 free, so the console will be able to reach this computer
[ ok ] Run folder writable
It exits non-zero if anything is wrong, so you can tell at a glance. It never contacts the console — it cannot tell you whether the exploit will work, only that your side is ready.
The three failures it catches, all of which look like "the exploit is broken":
| What it reports | Why it matters |
|---|---|
File integrity — a changed or missing file |
You are running a modified or half-extracted tree. Re-download rather than debug. |
Port 39380 — already in use |
A previous run is still open, or Windows left the socket behind. The console will never connect and the host will sit at "Waiting for Switch 2". |
Network address — several candidates |
You will type the wrong START line and the console will connect to nothing. |
This is a one-time job per console.
- On the console, open SmileBASIC and choose Make a Program.
- Enter the contents of
bootstrap.bas— all 229 lines — exactly as written. A USB keyboard makes this bearable; the console accepts one via System Settings → Controllers & Accessories → USB Keyboard. - Save it into slot 0 with the name
NS2.
There is no import file and no download key. A .bas file on your computer
cannot be transferred to the console — SmileBASIC has no import path for it. The
bootstrap has to be typed, or sent by something that types for you, which is
exactly what mac2jb does over USB.
Upgrading from an older copy? If you already saved an
NS2from pepsipu's original release, it has a nine-argumentSTART. This tree's bootstrap uses the five-argumentSTART(PORT,A,B,C,D)form. Replace the saved program, or use the fallback line in Step 3.
-
Get this repository onto the computer —
git clone, or Code → Download ZIP and extract. Nonpm installis needed; the engine has no third-party dependencies. -
Restart the console, open SmileBASIC, and leave it in Direct mode.
-
In a terminal in the repository folder, run:
node doctor.cjs # one-time check: Node, files, address, port 39380 node run.cjsIf more than one network address is available it lists them and asks you to pick the one the console can reach. You can skip the question:
node run.cjs 192.168.1.10
The host listens on TCP port 39380. If that port is blocked, the console will never connect, and the host will sit at "Waiting for Switch 2".
Forwarded hosts (WSL 2, Docker, a VM). Normally the address the console dials and the address the host binds are the same. When a forwarder sits between them they are not, so
--bindsets the listening address independently:node run.cjs --bind 172.20.0.5 192.168.1.10
The console still connects to
192.168.1.10— that is whatV=STARTcarries — while the socket is bound to172.20.0.5.--bind 0.0.0.0listens on every interface.
Leave that terminal open. It prints the exact V=START(...) line for your
computer's address — you will need it in a moment.
In SmileBASIC, in Direct mode, enter these one at a time, confirming each:
LOAD "TXT:NS2"RUNREMOVE NSR,&H60000000,&H3FFFFFFFOut of memory from REMOVE is expected. That is the bug being used, not a
failure. The host terminal should now say it is waiting.
Then enter the V=START(...) line the host printed:
V=START(39380,192,168,1,10)(with your own address). If SmileBASIC answers Wrong number of arguments,
your saved NS2 is the older nine-argument bootstrap — enter the second
V=START(...) line the host prints, which has ,0,0,0,0 on the end. Do not
repeat RUN or REMOVE; just enter the replacement START while the host is
still waiting.
Now do not touch the console. Keep SmileBASIC in the foreground and the computer awake.
The host prints progress and finishes with something like:
4/4 verified
That means both native functions ran, their results matched, and the original instructions were restored. The run folder is printed too — it holds the logs and a JSON record of what happened.
When you are done: restart the console. That restores the original ro
process and clears everything. There is nothing to uninstall.
| What you see | What it means | What to do |
|---|---|---|
| Host sits at "Waiting for Switch 2" | The console never reached port 39380 | Check both are on the same network and that you entered the START line. Windows firewall may need to allow Node.js on private networks. |
Wrong number of arguments |
Old nine-argument NS2 is saved |
Enter the second V=START(...,0,0,0,0) line the host printed. |
| Fails late, at a validation check | Timing, or the wrong memory layout | Restart the console and try again. Upstream needed three attempts. Confirm your firmware item 3 status. |
Out of memory at REMOVE |
Expected | Continue. |
Out of memory anywhere else, or the app closes |
The run disturbed the app | Restart the console. Keep the runs/ folder if you want to report it. |
A second REMOVE was entered |
Application state is now inconsistent | Restart the console. Do not try to "fix" it with more commands. |
Always restart the console between attempts. Re-running in the same SmileBASIC session is not supported and produces confusing errors.
The host adds a plain-language explanation to the failures it recognises — the
canary timing race in particular, which is the most common way a run dies and is
not a sign that anything is broken. A message it does not recognise is printed
unchanged rather than guessed at. What each one means is in
docs/UPSTREAM-ISSUES.md.
Keep the runs/ folder when a run fails — it is the only record of what the
engine saw. It is git-ignored and never part of the distributable.
If you want to report a problem, this produces one file with everything needed:
node doctor.cjs --bundleIt writes ns2jb-report-<timestamp>.txt in the current folder: the preflight
results, your Node.js version and platform, your local network address, the port
status, and the result.json of your most recent run (or a note saying the run
died before writing one). It contains no console keys, no memory contents, and
no game or system files, and absolute file paths are replaced with <home>,
<repo> and <temp> labels before it is written, so your user name and the
folder you unpacked this into are not in it. Review it before posting anyway:
your local IP address and the run folder name are.
That report is worth more than a description, because the two questions that decide almost every report are answered by it: did the console ever connect? and which check failed?
node run.cjs verifies the exploit and then closes everything. If you want the
connection to stay alive after the 4/4 check — for the homebrew launcher, or to
poke at memory yourself:
node run.cjs --rpcAfter 4/4 it prints a runs/switch2-* folder and stays open. From another
terminal:
node rpc.cjs runs/switch2-RUNID status
node rpc.cjs runs/switch2-RUNID '{"command":"query","address":4294967296}'This is live memory access, not a sandbox. A wrong request can crash the app or the console. Requests are serialized, and a request that timed out must never be replayed — its outcome is unknown. The bridge listens on the computer's loopback interface only and has no authentication, so use a machine you trust. Stop it with Ctrl-C when finished; only then does it clean up the FS endpoint and close the console connection. Keep SmileBASIC open the whole time — this does not survive closing the app or restarting the console.
Held mode is also what other tools attach to — see docs/ECOSYSTEM.md.
To capture installed system-program code from your own console, use this instead
of node run.cjs for a fresh attempt:
node dump-firmware.cjsIt completes the normal 4/4 exploit, then asks the console's NCM for the
installed system-program IDs and reads each accessible main.npdm and main.
Output goes to dumps/, with dumps/firmware-dump.json holding the inventory,
per-file SHA-256 hashes, and any titles whose CodeFS was unavailable. You need
512 MiB free on the computer.
Starting another dump run clears the previous dumps/ contents. Copy
anything you want to keep first. An unrecognised non-empty dumps/ directory is
never cleared automatically. You can combine it with held RPC:
node run.cjs --dump-firmware --rpc.
At startup the runner also deletes files and directories whose names start with
NS2_GAP from the roots of SmileBASIC's save and tmp mounts, recursively.
Do not keep personal files with that prefix on either mount.
bootstrap.bas the 229-line program you type into SmileBASIC
run.cjs host entrypoint: listens, prints START, orchestrates the run
doctor.cjs preflight: Node, file integrity, address, port, run folder
dump-firmware.cjs same run, plus a system-code dump
rpc.cjs client for the optional held RPC
test.cjs host-side checks (node test.cjs) — needs no console
engine/ the exploit engine, unchanged from the fork except for
two Windows portability fixes (see docs/PROVENANCE.md)
scripts/ maintainer tooling (make-sums.cjs regenerates SHA256SUMS)
SHA256SUMS hashes of every shipped file
docs/ provenance, exploit analysis, upstream READMEs, mac2jb guide,
upstream issue triage, and a list of tools built on this engine
The engine layout: engine/switch2/memory/ (arbitrary read/write via the 0x301
path-overflow and a BufferedStorage cache entry), engine/switch2/ro/ (the native
helper, domain-table swap, launch), engine/switch2/process/ (the FS instruction
demo), engine/switch2/app/ (SmileBASIC-side workers), engine/shared/ (RPC and
framing).
Reading order for the curious: docs/PROVENANCE.md (what
was captured and verified), docs/ANALYSIS.md (the exploit
chain against the actual code), docs/UPSTREAM-ISSUES.md
(what the two upstream issue trackers report, and what was done about each), and
docs/blog-capture.md (pepsipu's write-up).
This repository is the engine and the host. Other projects sit on top of it, each with its own repository and licence:
- mac2jb — the macOS USB-typing front end. docs/MAC2JB.md
- HBLauncher and ftpd-sb — a homebrew launcher and FTP access, bundled with mac2jb.
- OunceCake — mounts a game's romFS/exeFS and serves it over FTP from a held RPC session. It is a separate GPL-2.0 project and is not vendored here.
Details, and what is deliberately out of scope in this repository: docs/ECOSYSTEM.md.
Every shipped file's SHA-256 is in SHA256SUMS — all 117 of them, including the
docs you are reading. The quickest check is the one the preflight already does:
node doctor.cjsIf you would rather not run any of this code, verify by hand. On Linux or macOS:
shasum -a 256 -c SHA256SUMSOn Windows, PowerShell:
Get-Content SHA256SUMS | ForEach-Object {
$h, $f = $_ -split '\s+', 2
$actual = (Get-FileHash $f -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $h) { "MISMATCH $f" }
}An empty result means every file matches. If you changed a shipped file yourself, regenerate the list rather than editing it by hand, and check it before committing:
node scripts/make-sums.cjs # rewrite SHA256SUMS from the tree
node scripts/make-sums.cjs --check # exit 1 if it is out of dateThe host-side checks need no console:
node test.cjsThe native payload is also reproducible from its own source with Zig 0.15.2:
node engine/build-payload.cjs --check --zig /path/to/zigThat is a stronger statement than any checksum file: the shipped binary can be
rebuilt byte-for-byte from the shipped source on an independent machine. See
docs/PROVENANCE.md for the recorded result.
- pepsipu — original ns2jb: exploit, engine, and write-up. Upstream: https://gitlab.com/pepsipu/ns2jb.
- kurumuz — the fork this repository now ships by default: memory-layout adaptation, bounded retries, longer timeouts, held RPC, firmware dump, and the five-argument bootstrap. Upstream: https://gitlab.com/kurumuz/ns2jb.
- cpypwn — mac2jb, the macOS USB-typing front end built on the same fork.
- shinyquagsire23 —
libusbdandmacos_usb_gadget_poc, the USB gadget work mac2jb depends on.
Mirrors of this rework: GitHub · GitLab · Codeberg.
No licence file was published upstream. This is research code; the copyright stays with the authors above. Treat it as something to study and run on your own hardware, not something to redistribute as your own.