I make Linux boot on hardware that was never supposed to run it. Since 6 years now that hardware has been the original Nintendo Switch, and the Linux has been L4S – Linux 4 Switch: stock desktop Fedora, KDE Plasma and all, running on Nintendo’s 2017 handheld.
Here’s the current state of the project, what shipped, what’s still cooking, and a few war stories from the kernel trenches. If you just want the headline: a Fedora 44 KDE image now cold-boots to a rendering Plasma desktop on the Switch, with zero failed systemd units and getting there required backporting about a decade of kernel interfaces into the Tegra vendor kernel.
The constraint that shapes everything
The Switch’s GPU only works at his best with NVIDIA’s L4T BSP, and that BSP is frozen on Linux 4.9.140 a kernel line that debuted in December 2016. The CPU is four Cortex-A57 cores: ARMv8.0, which matters more than you’d think, because it predates the LSE atomic instructions that basically every modern arm64 binary is compiled to use. And there’s 4 GB of RAM. Every problem below traces back to one of those three facts.
The main event: systemd 259 vs. Linux 4.9
Fedora 44 ships systemd 259. systemd 258 removed cgroup v1 support entirely and raised its kernel baseline to 5.4 there is no bootarg escape hatch anymore. Fedora 42 (systemd 257) ran fine on 4.9; the 257→259 jump broke everything. The choices were: pin an ancient systemd forever, ship a degraded system, or backport what systemd needs into the kernel. I backported.
What “systemd needs” turned out to mean, in the order the boot process slammed into each one:
- cgroup v2 cpu controller: upstream added it in 4.15; without it, CPUWeight/CPUQuota were silent no-ops.
- nsdelegate: systemd mounts cgroup2 with this option and no longer tolerates rejection. 4.9 rejected all cgroup2 mount options, so PID 1 froze at “Failed to mount API filesystems.”
- clone3 + CLONE_PIDFD, waitid(P_PIDFD), CLONE_CLEAR_SIGHAND, CLONE_INTO_CGROUP, pidfd_open + pidfd fdinfo: the whole modern process spawning surface glibc’s
pidfd_spawnrelies on. Get clone3’s stack convention wrong (it passes base+size, not the top like legacy clone) and PID 1 SIGSEGVs. - The new mount API:
fsopen/fsconfig/fsmount/move_mount, syscalls 429 to 433. systemd 259’s credential setup uses these with no mount(2) fallback, so journald and everyImportCredential=service died withstatus=243/CREDENTIALS. I portedfs_contextfrom v5.2 into 4.9 (~400 lines plus the mount-lifecycle plumbing), driving every filesystem through a legacy shim so nothing needed per-fs conversion. - pidfd_send_signal: turned out to be why WiFi died after suspend: systemd couldn’t kill a stale NetworkManager to restart it. Backported; suspend/resume now reconnects cleanly.
Three bugs worth retelling
The mountinfo use-after-free. First boot with the new mount API, the kernel panicked with PID 1 reading /proc/self/mountinfo a mount’s parent pointer was dangling, registers full of slab-poison bytes. Moving a mount out of an anonymous namespace needs list handling that 4.9’s commit_tree() never anticipated. The follow-up EBUSY was even better: I blamed mount propagation, instrumented a full boot over UART, counted 21 fsconfig(CMD_CREATE) / 21 fsmount / exactly one move_mount and the real culprit was my own backport never entering its reconfigure state when systemd flips the credential tmpfs read-only. Measure, don’t theorize.
The TLS massacre. Once boot got far enough, every multithreaded daemon NetworkManager, firewalld, upowerd, resolved segfaulted, while single-threaded ones survived. Every coredump showed a new thread dying at its first instruction. Cause: arm64 4.9 reads a new thread’s TLS pointer from register x3, per the legacy clone() ABI. clone3 passes TLS in a struct. Every clone3 spawned thread got a garbage thread pointer. The fix is upstream’s copy_thread_tls change two files.
The Steam corner: FEX, Proton 11, and the ARMv8.0 problem
Every article about Switch gaming-on-Linux repeats the same line: the L4T 4.9 kernel is too old to support FEX, which is why the Switchdeck project used Box64. That blocker is gone. I backported FEX’s kernel prerequisites into 4.9 EL0 MRS ID-register emulation, openat2, 48-bit VA, futex_waitv (hello fsync), the 32-bit compat series and FEX now JIT-runs x86-64 binaries on the stock L4S kernel for better or worst.
The other half of the problem is ISA baseline. Fedora’s FEX build had 67 unguarded ARMv8.3 instructions baked in (built on modern server cores); rebuilt for A57, it runs. Valve’s Proton 11 arm64 was worse: the bundled FEX DLL had 333 inline LSE atomics, and it turned out the entire Wine arm64 PE stack was compiled -march=armv8.2-a. So I rebuilt the whole thing wine, FEX, dxvk, vkd3d-proton for ARMv8.0/Cortex-A57: 610 DLLs, verified zero LSE / zero RCpc in the hot path. The overlay is an 82 MB download (719 MB uncompressed PE files compress absurdly well) published at github.com/Azkali/proton-arm64-a57, with Switchdeck pull requests to auto-apply it on A57 hardware and to enable fsync/esync.
Honesty section: wineboot and FEX’s x86→arm64 translation are verified working on-device; real-game validation is ongoing (one VR-only DLL escaped the rebuild and SIGILLs there’s a PR that disables it on A57), and Steam’s Big Picture / gamepad UI still won’t initialize its CEF renderer on Tegra at all. This corner is exciting, not finished.
Memory work on a 4 GB console
- zram writeback, backported to 4.9 and shipped: incompressible pages used to sit in zram wasting RAM at a ~1:1 ratio; now they write back to a backing device. In the end-to-end test, 16 MB of incompressible data went from 16 MB of zram residency to zero RAM used, data intact. In practice this class of work claws back on the order of a gigabyte on a 4 GB machine.
- MGLRU, backported to 4.9, off by default: the multi-gen LRU came over from a page-based 5.15 tree. It survived OOM torture tests after I root-caused a THP-split accounting bug that could panic the kernel under real memory pressure and fixed it. It stays a sysfs opt-in until it’s earned more trust; measured benefit so far is modest (~2% on my swap benchmarks), which is exactly why I’m not hyping it.
The Xorg confession
I built a clean little ABI shim five missing symbols, attached with patchelf --add-needed that makes the frozen L4T NVIDIA X driver load on modern Xorg 21.1. The shim works; I was quite pleased with it. Then on real hardware the driver’s PreInit failed anyway: I disassembled its ABI check and found it’s purely informational, but the ABI-24 struct layouts are hardcoded throughout. So L4S ships a pinned Xorg 1.20.14 instead, and the shim is retired. Sometimes the elegant hack loses to the boring pin, and you should hear about those times too.
What you can run today, and what’s next
Today: Fedora 44 KDE images (~5 GB .7z for hekate’s installer) built from the project’s CI, with the full backported kernel via the linux4switch/l4s COPR plus a rolling l4s-testing COPR with daily builds if you like living dangerously.
Next: the big architectural bet is moving L4S to an atomic bootc/rpm-ostree image image-based updates with rollback, which is exactly what you want on a console you can’t easily rescue. The container builds pass lint already; the open problem is reconciling ostree’s boot model with the Switch’s hekate → U-Boot → uImage chain. WIP, no promises on dates.
L4S is free and open — kernel work in the l4t-community trees, packages in the linux4switch COPR, writeups at azka.li.
