fix: harden what files, downloads and guest memory hand to host code #1398

Merged
hckrman101 merged 9 commits from security-hardening into main 2026-11-10 00:43:52 +00:00
hckrman101 commented 2026-11-10 00:40:12 +00:00 (Migrated from github.com)

This PR checks inputs before host code uses them: files players pass around, release downloads, setup lists, and
guest memory. Each commit is one topic and can be reviewed alone.

Changes

fix(nvn): keep host pointers in host-side tables; bounds-check guest ranges

  • What: the native NVN and video layers kept some host pointers inside the game's objects, which are guest
    memory. They now live on the host side:
    • the command buffers' recording buffers (a slot table; the object holds only its slot, and a slot names its owner);
    • recording handles (a set of live recordings);
    • virtual pools' page tables (found by the pool's GPU address);
    • memory pool storage (a guest address);
    • window textures (guest addresses, checked at each use);
    • video pools and decoders (a table by guest object address).
  • Guest addresses and sizes are checked against the guest window before host code reads or writes there:
    • NVN object and array arguments;
    • the GPU address translation the renderer reads and writes through;
    • virtual mappings, packaged texture data and deferred binds;
    • nvdrv ioctl buffers;
    • the video readback targets.
  • UpdateUniformBuffer takes at most NVN's 64 KiB.
  • Why: host code should not trust a pointer or a size that guest memory holds.
  • Tested:
    • tests/test_nvn_host_tables.py (new): the real functions, covering window ranges including wrap-around, slots by
      owner and the live handle set;
    • tests/test_nvn_buffer_map.py, updated for the pool's guest address;
    • game runs (below).

ci: check release downloads and keep caches out of release builds

  • What:
    • scripts/linux/deps.sh pins the SHA-256 of the LLVM, Python, FFmpeg, SDL2 and appimagetool downloads and checks
      them on every run. The Python one matches python-build-standalone's published SHA256SUMS.
    • A tag build no longer restores built dependencies (Linux) or the unpacked llvm-mingw (Windows) from a cache. It
      unpacks them from their checked downloads. The Linux job's timeout goes to 60 minutes for that.
    • release_parts.py extract skips zip members that resolve outside the target folder.
    • publish_parts.sh writes the release JSON to a private temporary file.
  • Why: what a release ships should come from checked downloads, not from a cache that other runs write.
  • Tested:
    • fetch() with a matching and a wrong hash, by hand;
    • bash -n on both scripts.
    • The workflow edits are small and use the existing steps. actionlint runs on this PR in CI.

fix(builder): check setup list names; load the setup's libraries from System32

  • What:
    • setup.c and linux_update.py only accept setup lists whose tag and part names are plain version names and
      relative paths inside the install folder. Checksums must be 64 hex digits, and links must be https (also file://
      on Linux, for local lists). The tag names a folder the update removes and remakes.
    • The Windows setup exe calls SetDefaultDllDirectories(System32) and is linked with
      --dependent-load-flag 0x800, so the libraries it imports come from System32, not from the folder it was
      downloaded to.
    • Runner.cs uses a source tree only when the app sits in that tree's builder folder.
    • The Linux builder makes no menu entry for a folder name with a line break.
  • Tested:
    • tests/test_linux_update.py: a new case with bad tags, paths, checksums and links;
    • setup.c cross-built with llvm-mingw; llvm-readobj --coff-load-config shows DependentLoadFlags: 0x800;
    • the builder tests.
    • Not run on Windows: worth a Process Monitor check that no library lookups land in the download folder.

fix(vk): check the shaders seed, program cache, pipeline notes and state list as read

  • What:
    • hookshot_shaders.bin (seed_open): every count, size and stage index is checked against the file.
    • hookshot_programs.bin: record sizes are compared unsigned, so a size past 2^63 no longer makes the scan go
      backwards.
    • hookshot_pipeline_states.bin and hookshot_pipelines.bin:
      • at most 8 colour targets, a sample count and topology Vulkan has, and at most 16 vertex attributes;
      • a note's render-target pointers are cleared and its booleans made 0 or 1;
      • build_graphics, gpl_link and pipeline_key also refuse more than 8 targets.
    • mkw_vk_pipelines.bin: the header's length and version are checked too.
  • Why: these files are read at start and when shaders are prepared, and the state list is meant to be shareable.
  • Tested:
    • tests/test_seed_reader.py (new): the real reader under ASan/UBSan, on cut, oversized and mutated files
      (20,000 mutations). It fails on the old reader;
    • new cases in tests/test_pipeline_state_counts.py and tests/test_program_cache_index.py.
  • What: scripts/oot_saves.py copied a save with copytree/copy2, which follow links. A link in a save from
    another program now stops the copy before anything changes, and the copy no longer follows links.
  • Tested: tests/test_oot_saves.py test_links_in_a_save_refused (a linked .sav, a linked account.sav, a
    folder link).
  • What:
    • host_path maps no guest path with a . or .. part, a \ or :, or a control character. On Windows it also
      refuses a part that ends in a dot or a space, and device names. It does not use a host path that was cut short.
    • DeleteDirectoryRecursively removes a link inside the folder itself, never what it points to.
    • The mod index does not follow links to folders and stops 32 levels down. Links to files still work, since mod
      managers place files that way.
    • The posix layer marks symbolic links as reparse points, as Windows does.
  • Tested: tests/test_fs_links.py (new; the real functions over the posix layer).

fix(env): hookshot_env.txt sets only the game's own switches

  • What: the file beside the game now sets only names that start with RX_, HOS_, HOOKSHOT_, EDEN_, NVN_,
    TOTK_ or SDL_. It never sets HOS_ENV_FILE or SDL_DYNAMIC_API, and takes no path as a value except
    RX_VK_DLSS_DIR's. Each line it leaves out is logged. A file named by HOS_ENV_FILE, as test runs use, still sets
    anything. The readme says so.
  • Why: players are asked to edit this file and pass it around. Variables outside the game's own switches change
    how the system's libraries behave, and some switches name paths.
  • Tested: tests/test_env_file.py (the player's file and HOS_ENV_FILE's).

fix(update): check the latest release's tag on Windows too

  • What: Windows now uses the same tag check as Linux (letters, digits and . _ + -) before the tag goes on the
    Builder's command line.
  • Tested: cross-compiled with llvm-mingw; the Linux path is unchanged.

fix(windows): load the Vulkan loader and SDL from known folders only

  • What: vulkan-1.dll is loaded from System32. SDL, when it is not beside the executable, is found without the
    current folder or PATH.
  • Tested: hookshot_vk.dll cross-built with llvm-mingw.

How it was tested

  • python -I -B tests/run_all.py: 200 passed, 7 skipped (no tool for them here). nx2/test_nup_read_name.py needs zstandard; it passes with the bundled Python, as do test_nup_codec.py and test_nup_verify.py.
  • Linux build of the game and hookshot_vk.so: a full build passes (hookshot and hookshot_vk.so), and the vk ABI check is clean.
  • Windows cross-build (llvm-mingw) of hookshot_vk.dll, the setup exe, and a syntax check of every changed host file.
  • Game run on Linux: in a headless compositor, from a copy of a save:
    • the game boots, loads the save and plays (walking, camera, a sword swing) at about 55-57 fps;
    • the Insert menu opens, moves and closes;
    • three runs, one of them a first start that prepared the shaders;
    • no new error lines in the log;
    • a hookshot_env.txt with VK_LOADER_DEBUG and HOS_SAMPLE_FILE lines logged both as left out.

Not in this PR

  • A lock file for the release workflow's npm packages (npm ci instead of npm install).
  • Pinning the hash of the earlier release's setup list, which the Windows release job reuses parts from.
  • Signing the setup lists or the release binaries.
  • An install key in the program cache's stamp, so that a cache made elsewhere is rebuilt.

