Repository navigation
Conversation
|
Thank you for this, and for the care in it. The problem is real. On our own host, starting Steam in one Windows session without an override can deliver its command line to another session's Steam. We reproduced it. We read the whole change line by line, including the native code. We found no network access, no persistence and no privilege changes. We also found no memory-safety bugs. The bundled MinHook matches upstream v1.3.4. The hook DLL loads only into We can't merge it yet. These are the things we'd need changed:
Behaviours worth documenting: every child Steam starts gets the extra environment variable, The case we're still working on is a plain launch, such as a Start menu shortcut. Your docs say it's outside the feature, and we agree. In our tests, a Steam started without an override still reached a seat's Steam. We don't know yet how it finds it. We're looking into it, and what we learn will decide how we handle isolation. We're keeping this open. Would you like to make the changes above yourself, or would you like us to make them? Either works. If you'd rather make them, tell us roughly when, so we can plan around it. If you'd like us to do it, we'll build on your branch and credit you in the commits. If neither is practical for you right now, that's fine too. We'll keep it as a reference while we work on the plain-launch case. |
|
Thank you for the thorough review and for taking the time to reproduce the issue. I really appreciate the detailed feedback and your offer to credit my contribution. Unfortunately, I don't have enough time to continue working on the PR, and the current implementation already meets my personal needs. I'd be more than happy for you to take over and build on my branch to address the remaining issues. Thanks again for considering the contribution, and I'm looking forward to seeing how Multiseat evolves! |
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>
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>
|
Thank you for handing this over, and for the work behind it. We've taken it on, as you suggested. It continued in #113, which is now merged. Your commit is the first commit of that work on Three corrections to our review above, stated as fact:
#113 lists what we changed, what we tested and what we did not. We're closing this PR because #113 replaces it. Your branch is not touched. Thanks again. |
Launching Steam in multiple Windows seat sessions can connect a launch to another client or leave games unable to find the intended Steam instance. This adds an optional native supervisor and targeted Steam hooks, plus service integration and an EXE picker for direct game launches. The feature defaults to disabled and is proposed as a draft for compatibility review.
Changes
steam.exelaunches from the service's dashboard/app and launch-on-connect paths whenSteamIsolationEnabledis enabled. Validate executable/hook architecture and fail the launch if required components are missing.The IPC technique predates this work; steam-multibox-easysetup is a simpler existing alternative. This PR does not claim general superiority over that approach.
Validation
Draft limitations
Real Steam updates, unmodified retail Steamworks games, anti-cheat compatibility, broad restart scenarios, and disconnect/reconnect behavior still need runtime validation. Architecture-changing replacements require relaunch through the supervisor, and replacements bypassing Win32 process creation have not been validated. Apollo launches bypass automatic service wrapping and require the documented explicit launcher command. Native binaries must be built and deployed separately; this PR does not publish release binaries.