WIP: Linux: the game process in a Landlock sandbox #882

Draft
hckrman101 wants to merge 2 commits from hckrman101/harden-landlock into main
hckrman101 commented 2026-11-09 00:09:50 +00:00 (Migrated from github.com)

Should a crafted file (a save from someone else) ever take the game's process over, it should not reach the player's other files. At start, before the renderer and before any save is read, host/posix/sandbox_linux.c restricts the process with Landlock: the system calls directly, no new dependency, every ABI from 1 (rights the kernel does not know are left out). With ABI 8 or later the restriction covers every thread at once (the window's main thread starts first); before that the game's thread and everything it starts.

What the game may still do:

paths
read and run /usr, /lib*, /opt, ~/.local/share/vulkan, the game folder
read /etc, /sys, /run/udev, /dev, ~/.config/vulkan, fonts, PulseAudio's cookie, Xauthority
read and write the saves ($XDG_DATA_HOME/hookshot), ~/.cache/nvidia, mesa_shader_cache (and the drivers' env overrides), the temporary folder, /dev/shm, /proc (thread names need write access there: a rule on /proc/self/task does not hold, since procfs makes its inodes anew; Landlock's ptrace rule still keeps other processes' files out)
devices /dev/dri, /dev/nvidia*, /dev/input, /dev/hidraw*, /dev/snd, /dev/udmabuf
in the game folder only its own state files, written in place: hookshot_options.ini, the *.bin caches, the log
Unix sockets Wayland, X11, PipeWire, PulseAudio (not the session bus)
network no TCP
signals none to other processes

The game folder is read-only apart from those files, so nothing can be put there for the next start to load (a library beside the executable, an env file). Missing state files are made empty first; each reader takes an empty file as none. The Vulkan pipeline cache, written through a .tmp and a rename, falls back to an in-place write when the .tmp cannot be made. The precompile and shader-preparation crash markers (*.running, *.warming) cannot be made under the sandbox, so those two crash-loop guards are off while it is on.

The update check runs before the sandbox, and the launcher is another process. Without Landlock the game runs as before, said once in the log. HOS_SANDBOX=0 turns it off; HOS_SANDBOX_RW=<path>[:...] adds writable paths for debugging; the test harness's own paths (RX_VK_SHOTS, RX_VK_FRAMELOG's folder, HOS_INPUT_LIVE, HOS_ENV_FILE) are allowed by themselves. HOS_SANDBOX_PROBE=<path>[:...] logs whether each path can be read, written or created beside.

Tested (Linux, kernel with Landlock ABI 11, NVIDIA, Sway)

  • windowed and fullscreen to play; sound (the game's stream on PipeWire); a Switch Pro controller found through SDL; saves written; hookshot_options.ini rewritten at exit; program, pipeline and driver caches written; screenshots
  • the log: sandbox: Landlock ABI 11 on, every thread, ... rules (TCP none): ...
  • the probe, from inside the game:
    • ~/.ssh/config, ~/.bashrc, a file in $HOME: read, write, create beside all Permission denied
    • hookshot_vk.so: read allowed, write Permission denied; nothing can be created in the game folder
    • hookshot_options.ini: read and write allowed
    • the save folder and the NVIDIA shader cache: create allowed
  • no frame-rate change beyond run-to-run noise

Not tested: controller hotplug (no unplugging during the runs; SDL's udev monitor is a netlink socket and /dev/input is allowed), AMD/Intel (Mesa) drivers, X11 sessions, older kernels (ABI < 8 restricts only the game's thread).

Should a crafted file (a save from someone else) ever take the game's process over, it should not reach the player's other files. At start, before the renderer and before any save is read, `host/posix/sandbox_linux.c` restricts the process with Landlock: the system calls directly, no new dependency, every ABI from 1 (rights the kernel does not know are left out). With ABI 8 or later the restriction covers every thread at once (the window's main thread starts first); before that the game's thread and everything it starts. What the game may still do: | | paths | |---|---| | read and run | /usr, /lib*, /opt, ~/.local/share/vulkan, the game folder | | read | /etc, /sys, /run/udev, /dev, ~/.config/vulkan, fonts, PulseAudio's cookie, Xauthority | | read and write | the saves ($XDG_DATA_HOME/hookshot), ~/.cache/nvidia, mesa_shader_cache (and the drivers' env overrides), the temporary folder, /dev/shm, /proc (thread names need write access there: a rule on /proc/self/task does not hold, since procfs makes its inodes anew; Landlock's ptrace rule still keeps other processes' files out) | | devices | /dev/dri, /dev/nvidia*, /dev/input, /dev/hidraw*, /dev/snd, /dev/udmabuf | | in the game folder | only its own state files, written in place: hookshot_options.ini, the *.bin caches, the log | | Unix sockets | Wayland, X11, PipeWire, PulseAudio (not the session bus) | | network | no TCP | | signals | none to other processes | The game folder is read-only apart from those files, so nothing can be put there for the next start to load (a library beside the executable, an env file). Missing state files are made empty first; each reader takes an empty file as none. The Vulkan pipeline cache, written through a `.tmp` and a rename, falls back to an in-place write when the `.tmp` cannot be made. The precompile and shader-preparation crash markers (`*.running`, `*.warming`) cannot be made under the sandbox, so those two crash-loop guards are off while it is on. The update check runs before the sandbox, and the launcher is another process. Without Landlock the game runs as before, said once in the log. `HOS_SANDBOX=0` turns it off; `HOS_SANDBOX_RW=<path>[:...]` adds writable paths for debugging; the test harness's own paths (RX_VK_SHOTS, RX_VK_FRAMELOG's folder, HOS_INPUT_LIVE, HOS_ENV_FILE) are allowed by themselves. `HOS_SANDBOX_PROBE=<path>[:...]` logs whether each path can be read, written or created beside. ## Tested (Linux, kernel with Landlock ABI 11, NVIDIA, Sway) - windowed and fullscreen to play; sound (the game's stream on PipeWire); a Switch Pro controller found through SDL; saves written; hookshot_options.ini rewritten at exit; program, pipeline and driver caches written; screenshots - the log: `sandbox: Landlock ABI 11 on, every thread, ... rules (TCP none): ...` - the probe, from inside the game: - `~/.ssh/config`, `~/.bashrc`, a file in `$HOME`: read, write, create beside all Permission denied - `hookshot_vk.so`: read allowed, write Permission denied; nothing can be created in the game folder - `hookshot_options.ini`: read and write allowed - the save folder and the NVIDIA shader cache: create allowed - no frame-rate change beyond run-to-run noise Not tested: controller hotplug (no unplugging during the runs; SDL's udev monitor is a netlink socket and /dev/input is allowed), AMD/Intel (Mesa) drivers, X11 sessions, older kernels (ABI < 8 restricts only the game's thread).
NotRexed commented 2026-11-09 05:30:48 +00:00 (Migrated from github.com)

Thank you for this! It changes how the game runs at a level that needs a maintainer design decision first, so I'm closing it to keep the queue clear: needs maintainer design decision, reopen after discussion. (A Landlock sandbox can break saves, caches and paths on some Linux setups and the Steam Deck: needs a discussion of the allowed paths first.)

Thank you for this! It changes how the game runs at a level that needs a maintainer design decision first, so I'm closing it to keep the queue clear: needs maintainer design decision, reopen after discussion. (A Landlock sandbox can break saves, caches and paths on some Linux setups and the Steam Deck: needs a discussion of the allowed paths first.)
hckrman101 commented 2026-11-10 20:05:15 +00:00 (Migrated from github.com)

Updated onto main (merge 990febc; no force push). This PR doesn't depend on #880.

Pipeline cache save (vk_draw.cpp): main's new save is kept as it is (busy guard, unchanged-size check, checked close, log). This PR's fallback sits inside it: when the sandbox won't let the .tmp file be created, the cache is written in place with a checked close, and "saved in place" is logged.

#1772's play cache files: main now saves what play builds as new mkw_vk_pipelines_play_N.bin files, and the sandbox can't allow new files in the game folder. Under the sandbox, RX_VK_PLAY_CACHE=0 is set unless the player set it, so play goes back into mkw_vk_pipelines.bin in place. That brings back larger in-place rewrites, but only with the sandbox on.

Allow-list, brought up to main:

  • Game-folder state files the game now writes: hookshot_programs.idx, hookshot_computes.bin, hookshot_pipeline_states.bin, prompts/Common.bntx.zs, prompts/Common.key.
  • The options file: made empty before the sandbox (no longer options_version=3). A file with no key=value line now counts as no file, so the low-spec first-start profile still applies.
  • The saves folder: no longer writable as a whole. Only save/, cache/, temp/, present.txt and mouse_sens.txt are. builder/ (the updater's AppRun) and ngx/ (a DLSS library search path) stay read-only, so a hijacked game can't plant code for a later start.
  • DLSS: <game>/ngx is made before the sandbox and stays read-only. RX_VK_DLSS_DIR and RX_VK_FSR_DIR are readable.
  • Discord: its IPC sockets, including the Flatpak and Snap paths, are allowed.
  • Reads: ~/.config/pipewire, ~/.asoundrc and the icon folders are readable.

New tests/test_sandbox_linux.py applies the sandbox in a child process. It checks that the state files are writable, and that planting files in the game folder, the saves-folder root, builder/ and ngx/ is refused.

Tested:

  • tests/run_all.py shows the same failures as main.
  • A headless run logged sandbox: Landlock ABI 11 on, every thread, 65 rules.
  • The probe refused ~/.bashrc and new files beside the options file and in the save folder, and allowed writes to the options file.
  • About 2,000 presents at 60 fps, the in-place state files updated, no permission errors.

Not tested yet:

  • Saves: the run only reached the title screen, so no game save was written.
  • The in-place pipeline cache save: only covered by the test, not in game, since too few pipelines were built in the run to trigger a save.
  • DLSS: whether it starts when NGX can't write logs into the read-only <game>/ngx.
  • Discord: Rich Presence under the sandbox.
  • Other setups: AMD and Intel drivers, X11, Steam Deck, kernels before Landlock ABI 8.
  • The Insert menu's "Restart now": the restarted game inherits the sandbox, so its log rotation and update check will fail. This needs a follow-up.
Updated onto `main` (merge 990febc; no force push). This PR doesn't depend on #880. **Pipeline cache save (`vk_draw.cpp`):** `main`'s new save is kept as it is (busy guard, unchanged-size check, checked close, log). This PR's fallback sits inside it: when the sandbox won't let the `.tmp` file be created, the cache is written in place with a checked close, and "saved in place" is logged. **#1772's play cache files:** `main` now saves what play builds as new `mkw_vk_pipelines_play_N.bin` files, and the sandbox can't allow new files in the game folder. Under the sandbox, `RX_VK_PLAY_CACHE=0` is set unless the player set it, so play goes back into `mkw_vk_pipelines.bin` in place. That brings back larger in-place rewrites, but only with the sandbox on. **Allow-list, brought up to `main`:** - **Game-folder state files the game now writes:** `hookshot_programs.idx`, `hookshot_computes.bin`, `hookshot_pipeline_states.bin`, `prompts/Common.bntx.zs`, `prompts/Common.key`. - **The options file:** made empty before the sandbox (no longer `options_version=3`). A file with no `key=value` line now counts as no file, so the low-spec first-start profile still applies. - **The saves folder:** no longer writable as a whole. Only `save/`, `cache/`, `temp/`, `present.txt` and `mouse_sens.txt` are. `builder/` (the updater's AppRun) and `ngx/` (a DLSS library search path) stay read-only, so a hijacked game can't plant code for a later start. - **DLSS:** `<game>/ngx` is made before the sandbox and stays read-only. `RX_VK_DLSS_DIR` and `RX_VK_FSR_DIR` are readable. - **Discord:** its IPC sockets, including the Flatpak and Snap paths, are allowed. - **Reads:** `~/.config/pipewire`, `~/.asoundrc` and the icon folders are readable. New `tests/test_sandbox_linux.py` applies the sandbox in a child process. It checks that the state files are writable, and that planting files in the game folder, the saves-folder root, `builder/` and `ngx/` is refused. **Tested:** - `tests/run_all.py` shows the same failures as `main`. - A headless run logged `sandbox: Landlock ABI 11 on, every thread, 65 rules`. - The probe refused `~/.bashrc` and new files beside the options file and in the save folder, and allowed writes to the options file. - About 2,000 presents at 60 fps, the in-place state files updated, no permission errors. **Not tested yet:** - **Saves:** the run only reached the title screen, so no game save was written. - **The in-place pipeline cache save:** only covered by the test, not in game, since too few pipelines were built in the run to trigger a save. - **DLSS:** whether it starts when NGX can't write logs into the read-only `<game>/ngx`. - **Discord:** Rich Presence under the sandbox. - **Other setups:** AMD and Intel drivers, X11, Steam Deck, kernels before Landlock ABI 8. - **The Insert menu's "Restart now":** the restarted game inherits the sandbox, so its log rotation and update check will fail. This needs a follow-up.
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:46:22 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Besides the nx2/build.py conflict, two things block this on current main. First, #926 (the HOS_CONTROL bridge) landed after this branch, and docs/control.md already describes how the two interact. The bridge binds its socket and starts its thread at host/hos_main.c:1656, which is before this PR's hos_sandbox_apply. On a kernel below Landlock ABI 8 (so no TSYNC), that thread stays outside the sandbox, and a game that has been taken over could act through it. Either start the bridge after the restriction, with its folder allowed (bind needs MAKE_SOCK there), or refuse HOS_CONTROL under the sandbox without ABI 8.

Second, sandbox_linux.c:143-144 turns the sandbox off only for the exact string "0". Since #2042, switches go through env_value_off in gpu/vk/compat/hookshot_env.h, so HOS_SANDBOX=off or =false should turn it off too.

@NotRexed's question about turning this on by default for Steam Deck and other Linux setups is still open.

Besides the nx2/build.py conflict, two things block this on current main. First, #926 (the HOS_CONTROL bridge) landed after this branch, and docs/control.md already describes how the two interact. The bridge binds its socket and starts its thread at host/hos_main.c:1656, which is before this PR's hos_sandbox_apply. On a kernel below Landlock ABI 8 (so no TSYNC), that thread stays outside the sandbox, and a game that has been taken over could act through it. Either start the bridge after the restriction, with its folder allowed (bind needs MAKE_SOCK there), or refuse HOS_CONTROL under the sandbox without ABI 8. Second, sandbox_linux.c:143-144 turns the sandbox off only for the exact string "0". Since #2042, switches go through env_value_off in gpu/vk/compat/hookshot_env.h, so HOS_SANDBOX=off or =false should turn it off too. @NotRexed's question about turning this on by default for Steam Deck and other Linux setups is still open.
Commenting is not possible because the repository is archived.
No description provided.