Repository navigation
Steam isolation: per-session Steam launcher and hook (continues #110) - #113
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Continues bendaali's PR #110. Their commit
e03b077is the first commit here, with their authorship. On top of it: a merge of currentmaster, 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
steam.exe: trailing dots and spaces, and the default stream. They used to launch unwrapped with no error.CreateProcessInternalW, where every Win32 creation API ends, so a Steam restarting throughCreateProcessAsUseris isolated too. An unquoted path with spaces is recognized the wayCreateProcessreads it. Inject's buffers moved off the stack. The hook logs every restart it isolated or refused, with the reason.SteamIsolationReadyTimeoutSecondssets 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.install-service.ps1 -BuildSteamIsolationbuilds them on a source install. An upgrade never deletes an existingSteamIsolation\folder, and a payload with only some of the four files is refused.CREATE_NO_WINDOW. Failures go to%LOCALAPPDATA%\MultiSeat\SteamIsolation\launcher.log.Checked
CreateProcessW,A,AsUserWand an unquoted path, plus the hook's log lines), and the game launcher check.Not tested
CREATE_NO_WINDOW.-FromZipinstall over an existing install on a real host.CreateProcessWithTokenW,CreateProcessWithLogonW, elevatedShellExecute). These are not isolated and not refused. Steam is not known to use them.ProcessInjectorend 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.Notes.
CreateProcessInternalWis 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