# patches Everything needed to make **Arch Linux ARM boot and be usable on the Nintendo Switch**, on top of what `alarm-build` already produces. ## Why patches are needed at all The only kernel with working Switch display support is switchroot's **L4T 4.9.140**. Current Arch Linux ARM ships **systemd 261**, whose baseline is kernel 4.15. systemd 261 also removed cgroup v1 support and offers no boot-argument escape hatch, so it cannot start on 4.9.140: ``` systemd[1]: Failed to mount early API filesystems ... No working init found (or a systemd boot that never reaches a login) ``` The fix taken here is to **not run systemd**: the initramfs hands PID 1 to a small shell script. The system boots, networking works, and SSH is reachable. A *proper* fix means a newer Switch-capable kernel (see the project README, "Azkali/L4S"). ## The pieces | Piece | Now lives in | Regenerated by | |---|---|---| | Patched L4T initramfs | `switchroot/ubuntu-jammy/initramfs` | `patches/patch-initramfs.sh` | | Initramfs source change | `patches/initramfs.patch` | (hand-edited) | | PID 1 (`/sbin/init`) | `rootfs/sbin-init` → `src/rootfs.rs` | `alarm-build rootfs` | | PID 1 script (`/etc/alarm-init.sh`) | `rootfs/alarm-init.sh` → `src/rootfs.rs` | `alarm-build rootfs` | | hekate entry (rootdev, usb_logging, …) | `src/payload.rs` | `alarm-build payload` | | Mainline boot script (`rdinit`, `rootwait`) | `device-tree/boot-mainline.cmd` | `mkimage` + `alarm-build payload` | | Edits to *upstream* kernel files (boot markers, DSI hotplug, touchscreen) | `patches/kernel-*.patch` | hand-edit the tree, then `git diff` | | Joy-Con rails driver (switchroot patch 0016) | `drivers/joycon.c` → `src/kernel.rs` | `alarm-build kernel` | | DNS, `pacman` Landlock, SSH root login | `src/rootfs.rs` | `alarm-build rootfs` | | Runtime packages (openssh, wpa_supplicant, iw, dhcpcd) | `src/rootfs.rs` | `alarm-build rootfs` | | WiFi credentials | **not kept** — see `sdcard-snapshot/wpa_supplicant.conf.template` | manual (secret) | | Exact files as they are on the working card | `sdcard-snapshot/` | — | ## 1. The initramfs The initramfs runs *before* the root filesystem exists, so it cannot be fixed from the Arch side — it has to be patched as a binary asset. `switchroot/ubuntu-jammy/initramfs.orig` is the pristine switchroot file; `patch-initramfs.sh` turns it into the patched one: ```sh patches/patch-initramfs.sh # -> switchroot/ubuntu-jammy/initramfs ``` What `initramfs.patch` changes, and why: - **`fsck -y` → `fsck.ext4 -yD`** — the initramfs's `fsck` does not repair the ext4 filesystem properly, leaving it unmountable. - **`mount -o noatime` → `mount -t ext4 -o noatime`** — without an explicit type the mount fails / misdetects. - **skip `resize2fs`** — the root filesystem is already the right size; running it wasted time and could fail. - **do not `mount --move` /proc, /sys, /dev** — that is systemd's job. Instead copy the essential device nodes, unmount the virtual filesystems, and drop a small PID 1 at `/sbin/init`: ```sh #!/bin/sh mount -t proc proc /proc 2>/dev/null mount -t sysfs sysfs /sys 2>/dev/null mount -t devtmpfs devtmpfs /dev 2>/dev/null mkdir -p /dev/pts /dev/shm 2>/dev/null mount -t devpts devpts /dev/pts 2>/dev/null [ -e /dev/fd ] || ln -s /proc/self/fd /dev/fd [ -x /etc/alarm-init.sh ] && exec /etc/alarm-init.sh exec /bin/sh ``` This replaces Arch's `/sbin/init` (`→ /usr/lib/systemd/systemd`) for that boot only; the root filesystem itself is untouched. - **disable the initramfs getty on `ttyGS0`** — `/etc/alarm-init.sh` starts its own shell there, and two would fight. - **accept both Tegra XUDC names** — `usb-serial-mode-start` hardcodes the L4T device name `700d0000.xudc` when it waits for the USB gadget controller. Mainline names the same device after its DT node, `usb@700d0000`, i.e. `700d0000.usb`. Without this the script never finds a UDC, logs *"No known UDC device found"* and exits — leaving a mainline boot with **no console at all**. > Repacking pitfall: `init` must be at the **top level** of the cpio archive. > Repacking with the wrong working directory produces `./root/init` and the > kernel reports `No working init found`. The script always packs from inside > the extracted directory. ### Traps found getting a mainline kernel through this initramfs Every one of these is silent in a different way, and each cost real debugging time. They are recorded because the initramfs is still shipped (the boot script loads it) even though the mainline boot path now bypasses it entirely — see "The mainline boot path" below. | Trap | What you see | Why | |---|---|---| | `tegra_gpio` format mismatch | drops into `exec /bin/bash` and hangs forever | the "forced debug" check parses `/sys/kernel/debug/tegra_gpio`. Mainline's `gpio-tegra.c` creates a file of the **same name** (`debugfs_create_devm_seqfile`, `gpio-tegra.c:655`) with a completely different format, so the parse yields `0` — which the script reads as "both volume buttons held" | | `ply-image` can block | boot stops with no output at all | the boot logos are drawn by a framebuffer client. With no working framebuffer (`simple-framebuffer` fails with `-22`) it blocks, and it is called unconditionally at several points, including straight after the gadget setup | | userspace `mount` hangs | stops right after `Checking dev node for errors` | `mount -t ext4 -o noatime /dev/mmcblk0p2 /mnt` hangs **inside the userspace `mount` binary** — the kernel logs nothing, no ext4 message, no error — while the kernel itself mounts the same filesystem fine | | `fsck.ext4` is a dangling symlink | silently does nothing | it points at `fsck.ext3`, which does not exist in the archive | | `cut` is missing | `cut: command not found` | used by diagnostics written for a fuller userspace | | `boot_m` / `boot_p` cmdline args | `mount_boot_disk` finds nothing | the L4T boot script supplies them; ours does not | | `CONFIG_DEVTMPFS_MOUNT` | `/dev` has no device nodes when `/init` starts | that option only mounts devtmpfs from `prepare_namespace()`, which never runs when the initramfs provides `/init`. The initramfs does mount it itself — but not until line 88, after any early code | `ply-image` is neutralised with a no-op stub at `/bin/ply-image`, installed by `patch-initramfs.sh` after the patch is applied. The `tegra_gpio` parse is removed outright (`vol_btn` is pinned to `3`), because the forced-debug shell is useless on a machine with no console. ## 2. The mainline boot path (`rdinit=/nonexistent`) For the mainline kernel the initramfs is **skipped entirely**. The boot script passes: ``` rdinit=/nonexistent ``` so `kernel_init` falls through to `prepare_namespace()`, the kernel mounts `/dev/mmcblk0p2` itself, and `/sbin/init` from the root filesystem runs. This is deliberate: the initramfs's userspace `mount` hangs on that filesystem while the kernel's own mount works, and bypassing it also sidesteps every trap in the table above. `/sbin/init` is therefore **our own PID 1**, installed by `alarm-build rootfs` from `rootfs/sbin-init` (replacing the image's symlink to `/usr/lib/systemd/systemd`). It: - mounts proc, sysfs, devtmpfs and devpts, and sets the hostname; - builds the USB CDC-ACM gadget on `700d0000.usb` and binds it; - verifies and, if necessary, forces the padctl port's USB role to `device`; - publishes its complete state to `ALARM_USERS.txt` on the FAT partition, so a boot with no console is still readable on a PC; - hands over to `/etc/alarm-init.sh`. `rootfs/alarm-init.sh` then starts the console shell on `ttyGS0`, brings up WiFi and `sshd`, and keeps a shell alive as PID 1. Both scripts are real files under `rootfs/`, pulled in with `include_str!`, so they can be shell-checked (`sh -n`) and read normally instead of being escaped Rust string literals. ### Reading a boot that has no console The Switch has no wired UART and the panel scanout is block-linear, so anything drawn into it renders as speckle. Two channels work: 1. **The FAT partition.** `/sbin/init` and `/etc/alarm-init.sh` write their state — mounts, UDC list and state, ttyGS nodes, process list, `dmesg` — to `ALARM_USERS.txt` and `ALARM_UP.txt` there, readable on any PC. 2. **ramoops plus a warm reboot.** The kernel logs continuously into the ramoops region at `0xB0000000`, which hekate preserves across a warm reset. A panic reboots warm via the Tegra PMC and the boot script dumps that region to the SD card, where `dd` can read it. Note the panic path needed fixing before either channel was usable: PSCI's `SYSTEM_RESET` is non-functional on this hardware, and the Tegra PMC restart handler was registered at `SYS_OFF_PRIO_LOW` — *behind* PSCI's priority 129 on the same notifier chain — so every panic simply hung. See the project README. ## 3. The rootfs (`/etc/alarm-init.sh`) Generated by `alarm-build rootfs` (`src/rootfs.rs`) from `rootfs/alarm-init.sh`. It starts the console shell on `ttyGS0`, brings up `wlan0` with `wpa_supplicant` + `dhcpcd`, starts `sshd`, and keeps a shell alive as PID 1. The console shell is started as: ```sh stty -F /dev/ttyGS0 sane setsid /bin/bash --rcfile /etc/alarm-console.bashrc -i < /dev/ttyGS0 > /dev/ttyGS0 2>&1 & ``` Three things there are deliberate: - **All three descriptors bound explicitly.** `agetty -n -l /bin/bash ttyGS0` produced a prompt but no command output: input reached the shell, but its output never came back down the port. - **`stty sane` first.** Without it a CR-only terminal can leave every command unsubmitted, because nothing translates the carriage return into a newline. - **`--rcfile` disabling bracketed paste.** Bash emits `ESC[?2004h`; minicom echoes it partially and the stray fragment gets executed as a bogus command (it appears as a `:s` substitution error on connect). ### USB networking (NCM) `/sbin/init` also presents a **CDC-NCM Ethernet function** alongside the serial console, so the PC gets a network path to the Switch with **no WiFi and no PCIe involved**. That is what makes `ssh` — and therefore a headless desktop over VNC — possible while the display question is still open. **NCM, not ECM.** ECM sends one Ethernet frame per USB transfer and measured ~33 KiB/s here — roughly a thousandth of what USB 2.0 can do. NCM aggregates many frames per transfer and is what the host's `cdc_ncm` driver expects. `qmult` is raised to 10 as well; note it is only writable while the function is *unbound*, so it is set during gadget setup rather than after the gadget is live (`echo 10 > .../qmult` on a bound gadget fails with `Device or resource busy`). The link comes up fully configured, so nothing has to be done by hand each boot: `usb0` is given `192.168.7.2/24`, a default route via `192.168.7.1`, and `/etc/resolv.conf`. The host side uses `192.168.7.1/24`: ```sh # on the PC, once, against whatever interface the NCM function created sudo ip addr add 192.168.7.1/24 dev sudo ip link set up ssh root@192.168.7.2 ``` To give the Switch internet, the PC has to route and NAT — and since UFW is active, it needs a *route* rule, not just NAT: ```sh sudo sysctl -w net.ipv4.ip_forward=1 sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE sudo iptables -A FORWARD -i -o wlan0 -j ACCEPT sudo iptables -A FORWARD -i wlan0 -o -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT sudo ufw route allow in on out on wlan0 ``` The kernel side is already built in: `USB_F_NCM`, `USB_U_ETHER` and `USB_CONFIGFS_NCM` are all `=y`. Note that a build with `ALARM_NO_MODULES=1` would silently omit them if they were `=m` — the same trap that hid the host-side `USB_ACM` driver for so long. `sshd` is already running (`/etc/alarm-init.sh`), but the ALARM image ships a **locked root account**, so set a password once from the serial console before expecting SSH to work — see §5. WiFi is skipped when `/etc/wpa_supplicant/wpa_supplicant.conf` is absent, so an unconfigured card still boots (serial console only). The build writes only `wpa_supplicant.conf.example`; copy it and fill in the PSK: ```sh cp /etc/wpa_supplicant/wpa_supplicant.conf.example /etc/wpa_supplicant/wpa_supplicant.conf wpa_passphrase "YourSSID" "YourPassphrase" > /etc/wpa_supplicant/wpa_supplicant.conf ``` ## 4. The hekate entry `alarm-build payload` now writes: ```ini [L4T ALARM] l4t=1 boot_prefixes=/switchroot/alarm/ id=ALARM rootdev=mmcblk0p2 rootlabel_retries=1 r2p_action=self fbconsole=0 bootargs_extra=usb_logging auto_rootdev_disable ``` - `rootdev=mmcblk0p2` pins the root filesystem (SD card, partition 2). - `usb_logging` creates the USB serial gadget — without it there is no console. - `auto_rootdev_disable` stops the initramfs from overriding `rootdev`. ## 5. The rootfs build adaptations `src/rootfs.rs` additionally: - installs `openssh wpa_supplicant iw dhcpcd` (the ALARM tarball ships none); - points `/etc/resolv.conf` at real nameservers instead of the dangling systemd-resolved symlink; - comments out `DownloadUser` in `pacman.conf` — systemd 258+ uses Landlock for unprivileged downloads, which the 4.9 kernel does not implement, so `pacman -Sy` fails without this; - writes `/etc/ssh/sshd_config.d/10-alarm.conf` allowing root + password login. Note: the root **password is not set** by the build. The ALARM tarball ships a locked root account. From the serial console, set one: ```sh passwd root ``` `passwd` may fail because it routes through PAM (systemd's PAM modules are absent). If so, write the hash straight into `/etc/shadow`: ```sh HASH=$(openssl passwd -6 'yourpassword') cp /etc/shadow /etc/shadow.bak grep -v '^root:' /etc/shadow > /tmp/shadow.new echo "root:${HASH}:0:0:99999:7:::" >> /tmp/shadow.new cat /tmp/shadow.new > /etc/shadow ``` ## 6. `sdcard-snapshot/` The exact files from the working card, for reference: - `L4T-alarm.ini` — as it was during bring-up (includes `loglevel=8` and systemd debug args that are no longer needed). - `alarm-init.sh` — the hand-written PID 1 script before it was folded into the build. - `wpa_supplicant.conf` — **deliberately not kept.** It holds a live credential, and `wpa_passphrase` leaves the plaintext passphrase in a comment above the hash. Use `wpa_supplicant.conf.template` as the shape and generate your own; `.ndignore` blocks any file of that name from being packed. - `resolv.conf`, `pacman.conf`, `sshd_config` — the edited system files. `sshd_config` allows root + password login, which is a deliberate choice for a device reachable only over a USB point-to-point link; harden it if this machine ever joins a real network. ## 7. Kernel patches to upstream files Most of this project's kernel-side work *adds* files — the device tree, the panel driver, `switch-fb` — and `alarm-build kernel` installs them by copying. But some changes have to edit an upstream file in place: | Patch | What it does | |---|---| | `kernel-boot-markers.patch` | writes stage markers to `0xB0000000` from `head.S` and `start_kernel()`, so a kernel that dies before its first `printk` still leaves a trace | | `kernel-display-hotplug.patch` | `tegra_dsi_host_attach()` calls `drm_kms_helper_connector_hotplug_event()` instead of `drm_helper_hpd_irq_event()` | | `kernel-stmfts-switch.patch` | the three Switch touchscreen quirks — event layout, interrupt unmasking, `input_mt_sync_frame` | `install_switch_patches()` in `src/kernel.rs` applies every `patches/kernel-*.patch` to the tree before building, and skips the ones already applied. The already-applied test is worth knowing, because it is what makes repeat builds work: it runs `git apply --check --reverse`, which succeeds only when the tree already contains the change. A patch that applies in *neither* direction is therefore a bug, not a no-op — and that is exactly how the boot-markers patch was caught: its `init/main.c` half was in the tree while its `head.S` half had been partially reverted, leaving a stray blank line where the marker block used to be. Before this existed, nothing re-applied these patches at all. A re-cloned kernel tree would have silently lost the DSI hotplug fix and the touchscreen quirks — the same class of silent failure the rest of this project documents.