fix(control): the bridge's pipe lets a sandboxed game's agent in (AppContainer, a farm user) #544

Merged
HumanBean merged 1 commit from HumanBean/Longshot:fix/control-pipe-sandbox into main 2026-11-14 23:31:54 +00:00
Owner

Why

The Test Farm's Windows agent (Beanstalk) runs the game in a sandbox, either an AppContainer or a dedicated low-rights Windows user. Today the control bridge's pipe admits only the game's own user. So the agent can't reach a game in a low-rights user at all, and an AppContainer game's pipe can't make its next instances.

What changed

  • control: pipe_security() in host/control.c now builds the pipe's permissions (DACL) as before, plus two additions:
    • When the game's token is an AppContainer's, the container's SID gets full access (GA). Each new pipe instance is checked against the existing one's DACL, and an AppContainer token needs its own SID granted.
    • HOS_CONTROL_PEER_SID=<SID> grants that account read and write (GRGW), so the agent's own account can connect to a game running as the farm's user. hos_control_peer_sid_check() accepts only a local user account:
      • the form S-1-5-21-a-b-c-RID, with exactly five parts after S-1-5 and a RID of 1000 or more;
      • Windows looks the account up and confirms it's a user;
      • the string round-trips unchanged through Windows' SID conversion.
    • Everyone, Authenticated Users, Users, Administrators and six other well-known SIDs are refused with a log line, and the pipe then stays the current user's alone.
  • Nothing else changes: same-user only by default, FILE_FLAG_FIRST_PIPE_INSTANCE, remote clients refused, and Linux's SO_PEERCRED check. Linux needs no counterpart, since the farm's bwrap sandbox runs the game as the same user.
  • docs: docs/control.md's Security section covers both additions and the SID rules.
  • tests:
    • a Windows-only peersid case in tests/control_test.c, run from tests/test_control.py;
    • new tests/test_control_pipe_security.py. It reads control.c's source for the AppContainer branch, the peer branch and the refused-SID list; it fails if either grant is removed. With LONGSHOT_MINGW set, it also compiles host/control.c and tests/control_test.c for x86_64-w64-mingw32 with -Werror.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_CONTROL_PEER_SID lets one more local user account open the control pipe (the farm agent's) unset: the game's user only set only by the farm agent in its low-rights-user mode Windows test farm agents Linux (no effect) real Windows

Measurements

No performance change: the pipe's permissions are built once, when the bridge starts.

main: 7ff5256e (2026-11-14)
this PR: fix/control-pipe-sandbox @ 11e884ce

main @ 7ff5256e this PR @ 11e884ce
tests/test_control.py (Linux) pass pass
tests/test_longshot_ctl.py pass 38/38 pass
x86_64-w64-mingw32 compile of host/control.c, -Werror builds builds

Testing

Tested: on Linux, tests/test_control.py, tests/test_longshot_ctl.py (38/38) and the new tests/test_control_pipe_security.py (source checks, and the llvm-mingw compile of host/control.c and tests/control_test.c). The source check was confirmed to fail when either grant is removed.

Not tested: nothing has run on Windows yet: the DACL as Windows reads it back, the peersid case, and a sandboxed game's pipe. tests/run_all.py wasn't re-run (to keep CPU free).

Needs other testers:

  • Windows with a GPU: the Beanstalk agent's beanstalk-agent sandbox-check (both modes) and python -I -B tests/test_control.py, expecting the pipe checks to pass in both sandbox modes (request to open).

Screenshots

Nothing on screen.

Risk and rollback

It only adds grants, and only for an AppContainer game or an explicitly named local user. Without HOS_CONTROL_PEER_SID, and outside an AppContainer, the DACL is the same as before. Reverting the commit restores the old behaviour.

Reviewer notes

