Skip to content

Steam isolation: per-session Steam launcher and hook (continues #110) - #113

Merged
vibesoftwarecoder merged 7 commits into
masterfrom
steam-isolation-takeover
Oct 14, 2026
Merged

vibesoftwarecoder merged 7 commits into
masterfrom
steam-isolation-takeover

Conversation

@vibesoftwarecoder

Copy link
Copy Markdown
Owner

Continues bendaali's PR #110. Their commit e03b077 is the first commit here, with their authorship. On top of it: a merge of current master, and five commits that make the changes we asked for in our review on #110. The feature is off by default (SteamIsolationEnabled: false).

The problem. Starting Steam in one Windows session without an override can deliver its command line to another session's Steam. We reproduced it on our own host. The launcher runs Steam under a supervisor and gives each seat its own Steam IPC name.

What we changed on top of #110

  1. Steam check. The wrap decision now recognizes the names Windows resolves to steam.exe: trailing dots and spaces, and the default stream. They used to launch unwrapped with no error.
  2. Restarts. The hook sits on CreateProcessInternalW, where every Win32 creation API ends, so a Steam restarting through CreateProcessAsUser is isolated too. An unquoted path with spaces is recognized the way CreateProcess reads it. Inject's buffers moved off the stack. The hook logs every restart it isolated or refused, with the reason.
  3. The wait. SteamIsolationReadyTimeoutSeconds sets it (default 180, kept between 10 and 900). A timeout answers 504 and an early helper exit answers 500. Bad paths and missing binaries stay 400.
  4. Upgrades. The release asset now ships the four helpers, built and smoke-tested in the release workflow. install-service.ps1 -BuildSteamIsolation builds them on a source install. An upgrade never deletes an existing SteamIsolation\ folder, and a payload with only some of the four files is refused.
  5. No console window. The launcher is a Windows-subsystem program and the service passes CREATE_NO_WINDOW. Failures go to %LOCALAPPDATA%\MultiSeat\SteamIsolation\launcher.log.

Checked

  • Full managed suite: 1,521 passed, 18 skipped.
  • Native checks on GitHub (windows-2022), x64 and x86: policy tests, the launcher smoke test (including restarts through CreateProcessW, A, AsUserW and an unquoted path, plus the hook's log lines), and the game launcher check.
  • The release workflow was run by hand on this branch (no release published). The asset contains all four helpers.
  • With each fix disabled, the matching tests fail.
  • The vendored MinHook: 56 of 57 files are byte-identical to upstream v1.3.4. The other differs only in line endings.

Not tested

  • A real Steam in a real seat: no Steam login, update, game or anti-cheat run, and nothing seen on a screen. "No console window" rests on the program's subsystem flag and CREATE_NO_WINDOW.
  • A -FromZip install over an existing install on a real host.
  • Restarts that Windows services carry out (CreateProcessWithTokenW, CreateProcessWithLogonW, elevated ShellExecute). These are not isolated and not refused. Steam is not known to use them.
  • Item 6 of our review (test gaps) is only partly addressed. Each change has tests, but nothing runs ProcessInjector end to end, there is no negative test that another user cannot open the readiness event, and the C# and native sides are not exercised together.
  • A plain Steam launch outside the wrapper, such as a Start menu shortcut, can still reach another session's Steam. Add experimental per-session Steam isolation and game launcher #110's docs call that outside the feature. We don't yet know how it finds it, and this PR does not address it.

Notes. CreateProcessInternalW is undocumented but exported, with a stable signature since Windows 7. If it is missing, the hook refuses to start, so isolation fails loudly instead of running without protection.

🤖 Generated with Claude Code

bendaali and others added 7 commits October 11, 2026 13:33
Brings the branch up to date with master (0.6.20 and later). Base is bendaali's
PR #110 commit e03b077.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The wrap decision compared the raw file name, so steam.exe. and steam.exe (trailing dot
or space) and steam.exe::$DATA were launched unwrapped with no error, although Windows
starts the real steam.exe for all of them. Normalize the last path segment first (trailing
dots and spaces, the default data stream) and hand the launcher the resolved name. Named
streams and lookalike names are still left alone, and relative or drive-relative forms are
refused like any other bad Steam path.

Tests cover each form, forward slashes, lookalikes and the normalizer. Twelve of them fail
against the old name comparison.

Part of the takeover of #110 (review item 1).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…failures

The wait for the isolation helper was fixed at 180 s, and its failures reached the API badly:
the timeout was a bare TimeoutException, which the launch endpoint does not catch (an
unexplained 500), and an early helper exit was a plain InvalidOperationException (400, 'bad
request').

- MultiSeat:SteamIsolationReadyTimeoutSeconds sets the wait (default 180, kept between 10 and
  900 so a typo cannot fail every launch or hold the seat's lifecycle gate for hours).
- LaunchTimedOutException answers 504 and LaunchFailedException answers 500. Both derive from
  InvalidOperationException, so existing callers keep catching them. Bad paths and missing
  helper binaries stay 400.
- The timeout message names the limit and the setting.

Tests pin the status mapping, the messages, the clamp, and that the shipped appsettings.json
carries the same default as the code. Whole managed suite: 1507 passed, 18 skipped.

Part of the takeover of #110 (review item 3).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The isolation launcher was a console program and the service started it with
CREATE_NEW_CONSOLE, so each isolated Steam launch probably showed a console window in the
seat. Two changes:

- The launcher is now a Windows-subsystem program (WIN32 with /ENTRY:wmainCRTStartup), for
  both architectures. Having no console, it reports every failure through one function: a log
  file in the seat user's profile (%LOCALAPPDATA%\MultiSeat\SteamIsolation\launcher.log,
  restarted at 256 KB), the debugger, stderr when redirected, and the parent's console when
  started from a terminal. Nothing it used to print is lost.
- The service passes CREATE_NO_WINDOW for a supervised launch instead of CREATE_NEW_CONSOLE.
  Other launches are unchanged.

The smoke test now reads both launchers' PE subsystem (checking that its probe can tell GUI
from the console fixture) and checks that a hook failure reaches the log file and stderr. Built
for x64 and Win32 with no warnings at /W4; the built launchers read as subsystem 2.

Part of the takeover of #110 (review item 5).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The installer replaces the whole install folder on every deploy and restored only the two
settings files. SteamIsolation\ held the native helpers, so an upgrade deleted them, and with
MultiSeat:SteamIsolationEnabled on every Steam launch was then refused for want of the helper.

- Install-Payload: a payload that ships SteamIsolation\ replaces the old copy; one that does not
  leaves the existing folder as it was (restored from the full-folder backup taken just before
  the wipe). A payload that carries only some of the four files is refused before anything is
  touched, since half a set works for one Steam architecture and fails for the other.
- release.yml: a steam-isolation job on windows-2022 builds both architectures and runs the policy,
  launcher and game smoke tests; the release job downloads the four binaries into
  publish/MultiSeat/SteamIsolation and its payload check fails the release if any is missing.
- install-service.ps1 -BuildSteamIsolation builds the helpers into the stage on a source install.
  They are built fresh on that run, never taken from an earlier build's output, and a failed
  build fails the install. Without the switch, behaviour is unchanged.
- docs/steam-isolation.md describes where the helpers come from and how upgrades treat them.

Tests run the real library under Windows PowerShell and PowerShell 7: keep on upgrade, replace
when shipped, no invented folder, partial set refused, and the wiring of the installer and the
release workflow. With each fix disabled the matching tests fail.

Part of the takeover of #110 (review item 4).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed paths

Three gaps in how a restarted steam.exe was recognized and isolated:

- Only CreateProcessW and CreateProcessA re-injected the hook, so a Steam that restarted itself
  through CreateProcessAsUser got the rewritten command line and environment but no hook and no
  supervision. The hook now sits on kernelbase's CreateProcessInternalW, where every Win32
  creation API ends (verified exported on this Windows build). The W/A pair and the ANSI
  conversion helper are gone.
- An unquoted program path that contains spaces was read by CommandLineToArgvW as its first
  word (C:\Program), so it was not recognized as Steam and was neither rewritten nor isolated.
  SplitImage reads it the way CreateProcess does, trying the text up to each space until a
  file exists; the first word is still taken on its name alone, and only steam.exe and
  steamwebhelper.exe are searched for.
- Inject() held about 192 KB of path buffers on the stack of whichever Steam thread restarted
  Steam. They are on the heap now, as are the buffers in MultiSeatInitialize.

The hook now logs through the same launcher.log as the launcher (new Log.h): when it becomes
active, and every restart it isolated or refused, with the reason. A refused restart used to
leave only a debugger string.

Policy tests cover the unquoted path (recognized, rewritten, not fooled by a missing folder or a
program that merely has Steam in its arguments), checked with a fake file-exists function. The
fixture can restart through CreateProcessW, A, AsUser and an unquoted path, and the smoke test
runs all four for both architectures and checks the hook's log lines. Built for x64 and Win32
at /W4 with no warnings; policy tests pass locally, the smoke tests run on CI.

Part of the takeover of #110 (review item 2).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vibesoftwarecoder
vibesoftwarecoder merged commit c966e5e into master Oct 14, 2026
5 checks passed
@vibesoftwarecoder
vibesoftwarecoder deleted the steam-isolation-takeover branch October 14, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants