Files
nix-config/hardening/tests/runtime-validation.md
T
ryan4yin | 二花 08d2ff7578 feat(nixpak): sandbox umu-launcher with ~/Gaming as its home (#519)
* feat(nixpak): sandbox umu-launcher with ~/Games as its home

umu-run, winetricks and bbe leave the host PATH, so umu launchers
generated before this stop working until rewritten with
`umu-install.nu <name> - <launcher> <gameid>`.

Assisted-by: Claude Agent SDK (claude-opus-5-5)

* refactor(nixpak): mount ~/Gaming as the game sandbox home

idols-ai now preserves ~/Gaming instead of ~/Games, so existing games
vanish from the host after deploy until the preserved ~/Games is moved
to ~/Gaming/Games.

Assisted-by: Claude Agent SDK (claude-opus-5-5)

* docs(nixpak): record umu runtime validation

Assisted-by: Claude Agent SDK (claude-opus-5-5)
Assisted-by: DeepSeek Harness (deepseek-flash)
2026-11-05 22:39:48 -06:00

9.5 KiB

Runtime validation

Nixpak wrappers are accepted in three layers:

  1. Policy/build: just test evaluates every wrapper and policy.nix rejects broad mounts, system D-Bus, keyring access and device grants outside each app's declared scope.
  2. Disposable smoke test: verify-chromium.py launches a Chromium-family wrapper with a temporary HOME and validates renderer sandboxing, environment clearing, bind scope, profile persistence and loopback policy.
  3. Interactive desktop check: launch the real app, use its normal file/network/device workflows, and confirm the sandbox does not break required integration.

Current status

App Policy/build Runtime
Firefox Pass Interactive pass; camera/remote sharing intentionally unavailable
Chromium Pass Automated smoke + interactive pass
Chrome Pass Interactive pass
Brave Origin Pass Interactive pass; backup browser without media grants
Edge Pass Interactive pass; backup browser without media grants
Zoom Pass Interactive pass
QQ / Telegram Pass Interactive startup/tray pass
WeChat (native nixpak) Pass Interactive Wayland validation pending
Foliate Pass Interactive validation pending
Krita Pass Interactive validation pending
Bambu Studio Pass Interactive pass; device_page/filament UI unavailable in nixpkgs
OrcaSlicer Pass Interactive pass; blank WebKit home page fixed
Remmina Pass Interactive validation pending
OBS Studio Pass Interactive pass; NVENC recording and local RTMP streaming work
Blender Pass Interactive validation pending
MuseScore Pass Interactive launch pass; ALSA routed to default PipeWire sink
mpv Pass Interactive validation pending
imv Pass Interactive validation pending
umu Pass Wuthering Waves and Star Rail from their desktop entries

Intentional exceptions:

  • Chromium keeps host loopback and personal files for local development. Firefox has the same exception. Chrome, Brave Origin and Edge use pasta-isolated loopback.
  • Brave Origin and Edge are backup browsers. They intentionally have no PulseAudio, Camera, ScreenCast or MPRIS grants.
  • Firefox camera and remote screen sharing are intentionally disabled. Firefox does not support the XDG Camera/ScreenCast portal path used here, so enabling them would require granting raw video devices or the direct PipeWire socket. We deliberately do not grant those privileges.

Known packaging limitations:

  • Bambu Studio: the nixpkgs build removes the device_page frontend build, so share/BambuStudio/web/device_page/dist is missing and filament management cannot load.
  • Bambu Studio: the MakerWorld sidebar entry requires firstguide.finish = "true" in BambuStudio.conf; the upstream guide writes "1" while the web-view code only accepts "true".
  • Bambu Studio: the startup splash can render black under XWayland. This is cosmetic; the main UI renders normally.
  • MuseScore: uses Qt's XCB backend, so it runs through XWayland. Its ALSA driver uses the host pcm.!default configuration, which forwards to PipeWire; this requires the raw PipeWire socket. Launch and audio routing pass, but audible playback and score/print/export workflows still need user confirmation.

Game launcher checklist

For umu now, and Heroic and Steam when they move into the sandbox. Run each with the game installed through the nix-config-umu-game skill and launched from its desktop entry:

  1. The launcher window opens and the nested pressure-vessel container starts (ENABLE_LOG=1, then check last-run.log for bwrap or /.flatpak-info errors).
  2. Frame rate matches an unsandboxed run of the same scene with the MangoHud overlay; the game uses the NVIDIA GPU, and a 32-bit game renders.
  3. Audio plays; a USB or Bluetooth controller connected before launch works, and a keyboard does not appear under /dev/input inside the sandbox.
  4. The launcher downloads at the usual speed through pasta, and online play connects.
  5. Inside umu-sandbox bash: ls -A ~ shows the contents of the host's ~/Gaming and nothing else from the host home.
  6. Running ~/Gaming/Games/<name>/run from a host shell refuses to start.

umu on dwproton-11.0-15: Wuthering Waves and Star Rail both launch from their desktop entries, and a controller works. Wuthering Waves' PROTON_LOG shows pressure-vessel starting inside the sandbox, DXVK and vkd3d-proton on the RTX 4090, NVAPI, fsync and pasta networking, with nothing denied by the sandbox. Still to check: audio, a 32-bit game, the keyboard exclusion and item 5. Wuthering Waves' ACE anti-cheat driver crashes winedevice.exe on an unimplemented ntoskrnl.exe function, a Wine gap; the game runs regardless. The first launch after a Proton update or a moved umu runtime can show no window for a minute while steamrt4 is prepared and the prefix upgraded.

Browser smoke test

Build a wrapper from the locked inputs:

APP=chromium nix build --impure --out-link "browser-validation/$APP" --expr '
  let
    flake = builtins.getFlake (toString ./.);
    pkgs = import flake.inputs.nixpkgs {
      system = builtins.currentSystem;
      config.allowUnfree = true;
    };
  in
  pkgs.callPackage (./hardening/nixpaks + "/${builtins.getEnv "APP"}.nix") {
    mkNixPak = flake.inputs.nixpak.lib.nixpak { inherit pkgs; inherit (pkgs) lib; };
  }
'
python3 hardening/tests/verify-chromium.py "browser-validation/$APP/bin/$APP"

The script writes its disposable HOME under /var/tmp; the sandbox's private /tmp would otherwise hide profile binds. Pasta-wrapped browsers also require a usable /dev/net/tun on the host. The script checks live renderer Seccomp=2 and NoNewPrivs=1, absence of --no-sandbox, canary-free environments, effective bwrap arguments, profile persistence and loopback reachability.

Chromium-family browsers use the app-scoped Secret portal for v12 cookie encryption; they do not receive org.freedesktop.secrets. If the portal is unavailable, Chromium can fall back to weaker v10 storage; monitor Local State.os_crypt.portal.prev_init_success. Firefox uses NSS storage instead. Browserpass separately exposes the password store and GPG-agent socket to Firefox/Chromium/Chrome only.

FIDO2 passthrough

Passed after a reboot on ai (CanoKey Canary, PR #515 head): with the key present at boot, the host linked only its FIDO interface into /dev/fido-hidraw, and WebAuthn works in Firefox and Chrome. Chromium, plugging the key in after browser launch, and the in-sandbox keyboard-interface check are still pending. Full checklist:

  1. On the host, ls -l /dev/fido-hidraw/ lists only the FIDO interface (ID_FIDO_TOKEN=1); it follows plug and unplug.
  2. Firefox/Chromium: start without the key, plug it in, then register and sign in on https://webauthn.io. Unplug and replug without restarting, then sign in again.
  3. Chrome: the same flow with the key present at launch; record whether a later plug is detected.
  4. In each sandbox, the CanoKey keyboard interface's /dev/hidrawN fails to open with ENOENT.

Zoom

Zoom keeps its upstream FHS runtime inside the outer nixpak boundary. The nested sandbox propagates /.flatpak-info, uses pasta-isolated networking, and binds only DISPLAY, XAUTHORITY, /dev/video0, /dev/video1, DRM sysfs and the declared app directories. X11 remains a shared-display trust boundary: ScreenCast portal permission does not prevent capture through the X11 connection. Interactive login, audio, camera, screen sharing and file workflows passed.

OBS

Interactive validation covered PipeWire screen capture, V4L2 camera capture, desktop/microphone audio, NVENC H.264 Hybrid MP4 recording and RTMP publishing. Streaming was tested against a temporary MediaMTX server on the host's LAN address; the OBS pasta namespace cannot reach host loopback. Bilibili or another external provider still needs a real account-specific stream key to validate provider authentication and CDN ingest.

OBS records to the path configured under output settings. In Simple mode that is SimpleOutput.FilePath; Advanced mode uses AdvOut.RecFilePath and AdvOut.FFFilePath. These must point at a writable bind such as ~/Videos, not the sandbox's transient home root.

Commands

just fmt
just test
git diff --check
ruff format --check hardening/tests/verify-chromium.py
ruff check hardening/tests/verify-chromium.py
prettier --check hardening/tests/runtime-validation.md