These need a decision from the maintainer first.

This PR checks inputs before host code uses them: files players pass around, release downloads, setup lists, and guest memory. Each commit is one topic and can be reviewed alone. ## Changes ### fix(nvn): keep host pointers in host-side tables; bounds-check guest ranges - **What:** the native NVN and video layers kept some host pointers inside the game's objects, which are guest memory. They now live on the host side: - the command buffers' recording buffers (a slot table; the object holds only its slot, and a slot names its owner); - recording handles (a set of live recordings); - virtual pools' page tables (found by the pool's GPU address); - memory pool storage (a guest address); - window textures (guest addresses, checked at each use); - video pools and decoders (a table by guest object address). - Guest addresses and sizes are checked against the guest window before host code reads or writes there: - NVN object and array arguments; - the GPU address translation the renderer reads and writes through; - virtual mappings, packaged texture data and deferred binds; - nvdrv ioctl buffers; - the video readback targets. - UpdateUniformBuffer takes at most NVN's 64 KiB. - **Why:** host code should not trust a pointer or a size that guest memory holds. - **Tested:** - `tests/test_nvn_host_tables.py` (new): the real functions, covering window ranges including wrap-around, slots by owner and the live handle set; - `tests/test_nvn_buffer_map.py`, updated for the pool's guest address; - game runs (below). ### ci: check release downloads and keep caches out of release builds - **What:** - `scripts/linux/deps.sh` pins the SHA-256 of the LLVM, Python, FFmpeg, SDL2 and appimagetool downloads and checks them on every run. The Python one matches python-build-standalone's published SHA256SUMS. - A tag build no longer restores built dependencies (Linux) or the unpacked llvm-mingw (Windows) from a cache. It unpacks them from their checked downloads. The Linux job's timeout goes to 60 minutes for that. - `release_parts.py extract` skips zip members that resolve outside the target folder. - `publish_parts.sh` writes the release JSON to a private temporary file. - **Why:** what a release ships should come from checked downloads, not from a cache that other runs write. - **Tested:** - `fetch()` with a matching and a wrong hash, by hand; - `bash -n` on both scripts. - The workflow edits are small and use the existing steps. actionlint runs on this PR in CI. ### fix(builder): check setup list names; load the setup's libraries from System32 - **What:** - `setup.c` and `linux_update.py` only accept setup lists whose tag and part names are plain version names and relative paths inside the install folder. Checksums must be 64 hex digits, and links must be https (also file:// on Linux, for local lists). The tag names a folder the update removes and remakes. - The Windows setup exe calls `SetDefaultDllDirectories(System32)` and is linked with `--dependent-load-flag 0x800`, so the libraries it imports come from System32, not from the folder it was downloaded to. - `Runner.cs` uses a source tree only when the app sits in that tree's builder folder. - The Linux builder makes no menu entry for a folder name with a line break. - **Tested:** - `tests/test_linux_update.py`: a new case with bad tags, paths, checksums and links; - `setup.c` cross-built with llvm-mingw; `llvm-readobj --coff-load-config` shows `DependentLoadFlags: 0x800`; - the builder tests. - Not run on Windows: worth a Process Monitor check that no library lookups land in the download folder. ### fix(vk): check the shaders seed, program cache, pipeline notes and state list as read - **What:** - `hookshot_shaders.bin` (`seed_open`): every count, size and stage index is checked against the file. - `hookshot_programs.bin`: record sizes are compared unsigned, so a size past 2^63 no longer makes the scan go backwards. - `hookshot_pipeline_states.bin` and `hookshot_pipelines.bin`: - at most 8 colour targets, a sample count and topology Vulkan has, and at most 16 vertex attributes; - a note's render-target pointers are cleared and its booleans made 0 or 1; - `build_graphics`, `gpl_link` and `pipeline_key` also refuse more than 8 targets. - `mkw_vk_pipelines.bin`: the header's length and version are checked too. - **Why:** these files are read at start and when shaders are prepared, and the state list is meant to be shareable. - **Tested:** - `tests/test_seed_reader.py` (new): the real reader under ASan/UBSan, on cut, oversized and mutated files (20,000 mutations). It fails on the old reader; - new cases in `tests/test_pipeline_state_counts.py` and `tests/test_program_cache_index.py`. ### fix(saves): refuse links in a save being imported - **What:** `scripts/oot_saves.py` copied a save with `copytree`/`copy2`, which follow links. A link in a save from another program now stops the copy before anything changes, and the copy no longer follows links. - **Tested:** `tests/test_oot_saves.py test_links_in_a_save_refused` (a linked `.sav`, a linked `account.sav`, a folder link). ### fix(fs): keep guest paths in their mounts; do not work through links - **What:** - `host_path` maps no guest path with a `.` or `..` part, a `\` or `:`, or a control character. On Windows it also refuses a part that ends in a dot or a space, and device names. It does not use a host path that was cut short. - `DeleteDirectoryRecursively` removes a link inside the folder itself, never what it points to. - The mod index does not follow links to folders and stops 32 levels down. Links to files still work, since mod managers place files that way. - The posix layer marks symbolic links as reparse points, as Windows does. - **Tested:** `tests/test_fs_links.py` (new; the real functions over the posix layer). ### fix(env): hookshot_env.txt sets only the game's own switches - **What:** the file beside the game now sets only names that start with `RX_`, `HOS_`, `HOOKSHOT_`, `EDEN_`, `NVN_`, `TOTK_` or `SDL_`. It never sets `HOS_ENV_FILE` or `SDL_DYNAMIC_API`, and takes no path as a value except `RX_VK_DLSS_DIR`'s. Each line it leaves out is logged. A file named by `HOS_ENV_FILE`, as test runs use, still sets anything. The readme says so. - **Why:** players are asked to edit this file and pass it around. Variables outside the game's own switches change how the system's libraries behave, and some switches name paths. - **Tested:** `tests/test_env_file.py` (the player's file and `HOS_ENV_FILE`'s). ### fix(update): check the latest release's tag on Windows too - **What:** Windows now uses the same tag check as Linux (letters, digits and `. _ + -`) before the tag goes on the Builder's command line. - **Tested:** cross-compiled with llvm-mingw; the Linux path is unchanged. ### fix(windows): load the Vulkan loader and SDL from known folders only - **What:** `vulkan-1.dll` is loaded from System32. SDL, when it is not beside the executable, is found without the current folder or PATH. - **Tested:** `hookshot_vk.dll` cross-built with llvm-mingw. ## How it was tested - `python -I -B tests/run_all.py`: 200 passed, 7 skipped (no tool for them here). `nx2/test_nup_read_name.py` needs `zstandard`; it passes with the bundled Python, as do `test_nup_codec.py` and `test_nup_verify.py`. - Linux build of the game and `hookshot_vk.so`: a full build passes (`hookshot` and `hookshot_vk.so`), and the vk ABI check is clean. - Windows cross-build (llvm-mingw) of `hookshot_vk.dll`, the setup exe, and a syntax check of every changed host file. - Game run on Linux: in a headless compositor, from a copy of a save: - the game boots, loads the save and plays (walking, camera, a sword swing) at about 55-57 fps; - the Insert menu opens, moves and closes; - three runs, one of them a first start that prepared the shaders; - no new error lines in the log; - a `hookshot_env.txt` with `VK_LOADER_DEBUG` and `HOS_SAMPLE_FILE` lines logged both as left out. ## Not in this PR - A lock file for the release workflow's npm packages (`npm ci` instead of `npm install`). - Pinning the hash of the earlier release's setup list, which the Windows release job reuses parts from. - Signing the setup lists or the release binaries. - An install key in the program cache's stamp, so that a cache made elsewhere is rebuilt. These need a decision from the maintainer first.
Commenting is not possible because the repository is archived.
No description provided.