hos_control_peer_sid_check() first. GRGW also allows creating pipe instances; a narrower grant would turn away normal clients (.NET's NamedPipeClientStream, a plain open), and this is documented in docs/control.md.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above). The touched suites were run; the full suite was not re-run, to keep CPU free (see Testing).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Not applicable (no performance change).
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • A game-free test added for the fix, failing on main and passing here.
  • Docs updated.
## Why The Test Farm's Windows agent ([Beanstalk](https://git.magicbean.dev/MagicBeanDev/Beanstalk/pulls/3)) runs the game in a sandbox, either an AppContainer or a dedicated low-rights Windows user. Today the control bridge's pipe admits only the game's own user. So the agent can't reach a game in a low-rights user at all, and an AppContainer game's pipe can't make its next instances. ## What changed - **control:** `pipe_security()` in `host/control.c` now builds the pipe's permissions (DACL) as before, plus two additions: - **When the game's token is an AppContainer's**, the container's SID gets full access (`GA`). Each new pipe instance is checked against the existing one's DACL, and an AppContainer token needs its own SID granted. - **`HOS_CONTROL_PEER_SID=<SID>`** grants that account read and write (`GRGW`), so the agent's own account can connect to a game running as the farm's user. `hos_control_peer_sid_check()` accepts only a local user account: - the form `S-1-5-21-a-b-c-RID`, with exactly five parts after `S-1-5` and a RID of 1000 or more; - Windows looks the account up and confirms it's a user; - the string round-trips unchanged through Windows' SID conversion. - Everyone, Authenticated Users, Users, Administrators and six other well-known SIDs are refused with a log line, and the pipe then stays the current user's alone. - Nothing else changes: same-user only by default, `FILE_FLAG_FIRST_PIPE_INSTANCE`, remote clients refused, and Linux's `SO_PEERCRED` check. Linux needs no counterpart, since the farm's bwrap sandbox runs the game as the same user. - **docs:** `docs/control.md`'s Security section covers both additions and the SID rules. - **tests:** - a Windows-only `peersid` case in `tests/control_test.c`, run from `tests/test_control.py`; - new `tests/test_control_pipe_security.py`. It reads `control.c`'s source for the AppContainer branch, the peer branch and the refused-SID list; it fails if either grant is removed. With `LONGSHOT_MINGW` set, it also compiles `host/control.c` and `tests/control_test.c` for `x86_64-w64-mingw32` with `-Werror`. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_CONTROL_PEER_SID` | lets one more local user account open the control pipe (the farm agent's) | unset: the game's user only | set only by the farm agent in its low-rights-user mode | Windows test farm agents | Linux (no effect) | real Windows | ## Measurements No performance change: the pipe's permissions are built once, when the bridge starts. main: 7ff5256e (2026-11-14) this PR: fix/control-pipe-sandbox @ 11e884ce | | main @ 7ff5256e | this PR @ 11e884ce | |---|---|---| | `tests/test_control.py` (Linux) | pass | pass | | `tests/test_longshot_ctl.py` | pass | 38/38 pass | | `x86_64-w64-mingw32` compile of `host/control.c`, `-Werror` | builds | builds | ## Testing **Tested:** on Linux, `tests/test_control.py`, `tests/test_longshot_ctl.py` (38/38) and the new `tests/test_control_pipe_security.py` (source checks, and the llvm-mingw compile of `host/control.c` and `tests/control_test.c`). The source check was confirmed to fail when either grant is removed. **Not tested:** nothing has run on Windows yet: the DACL as Windows reads it back, the `peersid` case, and a sandboxed game's pipe. `tests/run_all.py` wasn't re-run (to keep CPU free). **Needs other testers:** - Windows with a GPU: the Beanstalk agent's `beanstalk-agent sandbox-check` (both modes) and `python -I -B tests/test_control.py`, expecting the pipe checks to pass in both sandbox modes (request to open). ## Screenshots Nothing on screen. ## Risk and rollback It only adds grants, and only for an AppContainer game or an explicitly named local user. Without `HOS_CONTROL_PEER_SID`, and outside an AppContainer, the DACL is the same as before. Reverting the commit restores the old behaviour. ## Reviewer notes `hos_control_peer_sid_check()` first. `GRGW` also allows creating pipe instances; a narrower grant would turn away normal clients (.NET's `NamedPipeClientStream`, a plain open), and this is documented in `docs/control.md`. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). The touched suites were run; the full suite was not re-run, to keep CPU free (see Testing). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Not applicable (no performance change). - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] Docs updated.
fix(control): the bridge's pipe lets a sandboxed game's agent in (AppContainer, a farm user)
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Has been cancelled
c67b3215c4
The Beanstalk Test Farm agent runs the game in an AppContainer, or as the
local user beanstalk-runner, and connects to its control pipe from outside.
pipe_security() granted the current user alone, which broke both:

- In an AppContainer, a new pipe instance (for the next client) is checked
  against the pipe's DACL with the game's token, and an AppContainer's check
  also needs its own SID. Only the first client could connect. When the token
  is an AppContainer's (TokenIsAppContainer), its TokenAppContainerSid now
  gets GA too.
- As another account, the agent's user could not connect at all.
  HOS_CONTROL_PEER_SID names one more account, granted GRGW. It must be a
  user's SID: canonical (ConvertStringSidToSidA, IsValidSid, and the string
  round-trips), S-1-5-21- with five sub-authorities, a RID of 1000 or more,
  and SidTypeUser from LookupAccountSidA. Well-known groups (Everyone,
  Authenticated Users, Users, Administrators, ...) are refused by name. A
  refusal is logged, and the pipe stays this user's alone.

Nothing else changes: D:P, FILE_FLAG_FIRST_PIPE_INSTANCE, remote clients
refused, and Linux's SO_PEERCRED check (the farm's bwrap runs the game as the
same user there). docs/control.md's Security section describes both
additions. GRGW includes FILE_CREATE_PIPE_INSTANCE, and the docs say so.

Tests: control_test peersid (Windows) runs the SID check.
tests/test_control_pipe_security.py checks the source and compiles control.c
and control_test.c for x86_64-w64-mingw32 when LONGSHOT_MINGW is set.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

Pushed: HOS_CONTROL_PEER_SID is now in docs/development.md's switch reference too, next to HOS_CONTROL.

Pushed: HOS_CONTROL_PEER_SID is now in docs/development.md's switch reference too, next to HOS_CONTROL.
HumanBean force-pushed fix/control-pipe-sandbox from c67b3215c4
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Has been cancelled
to 11e884ce38
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
beanstalk/approval Test farm not requested
PR description / Description follows the template (pull_request_target) Failing after 6s
2026-11-14 18:57:27 +00:00
Compare
HumanBean changed title from WIP: fix(control): the bridge's pipe lets a sandboxed game's agent in (AppContainer, a farm user) to fix(control): the bridge's pipe lets a sandboxed game's agent in (AppContainer, a farm user) 2026-11-14 23:31:47 +00:00
HumanBean deleted branch fix/control-pipe-sandbox 2026-11-14 23:31:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!544
No description provided.