Skip to content

Kiosk Mode

How the KN-86 boots straight into the nOSh runtime with no login prompt, no display manager, and a read-only root filesystem — and how a developer drops out of that mode for debugging. Read this if you are configuring auto-login, changing the read-only-root layout, disabling a service, or debugging “why won’t my edit to /etc/... survive a reboot.”


There is no display manager (no LightDM, no GDM, no SDDM). The system boots to multi-user.target, then a getty override on tty1 auto-logs the kiosk user.

# /etc/systemd/system/getty@tty1.service.d/kn86-autologin.conf
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin nosh --noclear %I $TERM
Type=idle

The nosh user is created by stage-kn86-base of system-image-build.md:

Terminal window
useradd -m -s /bin/bash -G video,audio,input,dialout nosh

video for KMSDRM framebuffer access, audio (legacy — actual audio is via the Pico, not Linux), input for /dev/input/event*, dialout for /dev/serial0 to talk to the Pico. No sudo, no shell access in production mode (see “Recovery / dev-mode toggle” below).

The user’s ~/.profile chains directly into the nOSh runtime systemd unit:

Terminal window
# /home/nosh/.profile (snippet)
if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$KN86_BOOTED" ]]; then
export KN86_BOOTED=1
exec systemctl --user start kn86-nosh
fi

The combination of auto-login + the ~/.profile chain delivers the user to nOSh in <8 s from kernel handoff. The multi-user.target is the right systemd target — graphical.target would pull in display-manager assumptions we explicitly don’t want.

The rootfs (/dev/mmcblk0p4 or p5, depending on active slot per ../../adr/ADR-0011-device-firmware-update-system.md) is mounted read-only by /etc/fstab in the slot:

/dev/disk/by-label/rootfs-A / ext4 ro,noatime,errors=remount-ro 0 1
/dev/disk/by-label/bootfs-A /boot vfat ro,noatime 0 2
/dev/disk/by-label/share /home/shared ext4 rw,noatime 0 2
tmpfs /tmp tmpfs rw,nosuid,nodev,size=64m 0 0
tmpfs /var/log tmpfs rw,nosuid,nodev,size=32m 0 0

Ephemeral writes (anything that systemd, journald, or nOSh wants to scribble during a session) land on tmpfs at /tmp and /var/log. They evaporate on reboot. The only writable persistent location is /home/shared (p6) — the Universal Deck State partition.

We use pi-gen’s built-in overlayroot support (an overlay-root overlay that mounts an overlayfs over the root, backed by tmpfs) for any case where a runtime tool insists on writing to a non-tmpfs path under /. This is enabled in stage-kn86-base of system-image-build.md and handled by adding init=/init-overlay to cmdline.txt for that slot. We prefer the explicit fstab ro mount above when possible (smaller surface, no overlayfs surprise behaviour); overlayroot is the fallback for code that won’t honour ro.

/home/shared (p6 from ADR-0011) is the only persistent writable location the operator’s data ever touches. It carries:

/home/shared/
├── deckstate.bin # Universal Deck State (operator handle, credits,
│ # reputation, cartridge_history bitfield, phase chain)
├── boot-success-token # 60-second sentinel for ADR-0011 tryboot commit
├── kn86-mode.txt -> /boot/kn86-mode.txt # symlink for runtime read
└── (cart-side per-cart save data lives on each cart's own SD; not here)

The image-build pipeline never writes p6, the flasher never writes p6, the updater image never writes p6. Only nOSh (running as user nosh) writes p6. This isolation is the load-bearing reason ADR-0011 §Risks #5 holds — operator state survives every conceivable update / re-flash / slot swap.

Per-cartridge save data lives on the cart’s own SD per ../../adr/ADR-0019-cartridge-storage-and-form-factor.md — never on /home/shared.

stage-kn86-base of system-image-build.md masks the following systemd units (the -base flavor of pi-gen pulls some in by default that we don’t want):

UnitReason
getty@tty2.servicegetty@tty6.serviceProduction mode has only tty1 (autologin into nOSh).
bluetooth.serviceBluetooth is unused; the BT module is also disabled at the device-tree level (device-tree-overlays.md disable-bt) to free UART0.
avahi-daemon.servicemDNS exposes kn86.local on the network; in production mode we don’t want network discoverability. Re-enabled in dev mode.
triggerhappy.servicePi OS Lite ships a generic input-event-to-shell-command bridge; conflicts with nOSh’s exclusive ownership of evdev.
keyboard-setup.servicePi OS Lite tries to apply Linux console keymap; nOSh owns the keyboard, the Linux console is never user-visible.
systemd-resolved.serviceProduction mode has no DNS use case post-boot; static /etc/resolv.conf (empty) is sufficient.
wpa_supplicant.serviceWi-Fi is dev-mode-only; production has no network use case post-boot. Re-enabled in dev mode.
ssh.serviceProduction has no SSH; the service is masked. Dev mode enables it.

Mask command for reference (run in stage-kn86-base/run-chroot.sh):

Terminal window
systemctl mask bluetooth.service avahi-daemon.service triggerhappy.service \
keyboard-setup.service systemd-resolved.service \
wpa_supplicant.service ssh.service
for tty in 2 3 4 5 6; do
systemctl mask "getty@tty${tty}.service"
done

There is no in-device path to drop to a shell from production mode. By design — a kiosk user cannot click their way out of the kiosk, and an attacker with physical access can’t trick the device into a shell either.

Mode is set by a single file read at boot: /boot/kn86-mode.txt on p1 (the common boot region). Contents:

mode=production

or

mode=development

stage-kn86-runtime of system-image-build.md reads this file in kn86-display-init.service (early in boot) and exports KN86_MODE for the rest of the unit graph. nOSh re-reads it once at start.

ConcernProductionDevelopment
/etc/getty@tty1 autologinnosh user, no shell exitnosh user, but Ctrl+C drops to bash
getty@tty2tty6maskedenabled (alt + arrow on a USB keyboard switches; the KN-86’s mech keeb does not have alt + arrow, so this is effectively bench-keyboard-only)
ssh.servicemaskedenabled on the Wi-Fi interface
wpa_supplicant.servicemaskedenabled
avahi-daemon.servicemaskedenabled (kn86.local resolvable)
journaldvolatile (/run/log/journal)persistent (/var/log/journal, capped 100 MB)
Rootfsread-onlyread-only by default; mount -o remount,rw / works since the operator is root in dev
nEmacs REPL filesystem accessread-onlyread-write to /home/shared and mounted carts
  • Dev → Prod: edit /boot/kn86-mode.txt (mount p1 from any host with an SD reader, or via the updater MSC mount from ADR-0011), set mode=production, reboot.
  • Prod → Dev: physical SD access required. No in-device path. This is deliberate — production mode should not be re-enableable into dev mode without somebody opening the case and pulling the SD. The Pelican shell makes this a deliberate ritual, not a slip.

If the SD itself is corrupt and the system won’t boot to the point of mounting p1, the only recovery path is to re-image the SD using a fresh .img from the system-image-build.md pipeline. There is no bench-side rescue partition baked into the SD layout — ADR-0011’s six-partition table does not reserve one. /home/shared (p6) survives any re-image of slots A and B (the flasher and provisioning script never touch p6 — see update-system.md), so operator state survives bootloader-level recovery.

For total-loss situations (corrupt p1 or a totally bad card), reseed the SD from .img and on first boot of the new image nOSh will see an empty /home/shared and bootstrap a fresh deck state.