diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 745b46d..0fe6af6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,70 @@ permissions: contents: write # required to create the release jobs: + # The native Steam isolation helpers ship in the asset, under SteamIsolation\, so an upgrade + # replaces them together with the service. Without that, the installer's wipe of the install + # folder deleted any copy a host had placed there by hand, and with the feature switched on + # every Steam launch was then refused. + # + # Its own job, on windows-2022, because build.ps1 asks for Visual Studio 2022 and + # windows-latest is free to move to a newer Visual Studio. The same smoke tests that run on + # every push to src/MultiSeat.SteamIsolation run again here, so a shipped binary has passed + # them, rather than relying on the last push having been green. + steam-isolation: + name: Build Steam isolation helpers (x64 and x86) + runs-on: windows-2022 + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v5 + + - name: Build launchers, hooks and the safe fixtures + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/build.ps1 -BuildFixture + + - name: Check native isolation policies + shell: pwsh + run: | + foreach ($architecture in @('x64', 'Win32')) { + ctest --test-dir "src/MultiSeat.SteamIsolation/build-$architecture" -C Release --output-on-failure + if ($LASTEXITCODE -ne 0) { throw "Policy checks failed for $architecture" } + } + + - name: Check launch, restart, readiness, and cleanup + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/tests/LauncherSmoke.ps1 + + - name: Check direct game environment and working directory + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/tests/GameLauncherSmoke.ps1 + + - name: Require all four helper binaries + shell: pwsh + # Half a set is worse than none: the service picks the launcher and hook by the + # architecture of the Steam it starts, so a missing one fails for one Steam only. + run: | + $dir = 'src/MultiSeat.SteamIsolation/artifacts' + $names = @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamLauncher32.exe', + 'MultiSeat.SteamHook.dll', 'MultiSeat.SteamHook32.dll') + $missing = @($names | Where-Object { -not (Test-Path (Join-Path $dir $_)) }) + if ($missing.Count) { throw "build.ps1 did not produce: $($missing -join ', ')" } + $names | ForEach-Object { Get-Item (Join-Path $dir $_) | Select-Object Name, Length | Format-Table -HideTableHeaders } + + - name: Upload the helper binaries for the release job + uses: actions/upload-artifact@v6 + with: + name: steam-isolation-binaries + # Listed one by one so the test fixtures built beside them never reach the asset. + path: | + src/MultiSeat.SteamIsolation/artifacts/MultiSeat.SteamLauncher.exe + src/MultiSeat.SteamIsolation/artifacts/MultiSeat.SteamLauncher32.exe + src/MultiSeat.SteamIsolation/artifacts/MultiSeat.SteamHook.dll + src/MultiSeat.SteamIsolation/artifacts/MultiSeat.SteamHook32.dll + if-no-files-found: error + release: name: Publish windows-x64 asset + needs: steam-isolation runs-on: windows-latest steps: @@ -174,6 +236,12 @@ jobs: Copy-Item 'README.md' 'publish/MultiSeat/README.md' -Force Write-Host "staged scripts/, prerequisites/ and README.md into the asset" + - name: Download the Steam isolation helpers into the payload + uses: actions/download-artifact@v6 + with: + name: steam-isolation-binaries + path: publish/MultiSeat/SteamIsolation + - name: Verify the payload before zipping it shell: pwsh # A release that extracts to a broken tree is worse than no release, because it looks @@ -190,6 +258,11 @@ jobs: # Self-contained means the runtime must be inside. If this is absent the zip would # silently require a .NET install, which is the entire thing this release fixes. if (-not (Test-Path "$root/hostfxr.dll")) { $problems += "hostfxr.dll missing - build is NOT self-contained" } + # The installer replaces the whole install folder, so the helpers have to ride in the + # asset or an upgrade removes them. All four or nothing; see the steam-isolation job. + foreach ($n in @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamLauncher32.exe', 'MultiSeat.SteamHook.dll', 'MultiSeat.SteamHook32.dll')) { + if (-not (Test-Path "$root/SteamIsolation/$n")) { $problems += "SteamIsolation/$n missing - an upgrade would remove the Steam isolation helpers" } + } if ($problems.Count) { $problems | ForEach-Object { Write-Host "::error::$_" } throw "payload verification failed" diff --git a/.github/workflows/steam-isolation.yml b/.github/workflows/steam-isolation.yml new file mode 100644 index 0000000..3238436 --- /dev/null +++ b/.github/workflows/steam-isolation.yml @@ -0,0 +1,37 @@ +name: Steam isolation native checks + +on: + pull_request: + paths: + - 'src/MultiSeat.SteamIsolation/**' + - '.github/workflows/steam-isolation.yml' + push: + paths: + - 'src/MultiSeat.SteamIsolation/**' + - '.github/workflows/steam-isolation.yml' + +permissions: + contents: read + +jobs: + native-checks: + runs-on: windows-2022 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Build x64 and x86 launchers, hooks, and safe fixtures + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/build.ps1 -BuildFixture + - name: Check native isolation policies + shell: pwsh + run: | + foreach ($architecture in @('x64', 'Win32')) { + ctest --test-dir "src/MultiSeat.SteamIsolation/build-$architecture" -C Release --output-on-failure + if ($LASTEXITCODE -ne 0) { throw "Policy checks failed for $architecture" } + } + - name: Check launch, restart, readiness, and cleanup + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/tests/LauncherSmoke.ps1 + - name: Check direct game environment and working directory + shell: pwsh + run: ./src/MultiSeat.SteamIsolation/tests/GameLauncherSmoke.ps1 diff --git a/.gitignore b/.gitignore index 904748b..aa9431f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ PktMon.* # C++ InputHook DLL build output src/MultiSeat.InputHook/build/ +# Steam isolation native build products +src/MultiSeat.SteamIsolation/build-*/ +src/MultiSeat.SteamIsolation/artifacts/ + # TypeScript incremental build info *.tsbuildinfo diff --git a/README.md b/README.md index 6b0db8c..937d5bb 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,9 @@ Edit `appsettings.json` in `C:\Program Files\MultiSeat\` (restart the service af | `EnableKeyboardMouseIsolation` | `false` | Keyboard/mouse session isolation (no-op as architected — see Known Constraints in CLAUDE.md) | | `EnableSharedGameLibrary` | `true` | Create a shared games/ROMs folder all seats can use | | `SharedGameLibraryDir` | `C:\MultiSeatGames` | Root of the shared library (`\SteamLibrary` + `\ROMs`) | +| `SteamIsolationEnabled` | `false` | Wrap explicit `steam.exe` seat launches with the experimental native isolation helper. See [Steam isolation setup](docs/steam-isolation.md) for binaries, Apollo commands and limitations. | +| `SteamIsolationLauncherPath` | `C:\Program Files\MultiSeat\SteamIsolation\MultiSeat.SteamLauncher.exe` | Supervising x64 launcher; the matching x86 launcher and hooks live beside it. | +| `SteamIsolationReadyTimeoutSeconds` | `180` | How long a supervised Steam launch may take to report ready (kept between 10 and 900 seconds). The wait holds the seat's lifecycle gate. A timeout answers 504 through the API. | | `EnableEmulatorNetplay` | `true` | Assign + open a per-seat RetroArch netplay port (seats connect over `127.0.0.1`) | | `SeedRetroArchNetplayConfig` | `false` | Auto-write each seat's `retroarch.cfg` (netplay port + shared ROM dir) | | `UpdateCheckEnabled` | `false` | Ask GitHub's public releases API whether newer MultiSeat, ApolloVibe and MoonlightVibe releases exist, and show the answer on the dashboard. **Off by default**: it is the only internet connection the service would make. It sends an unauthenticated GET with a generic User-Agent and nothing about your host, and never downloads or installs anything. Set it in `appsettings.local.json` or from the dashboard's System page; no restart needed. Details: `docs/security-posture.md`, "Outbound connections". | diff --git a/docs/steam-isolation.md b/docs/steam-isolation.md new file mode 100644 index 0000000..874b350 --- /dev/null +++ b/docs/steam-isolation.md @@ -0,0 +1,75 @@ +# Steam isolation (experimental, opt in) + +MultiSeat can launch Steam through its own native isolation launcher inside a seat's existing Windows user and session. This feature does not change Steam installations, replace system DLLs, or affect unrelated applications. + +The native launcher and hooks live in `C:\Program Files\MultiSeat\SteamIsolation`, and you do not normally build or place them yourself: + +- **A release asset (`multiseat-windows-x64.zip`) already contains them.** `install-service.ps1 -FromZip` installs them beside the service. The release build runs the same native smoke tests that run on every push to `src/MultiSeat.SteamIsolation`, and refuses to publish an asset that lacks any of the four files. +- **A source install builds them only when asked:** `.\scripts\install-service.ps1 -BuildSteamIsolation` (Visual Studio 2022 C++ tools and CMake are required). They are built fresh on that run and never taken from an earlier build's output. If the build fails, the install fails, rather than leaving a host with the feature on and no helper. +- **An upgrade never silently deletes them.** The installer replaces the whole install folder. A payload that ships `SteamIsolation\` replaces the old copy, and one that does not (an older release, or a source install without the switch) leaves the existing `SteamIsolation\` folder as it was. A payload that ships only some of the four files is refused before anything is touched. + +The folder must contain the x64 `MultiSeat.SteamLauncher.exe` and `MultiSeat.SteamHook.dll`, and for 32-bit Steam also `MultiSeat.SteamLauncher32.exe` and `MultiSeat.SteamHook32.dll`. Keep these binaries in a directory standard seat accounts cannot modify, while granting them read/execute access. + +Enable it in the deployed `appsettings.local.json`, then restart MultiSeat: + +```json +{ + "MultiSeat": { + "SteamIsolationEnabled": true, + "SteamIsolationLauncherPath": "C:\\Program Files\\MultiSeat\\SteamIsolation\\MultiSeat.SteamLauncher.exe" + } +} +``` + +The dashboard's app launch and `MultiSeat:LaunchOnConnect` automatically wrap an explicit, absolute path whose filename is exactly `steam.exe` (case insensitive). Other executables keep their existing behavior. Relative Steam paths are refused when isolation is enabled. Steam's original arguments and the launch working directory are preserved. Missing binaries, invalid PE files, and mismatched hook architecture prevent the isolated launch; there is no fallback that silently starts unisolated Steam. + +For example, configure a launch-on-connect entry with an absolute installed Steam path and `-bigpicture`. The launcher supervises the whole process tree, including replacement Steam processes, and returns the initial Steam process's exit code after the tree exits. MultiSeat tracks the supervisor PID, so reconnect duplicate detection and disconnect process-tree cleanup continue to cover the isolated Steam process. + +The service waits for an explicit native initialization signal, rather than assuming a running launcher means isolation succeeded. It creates a unique protected `Global\MultiSeatSteamReady-` event and grants only SYSTEM, administrators and the exact seat account access (the seat can signal/wait only). The launcher receives `--ready-event ` and signals it after successful hook initialization. Startup exit before the signal, cancellation, or an initialization timeout fails the launch and terminates the supervisor, whose job cleans up Steam and its children. The timeout is `SteamIsolationReadyTimeoutSeconds` (default 180, kept between 10 and 900). The wait holds the seat's lifecycle gate, so other operations on that seat queue behind it; lower the value to fail a stuck launch sooner, raise it for a slow host. Through the API a timeout answers `504 Gateway Timeout` and a launcher that exits early answers `500`; a bad Steam path or a missing helper binary still answers `400`, because it is refused before anything starts. There is no unisolated retry. The launcher is a Windows-subsystem program, so starting it opens no console window in the seat. Because there is no console, it records every failure in `%LOCALAPPDATA%\MultiSeat\SteamIsolation\launcher.log` in the seat user's profile (the file restarts after 256 KB). It also writes the message to stderr when that is redirected, and to the parent's console when it was started from a terminal. Failures after readiness are visible there; the service does not continuously monitor the launcher's exit code. + +Apollo launches its Moonlight app entries directly, so those commands bypass the managed service's wrapping. To opt in for an Apollo Steam app, use an explicit command in the source Apollo app list, substituting the installed Steam path: + +```text +"C:\Program Files\MultiSeat\SteamIsolation\MultiSeat.SteamLauncher.exe" --steam "C:\Program Files (x86)\Steam\steam.exe" -- -bigpicture +``` + +MultiSeat copies the source app list into the seat when it is provisioned again. Do not put this command in a detached/preparation command: the supervisor needs to remain the app process. A `steam://open/bigpicture` command invokes Windows' protocol handler and is not automatically rewritten. Replace that entry with the explicit launcher command when isolation is desired. Invoking the native launcher directly is itself an opt in and works independently of the service setting. + +Steam launched manually from a shortcut, Explorer, a Startup entry, or another program does not pass through this feature. Close Steam in the target seat before starting the isolated instance, and use the wrapper for subsequent Steam launches. Separate Windows users already isolate user files; native hooks address the Steam-specific machine/global state and require multi-session runtime validation. The feature does not grant game licenses or change Steam account access rules. + +## Building and checking + +Build the managed service using the normal MultiSeat build process. Building the native component requires Visual Studio 2022 C++ tools and CMake: + +```powershell +.\src\MultiSeat.SteamIsolation\build.ps1 +``` + +The four release binaries are written to `src\MultiSeat.SteamIsolation\artifacts`. MinHook v1.3.4 is vendored with its license in `third_party\minhook`. To build safe test fixtures and run the launcher smoke checks without launching installed Steam: + +```powershell +.\src\MultiSeat.SteamIsolation\build.ps1 -BuildFixture +.\src\MultiSeat.SteamIsolation\tests\LauncherSmoke.ps1 -Artifacts .\src\MultiSeat.SteamIsolation\artifacts +.\src\MultiSeat.SteamIsolation\tests\GameLauncherSmoke.ps1 -Artifacts .\src\MultiSeat.SteamIsolation\artifacts +dotnet test src\MultiSeat.Tests\MultiSeat.Tests.csproj --filter FullyQualifiedName~SteamIsolation +``` + +## Isolation behavior and limits + +The hook creates an IPC name from the Windows session and user SID, replaces Steam's `-master_ipc_name_override` argument and `steam_master_ipc_name_override` environment entry, and separates browser `htmlcache` and `cef_log` paths. It redirects `TempAppPath` and `TempAppCmdLine` registry operations to the seat user's `HKCU\Software\Valve\Steam`, and returns zero for successful DWORD `SteamPID` reads. + +Child command lines and explicit environments are adjusted before process creation. Same-architecture replacement `steam.exe` launches receive the hook before their main thread runs. The hook sits on `CreateProcessInternalW`, where every Win32 creation API ends (`CreateProcessW` and `A`, `CreateProcessAsUser`, and a non-elevated `ShellExecute`), and it recognizes an unquoted program path that contains spaces the way `CreateProcess` does, by trying the text up to each space until a file exists. Two ways of starting a process cannot be seen from inside Steam: `CreateProcessWithTokenW` and `CreateProcessWithLogonW`, which the secondary logon service carries out, and an elevated `ShellExecute`, which the AppInfo service carries out. A Steam restarted that way is not isolated and is not refused, because nothing in the caller can tell. Steam is not known to restart itself through either. Browser and game processes receive no injected hook DLL. An architecture-changing Steam update requires relaunching through the supervisor; the current hook cannot inject a DLL of the other architecture, so such a restart is refused rather than let through unisolated. + +The hook writes one line to the same `launcher.log` when it becomes active in `steam.exe`, and one for every restart it isolated or refused (with the reason). After a real session, `launcher.log` in the seat user's profile shows whether Steam ever restarted itself and whether each restart was isolated. + +The feature defaults to disabled. Fixture checks cover both architectures, restart propagation, quoting, registry types, explicit child environments, readiness, and job cleanup. Automated tests do not validate actual Steam login, updates, games, anti-cheat behavior, or simultaneous real Windows seat sessions. Limited manual reports are described below. This source does not establish compatibility with every Steam release. + +## Launching a game directly + +A game launched from Explorer does not inherit the isolated Steam client's IPC environment. Launch it from that Steam client's library, or use `Launch-Game.cmd` and `Launch-IsolatedGame.ps1` together from this component's source folder. The command opens an EXE picker and launches the selected game with the same session/user-derived Steam connection name and its own installation folder as the working directory. Run it as the normal seat account, after starting Steam with the native supervisor. + +The picker changes only the new game's environment. It does not inject a DLL into the game, modify saves, or create a second copy. This directly launched game is outside the Steam supervisor's job, so stopping the supervisor does not guarantee that the selected game exits. Close the game separately. The helper checks for a Steam process in the session; it does not verify that process was launched with isolation. + +Manual user testing reported simultaneous Steam clients on the host and one Windows seat and successful Valheim launches via this helper. The game installation included a third-party Steamworks compatibility component; unmodified retail Steamworks games have not been validated. These reports supplement the fixture checks rather than establishing general game compatibility. + +The IPC override technique predates this implementation; see [steam-multibox-easysetup](https://github.com/dreamware-media/steam-multibox-easysetup). This implementation adds targeted hooks, startup readiness, restart propagation, and process-tree supervision. No claim is made that those additions outperform simpler launch scripts across all games. diff --git a/scripts/install-service.ps1 b/scripts/install-service.ps1 index 8e2c41a..b79d0d6 100644 --- a/scripts/install-service.ps1 +++ b/scripts/install-service.ps1 @@ -20,7 +20,14 @@ param( # The asset is self-contained, so this path needs NO .NET SDK, NO .NET runtime and NO # Node on the target - which is the entire point of issue #32. Everything after the # deploy step (RDP setup, certificates, service registration) is identical either way. - [string]$FromZip + [string]$FromZip, + + # Source build only: also build the native Steam isolation helpers (Visual Studio 2022 C++ + # tools and CMake are required) and install them into SteamIsolation\ beside the service. + # Off by default, like the feature itself. They are built fresh on every run that asks for + # them and never taken from an earlier build's output, so what gets installed is what the + # checkout contains. A release asset already carries them, so -FromZip does not need this. + [switch]$BuildSteamIsolation ) $ErrorActionPreference = "Stop" @@ -597,6 +604,21 @@ try { Write-Host " This is expected and safe: the feature is off by default and currently inert." -ForegroundColor DarkGray } + # -- Build the Steam isolation helpers into the stage (only when asked) ---- + # A failure here is fatal on purpose: the switch is an explicit request, and a quiet + # skip would leave a host with SteamIsolationEnabled refusing every Steam launch. + if ($BuildSteamIsolation) { + $steamSrc = Join-Path $PSScriptRoot "..\src\MultiSeat.SteamIsolation" + $steamOut = Join-Path $stage "SteamIsolation" + Write-Step "Building the Steam isolation helpers (x64 and x86)..." + & (Join-Path $steamSrc "build.ps1") -OutputDirectory $steamOut + $steamMissing = @(Get-MissingSteamIsolationFiles -Dir $steamOut) + if ($steamMissing.Count -gt 0) { + throw "The Steam isolation build did not produce $($steamMissing -join ', ')." + } + Write-Step "Steam isolation helpers staged in $steamOut" + } + # -- Build the dashboard into the stage's wwwroot ------------------------- $DashboardDir = Join-Path $PSScriptRoot "..\src\MultiSeat.Dashboard" if (Test-Path (Join-Path $DashboardDir "package.json")) { diff --git a/scripts/lib/install-lib.ps1 b/scripts/lib/install-lib.ps1 index d034b64..ee61013 100644 --- a/scripts/lib/install-lib.ps1 +++ b/scripts/lib/install-lib.ps1 @@ -32,6 +32,21 @@ function Get-MissingPayloadFiles { return @(Get-PayloadRequiredFiles | Where-Object { -not (Test-Path (Join-Path $Dir $_)) }) } +# The native Steam isolation helpers (issue/PR #110). They live in SteamIsolation\ beside the +# service, where MultiSeat:SteamIsolationLauncherPath points by default. They are all or nothing: +# the service picks the launcher and the hook by the architecture of the Steam it is about to +# start, so a folder holding only some of them works for one Steam and fails for the other. +function Get-SteamIsolationFiles { + return @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamLauncher32.exe', + 'MultiSeat.SteamHook.dll', 'MultiSeat.SteamHook32.dll') +} + +function Get-MissingSteamIsolationFiles { + param([Parameter(Mandatory = $true)][string]$Dir) + return @(Get-SteamIsolationFiles | Where-Object { + -not (Test-Path -LiteralPath (Join-Path $Dir $_) -PathType Leaf) }) +} + function Read-RuntimeConfig { # Returns the runtimeOptions object, or throws a message that names the file. param([Parameter(Mandatory = $true)][string]$Path) @@ -331,6 +346,15 @@ function Install-Payload { if (-not $check.Ok) { throw "The staged payload is inconsistent -- nothing was installed. $($check.Problems -join ' ')" } + # A payload that carries SteamIsolation\ must carry all of it. Half a set would replace a + # working folder with one that fails for one Steam architecture, and say nothing. + $stagedSteam = Join-Path $Stage 'SteamIsolation' + if (Test-Path -LiteralPath $stagedSteam) { + $lacking = @(Get-MissingSteamIsolationFiles -Dir $stagedSteam) + if ($lacking.Count -gt 0) { + throw "The staged SteamIsolation folder is missing $($lacking -join ', ') -- nothing was installed." + } + } # 2. Preserve host-local configuration BEFORE the wipe. See issue #45: this wipe once # deleted both files with no backup. Preserve the FILES, byte for byte, never their text: @@ -379,6 +403,18 @@ function Install-Payload { # without a clone. They are NOT part of the deployed service. Copy-DirectoryContents -From $Stage -To $InstallDir -SkipNames $SkipNames + # SteamIsolation\ is part of the install, not host configuration, so the wipe above + # removed it. A payload that ships its own replaces the old copy. One that does not (an + # older release, a source build run without -BuildSteamIsolation) must not delete a copy + # that was placed by hand: with MultiSeat:SteamIsolationEnabled on, every Steam launch + # would then be refused for want of the helper. + $keptSteam = if ($folderBackup) { Join-Path $folderBackup 'SteamIsolation' } else { $null } + if (-not (Test-Path -LiteralPath $stagedSteam) -and $keptSteam -and (Test-Path -LiteralPath $keptSteam)) { + Copy-Item -LiteralPath $keptSteam -Destination (Join-Path $InstallDir 'SteamIsolation') ` + -Recurse -Force -ErrorAction Stop + Write-Host " Kept the existing SteamIsolation folder (this payload does not ship one)" -ForegroundColor DarkGray + } + # Put host configuration back over the shipped defaults. An upgrade must not silently # change how this host is configured. if ($preserved.ContainsKey('appsettings.local.json')) { diff --git a/src/MultiSeat.Service/Configuration/MultiSeatOptions.cs b/src/MultiSeat.Service/Configuration/MultiSeatOptions.cs index 462b7c6..8f93c84 100644 --- a/src/MultiSeat.Service/Configuration/MultiSeatOptions.cs +++ b/src/MultiSeat.Service/Configuration/MultiSeatOptions.cs @@ -428,6 +428,26 @@ public sealed class MultiSeatOptions // at login run before any stream and never see the per-stream virtual pad. public LaunchOnConnectApp[] LaunchOnConnect { get; set; } = []; + /// Opt in to wrapping explicit steam.exe seat launches with the native isolation helper. + public bool SteamIsolationEnabled { get; set; } = false; + + /// Absolute path to the supervising x64 launcher; matching hooks live beside it. + public string SteamIsolationLauncherPath { get; set; } = + @"C:\Program Files\MultiSeat\SteamIsolation\MultiSeat.SteamLauncher.exe"; + + /// + /// How long a supervised Steam launch may take to report that its hook is in place, in + /// seconds. The wait holds the seat's lifecycle gate, so other operations on that seat queue + /// behind it; a shorter value fails a stuck launch sooner, a longer one suits a slow host. + /// Read through , which keeps it in range. + /// + public int SteamIsolationReadyTimeoutSeconds { get; set; } = 180; + + /// limited to 10 s .. 15 min, so a + /// typo cannot make every isolated launch fail at once or hold a seat for hours. + public TimeSpan UsableSteamIsolationReadyTimeout => + TimeSpan.FromSeconds(Math.Clamp(SteamIsolationReadyTimeoutSeconds, 10, 900)); + // Delay after the client-connect event before launching the apps, giving Apollo // a moment to create the virtual controller so the apps enumerate it at startup. public int LaunchOnConnectDelayMs { get; set; } = 4_000; diff --git a/src/MultiSeat.Service/HttpStatusExceptions.cs b/src/MultiSeat.Service/HttpStatusExceptions.cs index c77fcb8..efcf36d 100644 --- a/src/MultiSeat.Service/HttpStatusExceptions.cs +++ b/src/MultiSeat.Service/HttpStatusExceptions.cs @@ -6,7 +6,7 @@ namespace MultiSeat.Service; /// condition is temporary and the same request may succeed later, unlike a conflict with a /// specific existing resource. /// -/// ⭐ All three types here derive from deliberately. Every +/// ⭐ Every type here derives from deliberately. Every /// existing non-HTTP caller — worker autostart, the smoke script, tooling — already catches that, /// and keeps catching these unchanged. The HTTP layer gets more precision without anything else /// having to learn about it. @@ -44,6 +44,34 @@ internal sealed class SeatNotFoundException : InvalidOperationException public SeatNotFoundException(string message = "Seat not found.") : base(message) { } } +/// +/// A launch inside a seat started but did not finish in time: the isolation helper never reported +/// that it was ready. The API maps this to 504 Gateway Timeout, not 400 — the request was +/// fine, so a caller must not be told it was malformed, and a later attempt may well succeed. +/// +/// Before this type the wait threw a bare , which is not an +/// , so the launch endpoint did not catch it and the +/// caller got an unhandled 500 with no explanation. +/// +internal sealed class LaunchTimedOutException : InvalidOperationException +{ + public LaunchTimedOutException(string message) : base(message) { } +} + +/// +/// A launch inside a seat failed on the server side after the request was accepted: the isolation +/// helper exited before it reported ready. The API maps this to 500 Internal Server Error. +/// It used to surface as a plain , which means 400, and +/// told the caller their request was malformed when nothing about it was wrong. +/// +/// Bad Steam paths and missing helper binaries stay plain +/// (400): those are refused before anything starts, and the caller or administrator can fix them. +/// +internal sealed class LaunchFailedException : InvalidOperationException +{ + public LaunchFailedException(string message) : base(message) { } +} + /// /// Maps the exception types above onto HTTP results, so every endpoint answers the same way and a /// new endpoint cannot quietly disagree with the others. @@ -51,7 +79,7 @@ public SeatNotFoundException(string message = "Seat not found.") : base(message) internal static class ApiErrors { /// - /// ⚠️ Order matters: all three types derive from , so + /// ⚠️ Order matters: every type here derives from , so /// the specific cases must be tested before the fallback. A plain InvalidOperationException /// still means 400 — a genuinely bad request, such as an unusable resolution. /// @@ -60,6 +88,8 @@ internal static class ApiErrors SeatNotFoundException => Results.NotFound(new { error = ex.Message }), ResourceConflictException => Results.Conflict(new { error = ex.Message }), CapacityExhaustedException => Results.Json(new { error = ex.Message }, statusCode: 503), + LaunchTimedOutException => Results.Json(new { error = ex.Message }, statusCode: 504), + LaunchFailedException => Results.Json(new { error = ex.Message }, statusCode: 500), _ => Results.BadRequest(new { error = ex.Message }) }; @@ -70,6 +100,8 @@ internal static class ApiErrors SeatNotFoundException => StatusCodes.Status404NotFound, ResourceConflictException => StatusCodes.Status409Conflict, CapacityExhaustedException => StatusCodes.Status503ServiceUnavailable, + LaunchTimedOutException => StatusCodes.Status504GatewayTimeout, + LaunchFailedException => StatusCodes.Status500InternalServerError, _ => StatusCodes.Status400BadRequest }; } diff --git a/src/MultiSeat.Service/Sessions/ProcessInjector.cs b/src/MultiSeat.Service/Sessions/ProcessInjector.cs index a95056f..80c8b92 100644 --- a/src/MultiSeat.Service/Sessions/ProcessInjector.cs +++ b/src/MultiSeat.Service/Sessions/ProcessInjector.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.Principal; using Microsoft.Extensions.Options; using MultiSeat.Service.Configuration; using MultiSeat.Service.Interop; @@ -66,6 +67,11 @@ public async Task LaunchInSessionAsync( var consoleSessionId = (int)Kernel32.WTSGetActiveConsoleSessionId(); EnsureNotConsoleSession(sessionId, consoleSessionId, exePath, allowConsoleSession); + var launchPlan = SteamIsolationLaunchPlan.Create(exePath, arguments, _options); + if (launchPlan.Supervised) + _logger.LogInformation("Launching isolated Steam through '{Launcher}' in session {Sid}", + launchPlan.ExecutablePath, sessionId); + // Step 1: Get a primary token for the target session using var token = _sessionLauncher.GetSessionToken(sessionId, accountName); @@ -101,6 +107,12 @@ public async Task LaunchInSessionAsync( Marshal.GetLastWin32Error(), sessionId, exePath); } + using var tokenIdentity = launchPlan.Supervised ? new WindowsIdentity(token.DangerousGetHandle()) : null; + using var readiness = launchPlan.Supervised + ? new SteamIsolationReadiness(tokenIdentity!.User ?? throw new InvalidOperationException("Seat token has no user SID.")) + : null; + if (readiness is not null) launchPlan = launchPlan.WithReadyEvent(readiness.Name); + // Step 2: Create the environment block for the user if (!UserEnv.CreateEnvironmentBlock(out var envBlock, token, false)) { @@ -112,7 +124,7 @@ public async Task LaunchInSessionAsync( try { // Step 3: Build the command line - var cmdLine = FormatCommandLine(exePath, arguments); + var cmdLine = FormatCommandLine(launchPlan.ExecutablePath, launchPlan.Arguments); var si = new Kernel32.StartupInfo { @@ -122,10 +134,7 @@ public async Task LaunchInSessionAsync( wShowWindow = Kernel32.SW_SHOW // games should be visible }; - var creationFlags = - Kernel32.CREATE_UNICODE_ENVIRONMENT | - Kernel32.CREATE_NEW_CONSOLE | - Kernel32.NORMAL_PRIORITY_CLASS; + var creationFlags = CreationFlagsFor(launchPlan.Supervised); // Step 4: Launch the process if (!AdvApi.CreateProcessAsUserW( @@ -154,12 +163,25 @@ public async Task LaunchInSessionAsync( // session we ASKED for, which is a claim about our intent rather than about the world — // so a process running in the wrong session produced a perfectly reassuring log. Seven // rounds of issue #18 went past exactly that line. - VerifyLandedInSession(pi, exePath, sessionId, consoleSessionId); - try { + VerifyLandedInSession(pi, exePath, sessionId, consoleSessionId); // Step 5: Wait briefly for the process to initialize - await WaitForProcessStartAsync(pi, ct); + if (readiness is not null) + await readiness.WaitAsync(pi.hProcess, _options.UsableSteamIsolationReadyTimeout, ct); + else + await WaitForProcessStartAsync(pi, ct); + } + catch + { + // The supervisor owns a kill-on-close job containing Steam and its children. + // Terminating it also cleans up an incomplete or cancelled isolated launch. + if (readiness is not null && + Kernel32.WaitForSingleObject(pi.hProcess, 0) == Kernel32.WAIT_TIMEOUT && + !Kernel32.TerminateProcess(pi.hProcess, 1)) + _logger.LogError("Failed to terminate Steam isolation supervisor PID {Pid}: {Error}", + pi.dwProcessId, Marshal.GetLastWin32Error()); + throw; } finally { @@ -175,6 +197,18 @@ public async Task LaunchInSessionAsync( } } + /// + /// Process creation flags for a launch in a seat. An ordinary launch gets a new console, as it + /// always did. A supervised Steam launch never does: it starts the isolation launcher, which + /// has no console of its own, and a console created for it anyway would be one more window + /// in the seat that nobody asked for. The helper's own children are Steam, which is a + /// windowed program. + /// + internal static uint CreationFlagsFor(bool supervised) => + Kernel32.CREATE_UNICODE_ENVIRONMENT | + (supervised ? Kernel32.CREATE_NO_WINDOW : Kernel32.CREATE_NEW_CONSOLE) | + Kernel32.NORMAL_PRIORITY_CLASS; + /// /// Launch Apollo streaming server inside a target session. /// Special handling: Apollo needs specific env vars and the config path. diff --git a/src/MultiSeat.Service/Sessions/SteamIsolationLaunchPlan.cs b/src/MultiSeat.Service/Sessions/SteamIsolationLaunchPlan.cs new file mode 100644 index 0000000..7b69d1b --- /dev/null +++ b/src/MultiSeat.Service/Sessions/SteamIsolationLaunchPlan.cs @@ -0,0 +1,135 @@ +using System.Reflection.PortableExecutable; +using MultiSeat.Service.Configuration; + +namespace MultiSeat.Service.Sessions; + +/// Wraps explicit Steam launches without changing unrelated seat processes. +internal sealed record SteamIsolationLaunchPlan(string ExecutablePath, string? Arguments, bool Supervised) +{ + private string? SteamPath { get; init; } + private string? SteamArguments { get; init; } + + internal SteamIsolationLaunchPlan WithReadyEvent(string eventName) + { + if (!Supervised) throw new InvalidOperationException("Only isolated Steam launches use a readiness event."); + if (!eventName.StartsWith(@"Global\MultiSeatSteamReady-", StringComparison.Ordinal) || + !Guid.TryParseExact(eventName[@"Global\MultiSeatSteamReady-".Length..], "N", out _)) + throw new ArgumentException("Invalid Steam readiness event name.", nameof(eventName)); + return this with { Arguments = BuildArguments(SteamPath!, SteamArguments, eventName) }; + } + + internal static SteamIsolationLaunchPlan Create(string executablePath, string? arguments, + MultiSeatOptions options) + { + if (!options.SteamIsolationEnabled || !TryNormalizeSteamPath(executablePath, out var steamPath)) + return new(executablePath, arguments, false); + + // From here on the normalized path is the only one used: it is what the launcher is told + // to start and what is checked on disk, so the name Windows would resolve is the name + // that gets wrapped. + executablePath = steamPath; + RequireFile(executablePath, "Steam executable"); + var launcher = options.SteamIsolationLauncherPath; + RequireFile(launcher, "Steam isolation launcher"); + var directory = Path.GetDirectoryName(launcher)!; + var machine = ReadMachine(executablePath); + if (machine is not (Machine.I386 or Machine.Amd64)) + throw new InvalidOperationException("Steam isolation supports only x86 and x64 Steam executables."); + + RequireMachine(launcher, Machine.Amd64, "Steam isolation launcher"); + var hook = Path.Combine(directory, machine == Machine.I386 + ? "MultiSeat.SteamHook32.dll" : "MultiSeat.SteamHook.dll"); + RequireFile(hook, "Steam isolation hook"); + RequireMachine(hook, machine, "Steam isolation hook"); + if (machine == Machine.I386) + { + var launcher32 = Path.Combine(directory, "MultiSeat.SteamLauncher32.exe"); + RequireFile(launcher32, "32-bit Steam isolation launcher"); + RequireMachine(launcher32, Machine.I386, "32-bit Steam isolation launcher"); + } + + // Steam arguments are already a Windows command-line tail. Do not split/requote them. + return new(launcher, BuildArguments(executablePath, arguments, null), true) + { SteamPath = executablePath, SteamArguments = arguments }; + } + + /// + /// True when Windows would start steam.exe for this path, with the path rewritten to the + /// name it resolves to. Comparing the raw file name is not enough: Windows drops trailing dots + /// and spaces from the last path segment, and a :stream suffix names a stream of the same + /// file (only the default stream; a named one is not Steam), so steam.exe., + /// steam.exe and steam.exe::$DATA all start Steam and + /// would otherwise be launched unwrapped, with no error. Only the last segment is touched; + /// the directory part and the name's own casing are kept. + /// + internal static bool TryNormalizeSteamPath(string path, out string normalized) + { + normalized = path; + if (string.IsNullOrWhiteSpace(path)) return false; + + var prefixLength = path.LastIndexOfAny(['\\', '/']) + 1; + // A drive-relative path ("C:steam.exe") has no separator, but its drive prefix is not + // part of the name and must not be mistaken for a stream suffix. + if (prefixLength == 0 && path.Length >= 2 && path[1] == ':') prefixLength = 2; + + var name = path[prefixLength..]; + var colon = name.IndexOf(':'); + if (colon >= 0) + { + // Only the default data stream is the file itself. A named stream is something else. + var stream = name[(colon + 1)..]; + if (stream.Length != 0 && + !stream.Equals(":$DATA", StringComparison.OrdinalIgnoreCase) && + !stream.Equals("$DATA", StringComparison.OrdinalIgnoreCase)) + return false; + name = name[..colon]; + } + name = name.TrimEnd('.', ' '); + + if (!string.Equals(name, "steam.exe", StringComparison.OrdinalIgnoreCase)) return false; + normalized = path[..prefixLength] + name; + return true; + } + + private static string BuildArguments(string steam, string? arguments, string? readyEvent) + { + var wrapped = $"--steam {QuotePath(steam)}"; + if (readyEvent is not null) wrapped += $" --ready-event {readyEvent}"; + wrapped += " --"; + if (!string.IsNullOrWhiteSpace(arguments)) wrapped += " " + arguments; + return wrapped; + } + + private static void RequireFile(string path, string description) + { + if (string.IsNullOrWhiteSpace(path) || path.IndexOfAny(['"', '\0']) >= 0 || + !Path.IsPathFullyQualified(path) || !File.Exists(path)) + throw new InvalidOperationException( + $"{description} must be an existing absolute file path: '{path}'. " + + "Steam was not launched without isolation."); + } + + private static Machine ReadMachine(string path) + { + try + { + using var stream = File.OpenRead(path); + using var pe = new PEReader(stream); + if (pe.PEHeaders.PEHeader is null) throw new BadImageFormatException(); + return pe.PEHeaders.CoffHeader.Machine; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or BadImageFormatException) + { + throw new InvalidOperationException($"Cannot read Steam isolation executable architecture: '{path}'.", ex); + } + } + + private static void RequireMachine(string path, Machine expected, string description) + { + if (ReadMachine(path) != expected) + throw new InvalidOperationException($"{description} has the wrong architecture: '{path}' (expected {expected})."); + } + + // Validated file paths cannot contain a quote and cannot end in a directory separator. + private static string QuotePath(string path) => $"\"{path}\""; +} diff --git a/src/MultiSeat.Service/Sessions/SteamIsolationReadiness.cs b/src/MultiSeat.Service/Sessions/SteamIsolationReadiness.cs new file mode 100644 index 0000000..9853efc --- /dev/null +++ b/src/MultiSeat.Service/Sessions/SteamIsolationReadiness.cs @@ -0,0 +1,119 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Principal; +using Microsoft.Win32.SafeHandles; +using MultiSeat.Service.Interop; + +namespace MultiSeat.Service.Sessions; + +/// A service-owned event shared only with the exact seat account and host administrators. +internal sealed class SteamIsolationReadiness : IDisposable +{ + private readonly EventWaitHandle _event; + internal string Name { get; } + + internal SteamIsolationReadiness(SecurityIdentifier seatSid) + { + Name = @"Global\MultiSeatSteamReady-" + Guid.NewGuid().ToString("N"); + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(BuildSddl(seatSid), 1, + out var descriptor, out _)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot create Steam readiness event security."); + using (descriptor) + { + var attributes = new SecurityAttributes + { + Length = Marshal.SizeOf(), + SecurityDescriptor = descriptor.DangerousGetHandle(), + InheritHandle = false + }; + var handle = CreateEventW(ref attributes, true, false, Name); + var error = Marshal.GetLastWin32Error(); + if (handle.IsInvalid) + { + handle.Dispose(); + throw new Win32Exception(error, "Cannot create Steam readiness event."); + } + if (error == 183) // A fresh GUID must never reuse an existing object. + { + handle.Dispose(); + throw new InvalidOperationException("Steam readiness event name collision."); + } + try + { + _event = new EventWaitHandle(false, EventResetMode.ManualReset); + var originalHandle = _event.SafeWaitHandle; + _event.SafeWaitHandle = handle; + originalHandle.Dispose(); + } + catch + { + handle.Dispose(); + throw; + } + } + } + + internal static string BuildSddl(SecurityIdentifier sid) => + $"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x00100002;;;{sid.Value})"; + + internal Task WaitAsync(IntPtr processHandle, TimeSpan timeout, CancellationToken ct) => + WaitCoreAsync(() => _event.WaitOne(0), () => + { + var status = Kernel32.WaitForSingleObject(processHandle, 0); + if (status == Kernel32.WAIT_OBJECT_0) + { + if (!Kernel32.GetExitCodeProcess(processHandle, out var code)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot read Steam launcher exit code."); + return code; + } + if (status != Kernel32.WAIT_TIMEOUT) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot wait for Steam launcher."); + return null; + }, timeout, ct); + + internal static async Task WaitCoreAsync(Func isReady, Func exitCode, + TimeSpan timeout, CancellationToken ct) + { + var elapsed = System.Diagnostics.Stopwatch.StartNew(); + while (true) + { + ct.ThrowIfCancellationRequested(); + if (isReady()) return; + if (exitCode() is { } code) + throw new LaunchFailedException($"Steam isolation launcher exited before initialization succeeded (code {code})."); + if (elapsed.Elapsed >= timeout) + throw new LaunchTimedOutException( + $"Steam isolation initialization did not signal readiness within {timeout.TotalSeconds:0} s. " + + "A slow host can raise MultiSeat:SteamIsolationReadyTimeoutSeconds."); + await Task.Delay(TimeSpan.FromMilliseconds(50), ct); + } + } + + public void Dispose() => _event.Dispose(); + + [StructLayout(LayoutKind.Sequential)] + private struct SecurityAttributes + { + public int Length; + public IntPtr SecurityDescriptor; + [MarshalAs(UnmanagedType.Bool)] public bool InheritHandle; + } + + private sealed class SafeLocalMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeLocalMemoryHandle() : base(true) { } + protected override bool ReleaseHandle() => LocalFree(handle) == IntPtr.Zero; + } + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW( + string descriptor, uint revision, out SafeLocalMemoryHandle result, out uint size); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern SafeWaitHandle CreateEventW(ref SecurityAttributes attributes, + [MarshalAs(UnmanagedType.Bool)] bool manualReset, [MarshalAs(UnmanagedType.Bool)] bool initialState, string name); + + [DllImport("kernel32.dll")] + private static extern IntPtr LocalFree(IntPtr memory); +} diff --git a/src/MultiSeat.Service/appsettings.json b/src/MultiSeat.Service/appsettings.json index 0b28beb..688b007 100644 --- a/src/MultiSeat.Service/appsettings.json +++ b/src/MultiSeat.Service/appsettings.json @@ -43,6 +43,9 @@ "ProcessLaunchTimeoutMs": 10000, "HealthCheckIntervalMs": 5000, "LaunchOnConnectDelayMs": 4000, + "SteamIsolationEnabled": false, + "SteamIsolationLauncherPath": "C:\\Program Files\\MultiSeat\\SteamIsolation\\MultiSeat.SteamLauncher.exe", + "SteamIsolationReadyTimeoutSeconds": 180, "KillLaunchOnConnectAppsOnDisconnect": false, "LaunchOnConnect": [], "EnableSharedGameLibrary": true, diff --git a/src/MultiSeat.SteamIsolation/CMakeLists.txt b/src/MultiSeat.SteamIsolation/CMakeLists.txt new file mode 100644 index 0000000..418ec90 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.20) +project(MultiSeatSteamIsolation LANGUAGES C CXX) +if(NOT WIN32) + message(FATAL_ERROR "Steam isolation requires Windows") +endif() +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +set(BUILD_SHARED_LIBS OFF) +add_subdirectory(third_party/minhook) +add_library(MultiSeatSteamHook SHARED Hook.cpp) +target_link_libraries(MultiSeatSteamHook PRIVATE minhook advapi32 shell32 user32) +# A Windows-subsystem program, so the service starting it never opens a console window in the +# seat. It keeps the wmain entry point, which the linker does not look for under /SUBSYSTEM:WINDOWS +# unless told to. Launcher.cpp reports failures to a log file because there is no console. +add_executable(MultiSeatSteamLauncher WIN32 Launcher.cpp) +if(MSVC) + target_link_options(MultiSeatSteamLauncher PRIVATE /ENTRY:wmainCRTStartup) +endif() +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set_target_properties(MultiSeatSteamHook PROPERTIES OUTPUT_NAME "MultiSeat.SteamHook" PREFIX "") + set_target_properties(MultiSeatSteamLauncher PROPERTIES OUTPUT_NAME "MultiSeat.SteamLauncher") +else() + set_target_properties(MultiSeatSteamHook PROPERTIES OUTPUT_NAME "MultiSeat.SteamHook32" PREFIX "") + set_target_properties(MultiSeatSteamLauncher PROPERTIES OUTPUT_NAME "MultiSeat.SteamLauncher32") +endif() +foreach(target MultiSeatSteamHook MultiSeatSteamLauncher) + target_compile_definitions(${target} PRIVATE UNICODE _UNICODE NOMINMAX) + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /permissive-) + endif() +endforeach() +option(MULTISEAT_BUILD_STEAM_FIXTURE "Build safe native isolation test fixture (no Steam needed)" OFF) +if(MULTISEAT_BUILD_STEAM_FIXTURE) + enable_testing() + add_executable(SteamIsolationFixture tests/Fixture.cpp) + target_compile_definitions(SteamIsolationFixture PRIVATE UNICODE _UNICODE NOMINMAX) + target_link_libraries(SteamIsolationFixture PRIVATE advapi32) + add_executable(SteamIsolationPolicyTests tests/PolicyTests.cpp) + target_compile_definitions(SteamIsolationPolicyTests PRIVATE UNICODE _UNICODE NOMINMAX) + target_link_libraries(SteamIsolationPolicyTests PRIVATE shell32) + add_test(NAME SteamIsolationPolicyTests COMMAND SteamIsolationPolicyTests) +endif() diff --git a/src/MultiSeat.SteamIsolation/Hook.cpp b/src/MultiSeat.SteamIsolation/Hook.cpp new file mode 100644 index 0000000..bf7b745 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Hook.cpp @@ -0,0 +1,242 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include "IsolationPolicy.h" +#include "Inject.h" +#include "Log.h" + +namespace { +using namespace steam_isolation; +std::wstring ipcName, commandW; +std::string commandA; +bool isSteam = false; +HMODULE ownModule = nullptr; +std::wstring ownDllPath; +LONG initialization = 0; +thread_local bool creating = false; + +// Stable Windows process-parameter prefix, following winsiderss/phnt ntrtl.h. +// Only normalized parameters with the full prefix are admitted for rewriting. +struct CurrentDir { UNICODE_STRING path; HANDLE handle; }; +struct DriveDir { USHORT flags, length; ULONG timestamp; ANSI_STRING path; }; +struct Parameters { + ULONG maximumLength, length, flags, debugFlags; + HANDLE console; ULONG consoleFlags; + HANDLE input, output, error; + CurrentDir currentDirectory; + UNICODE_STRING dllPath, image, command; + PVOID environment; + ULONG startingX, startingY, countX, countY, countCharsX, countCharsY, fillAttribute; + ULONG windowFlags, showWindowFlags; + UNICODE_STRING windowTitle, desktop, shell, runtime; + DriveDir drives[32]; + ULONG_PTR environmentSize, environmentVersion; +}; +#ifdef _WIN64 +static_assert(offsetof(Parameters, command) == 0x70 && offsetof(Parameters, environmentSize) == 0x3f0); +#else +static_assert(offsetof(Parameters, command) == 0x40 && offsetof(Parameters, environmentSize) == 0x290); +#endif +using CreateUser = NTSTATUS (NTAPI*)(PHANDLE, PHANDLE, ACCESS_MASK, ACCESS_MASK, + POBJECT_ATTRIBUTES, POBJECT_ATTRIBUTES, ULONG, ULONG, PVOID, PVOID, PVOID); +CreateUser originalCreate = nullptr; +// Every Win32 process-creation API ends in kernelbase's CreateProcessInternalW: CreateProcessW and +// CreateProcessA, CreateProcessAsUserW and A, ShellExecute's non-elevated path. It is undocumented +// but exported and has kept this signature from Windows 7 to 11. Hooking it, rather than the +// CreateProcess pair, is what lets a Steam that restarts itself through CreateProcessAsUser be +// isolated too. (CreateProcessWithTokenW and CreateProcessWithLogonW are the exception: the +// secondary logon service creates that process, so no hook in the caller can see it.) +using CreateInternal = BOOL (WINAPI*)(HANDLE, LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, + BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION, PHANDLE); +CreateInternal createInternal = nullptr; +decltype(&RegQueryValueExW) queryW = nullptr; +decltype(&RegQueryValueExA) queryA = nullptr; +decltype(&RegSetValueExW) setW = nullptr; +decltype(&RegSetValueExA) setA = nullptr; +decltype(&RegDeleteValueW) deleteW = nullptr; +decltype(&RegDeleteValueA) deleteA = nullptr; + +bool Temporary(const wchar_t* value) { + return isSteam && value && (!_wcsicmp(value, L"TempAppPath") || !_wcsicmp(value, L"TempAppCmdLine")); +} +bool Temporary(const char* value) { + return isSteam && value && (!_stricmp(value, "TempAppPath") || !_stricmp(value, "TempAppCmdLine")); +} +struct UserSteamKey { + HKEY value = nullptr; + LSTATUS status = ERROR_INVALID_HANDLE; + explicit UserSteamKey(REGSAM access, bool create = false) { + if (!create) status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, access, &value); + else status = RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, nullptr, 0, access, nullptr, &value, nullptr); + } + ~UserSteamKey() { if (value) RegCloseKey(value); } +}; +LSTATUS WINAPI QueryW(HKEY key, LPCWSTR value, LPDWORD reserved, LPDWORD type, LPBYTE data, LPDWORD size) { + LSTATUS result; + DWORD actualType = 0; auto returnedType = type ? type : &actualType; + if (Temporary(value)) { UserSteamKey user(KEY_QUERY_VALUE); if (!user.value) return user.status; result = queryW(user.value, value, reserved, returnedType, data, size); } + else result = queryW(key, value, reserved, returnedType, data, size); + if (result == ERROR_SUCCESS && value && !_wcsicmp(value, L"SteamPID") && data && size && *size == sizeof(DWORD) && *returnedType == REG_DWORD) { + DWORD zero = 0; memcpy(data, &zero, sizeof(zero)); + } + return result; +} +LSTATUS WINAPI QueryA(HKEY key, LPCSTR value, LPDWORD reserved, LPDWORD type, LPBYTE data, LPDWORD size) { + LSTATUS result; + DWORD actualType = 0; auto returnedType = type ? type : &actualType; + if (Temporary(value)) { UserSteamKey user(KEY_QUERY_VALUE); if (!user.value) return user.status; result = queryA(user.value, value, reserved, returnedType, data, size); } + else result = queryA(key, value, reserved, returnedType, data, size); + if (result == ERROR_SUCCESS && value && !_stricmp(value, "SteamPID") && data && size && *size == sizeof(DWORD) && *returnedType == REG_DWORD) { + DWORD zero = 0; memcpy(data, &zero, sizeof(zero)); + } + return result; +} +LSTATUS WINAPI SetW(HKEY key, LPCWSTR value, DWORD reserved, DWORD type, const BYTE* data, DWORD size) { + if (!Temporary(value)) return setW(key, value, reserved, type, data, size); + UserSteamKey user(KEY_SET_VALUE, true); return user.value ? setW(user.value, value, reserved, type, data, size) : user.status; +} +LSTATUS WINAPI SetA(HKEY key, LPCSTR value, DWORD reserved, DWORD type, const BYTE* data, DWORD size) { + if (!Temporary(value)) return setA(key, value, reserved, type, data, size); + UserSteamKey user(KEY_SET_VALUE, true); return user.value ? setA(user.value, value, reserved, type, data, size) : user.status; +} +LSTATUS WINAPI DeleteW(HKEY key, LPCWSTR value) { + if (!Temporary(value)) return deleteW(key, value); + UserSteamKey user(KEY_SET_VALUE); return user.value ? deleteW(user.value, value) : user.status; +} +LSTATUS WINAPI DeleteA(HKEY key, LPCSTR value) { + if (!Temporary(value)) return deleteA(key, value); + UserSteamKey user(KEY_SET_VALUE); return user.value ? deleteA(user.value, value) : user.status; +} +LPWSTR WINAPI CommandW() { return commandW.data(); } +LPSTR WINAPI CommandA() { return commandA.data(); } +// Is this call starting steam.exe? With an application name that name decides, as CreateProcess +// does. Without one, the program is the start of the command line, which may be an unquoted path +// that contains spaces; SplitImage reads it the way CreateProcess does. +bool SteamLaunch(LPCWSTR application, LPCWSTR command) { + if (application) return SteamImage(application); + if (!command) return false; + return SteamImage(SplitImage(command).image); +} +BOOL FinishRestart(PROCESS_INFORMATION* pi, DWORD flags) { + std::wstring error; + try { + if (Inject(pi->hProcess, ownDllPath, error) && + ((flags & CREATE_SUSPENDED) || ResumeThread(pi->hThread) != static_cast(-1))) { + AppendLog(L"Steam restart isolated: replacement process " + std::to_wstring(pi->dwProcessId) + + L" has the hook"); + return TRUE; + } + } catch (...) { error = L"Resource allocation failed"; } + // A restart that cannot be isolated is stopped, not let through unisolated. The caller sees + // CreateProcess fail, which Steam only reports as a failed restart, so say why here. + AppendLog(L"Steam restart refused, replacement not isolated: " + error); + TerminateProcess(pi->hProcess, 1); CloseHandle(pi->hThread); CloseHandle(pi->hProcess); *pi = {}; + SetLastError(ERROR_DLL_INIT_FAILED); return FALSE; +} +BOOL WINAPI CreateInternalHook(HANDLE token, LPCWSTR app, LPWSTR command, LPSECURITY_ATTRIBUTES processAttributes, + LPSECURITY_ATTRIBUTES threadAttributes, BOOL inherit, DWORD flags, LPVOID environment, + LPCWSTR directory, LPSTARTUPINFOW startup, LPPROCESS_INFORMATION pi, PHANDLE newToken) { + // Decide before the call: CreateProcess is allowed to write into the command line buffer. + const bool restart = pi && SteamLaunch(app, command); + const BOOL result = createInternal(token, app, command, processAttributes, threadAttributes, inherit, + restart ? flags | CREATE_SUSPENDED : flags, environment, directory, startup, pi, newToken); + return result && restart ? FinishRestart(pi, flags) : result; +} +NTSTATUS NTAPI CreateUserHook(PHANDLE process, PHANDLE thread, ACCESS_MASK pa, ACCESS_MASK ta, + POBJECT_ATTRIBUTES po, POBJECT_ATTRIBUTES to, ULONG pf, ULONG tf, PVOID parameters, PVOID info, PVOID attributes) { + auto invoke = [&]() { return originalCreate(process, thread, pa, ta, po, to, pf, tf, parameters, info, attributes); }; + if (creating || !parameters) return invoke(); + auto* p = static_cast(parameters); + if (!(p->flags & 1) || p->length < offsetof(Parameters, environmentVersion) + sizeof(ULONG_PTR)) return invoke(); + creating = true; + struct Reset { ~Reset() { creating = false; } } reset; + UNICODE_STRING saved = p->command; + void* oldEnvironment = p->environment; + ULONG_PTR oldSize = p->environmentSize; + struct Restore { + Parameters* p; UNICODE_STRING command; PVOID environment; ULONG_PTR size; + ~Restore() { p->command = command; p->environment = environment; p->environmentSize = size; } + } restore{p, saved, oldEnvironment, oldSize}; + try { + std::wstring changed; + if (saved.Buffer && !(saved.Length % sizeof(wchar_t))) { + changed = RewriteCommand(std::wstring(saved.Buffer, saved.Length / sizeof(wchar_t)), ipcName); + if (changed.size() > 32766) return static_cast(0xc000000dL); + p->command.Buffer = changed.data(); + p->command.Length = static_cast(changed.size() * sizeof(wchar_t)); + p->command.MaximumLength = static_cast((changed.size() + 1) * sizeof(wchar_t)); + } + std::vector environment; + if (oldEnvironment && oldSize >= 4 && oldSize <= 16 * 1024 * 1024 && !(oldSize % sizeof(wchar_t))) { + environment = RewriteEnvironment(static_cast(oldEnvironment), oldSize / sizeof(wchar_t), ipcName); + if (environment.empty()) return static_cast(0xc000000dL); + p->environment = environment.data(); p->environmentSize = environment.size() * sizeof(wchar_t); + } + return invoke(); + } catch (...) { return static_cast(0xc0000017L); } +} +template bool Hook(const wchar_t* module, const char* function, void* replacement, T* original) { + HMODULE handle = GetModuleHandleW(module); + auto address = handle ? GetProcAddress(handle, function) : nullptr; + return address && MH_CreateHook(reinterpret_cast(address), replacement, reinterpret_cast(original)) == MH_OK; +} +} + +#if defined(_MSC_VER) && !defined(_WIN64) +#pragma comment(linker, "/EXPORT:MultiSeatInitialize=_MultiSeatInitialize@4") +#endif +extern "C" __declspec(dllexport) DWORD WINAPI MultiSeatInitialize(void*) { + if (InterlockedCompareExchange(&initialization, 1, 0) != 0) return initialization == 2 ? 1 : 0; + try { + DWORD session = 0; + if (!ProcessIdToSessionId(GetCurrentProcessId(), &session)) return 0; + HANDLE token = nullptr; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return 0; + DWORD bytes = 0; GetTokenInformation(token, TokenUser, nullptr, 0, &bytes); + std::vector data(bytes); + bool got = GetTokenInformation(token, TokenUser, data.data(), bytes, &bytes) != FALSE; + CloseHandle(token); if (!got) return 0; + LPWSTR sid = nullptr; if (!ConvertSidToStringSidW(reinterpret_cast(data.data())->User.Sid, &sid)) return 0; + uint64_t hash = 14695981039346656037ULL; + for (const wchar_t* c = sid; *c; ++c) { hash ^= static_cast(*c); hash *= 1099511628211ULL; } + LocalFree(sid); + ipcName = L"MultiSeat_" + std::to_wstring(session) + L"_" + std::to_wstring(hash); + std::vector image(32768); DWORD length = GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())); + if (!length || length >= image.size() || !SteamImage(std::wstring(image.data(), length))) return 0; + isSteam = true; + std::vector dllPath(32768); DWORD dllLength = GetModuleFileNameW(ownModule, dllPath.data(), static_cast(dllPath.size())); + if (!dllLength || dllLength >= dllPath.size()) return 0; + ownDllPath.assign(dllPath.data(), dllLength); + commandW = RewriteCommand(GetCommandLineW(), ipcName); + int narrowSize = WideCharToMultiByte(CP_ACP, 0, commandW.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (!narrowSize) return 0; + commandA.resize(narrowSize); WideCharToMultiByte(CP_ACP, 0, commandW.c_str(), -1, commandA.data(), narrowSize, nullptr, nullptr); + if (!SetEnvironmentVariableW(L"steam_master_ipc_name_override", ipcName.c_str())) return 0; + if (MH_Initialize() != MH_OK) return 0; + LPWSTR (WINAPI* originalCommandW)() = nullptr; LPSTR (WINAPI* originalCommandA)() = nullptr; + bool ok = Hook(L"ntdll.dll", "NtCreateUserProcess", reinterpret_cast(CreateUserHook), &originalCreate) + // Win32 completes subsystem registration before restart injection; doing this + // inside NtCreateUserProcess is too early for remote loader initialization. + && Hook(L"kernelbase.dll", "CreateProcessInternalW", reinterpret_cast(CreateInternalHook), &createInternal) + && Hook(L"kernelbase.dll", "GetCommandLineW", reinterpret_cast(CommandW), &originalCommandW) + && Hook(L"kernelbase.dll", "GetCommandLineA", reinterpret_cast(CommandA), &originalCommandA) + && Hook(L"kernelbase.dll", "RegQueryValueExW", reinterpret_cast(QueryW), &queryW) + && Hook(L"kernelbase.dll", "RegQueryValueExA", reinterpret_cast(QueryA), &queryA) + && Hook(L"kernelbase.dll", "RegSetValueExW", reinterpret_cast(SetW), &setW) + && Hook(L"kernelbase.dll", "RegSetValueExA", reinterpret_cast(SetA), &setA) + && Hook(L"kernelbase.dll", "RegDeleteValueW", reinterpret_cast(DeleteW), &deleteW) + && Hook(L"kernelbase.dll", "RegDeleteValueA", reinterpret_cast(DeleteA), &deleteA); + if (!ok || MH_EnableHook(MH_ALL_HOOKS) != MH_OK) { MH_Uninitialize(); return 0; } + InterlockedExchange(&initialization, 2); + AppendLog(L"Steam isolation hook active in steam.exe (ipc name " + ipcName + L")"); + return 1; + } catch (...) { return 0; } +} +BOOL WINAPI DllMain(HINSTANCE module, DWORD reason, LPVOID) { + if (reason == DLL_PROCESS_ATTACH) { ownModule = module; DisableThreadLibraryCalls(module); } + return TRUE; +} diff --git a/src/MultiSeat.SteamIsolation/Inject.h b/src/MultiSeat.SteamIsolation/Inject.h new file mode 100644 index 0000000..653115b --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Inject.h @@ -0,0 +1,160 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + +namespace steam_isolation { +struct Handle { + HANDLE value = nullptr; + explicit Handle(HANDLE h = nullptr) : value(h) {} + ~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); } + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; +}; +inline std::wstring ExecutableDirectory() { + std::vector path(32768); + DWORD n = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (!n || n >= path.size()) return {}; + std::wstring result(path.data(), n); + return result.substr(0, result.find_last_of(L"\\/")); +} +inline WORD TargetMachine(const std::wstring& path) { + Handle file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (file.value == INVALID_HANDLE_VALUE) return 0; + IMAGE_DOS_HEADER dos{}; DWORD read = 0; + if (!ReadFile(file.value, &dos, sizeof(dos), &read, nullptr) || read != sizeof(dos) || + dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < sizeof(dos)) return 0; + LARGE_INTEGER offset{}; offset.QuadPart = dos.e_lfanew; + if (!SetFilePointerEx(file.value, offset, nullptr, FILE_BEGIN)) return 0; + DWORD signature = 0; IMAGE_FILE_HEADER header{}; + if (!ReadFile(file.value, &signature, sizeof(signature), &read, nullptr) || + read != sizeof(signature) || signature != IMAGE_NT_SIGNATURE || + !ReadFile(file.value, &header, sizeof(header), &read, nullptr) || read != sizeof(header)) return 0; + return header.Machine; +} +inline uintptr_t FindModule(DWORD pid, const std::wstring& name, bool fullPath) { + HANDLE snapshot = INVALID_HANDLE_VALUE; + for (int attempt = 0; attempt < 8; ++attempt) { + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); + if (snapshot != INVALID_HANDLE_VALUE || GetLastError() != ERROR_BAD_LENGTH) break; + } + Handle owner(snapshot); + if (snapshot == INVALID_HANDLE_VALUE) return 0; + MODULEENTRY32W module{}; module.dwSize = sizeof(module); + if (Module32FirstW(snapshot, &module)) do { + if (_wcsicmp(fullPath ? module.szExePath : module.szModule, name.c_str()) == 0) + return reinterpret_cast(module.modBaseAddr); + } while (Module32NextW(snapshot, &module)); + return 0; +} +inline bool Inject(HANDLE process, const std::wstring& dllPath, std::wstring& error) { + auto fail = [&](const wchar_t* stage) { + error = std::wstring(stage) + L" (Windows error " + std::to_wstring(GetLastError()) + L")"; + return false; + }; + // Call only for a suspended process with the same architecture as this caller. + BOOL targetWow = FALSE, selfWow = FALSE; + if (!IsWow64Process(process, &targetWow) || !IsWow64Process(GetCurrentProcess(), &selfWow)) + return fail(L"Cannot inspect process architecture"); + if (targetWow != selfWow) { error = L"Injection requires matching process architecture"; return false; } + // Path buffers live on the heap, not the stack: from the hook this runs on whichever Steam + // thread restarts Steam, and three 64 KB arrays would be 192 KB of that thread's stack. + std::vector absolute(32768); + DWORD n = GetFullPathNameW(dllPath.c_str(), static_cast(absolute.size()), absolute.data(), nullptr); + if (!n || n >= absolute.size()) return fail(L"Cannot resolve isolation DLL path"); + std::wstring resolved(absolute.data(), n); + HMODULE localDll = LoadLibraryExW(resolved.c_str(), nullptr, DONT_RESOLVE_DLL_REFERENCES); + if (!localDll) return fail(L"Cannot inspect isolation DLL"); + auto init = GetProcAddress(localDll, "MultiSeatInitialize"); + if (!init) { FreeLibrary(localDll); error = L"Isolation DLL is missing MultiSeatInitialize"; return false; } + uintptr_t initOffset = reinterpret_cast(init) - reinterpret_cast(localDll); + FreeLibrary(localDll); + auto load = GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"); + HMODULE loadModule = nullptr; + if (!load || !GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(load), &loadModule)) + return fail(L"Cannot locate loader"); + std::vector modulePath(32768); + const DWORD moduleLength = GetModuleFileNameW(loadModule, modulePath.data(), static_cast(modulePath.size())); + if (!moduleLength || moduleLength >= modulePath.size()) return fail(L"Cannot locate loader module"); + std::wstring moduleName(modulePath.data(), moduleLength); + moduleName = moduleName.substr(moduleName.find_last_of(L"\\/") + 1); + DWORD pid = GetProcessId(process); + uintptr_t remoteModule = FindModule(pid, moduleName, false); + if (!remoteModule) { + // A newly suspended process may have only ntdll mapped. Creating its first + // remote thread lets Windows initialize imports; the application's primary + // thread remains suspended. Exit immediately once that loader work completes. + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + auto exitThread = GetProcAddress(ntdll, "RtlExitUserThread"); + uintptr_t remoteNtdll = FindModule(pid, L"ntdll.dll", false); + if (!remoteNtdll) { + // Toolhelp's loader list is empty before initialization. Query mapped + // images directly instead; unlike a guessed address this handles ASLR. + uintptr_t address = 0; + MEMORY_BASIC_INFORMATION region{}; + std::vector mapped(32768); + while (VirtualQueryEx(process, reinterpret_cast(address), ®ion, sizeof(region))) { + if (region.Type == MEM_IMAGE && region.BaseAddress == region.AllocationBase) { + DWORD count = K32GetMappedFileNameW(process, region.AllocationBase, mapped.data(), + static_cast(mapped.size())); + if (count && count < mapped.size()) { + std::wstring image(mapped.data(), count); + auto name = image.substr(image.find_last_of(L"\\/") + 1); + if (_wcsicmp(name.c_str(), L"ntdll.dll") == 0) { + remoteNtdll = reinterpret_cast(region.AllocationBase); break; + } + } + } + uintptr_t next = reinterpret_cast(region.BaseAddress) + region.RegionSize; + if (next <= address) break; + address = next; + } + } + if (!exitThread || !remoteNtdll) { error = L"Target bootstrap module unavailable"; return false; } + uintptr_t remoteExit = remoteNtdll + reinterpret_cast(exitThread) - reinterpret_cast(ntdll); + Handle bootstrap(CreateRemoteThread(process, nullptr, 0, + reinterpret_cast(remoteExit), nullptr, 0, nullptr)); + if (!bootstrap.value) return fail(L"Cannot initialize target loader"); + if (WaitForSingleObject(bootstrap.value, 60000) != WAIT_OBJECT_0) { + error = L"Target loader initialization failed or timed out"; return false; + } + remoteModule = FindModule(pid, moduleName, false); + } + if (!remoteModule) { error = L"Target loader module unavailable"; return false; } + uintptr_t remoteLoad = remoteModule + reinterpret_cast(load) - reinterpret_cast(loadModule); + SIZE_T bytes = (resolved.size() + 1) * sizeof(wchar_t); + void* remotePath = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!remotePath) return fail(L"Cannot allocate remote path"); + SIZE_T written = 0; + if (!WriteProcessMemory(process, remotePath, resolved.c_str(), bytes, &written) || written != bytes) { + auto result = fail(L"Cannot write remote path"); VirtualFreeEx(process, remotePath, 0, MEM_RELEASE); return result; + } + Handle loader(CreateRemoteThread(process, nullptr, 0, reinterpret_cast(remoteLoad), remotePath, 0, nullptr)); + if (!loader.value) { auto result = fail(L"Cannot start remote loader"); VirtualFreeEx(process, remotePath, 0, MEM_RELEASE); return result; } + DWORD wait = WaitForSingleObject(loader.value, 60000); + if (wait != WAIT_OBJECT_0) { + // Leave the allocation alive until the caller terminates the failed suspended child. + error = L"Remote loader failed or timed out"; return false; + } + VirtualFreeEx(process, remotePath, 0, MEM_RELEASE); + uintptr_t remoteDll = FindModule(pid, resolved, true); + if (!remoteDll) { error = L"Target did not load isolation DLL"; return false; } + Handle initializer(CreateRemoteThread(process, nullptr, 0, + reinterpret_cast(remoteDll + initOffset), nullptr, 0, nullptr)); + if (!initializer.value) return fail(L"Cannot start isolation initialization"); + if (WaitForSingleObject(initializer.value, 60000) != WAIT_OBJECT_0) { + error = L"Isolation initialization failed or timed out"; return false; + } + DWORD result = 0; + if (!GetExitCodeThread(initializer.value, &result) || result != 1) { + error = L"Isolation initialization refused to activate"; return false; + } + return true; +} +} diff --git a/src/MultiSeat.SteamIsolation/IsolationPolicy.h b/src/MultiSeat.SteamIsolation/IsolationPolicy.h new file mode 100644 index 0000000..5371a9c --- /dev/null +++ b/src/MultiSeat.SteamIsolation/IsolationPolicy.h @@ -0,0 +1,134 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace steam_isolation { +inline std::wstring BaseName(const std::wstring& path) { + return path.substr(path.find_last_of(L"\\/") + 1); +} +inline bool Equal(const std::wstring& a, const std::wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) == 0; } +inline bool SteamImage(const std::wstring& image) { return Equal(BaseName(image), L"steam.exe"); } +inline bool BrowserImage(const std::wstring& image) { return Equal(BaseName(image), L"steamwebhelper.exe"); } +inline std::wstring Quote(const std::wstring& value) { + std::wstring result = L"\""; size_t slashes = 0; + for (wchar_t c : value) { + if (c == L'\\') { ++slashes; continue; } + if (c == L'"') result.append(slashes * 2 + 1, L'\\'); + else result.append(slashes, L'\\'); + slashes = 0; result += c; + } + result.append(slashes * 2, L'\\'); return result + L"\""; +} +inline bool RegularFileExists(const std::wstring& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY); +} +using FileExists = bool (*)(const std::wstring&); +struct ImageSplit { std::wstring image, rest; }; +// Where the program name ends in a command line, the way CreateProcess finds it when it is given +// no application name. A quoted first token ends at its closing quote. An unquoted one may contain +// spaces ("C:\Program Files (x86)\Steam\steam.exe -silent"): CreateProcess tries the text up to +// each space in turn until a file exists. CommandLineToArgvW does not, and reads that example as +// "C:\Program", which is not Steam, so a restart written that way was neither rewritten nor given +// the hook. Only the two programs this component acts on are searched for; anything else comes +// back as its first word, which is all the caller needs to know it is not one of them. +// +// The first word is taken on its name alone: "steam.exe -silent" is found through the search path, +// so it need not exist where we stand. A longer candidate must exist on disk, so that a folder +// that merely contains a space and the word steam.exe is not mistaken for the program. +inline ImageSplit SplitImage(const std::wstring& command, FileExists exists = RegularFileExists) { + auto blank = [](wchar_t c) { return c == L' ' || c == L'\t'; }; + size_t start = 0; + while (start < command.size() && blank(command[start])) ++start; + if (start == command.size()) return {}; + if (command[start] == L'"') { + const size_t close = command.find(L'"', start + 1); + if (close == std::wstring::npos) return {command.substr(start + 1), {}}; + return {command.substr(start + 1, close - start - 1), command.substr(close + 1)}; + } + size_t firstEnd = start; + while (firstEnd < command.size() && !blank(command[firstEnd])) ++firstEnd; + for (size_t end = firstEnd;;) { + const std::wstring candidate = command.substr(start, end - start); + if ((SteamImage(candidate) || BrowserImage(candidate)) && (end == firstEnd || exists(candidate))) + return {candidate, command.substr(end)}; + size_t next = end; + while (next < command.size() && blank(command[next])) ++next; + if (next >= command.size()) break; + while (next < command.size() && !blank(command[next])) ++next; + end = next; + } + return {command.substr(start, firstEnd - start), command.substr(firstEnd)}; +} +inline std::wstring RewriteCommand(const std::wstring& raw, const std::wstring& name, + FileExists exists = RegularFileExists) { + const ImageSplit split = SplitImage(raw, exists); + if (split.image.empty()) return raw; + // CommandLineToArgvW reads its first token by special rules, so give it a placeholder and + // take the arguments from what follows; the real program name comes from SplitImage. + int count = 0; LPWSTR* parsed = CommandLineToArgvW((L"x" + split.rest).c_str(), &count); + if (!parsed || count == 0) { if (parsed) LocalFree(parsed); return raw; } + std::vector args(parsed, parsed + count); LocalFree(parsed); + args[0] = split.image; + if (!SteamImage(args[0]) && !BrowserImage(args[0])) return raw; + std::vector output{args[0]}; bool changed = false; + for (size_t i = 1; i < args.size(); ++i) { + auto arg = args[i]; + if (SteamImage(args[0]) && Equal(arg, L"-master_ipc_name_override")) { + if (i + 1 < args.size()) ++i; + changed = true; continue; + } + if (SteamImage(args[0]) && arg.size() >= 26 && Equal(arg.substr(0, 26), L"-master_ipc_name_override=")) { + changed = true; continue; + } + std::wstring lowerArg = arg; std::transform(lowerArg.begin(), lowerArg.end(), lowerArg.begin(), towlower); + bool cachePath = lowerArg.rfind(L"-cachedir=", 0) == 0 || lowerArg.rfind(L"--log-file=", 0) == 0 || + lowerArg.rfind(L"-cef-log-file=", 0) == 0 || (arg.size() >= 3 && arg[1] == L':' && (arg[2] == L'\\' || arg[2] == L'/')) || + arg.rfind(L"\\\\", 0) == 0; + // Limit rewriting to path arguments; URLs and arbitrary labels keep their values. + for (const auto& marker : {std::wstring(L"htmlcache"), std::wstring(L"cef_log")}) { + if (!cachePath) break; + std::wstring lower = arg; std::transform(lower.begin(), lower.end(), lower.begin(), towlower); + size_t position = 0; + while ((position = lower.find(marker, position)) != std::wstring::npos) { + size_t end = position + marker.size(); + bool left = position == 0 || arg[position - 1] == L'\\' || arg[position - 1] == L'/' || arg[position - 1] == L'='; + bool right = end == arg.size() || arg[end] == L'.' || arg[end] == L'\\' || arg[end] == L'/'; + const std::wstring suffix = L"." + name; + if (left && right && arg.compare(end, suffix.size(), suffix) != 0) { + arg.insert(end, suffix); lower.insert(end, suffix); changed = true; end += suffix.size(); + } + position = end; + } + } + output.push_back(std::move(arg)); + } + if (SteamImage(args[0])) { output.push_back(L"-master_ipc_name_override"); output.push_back(name); changed = true; } + if (!changed) return raw; + std::wstring result; + for (const auto& arg : output) { if (!result.empty()) result += L' '; result += Quote(arg); } + return result; +} +inline std::vector RewriteEnvironment(const wchar_t* data, size_t characters, const std::wstring& name) { + if (!data || characters < 2 || data[characters - 1] != 0 || data[characters - 2] != 0) return {}; + std::vector entries; + size_t pos = 0; + while (pos < characters && data[pos]) { + size_t end = pos; while (end < characters && data[end]) ++end; + if (end == characters) return {}; + std::wstring value(data + pos, end - pos); + const std::wstring prefix = L"steam_master_ipc_name_override="; + if (value.size() < prefix.size() || !Equal(value.substr(0, prefix.size()), prefix)) entries.push_back(value); + pos = end + 1; + } + entries.push_back(L"steam_master_ipc_name_override=" + name); + std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }); + std::vector result; + for (const auto& value : entries) { result.insert(result.end(), value.begin(), value.end()); result.push_back(0); } + result.push_back(0); return result; +} +} diff --git a/src/MultiSeat.SteamIsolation/Launch-Game.cmd b/src/MultiSeat.SteamIsolation/Launch-Game.cmd new file mode 100644 index 0000000..e518685 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Launch-Game.cmd @@ -0,0 +1,2 @@ +@echo off +start "" "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -STA -WindowStyle Hidden -ExecutionPolicy Bypass -File "%~dp0Launch-IsolatedGame.ps1" diff --git a/src/MultiSeat.SteamIsolation/Launch-IsolatedGame.ps1 b/src/MultiSeat.SteamIsolation/Launch-IsolatedGame.ps1 new file mode 100644 index 0000000..f7783e8 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Launch-IsolatedGame.ps1 @@ -0,0 +1,67 @@ +param( + [string]$Executable = '', + [string]$Arguments = '' +) +$ErrorActionPreference = 'Stop' +$taskInteractive = [string]::IsNullOrWhiteSpace($Executable) +try { +if ($taskInteractive) { + Add-Type -AssemblyName System.Windows.Forms + [Windows.Forms.Application]::EnableVisualStyles() + $taskPicker = New-Object Windows.Forms.OpenFileDialog + try { + $taskPicker.Title = 'Select a game to launch with isolated Steam' + $taskPicker.Filter = 'Game executable (*.exe)|*.exe' + $taskPicker.CheckFileExists = $true + $taskPicker.Multiselect = $false + $taskPicker.RestoreDirectory = $true + if ($taskPicker.ShowDialog() -ne [Windows.Forms.DialogResult]::OK) { return } + $Executable = $taskPicker.FileName + } finally { $taskPicker.Dispose() } +} +if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) { + throw "Game executable not found: $Executable" +} +if ([IO.Path]::GetExtension($Executable) -ine '.exe') { + throw 'Select an EXE file.' +} +$taskSession = [Diagnostics.Process]::GetCurrentProcess().SessionId +if (-not @(Get-Process steam -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $taskSession }).Count) { + throw 'Start Steam through MultiSeat.SteamLauncher in this Windows session first.' +} +# Match the session/SID-derived name used by MultiSeat.SteamHook.cpp. +if (-not ('MultiSeatGameIPC' -as [type])) { + Add-Type -TypeDefinition @' +using System; +public static class MultiSeatGameIPC { + public static string Name(int session, string sid) { + ulong hash = 14695981039346656037UL; + unchecked { + foreach (char character in sid) { hash ^= (ulong)character; hash *= 1099511628211UL; } + } + return "MultiSeat_" + session + "_" + hash; + } +} +'@ +} +$taskIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() +try { $taskSid = $taskIdentity.User.Value } finally { $taskIdentity.Dispose() } +$taskIPC = [MultiSeatGameIPC]::Name($taskSession, $taskSid) +$taskStart = New-Object Diagnostics.ProcessStartInfo +$taskStart.FileName = [IO.Path]::GetFullPath($Executable) +$taskStart.WorkingDirectory = [IO.Path]::GetDirectoryName($taskStart.FileName) +$taskStart.Arguments = $Arguments +$taskStart.UseShellExecute = $false +$taskStart.EnvironmentVariables['steam_master_ipc_name_override'] = $taskIPC +Write-Host "Launching game for isolated Steam connection: $taskIPC" +$taskGame = [Diagnostics.Process]::Start($taskStart) +try { Write-Host "Game process started: $($taskGame.Id)" } finally { $taskGame.Dispose() } +} catch { + if ($taskInteractive) { + Add-Type -AssemblyName System.Windows.Forms + [void][Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Steam game launcher', + [Windows.Forms.MessageBoxButtons]::OK, [Windows.Forms.MessageBoxIcon]::Error) + exit 1 + } + throw +} diff --git a/src/MultiSeat.SteamIsolation/Launcher.cpp b/src/MultiSeat.SteamIsolation/Launcher.cpp new file mode 100644 index 0000000..9776416 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Launcher.cpp @@ -0,0 +1,160 @@ +#include "Inject.h" +#include "Log.h" +#include + +using namespace steam_isolation; + +// This launcher is a Windows-subsystem program: it never has a console of its own, so no window +// appears in the seat when the service starts it. A failure therefore cannot depend on a console +// being there, and every message goes to four places: +// - a log file in the seat user's profile (always; this is where to look after the fact), +// and the debugger, both through AppendLog in Log.h, +// - stderr, when the caller redirected it (the smoke tests do), +// - the parent's console, when it was started from a terminal. +static void Report(const std::wstring& message) { + std::wstring text = message; + while (!text.empty() && (text.back() == L'\n' || text.back() == L'\r')) text.pop_back(); + AppendLog(text); // the log file and the debugger; see Log.h + + const std::wstring consoleText = text + L"\r\n"; + const std::string utf8 = Utf8(consoleText); + HANDLE error = GetStdHandle(STD_ERROR_HANDLE); + if (error && error != INVALID_HANDLE_VALUE) { + DWORD mode = 0, written = 0; + if (GetConsoleMode(error, &mode)) + WriteConsoleW(error, consoleText.c_str(), static_cast(consoleText.size()), &written, nullptr); + else if (!utf8.empty()) + WriteFile(error, utf8.data(), static_cast(utf8.size()), &written, nullptr); + } else if (AttachConsole(ATTACH_PARENT_PROCESS)) { + Handle console(CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)); + DWORD written = 0; + if (console.value != INVALID_HANDLE_VALUE) + WriteConsoleW(console.value, consoleText.c_str(), static_cast(consoleText.size()), &written, nullptr); + FreeConsole(); + } +} +// Stands in for the std::wcerr the launcher used when it was a console program: the message is +// collected across the << chain and reported once, when the temporary goes away. +class Log { + std::wostringstream text_; +public: + template Log& operator<<(const T& value) { text_ << value; return *this; } + ~Log() { Report(text_.str()); } +}; + +// Decode only the launcher prefix. The application's argument tail remains byte-for-byte intact. +static bool NextToken(const std::wstring& raw, size_t& cursor, std::wstring& token) { + token.clear(); + while (cursor < raw.size() && (raw[cursor] == L' ' || raw[cursor] == L'\t')) ++cursor; + if (cursor == raw.size()) return false; + bool quoted = false; + while (cursor < raw.size()) { + if (!quoted && (raw[cursor] == L' ' || raw[cursor] == L'\t')) break; + size_t slashes = 0; + while (cursor < raw.size() && raw[cursor] == L'\\') { ++slashes; ++cursor; } + if (cursor < raw.size() && raw[cursor] == L'"') { + token.append(slashes / 2, L'\\'); + if (slashes % 2) { token.push_back(L'"'); ++cursor; } + else { + ++cursor; + if (quoted && cursor < raw.size() && raw[cursor] == L'"') { token.push_back(L'"'); ++cursor; } + else quoted = !quoted; + } + } else { + token.append(slashes, L'\\'); + if (cursor < raw.size() && (quoted || (raw[cursor] != L' ' && raw[cursor] != L'\t'))) + token.push_back(raw[cursor++]); + } + } + return !quoted; +} +static DWORD Run(const std::wstring& executable, std::wstring command, const std::wstring& hook, + const std::wstring& workingDirectory, HANDLE readyEvent = nullptr) { + Handle job(CreateJobObjectW(nullptr, nullptr)); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!job.value || !SetInformationJobObject(job.value, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) { + Log() << L"Cannot create isolation process supervisor: " << GetLastError() << L"\n"; return 1; + } + STARTUPINFOW startup{}; startup.cb = sizeof(startup); + PROCESS_INFORMATION info{}; + if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, + nullptr, workingDirectory.c_str(), &startup, &info)) { + Log() << L"Cannot start Steam isolation process: " << GetLastError() << L"\n"; return 1; + } + Handle process(info.hProcess), thread(info.hThread); + auto stop = [&]() { TerminateProcess(process.value, 1); WaitForSingleObject(process.value, 5000); return DWORD{1}; }; + if (!AssignProcessToJobObject(job.value, process.value)) { + Log() << L"Cannot supervise isolation process: " << GetLastError() << L"\n"; return stop(); + } + std::wstring error; + if (!hook.empty() && !Inject(process.value, hook, error)) { + Log() << L"Steam isolation failed: " << error << L"\n"; return stop(); + } + if (ResumeThread(thread.value) == static_cast(-1)) { + Log() << L"Cannot resume isolated process: " << GetLastError() << L"\n"; return stop(); + } + if (readyEvent && !SetEvent(readyEvent)) { + Log() << L"Cannot signal Steam isolation readiness: " << GetLastError() << L"\n"; return stop(); + } + if (WaitForSingleObject(process.value, INFINITE) != WAIT_OBJECT_0) return stop(); + DWORD exitCode = 1; GetExitCodeProcess(process.value, &exitCode); + // Steam's updater can exit after creating its replacement. Keep owning the + // job until the entire descendant tree exits, rather than killing that replacement. + for (;;) { + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting{}; + if (!QueryInformationJobObject(job.value, JobObjectBasicAccountingInformation, + &accounting, sizeof(accounting), nullptr)) { + Log() << L"Cannot monitor isolation process tree: " << GetLastError() << L"\n"; return 1; + } + if (!accounting.ActiveProcesses) return exitCode; + Sleep(250); + } +} +int wmain() { + std::wstring raw(GetCommandLineW()), token, steam, readyEventName; + size_t cursor = 0; + bool valid = NextToken(raw, cursor, token); + [[maybe_unused]] size_t prefixStart = cursor; + valid = valid && NextToken(raw, cursor, token) && token == L"--steam"; + valid = valid && NextToken(raw, cursor, steam) && !steam.empty(); + valid = valid && NextToken(raw, cursor, token); + if (valid && token == L"--ready-event") { + valid = NextToken(raw, cursor, readyEventName) && !readyEventName.empty() && NextToken(raw, cursor, token); + } + valid = valid && token == L"--"; + const bool absolutePath = (steam.size() > 2 && steam[1] == L':' && (steam[2] == L'\\' || steam[2] == L'/')) || + (steam.size() > 2 && steam[0] == L'\\' && steam[1] == L'\\'); + const auto basename = steam.substr(steam.find_last_of(L"\\/") + 1); + if (!valid || !absolutePath || steam.find(L'"') != std::wstring::npos || _wcsicmp(basename.c_str(), L"steam.exe") != 0) { + Log() << L"Usage: MultiSeat.SteamLauncher.exe --steam [--ready-event ] -- \n"; return 2; + } + Handle readyEvent(readyEventName.empty() ? nullptr : OpenEventW(EVENT_MODIFY_STATE, FALSE, readyEventName.c_str())); + if (!readyEventName.empty() && !readyEvent.value) { + Log() << L"Cannot open Steam isolation readiness event: " << GetLastError() << L"\n"; return 2; + } + std::wstring directory = ExecutableDirectory(); + WORD machine = TargetMachine(steam); + if (machine != IMAGE_FILE_MACHINE_I386 && machine != IMAGE_FILE_MACHINE_AMD64) { + Log() << L"Steam executable is missing, invalid, or uses an unsupported architecture\n"; return 2; + } + std::vector currentDirectory(32768); + DWORD cwdLength = GetCurrentDirectoryW(static_cast(currentDirectory.size()), currentDirectory.data()); + if (!cwdLength || cwdLength >= currentDirectory.size()) { + Log() << L"Cannot resolve launch working directory\n"; return 2; + } + std::wstring workingDirectory(currentDirectory.data(), cwdLength); +#ifdef _WIN64 + if (machine == IMAGE_FILE_MACHINE_I386) { + std::wstring helper = directory + L"\\MultiSeat.SteamLauncher32.exe"; + return static_cast(Run(helper, L"\"" + helper + L"\"" + raw.substr(prefixStart), L"", workingDirectory)); + } + std::wstring hook = directory + L"\\MultiSeat.SteamHook.dll"; +#else + if (machine != IMAGE_FILE_MACHINE_I386) { + Log() << L"Use the 64-bit isolation launcher for 64-bit Steam\n"; return 2; + } + std::wstring hook = directory + L"\\MultiSeat.SteamHook32.dll"; +#endif + return static_cast(Run(steam, L"\"" + steam + L"\"" + raw.substr(cursor), hook, workingDirectory, readyEvent.value)); +} diff --git a/src/MultiSeat.SteamIsolation/Log.h b/src/MultiSeat.SteamIsolation/Log.h new file mode 100644 index 0000000..3e34eba --- /dev/null +++ b/src/MultiSeat.SteamIsolation/Log.h @@ -0,0 +1,64 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +namespace steam_isolation { + +// UTF-8 bytes of a wide string, for files and redirected handles. Empty on failure. +inline std::string Utf8(const std::wstring& text) { + if (text.empty()) return {}; + const int bytes = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), static_cast(text.size()), + nullptr, 0, nullptr, nullptr); + if (bytes <= 0) return {}; + std::string result(static_cast(bytes), '\0'); + WideCharToMultiByte(CP_UTF8, 0, text.c_str(), static_cast(text.size()), + result.data(), bytes, nullptr, nullptr); + return result; +} + +// Records one line in %LOCALAPPDATA%\MultiSeat\SteamIsolation\launcher.log, in the profile of the +// user the calling process runs as, and sends it to the debugger. Used by the launcher, which has +// no console, and by the hook inside Steam, which has nowhere else to say that a restart was or +// was not isolated. The file restarts after 256 KB instead of growing for ever. Never throws and +// never fails the caller: logging is evidence, not a dependency. +inline void AppendLog(const std::wstring& message) { + std::wstring text = message; + while (!text.empty() && (text.back() == L'\n' || text.back() == L'\r')) text.pop_back(); + OutputDebugStringW((L"MultiSeat.SteamIsolation: " + text + L"\n").c_str()); + + try { + SYSTEMTIME now{}; GetLocalTime(&now); + wchar_t stamp[40]; + swprintf_s(stamp, L"%04u-%02u-%02u %02u:%02u:%02u [pid %lu] ", now.wYear, now.wMonth, now.wDay, + now.wHour, now.wMinute, now.wSecond, GetCurrentProcessId()); + const std::string line = Utf8(stamp + text + L"\r\n"); + if (line.empty()) return; + + wchar_t base[MAX_PATH]{}; + const DWORD length = GetEnvironmentVariableW(L"LOCALAPPDATA", base, MAX_PATH); + if (!length || length >= MAX_PATH) return; + std::wstring directory = std::wstring(base, length) + L"\\MultiSeat"; + CreateDirectoryW(directory.c_str(), nullptr); + directory += L"\\SteamIsolation"; + CreateDirectoryW(directory.c_str(), nullptr); + + HANDLE file = CreateFileW((directory + L"\\launcher.log").c_str(), FILE_APPEND_DATA | FILE_GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return; + LARGE_INTEGER size{}; + if (GetFileSizeEx(file, &size) && size.QuadPart > 256 * 1024) { + SetFilePointer(file, 0, nullptr, FILE_BEGIN); + SetEndOfFile(file); + } + SetFilePointer(file, 0, nullptr, FILE_END); + DWORD written = 0; + WriteFile(file, line.data(), static_cast(line.size()), &written, nullptr); + CloseHandle(file); + } catch (...) { + } +} + +} // namespace steam_isolation diff --git a/src/MultiSeat.SteamIsolation/build.ps1 b/src/MultiSeat.SteamIsolation/build.ps1 new file mode 100644 index 0000000..4ded230 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/build.ps1 @@ -0,0 +1,30 @@ +param( + [string]$OutputDirectory = (Join-Path $PSScriptRoot 'artifacts'), + [switch]$BuildFixture +) +$ErrorActionPreference = 'Stop' +$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +if (-not (Test-Path -LiteralPath $vswhere)) { throw 'Visual Studio 2022 C++ build tools are required.' } +$installation = & $vswhere -latest -version '[17.0,18.0)' -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $installation) { throw 'Visual Studio 2022 C++ build tools are required.' } +$cmake = if ($cmakeCommand) { $cmakeCommand.Source } else { Join-Path $installation 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' } +if (-not (Test-Path -LiteralPath $cmake)) { throw 'CMake is required (install the Visual Studio CMake tools component).' } +$fixtureFlag = if ($BuildFixture) { 'ON' } else { 'OFF' } +$destination = [IO.Path]::GetFullPath($OutputDirectory) +New-Item -ItemType Directory -Force -Path $destination | Out-Null +foreach ($architecture in @('x64', 'Win32')) { + $buildDirectory = Join-Path $PSScriptRoot "build-$architecture" + & $cmake -S $PSScriptRoot -B $buildDirectory -G 'Visual Studio 17 2022' -A $architecture "-DMULTISEAT_BUILD_STEAM_FIXTURE=$fixtureFlag" + if ($LASTEXITCODE -ne 0) { throw "CMake configuration failed for $architecture" } + & $cmake --build $buildDirectory --config Release --parallel + if ($LASTEXITCODE -ne 0) { throw "Build failed for $architecture" } + $suffix = if ($architecture -eq 'Win32') { '32' } else { '' } + foreach ($filename in @("MultiSeat.SteamLauncher$suffix.exe", "MultiSeat.SteamHook$suffix.dll")) { + Copy-Item -LiteralPath (Join-Path $buildDirectory "Release\$filename") -Destination $destination -Force + } + if ($BuildFixture) { + Copy-Item -LiteralPath (Join-Path $buildDirectory 'Release\SteamIsolationFixture.exe') -Destination (Join-Path $destination "SteamIsolationFixture$suffix.exe") -Force + } +} +Write-Host "Steam isolation binaries written to $destination" diff --git a/src/MultiSeat.SteamIsolation/tests/Fixture.cpp b/src/MultiSeat.SteamIsolation/tests/Fixture.cpp new file mode 100644 index 0000000..614d63a --- /dev/null +++ b/src/MultiSeat.SteamIsolation/tests/Fixture.cpp @@ -0,0 +1,162 @@ +// Harmless stand-in for Steam: records arguments/environment and optionally holds a test mutex. +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + +int wmain(int argc, wchar_t** argv) { + std::wstring report, mutexName, childReport, restartReport, restartVia = L"w"; + DWORD holdMs = 0; + DWORD childHoldMs = 0; + bool detachChild = false; + for (int i = 1; i < argc; ++i) { + std::wstring arg(argv[i]); + if (arg == L"--report" && i + 1 < argc) report = argv[++i]; + else if (arg == L"--child-report" && i + 1 < argc) childReport = argv[++i]; + else if (arg == L"--restart-report" && i + 1 < argc) restartReport = argv[++i]; + // How the stand-in Steam starts its replacement: w = CreateProcessW with an application + // name (what the first version of the tests did), a = CreateProcessA, user = + // CreateProcessAsUserW, unquoted = CreateProcessW with no application name and a path + // that contains spaces, which CreateProcess has to find by trying each space in turn. + else if (arg == L"--restart-via" && i + 1 < argc) restartVia = argv[++i]; + else if (arg == L"--mutex" && i + 1 < argc) mutexName = argv[++i]; + else if (arg == L"--hold-ms" && i + 1 < argc) holdMs = wcstoul(argv[++i], nullptr, 10); + else if (arg == L"--child-hold-ms" && i + 1 < argc) childHoldMs = wcstoul(argv[++i], nullptr, 10); + else if (arg == L"--detach-child") detachChild = true; + } + HANDLE mutex = nullptr; + DWORD mutexError = 0; + if (!mutexName.empty()) { + SetLastError(ERROR_SUCCESS); + mutex = CreateMutexW(nullptr, FALSE, mutexName.c_str()); + mutexError = GetLastError(); + if (!mutex) return 3; + } + if (!report.empty()) { + std::wofstream output{std::filesystem::path(report)}; + if (!output) { if (mutex) CloseHandle(mutex); return 4; } + output << L"command=" << GetCommandLineW() << L"\n"; + const char* narrow = GetCommandLineA(); + output << L"commandA="; + for (; *narrow; ++narrow) output << static_cast(static_cast(*narrow)); + output << L"\n"; + for (int i = 0; i < argc; ++i) output << L"arg" << i << L"=" << argv[i] << L"\n"; + DWORD session = 0; ProcessIdToSessionId(GetCurrentProcessId(), &session); + output << L"session=" << session << L"\nmutexError=" << mutexError << L"\n"; + const wchar_t* variables[] = {L"steam_master_ipc_name_override", L"LOCALAPPDATA", L"MULTISEAT_STEAM_ISOLATION"}; + for (auto variable : variables) { + DWORD needed = GetEnvironmentVariableW(variable, nullptr, 0); + std::vector value(needed ? needed : 1); + if (needed) GetEnvironmentVariableW(variable, value.data(), needed); + output << variable << L"=" << value.data() << L"\n"; + } + // A dedicated fixture-owned key avoids changing real Steam settings. + std::wstring keyPath = L"Software\\MultiSeat\\SteamIsolationTests\\" + std::to_wstring(GetCurrentProcessId()); + HKEY key = nullptr; + LONG registryStatus = RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, + KEY_SET_VALUE | KEY_QUERY_VALUE, nullptr, &key, nullptr); + DWORD steamPid = 123456, readPid = 0, size = sizeof(readPid), type = 0; + DWORD nullTypePid = 0; + wchar_t stringPid[64]{}; + if (registryStatus == ERROR_SUCCESS) { + registryStatus = RegSetValueExW(key, L"SteamPID", 0, REG_DWORD, + reinterpret_cast(&steamPid), sizeof(steamPid)); + if (registryStatus == ERROR_SUCCESS) registryStatus = RegQueryValueExW(key, L"SteamPID", nullptr, + &type, reinterpret_cast(&readPid), &size); + size = sizeof(nullTypePid); + if (registryStatus == ERROR_SUCCESS) registryStatus = RegQueryValueExW(key, L"SteamPID", nullptr, + nullptr, reinterpret_cast(&nullTypePid), &size); + const wchar_t stringValue[] = L"fixture-nonDWORD"; + if (registryStatus == ERROR_SUCCESS) registryStatus = RegSetValueExW(key, L"SteamPID", 0, REG_SZ, + reinterpret_cast(stringValue), sizeof(stringValue)); + size = sizeof(stringPid); + if (registryStatus == ERROR_SUCCESS) registryStatus = RegQueryValueExW(key, L"SteamPID", nullptr, + nullptr, reinterpret_cast(stringPid), &size); + RegCloseKey(key); + RegDeleteKeyW(HKEY_CURRENT_USER, keyPath.c_str()); + } + output << L"registryStatus=" << registryStatus << L"\nsteamPid=" << readPid + << L"\nsteamPidNullType=" << nullTypePid << L"\nsteamPidString=" << stringPid << L"\n"; + } + if (!childReport.empty() || !restartReport.empty()) { + std::vector path(32768); + DWORD n = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (!n || n >= path.size()) { if (mutex) CloseHandle(mutex); return 6; } + std::wstring helper(path.data(), n); + helper = helper.substr(0, helper.find_last_of(L"\\/")) + + (restartReport.empty() ? L"\\steamwebhelper.exe" : L"\\steam.exe"); + std::wstring command; + if (!restartReport.empty()) { + if (childReport.empty()) childReport = restartReport + L".browser"; + command = L"\"" + helper + L"\" --report \"" + restartReport + L"\" --child-report \"" + childReport + + L"\" --child-hold-ms " + std::to_wstring(childHoldMs) + L" --detach-child"; + } else { + command = L"\"" + helper + L"\" --report \"" + childReport + + L"\" -cachedir=C:\\fixture\\htmlcache --log-file=C:\\fixture\\cef_log.txt --hold-ms " + std::to_wstring(childHoldMs); + } + // Explicit environment exercises propagation even when the caller doesn't inherit it. + wchar_t environment[] = L"FIXTURE_EXPLICIT=1\0\0"; + STARTUPINFOW startup{}; startup.cb = sizeof(startup); + PROCESS_INFORMATION child{}; + BOOL created = FALSE; + if (restartReport.empty() || restartVia == L"w") { + created = CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, + environment, nullptr, &startup, &child); + } else if (restartVia == L"unquoted") { + // No application name and no quotes: the program is whatever prefix of the command line, + // cut at a space, turns out to be a file. + command = helper + command.substr(command.find(L'"', 1) + 1); + created = CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, + environment, nullptr, &startup, &child); + } else if (restartVia == L"a") { + auto narrow = [](const std::wstring& text) { + int size = WideCharToMultiByte(CP_ACP, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr); + std::string result(size > 0 ? size : 1, '\0'); + if (size > 0) WideCharToMultiByte(CP_ACP, 0, text.c_str(), -1, result.data(), size, nullptr, nullptr); + return result; + }; + std::string narrowCommand = narrow(command); + STARTUPINFOA narrowStartup{}; narrowStartup.cb = sizeof(narrowStartup); + created = CreateProcessA(nullptr, narrowCommand.data(), nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, + environment, nullptr, &narrowStartup, &child); + } else if (restartVia == L"user") { + // CreateProcessAsUser needs SeIncreaseQuota (and may need SeAssignPrimaryToken); an + // administrator holds both but has them switched off. Switch them on, best effort. + HANDLE self = nullptr, primary = nullptr; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES | TOKEN_ASSIGN_PRIMARY, &self)) { + for (const wchar_t* privilege : {L"SeIncreaseQuotaPrivilege", L"SeAssignPrimaryTokenPrivilege"}) { + TOKEN_PRIVILEGES on{}; on.PrivilegeCount = 1; on.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + if (LookupPrivilegeValueW(nullptr, privilege, &on.Privileges[0].Luid)) + AdjustTokenPrivileges(self, FALSE, &on, 0, nullptr, nullptr); + } + DuplicateTokenEx(self, MAXIMUM_ALLOWED, nullptr, SecurityImpersonation, TokenPrimary, &primary); + CloseHandle(self); + } + if (primary) { + created = CreateProcessAsUserW(primary, nullptr, command.data(), nullptr, nullptr, FALSE, + CREATE_UNICODE_ENVIRONMENT, environment, nullptr, &startup, &child); + CloseHandle(primary); + } else SetLastError(ERROR_NO_TOKEN); + } else { + std::wcerr << L"Unknown --restart-via " << restartVia << L"\n"; + if (mutex) CloseHandle(mutex); return 9; + } + if (!created) { + std::wcerr << L"Fixture child creation failed (" << restartVia << L"): " << GetLastError() << L"\n"; + if (mutex) CloseHandle(mutex); return 7; + } + CloseHandle(child.hThread); + if (detachChild || !restartReport.empty()) { CloseHandle(child.hProcess); if (mutex) CloseHandle(mutex); return 0; } + DWORD wait = WaitForSingleObject(child.hProcess, 10000), exitCode = 0; + if (wait != WAIT_OBJECT_0) TerminateProcess(child.hProcess, 8); + else GetExitCodeProcess(child.hProcess, &exitCode); + CloseHandle(child.hProcess); + if (wait != WAIT_OBJECT_0 || exitCode) { if (mutex) CloseHandle(mutex); return 8; } + } + if (holdMs) Sleep(holdMs); + if (mutex) CloseHandle(mutex); + return mutexError == ERROR_ALREADY_EXISTS ? 5 : 0; +} diff --git a/src/MultiSeat.SteamIsolation/tests/GameLauncherSmoke.ps1 b/src/MultiSeat.SteamIsolation/tests/GameLauncherSmoke.ps1 new file mode 100644 index 0000000..e8a3556 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/tests/GameLauncherSmoke.ps1 @@ -0,0 +1,41 @@ +param( + [string]$Artifacts = (Join-Path $PSScriptRoot '..\artifacts'), + [string]$TestRoot = (Join-Path $PSScriptRoot '..\build-game-smoke') +) +$ErrorActionPreference = 'Stop' +$taskSource = [IO.Path]::GetFullPath($Artifacts) +$taskRun = Join-Path ([IO.Path]::GetFullPath($TestRoot)) ([guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $taskRun -Force | Out-Null +$taskFixture = Join-Path $taskSource 'SteamIsolationFixture.exe' +foreach ($taskName in @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamHook.dll')) { + Copy-Item -LiteralPath (Join-Path $taskSource $taskName) -Destination $taskRun +} +Copy-Item -LiteralPath $taskFixture -Destination (Join-Path $taskRun 'steam.exe') +Copy-Item -LiteralPath $taskFixture -Destination (Join-Path $taskRun 'game.exe') +$taskLauncher = $null +try { + $taskSteamReport = Join-Path $taskRun 'steam-report.txt' + $taskSteamArgs = '--steam "' + (Join-Path $taskRun 'steam.exe') + '" -- --report "' + $taskSteamReport + '" --hold-ms 30000' + $taskLauncher = Start-Process -FilePath (Join-Path $taskRun 'MultiSeat.SteamLauncher.exe') -ArgumentList $taskSteamArgs -WorkingDirectory $taskRun -WindowStyle Hidden -PassThru + $taskDeadline = [DateTime]::UtcNow.AddSeconds(10) + while (-not (Test-Path -LiteralPath $taskSteamReport) -and [DateTime]::UtcNow -lt $taskDeadline) { Start-Sleep -Milliseconds 100 } + if (-not (Test-Path -LiteralPath $taskSteamReport)) { throw 'Isolated Steam fixture did not start.' } + $taskOriginal = $env:steam_master_ipc_name_override + & (Join-Path $PSScriptRoot '..\Launch-IsolatedGame.ps1') -Executable (Join-Path $taskRun 'game.exe') -Arguments '--report game-report.txt' + if ($env:steam_master_ipc_name_override -cne $taskOriginal) { throw 'Game launcher changed the caller environment.' } + $taskGameReport = Join-Path $taskRun 'game-report.txt' + $taskDeadline = [DateTime]::UtcNow.AddSeconds(10) + while (-not (Test-Path -LiteralPath $taskGameReport) -and [DateTime]::UtcNow -lt $taskDeadline) { Start-Sleep -Milliseconds 100 } + if (-not (Test-Path -LiteralPath $taskGameReport)) { throw 'Game did not write its report in its installation folder.' } + $taskSteamIPC = @(Get-Content -LiteralPath $taskSteamReport | Where-Object { $_ -like 'steam_master_ipc_name_override=*' }) + $taskGameIPC = @(Get-Content -LiteralPath $taskGameReport | Where-Object { $_ -like 'steam_master_ipc_name_override=*' }) + if ($taskSteamIPC.Count -ne 1 -or $taskGameIPC.Count -ne 1 -or $taskSteamIPC[0] -cne $taskGameIPC[0]) { throw 'Game and isolated Steam used different IPC names.' } + if (-not (Select-String -LiteralPath $taskGameReport -Pattern '^SteamPID=123456$' -Quiet)) { throw 'Direct game received an unexpected registry hook.' } + Write-Host 'Game launcher environment and working-directory checks passed.' +} finally { + if ($taskLauncher -and -not $taskLauncher.HasExited) { + Stop-Process -Id $taskLauncher.Id + [void]$taskLauncher.WaitForExit(5000) + } + if ($taskLauncher) { $taskLauncher.Dispose() } +} diff --git a/src/MultiSeat.SteamIsolation/tests/LauncherSmoke.ps1 b/src/MultiSeat.SteamIsolation/tests/LauncherSmoke.ps1 new file mode 100644 index 0000000..ad410dc --- /dev/null +++ b/src/MultiSeat.SteamIsolation/tests/LauncherSmoke.ps1 @@ -0,0 +1,115 @@ +param( + [string]$Artifacts = (Join-Path $PSScriptRoot '..\artifacts'), + [string]$TestRoot = (Join-Path $PSScriptRoot '..\build-smoke') +) +$ErrorActionPreference = 'Stop' +$source = [IO.Path]::GetFullPath($Artifacts) +$runRoot = Join-Path ([IO.Path]::GetFullPath($TestRoot)) ([guid]::NewGuid().ToString('N')) +function Assert-True($condition, [string]$message) { if (-not $condition) { throw $message } } +# OptionalHeader.Subsystem sits at the same offset (68) in PE32 and PE32+ images; 2 = Windows GUI, 3 = console. +function Get-PeSubsystem([string]$path) { + $bytes = [IO.File]::ReadAllBytes($path) + $peOffset = [BitConverter]::ToInt32($bytes, 0x3C) + [int][BitConverter]::ToUInt16($bytes, $peOffset + 24 + 68) +} +# The service starts this launcher inside a seat; as a console program it opened a window there. +foreach ($launcherName in @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamLauncher32.exe')) { + $subsystem = Get-PeSubsystem (Join-Path $source $launcherName) + Assert-True ($subsystem -eq 2) "$launcherName is subsystem $subsystem, expected 2 (Windows GUI) so it opens no console window" +} +# Prove the probe can tell the two apart: the fixture is a console program. +Assert-True ((Get-PeSubsystem (Join-Path $source 'SteamIsolationFixture.exe')) -eq 3) 'Subsystem probe did not read the console fixture as subsystem 3' +function Invoke-Fixture([string]$directory, [string]$tail, [bool]$expectReady, [int]$expectExit, [string]$steamDirectory = '') { + # The launcher and its hook live in $directory; the stand-in steam.exe may live elsewhere + # (a folder with a space in its name, to test an unquoted program path). + $steamDir = if ($steamDirectory) { $steamDirectory } else { $directory } + $name = 'Global\MultiSeatSteamReady-' + [guid]::NewGuid().ToString('N') + $ready = [Threading.EventWaitHandle]::new($false, [Threading.EventResetMode]::ManualReset, $name) + $launcher = $null + try { + $args = '--steam "' + (Join-Path $steamDir 'steam.exe') + '" --ready-event "' + $name + '" -- ' + $tail + $launcher = Start-Process -FilePath (Join-Path $directory 'MultiSeat.SteamLauncher.exe') -ArgumentList $args ` + -WorkingDirectory $directory -WindowStyle Hidden -RedirectStandardError (Join-Path $directory 'stderr.txt') -PassThru + $signaled = $ready.WaitOne(5000) + Assert-True ($signaled -eq $expectReady) "Readiness was $signaled, expected $expectReady in $directory" + Assert-True ($launcher.WaitForExit(5000)) "Fixture process tree did not finish in $directory" + Assert-True ($launcher.ExitCode -eq $expectExit) "Unexpected launcher exit $($launcher.ExitCode) in $directory" + } finally { + if ($launcher -and -not $launcher.HasExited) { Stop-Process -Id $launcher.Id } + $ready.Dispose() + } +} +foreach ($suffix in @('', '32')) { + $directory = Join-Path $runRoot $(if ($suffix) { 'x86' } else { 'x64' }) + New-Item -ItemType Directory -Force -Path $directory | Out-Null + foreach ($name in @('MultiSeat.SteamLauncher.exe', 'MultiSeat.SteamLauncher32.exe', "MultiSeat.SteamHook$suffix.dll")) { + Copy-Item -LiteralPath (Join-Path $source $name) -Destination $directory + } + # Only the compiled harmless fixture is named steam.exe; installed Steam is never used. + Copy-Item -LiteralPath (Join-Path $source "SteamIsolationFixture$suffix.exe") -Destination (Join-Path $directory 'steam.exe') + Copy-Item -LiteralPath (Join-Path $source "SteamIsolationFixture$suffix.exe") -Destination (Join-Path $directory 'steamwebhelper.exe') + Invoke-Fixture $directory '--report ready.txt --hold-ms 100 "two words" "quote\"inside"' $true 0 + $report = Get-Content -LiteralPath (Join-Path $directory 'ready.txt') -Raw + Assert-True ($report -match '(?m)^steamPid=0\r?$') 'SteamPID DWORD was not isolated' + Assert-True ($report -match '(?m)^steamPidNullType=0\r?$') 'SteamPID DWORD with null type was not isolated' + Assert-True ($report -match '(?m)^steamPidString=fixture-nonDWORD\r?$') 'SteamPID string was corrupted' + Assert-True ($report -match '(?m)^arg5=two words\r?$') 'Quoted argument was not preserved' + # A Steam that restarts itself can do it through any creation API, and with an unquoted program + # path that contains spaces. Each way has to come back hooked, with its browser cache separated. + # The hook also says so in the launcher log, which is what a real-Steam test would read. + $restartLog = Join-Path $env:LOCALAPPDATA 'MultiSeat\SteamIsolation\launcher.log' + $restartLogBefore = if (Test-Path -LiteralPath $restartLog) { (Get-Item -LiteralPath $restartLog).Length } else { 0 } + $spaced = Join-Path $directory 'with space' + New-Item -ItemType Directory -Force -Path $spaced | Out-Null + foreach ($exe in @('steam.exe', 'steamwebhelper.exe')) { + Copy-Item -LiteralPath (Join-Path $source "SteamIsolationFixture$suffix.exe") -Destination (Join-Path $spaced $exe) + } + foreach ($via in @('w', 'a', 'user', 'unquoted')) { + $steamIn = if ($via -eq 'unquoted') { $spaced } else { '' } + Invoke-Fixture $directory "--report first-$via.txt --restart-via $via --restart-report restarted-$via.txt --child-report browser-$via.txt --child-hold-ms 200" $true 0 $steamIn + $replacement = Get-Content -LiteralPath (Join-Path $directory "restarted-$via.txt") -Raw + $browser = Get-Content -LiteralPath (Join-Path $directory "browser-$via.txt") -Raw + Assert-True ($replacement -match '(?m)^steamPid=0\r?$') "Replacement Steam was not hooked (restart via $via)" + Assert-True ($browser -match 'htmlcache\.MultiSeat_') "Replacement browser cache was not isolated (restart via $via)" + } + $restartLogText = [IO.File]::ReadAllText($restartLog) + $restartLogNew = $restartLogText.Substring([Math]::Min($restartLogBefore, $restartLogText.Length)) + $isolatedRestarts = ([regex]::Matches($restartLogNew, 'Steam restart isolated')).Count + Assert-True ($isolatedRestarts -ge 4) "The hook logged $isolatedRestarts isolated restarts, expected one per way of restarting (4)" + $supervisor = $null + try { + $args = '--steam "' + (Join-Path $directory 'steam.exe') + '" -- --restart-report live-restarted.txt --child-report live-browser.txt --child-hold-ms 30000' + $supervisor = Start-Process -FilePath (Join-Path $directory 'MultiSeat.SteamLauncher.exe') -ArgumentList $args ` + -WorkingDirectory $directory -WindowStyle Hidden -PassThru + $childInfo = $null + for ($attempt = 0; $attempt -lt 30; $attempt++) { + $childInfo = Get-CimInstance Win32_Process -Filter "Name='steamwebhelper.exe'" | + Where-Object ExecutablePath -eq (Join-Path $directory 'steamwebhelper.exe') + if ($childInfo -and (Test-Path -LiteralPath (Join-Path $directory 'live-browser.txt'))) { break } + Start-Sleep -Milliseconds 100 + } + Assert-True ([bool]$childInfo) 'Replacement browser fixture did not start' + $childProcess = Get-Process -Id $childInfo.ProcessId + Start-Sleep -Milliseconds 300 + Assert-True (-not $supervisor.HasExited) 'Supervisor stopped while replacement descendants were running' + Stop-Process -Id $supervisor.Id + Assert-True ($childProcess.WaitForExit(3000)) 'Supervisor termination left a browser descendant running' + } finally { + if ($supervisor -and -not $supervisor.HasExited) { Stop-Process -Id $supervisor.Id } + } +} +$failureDirectory = Join-Path $runRoot 'missing-hook' +New-Item -ItemType Directory -Force -Path $failureDirectory | Out-Null +Copy-Item -LiteralPath (Join-Path $source 'MultiSeat.SteamLauncher.exe') -Destination $failureDirectory +Copy-Item -LiteralPath (Join-Path $source 'SteamIsolationFixture.exe') -Destination (Join-Path $failureDirectory 'steam.exe') +$launcherLog = Join-Path $env:LOCALAPPDATA 'MultiSeat\SteamIsolation\launcher.log' +$logBefore = if (Test-Path -LiteralPath $launcherLog) { (Get-Item -LiteralPath $launcherLog).Length } else { 0 } +Invoke-Fixture $failureDirectory '--report unexpected.txt' $false 1 +Assert-True (-not (Test-Path -LiteralPath (Join-Path $failureDirectory 'unexpected.txt'))) 'Unisolated child ran after a hook failure' +# With no console, a failure has to be findable afterwards: in the log file, and on stderr when it was redirected. +Assert-True (Test-Path -LiteralPath $launcherLog) "The launcher wrote no log file at $launcherLog" +$logText = [IO.File]::ReadAllText($launcherLog) +Assert-True ($logText.Substring([Math]::Min($logBefore, $logText.Length)) -match 'Steam isolation failed') 'The hook failure was not written to the launcher log' +$stderrText = Get-Content -LiteralPath (Join-Path $failureDirectory 'stderr.txt') -Raw +Assert-True ($stderrText -match 'Steam isolation failed') 'The hook failure was not written to the redirected stderr' +Write-Host "Native launcher smoke tests passed. Fixture reports: $runRoot" diff --git a/src/MultiSeat.SteamIsolation/tests/PolicyTests.cpp b/src/MultiSeat.SteamIsolation/tests/PolicyTests.cpp new file mode 100644 index 0000000..cd4c2c1 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/tests/PolicyTests.cpp @@ -0,0 +1,79 @@ +#include "../IsolationPolicy.h" +#include +#include +using namespace steam_isolation; +static void Expect(bool result, const char* message) { if (!result) throw std::runtime_error(message); } +static std::vector Args(const std::wstring& command) { + int count = 0; LPWSTR* parsed = CommandLineToArgvW(command.c_str(), &count); + if (!parsed) throw std::runtime_error("Cannot parse command"); + std::vector result(parsed, parsed + count); LocalFree(parsed); return result; +} +int main() { + try { + const std::wstring key = L"MultiSeat_17_123"; + auto input = L"\"C:\\Program Files (x86)\\Steam\\steam.exe\" -bigpicture \"a\\\"b\" \"trailing\\\\\""; + auto before = Args(input), after = Args(RewriteCommand(input, key)); + Expect(after.size() == before.size() + 2, "IPC option missing"); + for (size_t i = 0; i < before.size(); ++i) Expect(before[i] == after[i], "Argument semantics changed"); + Expect(after.back() == key, "Wrong namespace"); + after = Args(RewriteCommand(L"steam.exe -master_ipc_name_override wrong -bigpicture", key)); + Expect(after.size() == 4 && after[1] == L"-bigpicture" && after[3] == key, "IPC duplicate not replaced"); + after = Args(RewriteCommand(L"steam.exe -master_ipc_name_override=wrong", key)); + Expect(after.size() == 3 && after[2] == key, "Inline IPC duplicate not replaced"); + const std::wstring browser = L"steamwebhelper.exe -cachedir=\"C:\\cache folder\\htmlcache\" --log-file=C:\\logs\\cef_log.txt --other=x"; + auto changed = RewriteCommand(browser, key); after = Args(changed); + Expect(after[1] == L"-cachedir=C:\\cache folder\\htmlcache." + key, "Browser cache not separated"); + Expect(after[2] == L"--log-file=C:\\logs\\cef_log." + key + L".txt", "Browser log not separated"); + Expect(RewriteCommand(changed, key) == changed, "Browser suffix added twice"); + Expect(RewriteCommand(L"mySteam.exe -cachedir=C:\\htmlcache", key) == L"mySteam.exe -cachedir=C:\\htmlcache", "Unrelated program changed"); + Expect(RewriteCommand(L"steamwebhelper.exe -cachedir=C:\\myhtmlcache", key) == L"steamwebhelper.exe -cachedir=C:\\myhtmlcache", "Substring changed"); + after = Args(RewriteCommand(L"steam.exe https://example.test/htmlcache --label=cef_log", key)); + Expect(after[1] == L"https://example.test/htmlcache" && after[2] == L"--label=cef_log", "URL or label changed"); + // An unquoted path that contains spaces. CommandLineToArgvW reads its first word as + // "C:\Program", so before SplitImage such a command was neither rewritten nor isolated. + const std::wstring spaced = L"C:\\Program Files (x86)\\Steam\\steam.exe"; + auto spacedExists = [](const std::wstring& path) { return path == L"C:\\Program Files (x86)\\Steam\\steam.exe"; }; + auto neverExists = [](const std::wstring&) { return false; }; + const std::wstring unquoted = spaced + L" -silent \"two words\""; + auto rewritten = Args(RewriteCommand(unquoted, key, spacedExists)); + Expect(rewritten.size() == 5, "Unquoted spaced path: argument count wrong"); + Expect(rewritten[0] == spaced && rewritten[1] == L"-silent" && rewritten[2] == L"two words", "Unquoted spaced path: arguments changed"); + Expect(rewritten[3] == L"-master_ipc_name_override" && rewritten[4] == key, "Unquoted spaced path: IPC option missing"); + Expect(RewriteCommand(unquoted, key, neverExists) == unquoted, "A folder that does not exist was taken for Steam"); + const std::wstring spacedBrowser = L"C:\\Program Files (x86)\\Steam\\bin\\cef\\steamwebhelper.exe"; + auto browserExists = [](const std::wstring& path) { return path == L"C:\\Program Files (x86)\\Steam\\bin\\cef\\steamwebhelper.exe"; }; + auto browserArgs = Args(RewriteCommand(spacedBrowser + L" -cachedir=C:\\cache\\htmlcache", key, browserExists)); + Expect(browserArgs[0] == spacedBrowser && browserArgs[1] == L"-cachedir=C:\\cache\\htmlcache." + key, "Unquoted spaced browser path not isolated"); + Expect(RewriteCommand(L"C:\\Tools\\runner.exe " + spaced, key, spacedExists) == L"C:\\Tools\\runner.exe " + spaced, + "A program that merely has Steam in its arguments was taken for Steam"); + // The first word is taken on its name alone: "steam.exe -silent" is found through the search path. + Expect(Args(RewriteCommand(L"steam.exe -silent", key, neverExists)).size() == 4, "Bare steam.exe needs no file on disk"); + + auto split = SplitImage(L" \"C:\\Program Files\\x\\steam.exe\" -a -b", neverExists); + Expect(split.image == L"C:\\Program Files\\x\\steam.exe" && split.rest == L" -a -b", "Quoted split wrong"); + split = SplitImage(L"steam.exe -a", neverExists); + Expect(split.image == L"steam.exe" && split.rest == L" -a", "First-word split wrong"); + split = SplitImage(spaced + L" -a", spacedExists); + Expect(split.image == spaced && split.rest == L" -a", "Spaced split wrong"); + split = SplitImage(L"other.exe -a", neverExists); + Expect(split.image == L"other.exe" && split.rest == L" -a", "Other programs come back as their first word"); + Expect(SplitImage(L" ", neverExists).image.empty() && SplitImage(L"", neverExists).image.empty(), "Blank command not empty"); + split = SplitImage(L"\"C:\\unclosed\\steam.exe -a", neverExists); + Expect(split.image == L"C:\\unclosed\\steam.exe -a" && split.rest.empty(), "Unclosed quote handled wrong"); + + const wchar_t environment[] = L"=C:=C:\\work\0HOME=C:\\profile\0StEaM_MaStEr_IpC_NaMe_OvErRiDe=old\0TEST=ok\0"; + auto block = RewriteEnvironment(environment, sizeof(environment) / sizeof(wchar_t), key); + Expect(!block.empty() && block.back() == 0 && block[block.size() - 2] == 0, "Environment terminator missing"); + int found = 0; bool drive = false, test = false; + for (const wchar_t* p = block.data(); *p; p += wcslen(p) + 1) { + std::wstring value(p); + if (value == L"steam_master_ipc_name_override=" + key) ++found; + if (value == L"=C:=C:\\work") drive = true; + if (value == L"TEST=ok") test = true; + Expect(value.find(L"=old") == std::wstring::npos, "Old IPC retained"); + } + Expect(found == 1 && drive && test, "Environment unrelated entries lost"); + Expect(RewriteEnvironment(L"broken", 6, key).empty(), "Malformed block accepted"); + std::cout << "Steam command-line and environment policy tests passed\n"; return 0; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/.editorconfig b/src/MultiSeat.SteamIsolation/third_party/minhook/.editorconfig new file mode 100644 index 0000000..735e983 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/.editorconfig @@ -0,0 +1,26 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Windows-style newlines with a newline ending every file +[*] +end_of_line = crlf +insert_final_newline = true + +# 4 space indentation +[*.{c,h,def}] +indent_style = space +indent_size = 4 + +# Trim trailing whitespaces +[*.{c,h,def,txt}] +trim_trailing_whitespace = true + +# UTF-8 with BOM +[*.{c,h,def,txt}] +charset=utf-8-bom + +# C/C++ code formatting +[*.{c,h}] +cpp_space_pointer_reference_alignment = right diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/.github/workflows/msbuild.yml b/src/MultiSeat.SteamIsolation/third_party/minhook/.github/workflows/msbuild.yml new file mode 100644 index 0000000..5575ff2 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/.github/workflows/msbuild.yml @@ -0,0 +1,59 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: [ "master" ] + +env: + # Path to the solution file relative to the root of the project. + SOLUTION_FILE_PATH: build/VC17/MinHookVC17.sln + + # Configuration type to build. + # You can convert this to a build matrix if you need coverage of multiple configuration types. + # https://docs.github.com/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + BUILD_CONFIGURATION: Release + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build Win32 + # Add additional options to the MSBuild command line here (like platform or verbosity level). + # See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference + run: msbuild /m /p:Configuration=${{ env.BUILD_CONFIGURATION }} /p:Platform="Win32" ${{ env.SOLUTION_FILE_PATH }} + + - name: Build x64 + # Add additional options to the MSBuild command line here (like platform or verbosity level). + # See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference + run: msbuild /m /p:Configuration=${{ env.BUILD_CONFIGURATION }} /p:Platform="x64" ${{ env.SOLUTION_FILE_PATH }} + + - name: Collect artifacts + shell: bash + run: | + mkdir "${{ runner.temp }}/artifacts" + mv "build/VC17/bin/${{ env.BUILD_CONFIGURATION }}" "${{ runner.temp }}/artifacts/bin" + mv "build/VC17/lib/${{ env.BUILD_CONFIGURATION }}" "${{ runner.temp }}/artifacts/lib" + cp -r include "${{ runner.temp }}/artifacts/include" + + - name: Package bin + uses: actions/upload-artifact@v4 + with: + name: MinHook_bin + path: | + ${{ runner.temp }}/artifacts/bin/ + ${{ runner.temp }}/artifacts/include/ + + - name: Package lib + uses: actions/upload-artifact@v4 + with: + name: MinHook_lib + path: | + ${{ runner.temp }}/artifacts/lib/ + ${{ runner.temp }}/artifacts/include/ diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/.gitignore b/src/MultiSeat.SteamIsolation/third_party/minhook/.gitignore new file mode 100644 index 0000000..ad165d5 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/.gitignore @@ -0,0 +1,44 @@ +#OS junk files +[Tt]humbs.db +*.DS_Store + +#Visual Studio files +*.[Oo]bj +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.[Cc]ache +*.ilk +*.log +*.sbr +*.sdf +*.opensdf +*.unsuccessfulbuild +ipch/ +obj/ +[Ll]ib +[Bb]in +[Dd]ebug*/ +[Rr]elease*/ +Ankh.NoLoad +*.VC.db +.vs/ + +#GCC files +*.o +*.d +*.res +*.dll +*.a + +#Visual Studio Code files +.vscode/ diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/AUTHORS.txt b/src/MultiSeat.SteamIsolation/third_party/minhook/AUTHORS.txt new file mode 100644 index 0000000..ebef1a6 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/AUTHORS.txt @@ -0,0 +1,8 @@ +Tsuda Kageyu + Creator, maintainer + +Michael Maltsev + Added "Queue" functions. A lot of bug fixes. + +Andrey Unis + Rewrote the hook engine in plain C. diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/CMakeLists.txt b/src/MultiSeat.SteamIsolation/third_party/minhook/CMakeLists.txt new file mode 100644 index 0000000..845e3c7 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/CMakeLists.txt @@ -0,0 +1,141 @@ +# MinHook - The Minimalistic API Hooking Library for x64/x86 +# Copyright (C) 2009-2017 Tsuda Kageyu. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER +# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +cmake_minimum_required(VERSION 3.0...3.5) + +project(minhook LANGUAGES C) + +include(CMakePackageConfigHelpers) + +set(MINHOOK_MAJOR_VERSION 1) +set(MINHOOK_MINOR_VERSION 3) +set(MINHOOK_PATCH_VERSION 3) +set(MINHOOK_VERSION ${MINHOOK_MAJOR_VERSION}.${MINHOOK_MINOR_VERSION}.${MINHOOK_PATCH_VERSION}) + +################ +# BUILD # +################ + +option(BUILD_SHARED_LIBS "build shared version" OFF) + +set(SOURCES_MINHOOK + "src/buffer.c" + "src/hook.c" + "src/trampoline.c" +) + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(SOURCES_HDE "src/hde/hde64.c") +else() + set(SOURCES_HDE "src/hde/hde32.c") +endif() + +if(BUILD_SHARED_LIBS) + set(RESOURCES + "dll_resources/MinHook.rc" + "dll_resources/MinHook.def" + ) +endif() + +add_library(minhook ${SOURCES_MINHOOK} ${SOURCES_HDE} ${RESOURCES}) + +target_include_directories(minhook PUBLIC + $ + $ +) + +target_include_directories(minhook PRIVATE "src/") +target_include_directories(minhook PRIVATE "src/hde/") + +if(WIN32) + set_target_properties(minhook PROPERTIES PREFIX "") + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set_target_properties(minhook PROPERTIES DEBUG_POSTFIX ".x64d") + set_target_properties(minhook PROPERTIES RELEASE_POSTFIX ".x64") + set_target_properties(minhook PROPERTIES RELWITHDEBINFO_POSTFIX ".x64") + set_target_properties(minhook PROPERTIES MINSIZEREL_POSTFIX ".x64") + else() + set_target_properties(minhook PROPERTIES DEBUG_POSTFIX ".x32d") + set_target_properties(minhook PROPERTIES RELEASE_POSTFIX ".x32") + set_target_properties(minhook PROPERTIES RELWITHDEBINFO_POSTFIX ".x32") + set_target_properties(minhook PROPERTIES MINSIZEREL_POSTFIX ".x32") + endif() +else() + set_target_properties(minhook PROPERTIES PREFIX "lib") + set_target_properties(minhook PROPERTIES POSTFIX "") + set_target_properties(minhook PROPERTIES DEBUG_POSTFIX "d") +endif() + +################ +# CMAKE CONFIG # +################ + +configure_package_config_file( + "cmake/minhook-config.cmake.in" + "minhook-config.cmake" + INSTALL_DESTINATION + "lib/minhook" +) + +write_basic_package_version_file( + "minhook-config-version.cmake" +VERSION + ${MINHOOK_VERSION} +COMPATIBILITY + AnyNewerVersion +) + +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/minhook-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/minhook-config-version.cmake" + DESTINATION + "lib/minhook" +) + +################### +# INSTALL # +################### + +install(TARGETS minhook + EXPORT minhook-targets + RUNTIME DESTINATION "bin" + ARCHIVE DESTINATION "lib" + LIBRARY DESTINATION "lib" +) + +install( + EXPORT + minhook-targets + NAMESPACE + minhook:: + DESTINATION + "lib/minhook" +) + +install( + DIRECTORY include DESTINATION . +) diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/LICENSE.txt b/src/MultiSeat.SteamIsolation/third_party/minhook/LICENSE.txt new file mode 100644 index 0000000..74dea27 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/LICENSE.txt @@ -0,0 +1,81 @@ +MinHook - The Minimalistic API Hooking Library for x64/x86 +Copyright (C) 2009-2017 Tsuda Kageyu. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +================================================================================ +Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov. +================================================================================ +Hacker Disassembler Engine 32 C +Copyright (c) 2008-2009, Vyacheslav Patkov. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- +Hacker Disassembler Engine 64 C +Copyright (c) 2008-2009, Vyacheslav Patkov. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/README.md b/src/MultiSeat.SteamIsolation/third_party/minhook/README.md new file mode 100644 index 0000000..b77edb5 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/README.md @@ -0,0 +1,94 @@ +# MinHook + +[![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause) + +The Minimalistic x86/x64 API Hooking Library for Windows + +http://www.codeproject.com/KB/winsdk/LibMinHook.aspx + +### Version history + +- **v1.3.4 - 28 Mar 2025** + * Improved error handling for enumerating and suspending threads. + * Visual Studio VS2022 support. + * CMake support. + * Fixed compilation with Clang. + * Fixed compilation as C++ code. + +- **v1.3.3 - 8 Jan 2017** + * Added a helper function ```MH_CreateHookApiEx```. (Thanks to asm256) + * Support Visual Studio 2017 RC. + +- **v1.3.2.1 - 9 Nov 2015** (Nuget package only) + * Fixed an insufficient support for Visual Studio 2015. + +- **v1.3.2 - 1 Nov 2015** + * Support Visual Studio 2015. + * Support MinGW. + +- **v1.3.2-beta3 - 21 Jul 2015** (Nuget package only) + * Support MinGW. (Experimental) + +- **v1.3.2-beta2 - 18 May 2015** + * Fixed some subtle bugs. (Thanks to RaMMicHaeL) + * Added a helper function ```MH_StatusToString```. (Thanks to Jan Klass) + +- **v1.3.2-beta - 12 May 2015** + * Fixed a possible thread deadlock in x64 mode. (Thanks to Aleh Kazakevich) + * Reduced the footprint a little more. + * Support Visual Studio 2015 RC. (Experimental) + +- **v1.3.1.1 - 7 Apr 2015** (Nuget package only) + * Support for WDK8.0 and 8.1. + +- **v1.3.1 - 19 Mar 2015** + * No major changes from v1.3.1-beta. + +- **v1.3.1-beta - 11 Mar 2015** + * Added a helper function ```MH_CreateHookApi```. (Thanks to uniskz). + * Fixed a false memory leak reported by some tools. + * Fixed a degradated compatibility issue. + +- **v1.3 - 13 Sep 2014** + * No major changes from v1.3-beta3. + +- **v1.3-beta3 - 31 Jul 2014** + * Fixed some small bugs. + * Improved the memory management. + +- **v1.3-beta2 - 21 Jul 2014** + * Changed the parameters to Windows-friendly types. (void* to LPVOID) + * Fixed some small bugs. + * Reorganized the source files. + * Reduced the footprint a little more. + +- **v1.3-beta - 17 Jul 2014** + * Rewrote in plain C to reduce the footprint and memory usage. (suggested by Andrey Unis) + * Simplified the overall code base to make it more readable and maintainable. + * Changed the license from 3-clause to 2-clause BSD License. + +- **v1.2 - 28 Sep 2013** + * Removed boost dependency ([jarredholman](https://github.com/jarredholman/minhook)). + * Fixed a small bug in the GetRelativeBranchDestination function ([pillbug99](http://www.codeproject.com/Messages/4058892/Small-Bug-Found.aspx)). + * Added the ```MH_RemoveHook``` function, which removes a hook created with the ```MH_CreateHook``` function. + * Added the following functions to enable or disable multiple hooks in one go: ```MH_QueueEnableHook```, ```MH_QueueDisableHook```, ```MH_ApplyQueued```. This is the preferred way of handling multiple hooks as every call to `MH_EnableHook` or `MH_DisableHook` suspends and resumes all threads. + * Made the functions ```MH_EnableHook``` and ```MH_DisableHook``` enable/disable all created hooks when the ```MH_ALL_HOOKS``` parameter is passed. This, too, is an efficient way of handling multiple hooks. + * If the target function is too small to be patched with a jump, MinHook tries to place the jump above the function. If that fails as well, the ```MH_CreateHook``` function returns ```MH_ERROR_UNSUPPORTED_FUNCTION```. This fixes an issue of hooking the LoadLibraryExW function on Windows 7 x64 ([reported by Obble](http://www.codeproject.com/Messages/4578613/Re-Bug-LoadLibraryExW-hook-fails-on-windows-2008-r.aspx)). + +- **v1.1 - 26 Nov 2009** + * Changed the interface to create a hook and a trampoline function in one go to prevent the detour function from being called before the trampoline function is created. ([reported by xliqz](http://www.codeproject.com/Messages/3280374/Unsafe.aspx)) + * Shortened the function names from ```MinHook_*``` to ```MH_*``` to make them handier. + +- **v1.0 - 22 Nov 2009** + * Initial release. + +### Building MinHook - Using vcpkg + +You can download and install MinHook using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: + + git clone https://github.com/microsoft/vcpkg + .\vcpkg\bootstrap-vcpkg.bat + .\vcpkg\vcpkg integrate install + .\vcpkg\vcpkg install minhook + +The MinHook port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/Makefile b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/Makefile new file mode 100644 index 0000000..9784745 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/Makefile @@ -0,0 +1,33 @@ +WINDRES:=$(CROSS_PREFIX)windres +DLLTOOL:=$(CROSS_PREFIX)dlltool +AR:=$(CROSS_PREFIX)ar +CC:=$(CROSS_PREFIX)gcc +CCLD:=$(CC) +SRCS:=$(wildcard src/*.c src/hde/*.c) +OBJS:=$(SRCS:%.c=%.o) +DEPS:=$(SRCS:%.c=%.d) +INCS:=-Isrc -Iinclude +CFLAGS:=-masm=intel -Wall -Werror -std=c11 +LDFLAGS:=-Wl,-enable-stdcall-fixup -s -static-libgcc + +all: MinHook.dll libMinHook.dll.a libMinHook.a + +-include $(DEPS) + +libMinHook.a: $(OBJS) + $(AR) r $@ $^ +libMinHook.dll.a: MinHook.dll dll_resources/MinHook.def + $(DLLTOOL) --dllname MinHook.dll --input-def dll_resources/MinHook.def --output-lib $@ +MinHook.dll: $(OBJS) dll_resources/MinHook.res dll_resources/MinHook.def + $(CCLD) -o $@ -shared $(LDFLAGS) $^ + +.rc.res: + $(WINDRES) -o $@ --input-format=rc --output-format=coff $< +.c.o: + $(CC) -o $@ -c -MMD -MP $(INCS) $(CFLAGS) $< + +clean: + rm -f $(OBJS) $(DEPS) MinHook.dll libMinHook.dll.a libMinHook.a dll_resources/MinHook.res + +.PHONY: clean +.SUFFIXES: .rc .res diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.bat b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.bat new file mode 100644 index 0000000..7671878 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.bat @@ -0,0 +1 @@ +windres -i ../../dll_resources/MinHook.rc -o MinHook_rc.o && dllwrap --driver-name g++ -o MinHook.dll -masm=intel --def ../../dll_resources/MinHook.def -Wl,-enable-stdcall-fixup -Wall MinHook_rc.o ../../src/*.c ../../src/HDE/*.c -I../../include -I../../src -Werror -std=c++11 -s -static-libgcc -static-libstdc++|| pause \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.sh b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.sh new file mode 100644 index 0000000..17eddbc --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/MinGW/make.sh @@ -0,0 +1,2 @@ +x86_64-w64-mingw32-windres -i ../../dll_resources/MinHook.rc -o MinHook_rc.o && +x86_64-w64-mingw32-dllwrap -o MinHook.dll -masm=intel --def ../../dll_resources/MinHook.def -Wl,-enable-stdcall-fixup -Wall MinHook_rc.o ../../src/*.c ../../src/hde/*.c -I../../include -I../../src -Werror -s -static-libgcc -static-libstdc++ diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHook.vcxproj new file mode 100644 index 0000000..3944d80 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + + + DynamicLibrary + Unicode + + + DynamicLibrary + Unicode + true + + + DynamicLibrary + Unicode + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + + + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + + + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + + + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + + + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHookVC10.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHookVC10.sln new file mode 100644 index 0000000..dcc1d5c --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/MinHookVC10.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj new file mode 100644 index 0000000..589ff9a --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj @@ -0,0 +1,172 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + + + StaticLibrary + Unicode + + + StaticLibrary + Unicode + true + + + StaticLibrary + Unicode + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + + + false + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + + + false + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + + + true + AnySuitable + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + + + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC10/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHook.vcxproj new file mode 100644 index 0000000..4c0e212 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + v110_xp + + + DynamicLibrary + Unicode + v110_xp + + + DynamicLibrary + Unicode + true + v110_xp + + + DynamicLibrary + Unicode + v110_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHookVC11.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHookVC11.sln new file mode 100644 index 0000000..5b56553 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/MinHookVC11.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj new file mode 100644 index 0000000..1a65499 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj @@ -0,0 +1,172 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + v110_xp + + + StaticLibrary + Unicode + v110_xp + + + StaticLibrary + Unicode + true + v110_xp + + + StaticLibrary + Unicode + v110_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC11/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHook.vcxproj new file mode 100644 index 0000000..40ec836 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + v120_xp + + + DynamicLibrary + Unicode + v120_xp + + + DynamicLibrary + Unicode + true + v120_xp + + + DynamicLibrary + Unicode + v120_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHookVC12.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHookVC12.sln new file mode 100644 index 0000000..cfd928b --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/MinHookVC12.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30501.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj new file mode 100644 index 0000000..7e7fe8e --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj @@ -0,0 +1,173 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + v120_xp + + + StaticLibrary + Unicode + v120_xp + + + StaticLibrary + Unicode + true + v120_xp + + + StaticLibrary + Unicode + v120_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NotSet + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + AnySuitable + true + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC12/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHook.vcxproj new file mode 100644 index 0000000..d5ecda2 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + v140_xp + + + DynamicLibrary + Unicode + v140_xp + + + DynamicLibrary + Unicode + true + v140_xp + + + DynamicLibrary + Unicode + v140_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHookVC14.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHookVC14.sln new file mode 100644 index 0000000..258c192 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/MinHookVC14.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22823.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj new file mode 100644 index 0000000..a429e69 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj @@ -0,0 +1,173 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + v140_xp + + + StaticLibrary + Unicode + v140_xp + + + StaticLibrary + Unicode + true + v140_xp + + + StaticLibrary + Unicode + v140_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NotSet + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + AnySuitable + true + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC14/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHook.vcxproj new file mode 100644 index 0000000..1d51833 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + v141_xp + + + DynamicLibrary + Unicode + v141_xp + + + DynamicLibrary + Unicode + true + v141_xp + + + DynamicLibrary + Unicode + v141_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHookVC15.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHookVC15.sln new file mode 100644 index 0000000..946dc70 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/MinHookVC15.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.25123.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj new file mode 100644 index 0000000..829ac34 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj @@ -0,0 +1,173 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + v141_xp + + + StaticLibrary + Unicode + v141_xp + + + StaticLibrary + Unicode + true + v141_xp + + + StaticLibrary + Unicode + v141_xp + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NotSet + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + AnySuitable + true + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC15/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHook.vcxproj new file mode 100644 index 0000000..23ddafd --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHook.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + + + + DynamicLibrary + Unicode + true + v142 + + + DynamicLibrary + Unicode + v142 + + + DynamicLibrary + Unicode + true + v142 + + + DynamicLibrary + Unicode + v142 + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHookVC16.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHookVC16.sln new file mode 100644 index 0000000..191b75c --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/MinHookVC16.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.352 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj new file mode 100644 index 0000000..605871b --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj @@ -0,0 +1,173 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + + + + StaticLibrary + Unicode + true + v142 + + + StaticLibrary + Unicode + v142 + + + StaticLibrary + Unicode + true + v142 + + + StaticLibrary + Unicode + v142 + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NotSet + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + AnySuitable + true + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC16/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHook.vcxproj new file mode 100644 index 0000000..1a7aae5 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHook.vcxproj @@ -0,0 +1,190 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {027FAC75-3FDB-4044-8DD0-BC297BD4C461} + MinHook + Win32Proj + 10.0 + + + + DynamicLibrary + Unicode + true + v143 + + + DynamicLibrary + Unicode + v143 + + + DynamicLibrary + Unicode + true + v143 + + + DynamicLibrary + Unicode + v143 + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(SolutionDir)bin\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + false + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + + + Level3 + None + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + + + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX86 + $(SolutionDir)lib\$(Configuration)\libMinHook.x86.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + X64 + + + MinSpace + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;MINHOOK_EXPORTS;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + None + false + + + $(SolutionDir)..\..\dll_resources\MinHook.def + false + Windows + true + true + MachineX64 + $(SolutionDir)lib\$(Configuration)\libMinHook.x64.lib;%(AdditionalDependencies) + true + .CRT=.text + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHookVC17.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHookVC17.sln new file mode 100644 index 0000000..3c40ce3 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/MinHookVC17.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcxproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj new file mode 100644 index 0000000..21197df --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj @@ -0,0 +1,174 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + libMinHook + Win32Proj + 10.0 + + + + StaticLibrary + Unicode + true + v143 + + + StaticLibrary + Unicode + v143 + + + StaticLibrary + Unicode + true + v143 + + + StaticLibrary + Unicode + v143 + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)lib\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(ProjectName).x86 + $(ProjectName).x86 + $(ProjectName).x64 + $(ProjectName).x64 + + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NoExtensions + + + + + + X64 + + + Disabled + %(AdditionalIncludeDirectories) + WIN32;_DEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebug + Level3 + None + NotSet + + + + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + AnySuitable + true + NoExtensions + + + + + + X64 + + + MinSpace + true + %(AdditionalIncludeDirectories) + WIN32;NDEBUG;_LIB;STRICT;%(PreprocessorDefinitions) + false + MultiThreaded + true + Level3 + None + true + AnySuitable + + + + + + + true + true + + + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj.filters b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj.filters new file mode 100644 index 0000000..f2d1d97 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC17/libMinHook.vcxproj.filters @@ -0,0 +1,55 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + HDE + + + HDE + + + + + Header Files + + + Header Files + + + + HDE + + + HDE + + + HDE + + + HDE + + + HDE + + + + + {9d24b740-be2e-4cfd-b9a4-340a50946ee9} + + + {76381bc7-2863-4cc5-aede-926ec2c506e4} + + + {56ddb326-6179-430d-ae19-e13bfd767bfa} + + + \ No newline at end of file diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHook.vcproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHook.vcproj new file mode 100644 index 0000000..4bad257 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHook.vcproj @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHookVC9.sln b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHookVC9.sln new file mode 100644 index 0000000..869f5b6 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/MinHookVC9.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "libMinHook.vcproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MinHook", "MinHook.vcproj", "{027FAC75-3FDB-4044-8DD0-BC297BD4C461}" + ProjectSection(ProjectDependencies) = postProject + {F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.ActiveCfg = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|Win32.Build.0 = Debug|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.ActiveCfg = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|Win32.Build.0 = Release|Win32 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64 + {F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.ActiveCfg = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|Win32.Build.0 = Debug|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.ActiveCfg = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Debug|x64.Build.0 = Debug|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.ActiveCfg = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|Win32.Build.0 = Release|Win32 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.ActiveCfg = Release|x64 + {027FAC75-3FDB-4044-8DD0-BC297BD4C461}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/libMinHook.vcproj b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/libMinHook.vcproj new file mode 100644 index 0000000..68b0c05 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/build/VC9/libMinHook.vcproj @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/cmake/minhook-config.cmake.in b/src/MultiSeat.SteamIsolation/third_party/minhook/cmake/minhook-config.cmake.in new file mode 100644 index 0000000..14e6463 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/cmake/minhook-config.cmake.in @@ -0,0 +1,39 @@ +# MinHook - The Minimalistic API Hooking Library for x64/x86 +# Copyright (C) 2009-2017 Tsuda Kageyu. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER +# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(MINHOOK_MAJOR_VERSION "@MINHOOK_MAJOR_VERSION@") +set(MINHOOK_MINOR_VERSION "@MINHOOK_MINOR_VERSION@") +set(MINHOOK_PATCH_VERSION "@MINHOOK_PATCH_VERSION@") +set(MINHOOK_VERSION "@MINHOOK_VERSION@") + +@PACKAGE_INIT@ + +set(MINHOOK_FOUND ON) + +set_and_check(MINHOOK_INCLUDE_DIRS "${PACKAGE_PREFIX_DIR}/include/") +set_and_check(MINHOOK_LIBRARY_DIRS "${PACKAGE_PREFIX_DIR}/lib") + +include("${PACKAGE_PREFIX_DIR}/lib/minhook/minhook-targets.cmake") diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.def b/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.def new file mode 100644 index 0000000..c6af698 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.def @@ -0,0 +1,14 @@ +EXPORTS + MH_Initialize + MH_Uninitialize + + MH_CreateHook + MH_CreateHookApi + MH_CreateHookApiEx + MH_RemoveHook + MH_EnableHook + MH_DisableHook + MH_QueueEnableHook + MH_QueueDisableHook + MH_ApplyQueued + MH_StatusToString diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.rc b/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.rc new file mode 100644 index 0000000..d59db98 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/dll_resources/MinHook.rc @@ -0,0 +1,32 @@ +1 VERSIONINFO + FILEVERSION 1,3,4,0 + PRODUCTVERSION 1,3,4,0 + FILEFLAGSMASK 0x17L +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Tsuda Kageyu" + VALUE "FileDescription", "MinHook - The Minimalistic API Hook Library for x64/x86" + VALUE "FileVersion", "1.3.4.0" + VALUE "InternalName", "MinHookD" + VALUE "LegalCopyright", "Copyright (C) 2009-2017 Tsuda Kageyu. All rights reserved." + VALUE "LegalTrademarks", "Tsuda Kageyu" + VALUE "ProductName", "MinHook DLL" + VALUE "ProductVersion", "1.3.4.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/include/MinHook.h b/src/MultiSeat.SteamIsolation/third_party/minhook/include/MinHook.h new file mode 100644 index 0000000..492d83f --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/include/MinHook.h @@ -0,0 +1,185 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__) + #error MinHook supports only x86 and x64 systems. +#endif + +#include + +// MinHook Error Codes. +typedef enum MH_STATUS +{ + // Unknown error. Should not be returned. + MH_UNKNOWN = -1, + + // Successful. + MH_OK = 0, + + // MinHook is already initialized. + MH_ERROR_ALREADY_INITIALIZED, + + // MinHook is not initialized yet, or already uninitialized. + MH_ERROR_NOT_INITIALIZED, + + // The hook for the specified target function is already created. + MH_ERROR_ALREADY_CREATED, + + // The hook for the specified target function is not created yet. + MH_ERROR_NOT_CREATED, + + // The hook for the specified target function is already enabled. + MH_ERROR_ENABLED, + + // The hook for the specified target function is not enabled yet, or already + // disabled. + MH_ERROR_DISABLED, + + // The specified pointer is invalid. It points the address of non-allocated + // and/or non-executable region. + MH_ERROR_NOT_EXECUTABLE, + + // The specified target function cannot be hooked. + MH_ERROR_UNSUPPORTED_FUNCTION, + + // Failed to allocate memory. + MH_ERROR_MEMORY_ALLOC, + + // Failed to change the memory protection. + MH_ERROR_MEMORY_PROTECT, + + // The specified module is not loaded. + MH_ERROR_MODULE_NOT_FOUND, + + // The specified function is not found. + MH_ERROR_FUNCTION_NOT_FOUND +} +MH_STATUS; + +// Can be passed as a parameter to MH_EnableHook, MH_DisableHook, +// MH_QueueEnableHook or MH_QueueDisableHook. +#define MH_ALL_HOOKS NULL + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize the MinHook library. You must call this function EXACTLY ONCE + // at the beginning of your program. + MH_STATUS WINAPI MH_Initialize(VOID); + + // Uninitialize the MinHook library. You must call this function EXACTLY + // ONCE at the end of your program. + MH_STATUS WINAPI MH_Uninitialize(VOID); + + // Creates a hook for the specified target function, in disabled state. + // Parameters: + // pTarget [in] A pointer to the target function, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal); + + // Creates a hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszProcName [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + // ppTarget [out] A pointer to the target function, which will be used + // with other functions. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal, LPVOID *ppTarget); + + // Removes an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget); + + // Enables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // enabled in one go. + MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); + + // Disables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // disabled in one go. + MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget); + + // Queues to enable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be enabled. + MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget); + + // Queues to disable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be disabled. + MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget); + + // Applies all queued changes in one go. + MH_STATUS WINAPI MH_ApplyQueued(VOID); + + // Translates the MH_STATUS to its name as a string. + const char * WINAPI MH_StatusToString(MH_STATUS status); + +#ifdef __cplusplus +} +#endif diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.c b/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.c new file mode 100644 index 0000000..55412b0 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.c @@ -0,0 +1,312 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include "buffer.h" + +// Size of each memory block. (= page size of VirtualAlloc) +#define MEMORY_BLOCK_SIZE 0x1000 + +// Max range for seeking a memory block. (= 1024MB) +#define MAX_MEMORY_RANGE 0x40000000 + +// Memory protection flags to check the executable address. +#define PAGE_EXECUTE_FLAGS \ + (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY) + +// Memory slot. +typedef struct _MEMORY_SLOT +{ + union + { + struct _MEMORY_SLOT *pNext; + UINT8 buffer[MEMORY_SLOT_SIZE]; + }; +} MEMORY_SLOT, *PMEMORY_SLOT; + +// Memory block info. Placed at the head of each block. +typedef struct _MEMORY_BLOCK +{ + struct _MEMORY_BLOCK *pNext; + PMEMORY_SLOT pFree; // First element of the free slot list. + UINT usedCount; +} MEMORY_BLOCK, *PMEMORY_BLOCK; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// First element of the memory block list. +PMEMORY_BLOCK g_pMemoryBlocks; + +//------------------------------------------------------------------------- +VOID InitializeBuffer(VOID) +{ + // Nothing to do for now. +} + +//------------------------------------------------------------------------- +VOID UninitializeBuffer(VOID) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + g_pMemoryBlocks = NULL; + + while (pBlock) + { + PMEMORY_BLOCK pNext = pBlock->pNext; + VirtualFree(pBlock, 0, MEM_RELEASE); + pBlock = pNext; + } +} + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindPrevFreeRegion(LPVOID pAddress, LPVOID pMinAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the previous allocation granularity multiply. + tryAddr -= dwAllocationGranularity; + + while (tryAddr >= (ULONG_PTR)pMinAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + if ((ULONG_PTR)mbi.AllocationBase < dwAllocationGranularity) + break; + + tryAddr = (ULONG_PTR)mbi.AllocationBase - dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +#if defined(_M_X64) || defined(__x86_64__) +static LPVOID FindNextFreeRegion(LPVOID pAddress, LPVOID pMaxAddr, DWORD dwAllocationGranularity) +{ + ULONG_PTR tryAddr = (ULONG_PTR)pAddress; + + // Round down to the allocation granularity. + tryAddr -= tryAddr % dwAllocationGranularity; + + // Start from the next allocation granularity multiply. + tryAddr += dwAllocationGranularity; + + while (tryAddr <= (ULONG_PTR)pMaxAddr) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((LPVOID)tryAddr, &mbi, sizeof(mbi)) == 0) + break; + + if (mbi.State == MEM_FREE) + return (LPVOID)tryAddr; + + tryAddr = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize; + + // Round up to the next allocation granularity. + tryAddr += dwAllocationGranularity - 1; + tryAddr -= tryAddr % dwAllocationGranularity; + } + + return NULL; +} +#endif + +//------------------------------------------------------------------------- +static PMEMORY_BLOCK GetMemoryBlock(LPVOID pOrigin) +{ + PMEMORY_BLOCK pBlock; +#if defined(_M_X64) || defined(__x86_64__) + ULONG_PTR minAddr; + ULONG_PTR maxAddr; + + SYSTEM_INFO si; + GetSystemInfo(&si); + minAddr = (ULONG_PTR)si.lpMinimumApplicationAddress; + maxAddr = (ULONG_PTR)si.lpMaximumApplicationAddress; + + // pOrigin ± 512MB + if ((ULONG_PTR)pOrigin > MAX_MEMORY_RANGE && minAddr < (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE) + minAddr = (ULONG_PTR)pOrigin - MAX_MEMORY_RANGE; + + if (maxAddr > (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE) + maxAddr = (ULONG_PTR)pOrigin + MAX_MEMORY_RANGE; + + // Make room for MEMORY_BLOCK_SIZE bytes. + maxAddr -= MEMORY_BLOCK_SIZE - 1; +#endif + + // Look the registered blocks for a reachable one. + for (pBlock = g_pMemoryBlocks; pBlock != NULL; pBlock = pBlock->pNext) + { +#if defined(_M_X64) || defined(__x86_64__) + // Ignore the blocks too far. + if ((ULONG_PTR)pBlock < minAddr || (ULONG_PTR)pBlock >= maxAddr) + continue; +#endif + // The block has at least one unused slot. + if (pBlock->pFree != NULL) + return pBlock; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Alloc a new block above if not found. + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc >= minAddr) + { + pAlloc = FindPrevFreeRegion(pAlloc, (LPVOID)minAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } + + // Alloc a new block below if not found. + if (pBlock == NULL) + { + LPVOID pAlloc = pOrigin; + while ((ULONG_PTR)pAlloc <= maxAddr) + { + pAlloc = FindNextFreeRegion(pAlloc, (LPVOID)maxAddr, si.dwAllocationGranularity); + if (pAlloc == NULL) + break; + + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + pAlloc, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (pBlock != NULL) + break; + } + } +#else + // In x86 mode, a memory block can be placed anywhere. + pBlock = (PMEMORY_BLOCK)VirtualAlloc( + NULL, MEMORY_BLOCK_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); +#endif + + if (pBlock != NULL) + { + // Build a linked list of all the slots. + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBlock + 1; + pBlock->pFree = NULL; + pBlock->usedCount = 0; + do + { + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pSlot++; + } while ((ULONG_PTR)pSlot - (ULONG_PTR)pBlock <= MEMORY_BLOCK_SIZE - MEMORY_SLOT_SIZE); + + pBlock->pNext = g_pMemoryBlocks; + g_pMemoryBlocks = pBlock; + } + + return pBlock; +} + +//------------------------------------------------------------------------- +LPVOID AllocateBuffer(LPVOID pOrigin) +{ + PMEMORY_SLOT pSlot; + PMEMORY_BLOCK pBlock = GetMemoryBlock(pOrigin); + if (pBlock == NULL) + return NULL; + + // Remove an unused slot from the list. + pSlot = pBlock->pFree; + pBlock->pFree = pSlot->pNext; + pBlock->usedCount++; +#ifdef _DEBUG + // Fill the slot with INT3 for debugging. + memset(pSlot, 0xCC, sizeof(MEMORY_SLOT)); +#endif + return pSlot; +} + +//------------------------------------------------------------------------- +VOID FreeBuffer(LPVOID pBuffer) +{ + PMEMORY_BLOCK pBlock = g_pMemoryBlocks; + PMEMORY_BLOCK pPrev = NULL; + ULONG_PTR pTargetBlock = ((ULONG_PTR)pBuffer / MEMORY_BLOCK_SIZE) * MEMORY_BLOCK_SIZE; + + while (pBlock != NULL) + { + if ((ULONG_PTR)pBlock == pTargetBlock) + { + PMEMORY_SLOT pSlot = (PMEMORY_SLOT)pBuffer; +#ifdef _DEBUG + // Clear the released slot for debugging. + memset(pSlot, 0x00, sizeof(MEMORY_SLOT)); +#endif + // Restore the released slot to the list. + pSlot->pNext = pBlock->pFree; + pBlock->pFree = pSlot; + pBlock->usedCount--; + + // Free if unused. + if (pBlock->usedCount == 0) + { + if (pPrev) + pPrev->pNext = pBlock->pNext; + else + g_pMemoryBlocks = pBlock->pNext; + + VirtualFree(pBlock, 0, MEM_RELEASE); + } + + break; + } + + pPrev = pBlock; + pBlock = pBlock->pNext; + } +} + +//------------------------------------------------------------------------- +BOOL IsExecutableAddress(LPVOID pAddress) +{ + MEMORY_BASIC_INFORMATION mi; + VirtualQuery(pAddress, &mi, sizeof(mi)); + + return (mi.State == MEM_COMMIT && (mi.Protect & PAGE_EXECUTE_FLAGS)); +} diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.h new file mode 100644 index 0000000..204d551 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/buffer.h @@ -0,0 +1,42 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +// Size of each memory slot. +#if defined(_M_X64) || defined(__x86_64__) + #define MEMORY_SLOT_SIZE 64 +#else + #define MEMORY_SLOT_SIZE 32 +#endif + +VOID InitializeBuffer(VOID); +VOID UninitializeBuffer(VOID); +LPVOID AllocateBuffer(LPVOID pOrigin); +VOID FreeBuffer(LPVOID pBuffer); +BOOL IsExecutableAddress(LPVOID pAddress); diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.c b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.c new file mode 100644 index 0000000..eb6af9b --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.c @@ -0,0 +1,324 @@ +/* + * Hacker Disassembler Engine 32 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#if defined(_M_IX86) || defined(__i386__) + +#include +#include "hde32.h" +#include "table32.h" + +unsigned int hde32_disasm(const void *code, hde32s *hs) +{ + uint8_t x, c, *p = (uint8_t *)code, cflags, opcode, pref = 0; + uint8_t *ht = hde32_table, m_mod, m_reg, m_rm, disp_size = 0; + + memset(hs, 0, sizeof(hde32s)); + + for (x = 16; x; x--) + switch (c = *p++) { + case 0xf3: + hs->p_rep = c; + pref |= PRE_F3; + break; + case 0xf2: + hs->p_rep = c; + pref |= PRE_F2; + break; + case 0xf0: + hs->p_lock = c; + pref |= PRE_LOCK; + break; + case 0x26: case 0x2e: case 0x36: + case 0x3e: case 0x64: case 0x65: + hs->p_seg = c; + pref |= PRE_SEG; + break; + case 0x66: + hs->p_66 = c; + pref |= PRE_66; + break; + case 0x67: + hs->p_67 = c; + pref |= PRE_67; + break; + default: + goto pref_done; + } + pref_done: + + hs->flags = (uint32_t)pref << 23; + + if (!pref) + pref |= PRE_NONE; + + if ((hs->opcode = c) == 0x0f) { + hs->opcode2 = c = *p++; + ht += DELTA_OPCODES; + } else if (c >= 0xa0 && c <= 0xa3) { + if (pref & PRE_67) + pref |= PRE_66; + else + pref &= ~PRE_66; + } + + opcode = c; + cflags = ht[ht[opcode / 4] + (opcode % 4)]; + + if (cflags == C_ERROR) { + hs->flags |= F_ERROR | F_ERROR_OPCODE; + cflags = 0; + if ((opcode & -3) == 0x24) + cflags++; + } + + x = 0; + if (cflags & C_GROUP) { + uint16_t t; + t = *(uint16_t *)(ht + (cflags & 0x7f)); + cflags = (uint8_t)t; + x = (uint8_t)(t >> 8); + } + + if (hs->opcode2) { + ht = hde32_table + DELTA_PREFIXES; + if (ht[ht[opcode / 4] + (opcode % 4)] & pref) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (cflags & C_MODRM) { + hs->flags |= F_MODRM; + hs->modrm = c = *p++; + hs->modrm_mod = m_mod = c >> 6; + hs->modrm_rm = m_rm = c & 7; + hs->modrm_reg = m_reg = (c & 0x3f) >> 3; + + if (x && ((x << m_reg) & 0x80)) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + + if (!hs->opcode2 && opcode >= 0xd9 && opcode <= 0xdf) { + uint8_t t = opcode - 0xd9; + if (m_mod == 3) { + ht = hde32_table + DELTA_FPU_MODRM + t*8; + t = ht[m_reg] << m_rm; + } else { + ht = hde32_table + DELTA_FPU_REG; + t = ht[t] << m_reg; + } + if (t & 0x80) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (pref & PRE_LOCK) { + if (m_mod == 3) { + hs->flags |= F_ERROR | F_ERROR_LOCK; + } else { + uint8_t *table_end, op = opcode; + if (hs->opcode2) { + ht = hde32_table + DELTA_OP2_LOCK_OK; + table_end = ht + DELTA_OP_ONLY_MEM - DELTA_OP2_LOCK_OK; + } else { + ht = hde32_table + DELTA_OP_LOCK_OK; + table_end = ht + DELTA_OP2_LOCK_OK - DELTA_OP_LOCK_OK; + op &= -2; + } + for (; ht != table_end; ht++) + if (*ht++ == op) { + if (!((*ht << m_reg) & 0x80)) + goto no_lock_error; + else + break; + } + hs->flags |= F_ERROR | F_ERROR_LOCK; + no_lock_error: + ; + } + } + + if (hs->opcode2) { + switch (opcode) { + case 0x20: case 0x22: + m_mod = 3; + if (m_reg > 4 || m_reg == 1) + goto error_operand; + else + goto no_error_operand; + case 0x21: case 0x23: + m_mod = 3; + if (m_reg == 4 || m_reg == 5) + goto error_operand; + else + goto no_error_operand; + } + } else { + switch (opcode) { + case 0x8c: + if (m_reg > 5) + goto error_operand; + else + goto no_error_operand; + case 0x8e: + if (m_reg == 1 || m_reg > 5) + goto error_operand; + else + goto no_error_operand; + } + } + + if (m_mod == 3) { + uint8_t *table_end; + if (hs->opcode2) { + ht = hde32_table + DELTA_OP2_ONLY_MEM; + table_end = ht + sizeof(hde32_table) - DELTA_OP2_ONLY_MEM; + } else { + ht = hde32_table + DELTA_OP_ONLY_MEM; + table_end = ht + DELTA_OP2_ONLY_MEM - DELTA_OP_ONLY_MEM; + } + for (; ht != table_end; ht += 2) + if (*ht++ == opcode) { + if ((*ht++ & pref) && !((*ht << m_reg) & 0x80)) + goto error_operand; + else + break; + } + goto no_error_operand; + } else if (hs->opcode2) { + switch (opcode) { + case 0x50: case 0xd7: case 0xf7: + if (pref & (PRE_NONE | PRE_66)) + goto error_operand; + break; + case 0xd6: + if (pref & (PRE_F2 | PRE_F3)) + goto error_operand; + break; + case 0xc5: + goto error_operand; + } + goto no_error_operand; + } else + goto no_error_operand; + + error_operand: + hs->flags |= F_ERROR | F_ERROR_OPERAND; + no_error_operand: + + c = *p++; + if (m_reg <= 1) { + if (opcode == 0xf6) + cflags |= C_IMM8; + else if (opcode == 0xf7) + cflags |= C_IMM_P66; + } + + switch (m_mod) { + case 0: + if (pref & PRE_67) { + if (m_rm == 6) + disp_size = 2; + } else + if (m_rm == 5) + disp_size = 4; + break; + case 1: + disp_size = 1; + break; + case 2: + disp_size = 2; + if (!(pref & PRE_67)) + disp_size <<= 1; + break; + } + + if (m_mod != 3 && m_rm == 4 && !(pref & PRE_67)) { + hs->flags |= F_SIB; + p++; + hs->sib = c; + hs->sib_scale = c >> 6; + hs->sib_index = (c & 0x3f) >> 3; + if ((hs->sib_base = c & 7) == 5 && !(m_mod & 1)) + disp_size = 4; + } + + p--; + switch (disp_size) { + case 1: + hs->flags |= F_DISP8; + hs->disp.disp8 = *p; + break; + case 2: + hs->flags |= F_DISP16; + hs->disp.disp16 = *(uint16_t *)p; + break; + case 4: + hs->flags |= F_DISP32; + hs->disp.disp32 = *(uint32_t *)p; + break; + } + p += disp_size; + } else if (pref & PRE_LOCK) + hs->flags |= F_ERROR | F_ERROR_LOCK; + + if (cflags & C_IMM_P66) { + if (cflags & C_REL32) { + if (pref & PRE_66) { + hs->flags |= F_IMM16 | F_RELATIVE; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + goto disasm_done; + } + goto rel32_ok; + } + if (pref & PRE_66) { + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + } else { + hs->flags |= F_IMM32; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } + } + + if (cflags & C_IMM16) { + if (hs->flags & F_IMM32) { + hs->flags |= F_IMM16; + hs->disp.disp16 = *(uint16_t *)p; + } else if (hs->flags & F_IMM16) { + hs->flags |= F_2IMM16; + hs->disp.disp16 = *(uint16_t *)p; + } else { + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + } + p += 2; + } + if (cflags & C_IMM8) { + hs->flags |= F_IMM8; + hs->imm.imm8 = *p++; + } + + if (cflags & C_REL32) { + rel32_ok: + hs->flags |= F_IMM32 | F_RELATIVE; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else if (cflags & C_REL8) { + hs->flags |= F_IMM8 | F_RELATIVE; + hs->imm.imm8 = *p++; + } + + disasm_done: + + if ((hs->len = (uint8_t)(p-(uint8_t *)code)) > 15) { + hs->flags |= F_ERROR | F_ERROR_LENGTH; + hs->len = 15; + } + + return (unsigned int)hs->len; +} + +#endif // defined(_M_IX86) || defined(__i386__) diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.h new file mode 100644 index 0000000..1112450 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde32.h @@ -0,0 +1,105 @@ +/* + * Hacker Disassembler Engine 32 + * Copyright (c) 2006-2009, Vyacheslav Patkov. + * All rights reserved. + * + * hde32.h: C/C++ header file + * + */ + +#ifndef _HDE32_H_ +#define _HDE32_H_ + +/* stdint.h - C99 standard header + * http://en.wikipedia.org/wiki/stdint.h + * + * if your compiler doesn't contain "stdint.h" header (for + * example, Microsoft Visual C++), you can download file: + * http://www.azillionmonkeys.com/qed/pstdint.h + * and change next line to: + * #include "pstdint.h" + */ +#include "pstdint.h" + +#define F_MODRM 0x00000001 +#define F_SIB 0x00000002 +#define F_IMM8 0x00000004 +#define F_IMM16 0x00000008 +#define F_IMM32 0x00000010 +#define F_DISP8 0x00000020 +#define F_DISP16 0x00000040 +#define F_DISP32 0x00000080 +#define F_RELATIVE 0x00000100 +#define F_2IMM16 0x00000800 +#define F_ERROR 0x00001000 +#define F_ERROR_OPCODE 0x00002000 +#define F_ERROR_LENGTH 0x00004000 +#define F_ERROR_LOCK 0x00008000 +#define F_ERROR_OPERAND 0x00010000 +#define F_PREFIX_REPNZ 0x01000000 +#define F_PREFIX_REPX 0x02000000 +#define F_PREFIX_REP 0x03000000 +#define F_PREFIX_66 0x04000000 +#define F_PREFIX_67 0x08000000 +#define F_PREFIX_LOCK 0x10000000 +#define F_PREFIX_SEG 0x20000000 +#define F_PREFIX_ANY 0x3f000000 + +#define PREFIX_SEGMENT_CS 0x2e +#define PREFIX_SEGMENT_SS 0x36 +#define PREFIX_SEGMENT_DS 0x3e +#define PREFIX_SEGMENT_ES 0x26 +#define PREFIX_SEGMENT_FS 0x64 +#define PREFIX_SEGMENT_GS 0x65 +#define PREFIX_LOCK 0xf0 +#define PREFIX_REPNZ 0xf2 +#define PREFIX_REPX 0xf3 +#define PREFIX_OPERAND_SIZE 0x66 +#define PREFIX_ADDRESS_SIZE 0x67 + +#pragma pack(push,1) + +typedef struct { + uint8_t len; + uint8_t p_rep; + uint8_t p_lock; + uint8_t p_seg; + uint8_t p_66; + uint8_t p_67; + uint8_t opcode; + uint8_t opcode2; + uint8_t modrm; + uint8_t modrm_mod; + uint8_t modrm_reg; + uint8_t modrm_rm; + uint8_t sib; + uint8_t sib_scale; + uint8_t sib_index; + uint8_t sib_base; + union { + uint8_t imm8; + uint16_t imm16; + uint32_t imm32; + } imm; + union { + uint8_t disp8; + uint16_t disp16; + uint32_t disp32; + } disp; + uint32_t flags; +} hde32s; + +#pragma pack(pop) + +#ifdef __cplusplus +extern "C" { +#endif + +/* __cdecl */ +unsigned int hde32_disasm(const void *code, hde32s *hs); + +#ifdef __cplusplus +} +#endif + +#endif /* _HDE32_H_ */ diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.c b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.c new file mode 100644 index 0000000..ce773b1 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.c @@ -0,0 +1,335 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#if defined(_M_X64) || defined(__x86_64__) + +#include +#include "hde64.h" +#include "table64.h" + +unsigned int hde64_disasm(const void *code, hde64s *hs) +{ + uint8_t x, c, *p = (uint8_t *)code, cflags, opcode, pref = 0; + uint8_t *ht = hde64_table, m_mod, m_reg, m_rm, disp_size = 0; + uint8_t op64 = 0; + + memset(hs, 0, sizeof(hde64s)); + + for (x = 16; x; x--) + switch (c = *p++) { + case 0xf3: + hs->p_rep = c; + pref |= PRE_F3; + break; + case 0xf2: + hs->p_rep = c; + pref |= PRE_F2; + break; + case 0xf0: + hs->p_lock = c; + pref |= PRE_LOCK; + break; + case 0x26: case 0x2e: case 0x36: + case 0x3e: case 0x64: case 0x65: + hs->p_seg = c; + pref |= PRE_SEG; + break; + case 0x66: + hs->p_66 = c; + pref |= PRE_66; + break; + case 0x67: + hs->p_67 = c; + pref |= PRE_67; + break; + default: + goto pref_done; + } + pref_done: + + hs->flags = (uint32_t)pref << 23; + + if (!pref) + pref |= PRE_NONE; + + if ((c & 0xf0) == 0x40) { + hs->flags |= F_PREFIX_REX; + if ((hs->rex_w = (c & 0xf) >> 3) && (*p & 0xf8) == 0xb8) + op64++; + hs->rex_r = (c & 7) >> 2; + hs->rex_x = (c & 3) >> 1; + hs->rex_b = c & 1; + if (((c = *p++) & 0xf0) == 0x40) { + opcode = c; + goto error_opcode; + } + } + + if ((hs->opcode = c) == 0x0f) { + hs->opcode2 = c = *p++; + ht += DELTA_OPCODES; + } else if (c >= 0xa0 && c <= 0xa3) { + op64++; + if (pref & PRE_67) + pref |= PRE_66; + else + pref &= ~PRE_66; + } + + opcode = c; + cflags = ht[ht[opcode / 4] + (opcode % 4)]; + + if (cflags == C_ERROR) { + error_opcode: + hs->flags |= F_ERROR | F_ERROR_OPCODE; + cflags = 0; + if ((opcode & -3) == 0x24) + cflags++; + } + + x = 0; + if (cflags & C_GROUP) { + uint16_t t; + t = *(uint16_t *)(ht + (cflags & 0x7f)); + cflags = (uint8_t)t; + x = (uint8_t)(t >> 8); + } + + if (hs->opcode2) { + ht = hde64_table + DELTA_PREFIXES; + if (ht[ht[opcode / 4] + (opcode % 4)] & pref) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (cflags & C_MODRM) { + hs->flags |= F_MODRM; + hs->modrm = c = *p++; + hs->modrm_mod = m_mod = c >> 6; + hs->modrm_rm = m_rm = c & 7; + hs->modrm_reg = m_reg = (c & 0x3f) >> 3; + + if (x && ((x << m_reg) & 0x80)) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + + if (!hs->opcode2 && opcode >= 0xd9 && opcode <= 0xdf) { + uint8_t t = opcode - 0xd9; + if (m_mod == 3) { + ht = hde64_table + DELTA_FPU_MODRM + t*8; + t = ht[m_reg] << m_rm; + } else { + ht = hde64_table + DELTA_FPU_REG; + t = ht[t] << m_reg; + } + if (t & 0x80) + hs->flags |= F_ERROR | F_ERROR_OPCODE; + } + + if (pref & PRE_LOCK) { + if (m_mod == 3) { + hs->flags |= F_ERROR | F_ERROR_LOCK; + } else { + uint8_t *table_end, op = opcode; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_LOCK_OK; + table_end = ht + DELTA_OP_ONLY_MEM - DELTA_OP2_LOCK_OK; + } else { + ht = hde64_table + DELTA_OP_LOCK_OK; + table_end = ht + DELTA_OP2_LOCK_OK - DELTA_OP_LOCK_OK; + op &= -2; + } + for (; ht != table_end; ht++) + if (*ht++ == op) { + if (!((*ht << m_reg) & 0x80)) + goto no_lock_error; + else + break; + } + hs->flags |= F_ERROR | F_ERROR_LOCK; + no_lock_error: + ; + } + } + + if (hs->opcode2) { + switch (opcode) { + case 0x20: case 0x22: + m_mod = 3; + if (m_reg > 4 || m_reg == 1) + goto error_operand; + else + goto no_error_operand; + case 0x21: case 0x23: + m_mod = 3; + if (m_reg == 4 || m_reg == 5) + goto error_operand; + else + goto no_error_operand; + } + } else { + switch (opcode) { + case 0x8c: + if (m_reg > 5) + goto error_operand; + else + goto no_error_operand; + case 0x8e: + if (m_reg == 1 || m_reg > 5) + goto error_operand; + else + goto no_error_operand; + } + } + + if (m_mod == 3) { + uint8_t *table_end; + if (hs->opcode2) { + ht = hde64_table + DELTA_OP2_ONLY_MEM; + table_end = ht + sizeof(hde64_table) - DELTA_OP2_ONLY_MEM; + } else { + ht = hde64_table + DELTA_OP_ONLY_MEM; + table_end = ht + DELTA_OP2_ONLY_MEM - DELTA_OP_ONLY_MEM; + } + for (; ht != table_end; ht += 2) + if (*ht++ == opcode) { + if ((*ht++ & pref) && !((*ht << m_reg) & 0x80)) + goto error_operand; + else + break; + } + goto no_error_operand; + } else if (hs->opcode2) { + switch (opcode) { + case 0x50: case 0xd7: case 0xf7: + if (pref & (PRE_NONE | PRE_66)) + goto error_operand; + break; + case 0xd6: + if (pref & (PRE_F2 | PRE_F3)) + goto error_operand; + break; + case 0xc5: + goto error_operand; + } + goto no_error_operand; + } else + goto no_error_operand; + + error_operand: + hs->flags |= F_ERROR | F_ERROR_OPERAND; + no_error_operand: + + c = *p++; + if (m_reg <= 1) { + if (opcode == 0xf6) + cflags |= C_IMM8; + else if (opcode == 0xf7) + cflags |= C_IMM_P66; + } + + switch (m_mod) { + case 0: + if (pref & PRE_67) { + if (m_rm == 6) + disp_size = 2; + } else + if (m_rm == 5) + disp_size = 4; + break; + case 1: + disp_size = 1; + break; + case 2: + disp_size = 2; + if (!(pref & PRE_67)) + disp_size <<= 1; + break; + } + + if (m_mod != 3 && m_rm == 4) { + hs->flags |= F_SIB; + p++; + hs->sib = c; + hs->sib_scale = c >> 6; + hs->sib_index = (c & 0x3f) >> 3; + if ((hs->sib_base = c & 7) == 5 && !(m_mod & 1)) + disp_size = 4; + } + + p--; + switch (disp_size) { + case 1: + hs->flags |= F_DISP8; + hs->disp.disp8 = *p; + break; + case 2: + hs->flags |= F_DISP16; + hs->disp.disp16 = *(uint16_t *)p; + break; + case 4: + hs->flags |= F_DISP32; + hs->disp.disp32 = *(uint32_t *)p; + break; + } + p += disp_size; + } else if (pref & PRE_LOCK) + hs->flags |= F_ERROR | F_ERROR_LOCK; + + if (cflags & C_IMM_P66) { + if (cflags & C_REL32) { + if (pref & PRE_66) { + hs->flags |= F_IMM16 | F_RELATIVE; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + goto disasm_done; + } + goto rel32_ok; + } + if (op64) { + hs->flags |= F_IMM64; + hs->imm.imm64 = *(uint64_t *)p; + p += 8; + } else if (!(pref & PRE_66)) { + hs->flags |= F_IMM32; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else + goto imm16_ok; + } + + + if (cflags & C_IMM16) { + imm16_ok: + hs->flags |= F_IMM16; + hs->imm.imm16 = *(uint16_t *)p; + p += 2; + } + if (cflags & C_IMM8) { + hs->flags |= F_IMM8; + hs->imm.imm8 = *p++; + } + + if (cflags & C_REL32) { + rel32_ok: + hs->flags |= F_IMM32 | F_RELATIVE; + hs->imm.imm32 = *(uint32_t *)p; + p += 4; + } else if (cflags & C_REL8) { + hs->flags |= F_IMM8 | F_RELATIVE; + hs->imm.imm8 = *p++; + } + + disasm_done: + + if ((hs->len = (uint8_t)(p-(uint8_t *)code)) > 15) { + hs->flags |= F_ERROR | F_ERROR_LENGTH; + hs->len = 15; + } + + return (unsigned int)hs->len; +} + +#endif // defined(_M_X64) || defined(__x86_64__) diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.h new file mode 100644 index 0000000..ecbf4df --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/hde64.h @@ -0,0 +1,112 @@ +/* + * Hacker Disassembler Engine 64 + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + * hde64.h: C/C++ header file + * + */ + +#ifndef _HDE64_H_ +#define _HDE64_H_ + +/* stdint.h - C99 standard header + * http://en.wikipedia.org/wiki/stdint.h + * + * if your compiler doesn't contain "stdint.h" header (for + * example, Microsoft Visual C++), you can download file: + * http://www.azillionmonkeys.com/qed/pstdint.h + * and change next line to: + * #include "pstdint.h" + */ +#include "pstdint.h" + +#define F_MODRM 0x00000001 +#define F_SIB 0x00000002 +#define F_IMM8 0x00000004 +#define F_IMM16 0x00000008 +#define F_IMM32 0x00000010 +#define F_IMM64 0x00000020 +#define F_DISP8 0x00000040 +#define F_DISP16 0x00000080 +#define F_DISP32 0x00000100 +#define F_RELATIVE 0x00000200 +#define F_ERROR 0x00001000 +#define F_ERROR_OPCODE 0x00002000 +#define F_ERROR_LENGTH 0x00004000 +#define F_ERROR_LOCK 0x00008000 +#define F_ERROR_OPERAND 0x00010000 +#define F_PREFIX_REPNZ 0x01000000 +#define F_PREFIX_REPX 0x02000000 +#define F_PREFIX_REP 0x03000000 +#define F_PREFIX_66 0x04000000 +#define F_PREFIX_67 0x08000000 +#define F_PREFIX_LOCK 0x10000000 +#define F_PREFIX_SEG 0x20000000 +#define F_PREFIX_REX 0x40000000 +#define F_PREFIX_ANY 0x7f000000 + +#define PREFIX_SEGMENT_CS 0x2e +#define PREFIX_SEGMENT_SS 0x36 +#define PREFIX_SEGMENT_DS 0x3e +#define PREFIX_SEGMENT_ES 0x26 +#define PREFIX_SEGMENT_FS 0x64 +#define PREFIX_SEGMENT_GS 0x65 +#define PREFIX_LOCK 0xf0 +#define PREFIX_REPNZ 0xf2 +#define PREFIX_REPX 0xf3 +#define PREFIX_OPERAND_SIZE 0x66 +#define PREFIX_ADDRESS_SIZE 0x67 + +#pragma pack(push,1) + +typedef struct { + uint8_t len; + uint8_t p_rep; + uint8_t p_lock; + uint8_t p_seg; + uint8_t p_66; + uint8_t p_67; + uint8_t rex; + uint8_t rex_w; + uint8_t rex_r; + uint8_t rex_x; + uint8_t rex_b; + uint8_t opcode; + uint8_t opcode2; + uint8_t modrm; + uint8_t modrm_mod; + uint8_t modrm_reg; + uint8_t modrm_rm; + uint8_t sib; + uint8_t sib_scale; + uint8_t sib_index; + uint8_t sib_base; + union { + uint8_t imm8; + uint16_t imm16; + uint32_t imm32; + uint64_t imm64; + } imm; + union { + uint8_t disp8; + uint16_t disp16; + uint32_t disp32; + } disp; + uint32_t flags; +} hde64s; + +#pragma pack(pop) + +#ifdef __cplusplus +extern "C" { +#endif + +/* __cdecl */ +unsigned int hde64_disasm(const void *code, hde64s *hs); + +#ifdef __cplusplus +} +#endif + +#endif /* _HDE64_H_ */ diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/pstdint.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/pstdint.h new file mode 100644 index 0000000..84d82a0 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/pstdint.h @@ -0,0 +1,39 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +// Integer types for HDE. +typedef INT8 int8_t; +typedef INT16 int16_t; +typedef INT32 int32_t; +typedef INT64 int64_t; +typedef UINT8 uint8_t; +typedef UINT16 uint16_t; +typedef UINT32 uint32_t; +typedef UINT64 uint64_t; diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table32.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table32.h new file mode 100644 index 0000000..7b3e12e --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table32.h @@ -0,0 +1,73 @@ +/* + * Hacker Disassembler Engine 32 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#define C_NONE 0x00 +#define C_MODRM 0x01 +#define C_IMM8 0x02 +#define C_IMM16 0x04 +#define C_IMM_P66 0x10 +#define C_REL8 0x20 +#define C_REL32 0x40 +#define C_GROUP 0x80 +#define C_ERROR 0xff + +#define PRE_ANY 0x00 +#define PRE_NONE 0x01 +#define PRE_F2 0x02 +#define PRE_F3 0x04 +#define PRE_66 0x08 +#define PRE_67 0x10 +#define PRE_LOCK 0x20 +#define PRE_SEG 0x40 +#define PRE_ALL 0xff + +#define DELTA_OPCODES 0x4a +#define DELTA_FPU_REG 0xf1 +#define DELTA_FPU_MODRM 0xf8 +#define DELTA_PREFIXES 0x130 +#define DELTA_OP_LOCK_OK 0x1a1 +#define DELTA_OP2_LOCK_OK 0x1b9 +#define DELTA_OP_ONLY_MEM 0x1cb +#define DELTA_OP2_ONLY_MEM 0x1da + +unsigned char hde32_table[] = { + 0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3,0xa8,0xa3, + 0xa8,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,0xac,0xaa,0xb2,0xaa,0x9f,0x9f, + 0x9f,0x9f,0xb5,0xa3,0xa3,0xa4,0xaa,0xaa,0xba,0xaa,0x96,0xaa,0xa8,0xaa,0xc3, + 0xc3,0x96,0x96,0xb7,0xae,0xd6,0xbd,0xa3,0xc5,0xa3,0xa3,0x9f,0xc3,0x9c,0xaa, + 0xaa,0xac,0xaa,0xbf,0x03,0x7f,0x11,0x7f,0x01,0x7f,0x01,0x3f,0x01,0x01,0x90, + 0x82,0x7d,0x97,0x59,0x59,0x59,0x59,0x59,0x7f,0x59,0x59,0x60,0x7d,0x7f,0x7f, + 0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x9a,0x88,0x7d, + 0x59,0x50,0x50,0x50,0x50,0x59,0x59,0x59,0x59,0x61,0x94,0x61,0x9e,0x59,0x59, + 0x85,0x59,0x92,0xa3,0x60,0x60,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59,0x59, + 0x59,0x59,0x9f,0x01,0x03,0x01,0x04,0x03,0xd5,0x03,0xcc,0x01,0xbc,0x03,0xf0, + 0x10,0x10,0x10,0x10,0x50,0x50,0x50,0x50,0x14,0x20,0x20,0x20,0x20,0x01,0x01, + 0x01,0x01,0xc4,0x02,0x10,0x00,0x00,0x00,0x00,0x01,0x01,0xc0,0xc2,0x10,0x11, + 0x02,0x03,0x11,0x03,0x03,0x04,0x00,0x00,0x14,0x00,0x02,0x00,0x00,0xc6,0xc8, + 0x02,0x02,0x02,0x02,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0xff,0xca, + 0x01,0x01,0x01,0x00,0x06,0x00,0x04,0x00,0xc0,0xc2,0x01,0x01,0x03,0x01,0xff, + 0xff,0x01,0x00,0x03,0xc4,0xc4,0xc6,0x03,0x01,0x01,0x01,0xff,0x03,0x03,0x03, + 0xc8,0x40,0x00,0x0a,0x00,0x04,0x00,0x00,0x00,0x00,0x7f,0x00,0x33,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x07,0x00, + 0x00,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0xff,0xff,0x00,0x00,0x00,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x7f,0x00,0x00,0xff,0x4a,0x4a,0x4a,0x4a,0x4b,0x52,0x4a,0x4a,0x4a,0x4a,0x4f, + 0x4c,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x55,0x45,0x40,0x4a,0x4a,0x4a, + 0x45,0x59,0x4d,0x46,0x4a,0x5d,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a,0x4a, + 0x4a,0x4a,0x4a,0x4a,0x4a,0x61,0x63,0x67,0x4e,0x4a,0x4a,0x6b,0x6d,0x4a,0x4a, + 0x45,0x6d,0x4a,0x4a,0x44,0x45,0x4a,0x4a,0x00,0x00,0x00,0x02,0x0d,0x06,0x06, + 0x06,0x06,0x0e,0x00,0x00,0x00,0x00,0x06,0x06,0x06,0x00,0x06,0x06,0x02,0x06, + 0x00,0x0a,0x0a,0x07,0x07,0x06,0x02,0x05,0x05,0x02,0x02,0x00,0x00,0x04,0x04, + 0x04,0x04,0x00,0x00,0x00,0x0e,0x05,0x06,0x06,0x06,0x01,0x06,0x00,0x00,0x08, + 0x00,0x10,0x00,0x18,0x00,0x20,0x00,0x28,0x00,0x30,0x00,0x80,0x01,0x82,0x01, + 0x86,0x00,0xf6,0xcf,0xfe,0x3f,0xab,0x00,0xb0,0x00,0xb1,0x00,0xb3,0x00,0xba, + 0xf8,0xbb,0x00,0xc0,0x00,0xc1,0x00,0xc7,0xbf,0x62,0xff,0x00,0x8d,0xff,0x00, + 0xc4,0xff,0x00,0xc5,0xff,0x00,0xff,0xff,0xeb,0x01,0xff,0x0e,0x12,0x08,0x00, + 0x13,0x09,0x00,0x16,0x08,0x00,0x17,0x09,0x00,0x2b,0x09,0x00,0xae,0xff,0x07, + 0xb2,0xff,0x00,0xb4,0xff,0x00,0xb5,0xff,0x00,0xc3,0x01,0x00,0xc7,0xff,0xbf, + 0xe7,0x08,0x00,0xf0,0x02,0x00 +}; diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table64.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table64.h new file mode 100644 index 0000000..01d4541 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hde/table64.h @@ -0,0 +1,74 @@ +/* + * Hacker Disassembler Engine 64 C + * Copyright (c) 2008-2009, Vyacheslav Patkov. + * All rights reserved. + * + */ + +#define C_NONE 0x00 +#define C_MODRM 0x01 +#define C_IMM8 0x02 +#define C_IMM16 0x04 +#define C_IMM_P66 0x10 +#define C_REL8 0x20 +#define C_REL32 0x40 +#define C_GROUP 0x80 +#define C_ERROR 0xff + +#define PRE_ANY 0x00 +#define PRE_NONE 0x01 +#define PRE_F2 0x02 +#define PRE_F3 0x04 +#define PRE_66 0x08 +#define PRE_67 0x10 +#define PRE_LOCK 0x20 +#define PRE_SEG 0x40 +#define PRE_ALL 0xff + +#define DELTA_OPCODES 0x4a +#define DELTA_FPU_REG 0xfd +#define DELTA_FPU_MODRM 0x104 +#define DELTA_PREFIXES 0x13c +#define DELTA_OP_LOCK_OK 0x1ae +#define DELTA_OP2_LOCK_OK 0x1c6 +#define DELTA_OP_ONLY_MEM 0x1d8 +#define DELTA_OP2_ONLY_MEM 0x1e7 + +unsigned char hde64_table[] = { + 0xa5,0xaa,0xa5,0xb8,0xa5,0xaa,0xa5,0xaa,0xa5,0xb8,0xa5,0xb8,0xa5,0xb8,0xa5, + 0xb8,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xc0,0xac,0xc0,0xcc,0xc0,0xa1,0xa1, + 0xa1,0xa1,0xb1,0xa5,0xa5,0xa6,0xc0,0xc0,0xd7,0xda,0xe0,0xc0,0xe4,0xc0,0xea, + 0xea,0xe0,0xe0,0x98,0xc8,0xee,0xf1,0xa5,0xd3,0xa5,0xa5,0xa1,0xea,0x9e,0xc0, + 0xc0,0xc2,0xc0,0xe6,0x03,0x7f,0x11,0x7f,0x01,0x7f,0x01,0x3f,0x01,0x01,0xab, + 0x8b,0x90,0x64,0x5b,0x5b,0x5b,0x5b,0x5b,0x92,0x5b,0x5b,0x76,0x90,0x92,0x92, + 0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x6a,0x73,0x90, + 0x5b,0x52,0x52,0x52,0x52,0x5b,0x5b,0x5b,0x5b,0x77,0x7c,0x77,0x85,0x5b,0x5b, + 0x70,0x5b,0x7a,0xaf,0x76,0x76,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b,0x5b, + 0x5b,0x5b,0x86,0x01,0x03,0x01,0x04,0x03,0xd5,0x03,0xd5,0x03,0xcc,0x01,0xbc, + 0x03,0xf0,0x03,0x03,0x04,0x00,0x50,0x50,0x50,0x50,0xff,0x20,0x20,0x20,0x20, + 0x01,0x01,0x01,0x01,0xc4,0x02,0x10,0xff,0xff,0xff,0x01,0x00,0x03,0x11,0xff, + 0x03,0xc4,0xc6,0xc8,0x02,0x10,0x00,0xff,0xcc,0x01,0x01,0x01,0x00,0x00,0x00, + 0x00,0x01,0x01,0x03,0x01,0xff,0xff,0xc0,0xc2,0x10,0x11,0x02,0x03,0x01,0x01, + 0x01,0xff,0xff,0xff,0x00,0x00,0x00,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x10, + 0x10,0x10,0x10,0x02,0x10,0x00,0x00,0xc6,0xc8,0x02,0x02,0x02,0x02,0x06,0x00, + 0x04,0x00,0x02,0xff,0x00,0xc0,0xc2,0x01,0x01,0x03,0x03,0x03,0xca,0x40,0x00, + 0x0a,0x00,0x04,0x00,0x00,0x00,0x00,0x7f,0x00,0x33,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0xff,0xbf,0xff,0xff,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0xff,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff, + 0x00,0x00,0x00,0xbf,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0x00,0x00, + 0xff,0x40,0x40,0x40,0x40,0x41,0x49,0x40,0x40,0x40,0x40,0x4c,0x42,0x40,0x40, + 0x40,0x40,0x40,0x40,0x40,0x40,0x4f,0x44,0x53,0x40,0x40,0x40,0x44,0x57,0x43, + 0x5c,0x40,0x60,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40, + 0x40,0x40,0x64,0x66,0x6e,0x6b,0x40,0x40,0x6a,0x46,0x40,0x40,0x44,0x46,0x40, + 0x40,0x5b,0x44,0x40,0x40,0x00,0x00,0x00,0x00,0x06,0x06,0x06,0x06,0x01,0x06, + 0x06,0x02,0x06,0x06,0x00,0x06,0x00,0x0a,0x0a,0x00,0x00,0x00,0x02,0x07,0x07, + 0x06,0x02,0x0d,0x06,0x06,0x06,0x0e,0x05,0x05,0x02,0x02,0x00,0x00,0x04,0x04, + 0x04,0x04,0x05,0x06,0x06,0x06,0x00,0x00,0x00,0x0e,0x00,0x00,0x08,0x00,0x10, + 0x00,0x18,0x00,0x20,0x00,0x28,0x00,0x30,0x00,0x80,0x01,0x82,0x01,0x86,0x00, + 0xf6,0xcf,0xfe,0x3f,0xab,0x00,0xb0,0x00,0xb1,0x00,0xb3,0x00,0xba,0xf8,0xbb, + 0x00,0xc0,0x00,0xc1,0x00,0xc7,0xbf,0x62,0xff,0x00,0x8d,0xff,0x00,0xc4,0xff, + 0x00,0xc5,0xff,0x00,0xff,0xff,0xeb,0x01,0xff,0x0e,0x12,0x08,0x00,0x13,0x09, + 0x00,0x16,0x08,0x00,0x17,0x09,0x00,0x2b,0x09,0x00,0xae,0xff,0x07,0xb2,0xff, + 0x00,0xb4,0xff,0x00,0xb5,0xff,0x00,0xc3,0x01,0x00,0xc7,0xff,0xbf,0xe7,0x08, + 0x00,0xf0,0x02,0x00 +}; diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/hook.c b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hook.c new file mode 100644 index 0000000..68a006a --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/hook.c @@ -0,0 +1,939 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include "../include/MinHook.h" +#include "buffer.h" +#include "trampoline.h" + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +// Initial capacity of the HOOK_ENTRY buffer. +#define INITIAL_HOOK_CAPACITY 32 + +// Initial capacity of the thread IDs buffer. +#define INITIAL_THREAD_CAPACITY 128 + +// Special hook position values. +#define INVALID_HOOK_POS UINT_MAX +#define ALL_HOOKS_POS UINT_MAX + +// Freeze() action argument defines. +#define ACTION_DISABLE 0 +#define ACTION_ENABLE 1 +#define ACTION_APPLY_QUEUED 2 + +// Thread access rights for suspending/resuming threads. +#define THREAD_ACCESS \ + (THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT) + +// Hook information. +typedef struct _HOOK_ENTRY +{ + LPVOID pTarget; // Address of the target function. + LPVOID pDetour; // Address of the detour or relay function. + LPVOID pTrampoline; // Address of the trampoline function. + UINT8 backup[8]; // Original prologue of the target function. + + UINT8 patchAbove : 1; // Uses the hot patch area. + UINT8 isEnabled : 1; // Enabled. + UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. + + UINT nIP : 4; // Count of the instruction boundaries. + UINT8 oldIPs[8]; // Instruction boundaries of the target function. + UINT8 newIPs[8]; // Instruction boundaries of the trampoline function. +} HOOK_ENTRY, *PHOOK_ENTRY; + +// Suspended threads for Freeze()/Unfreeze(). +typedef struct _FROZEN_THREADS +{ + LPDWORD pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} FROZEN_THREADS, *PFROZEN_THREADS; + +//------------------------------------------------------------------------- +// Global Variables: +//------------------------------------------------------------------------- + +// Spin lock flag for EnterSpinLock()/LeaveSpinLock(). +volatile LONG g_isLocked = FALSE; + +// Private heap handle. If not NULL, this library is initialized. +HANDLE g_hHeap = NULL; + +// Hook entries. +struct +{ + PHOOK_ENTRY pItems; // Data heap + UINT capacity; // Size of allocated data heap, items + UINT size; // Actual number of data items +} g_hooks; + +//------------------------------------------------------------------------- +// Returns INVALID_HOOK_POS if not found. +static UINT FindHookEntry(LPVOID pTarget) +{ + UINT i; + for (i = 0; i < g_hooks.size; ++i) + { + if ((ULONG_PTR)pTarget == (ULONG_PTR)g_hooks.pItems[i].pTarget) + return i; + } + + return INVALID_HOOK_POS; +} + +//------------------------------------------------------------------------- +static PHOOK_ENTRY AddHookEntry() +{ + if (g_hooks.pItems == NULL) + { + g_hooks.capacity = INITIAL_HOOK_CAPACITY; + g_hooks.pItems = (PHOOK_ENTRY)HeapAlloc( + g_hHeap, 0, g_hooks.capacity * sizeof(HOOK_ENTRY)); + if (g_hooks.pItems == NULL) + return NULL; + } + else if (g_hooks.size >= g_hooks.capacity) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity * 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return NULL; + + g_hooks.capacity *= 2; + g_hooks.pItems = p; + } + + return &g_hooks.pItems[g_hooks.size++]; +} + +//------------------------------------------------------------------------- +static VOID DeleteHookEntry(UINT pos) +{ + if (pos < g_hooks.size - 1) + g_hooks.pItems[pos] = g_hooks.pItems[g_hooks.size - 1]; + + g_hooks.size--; + + if (g_hooks.capacity / 2 >= INITIAL_HOOK_CAPACITY && g_hooks.capacity / 2 >= g_hooks.size) + { + PHOOK_ENTRY p = (PHOOK_ENTRY)HeapReAlloc( + g_hHeap, 0, g_hooks.pItems, (g_hooks.capacity / 2) * sizeof(HOOK_ENTRY)); + if (p == NULL) + return; + + g_hooks.capacity /= 2; + g_hooks.pItems = p; + } +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindOldIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + + if (pHook->patchAbove && ip == ((DWORD_PTR)pHook->pTarget - sizeof(JMP_REL))) + return (DWORD_PTR)pHook->pTarget; + + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i])) + return (DWORD_PTR)pHook->pTarget + pHook->oldIPs[i]; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Check relay function. + if (ip == (DWORD_PTR)pHook->pDetour) + return (DWORD_PTR)pHook->pTarget; +#endif + + return 0; +} + +//------------------------------------------------------------------------- +static DWORD_PTR FindNewIP(PHOOK_ENTRY pHook, DWORD_PTR ip) +{ + UINT i; + for (i = 0; i < pHook->nIP; ++i) + { + if (ip == ((DWORD_PTR)pHook->pTarget + pHook->oldIPs[i])) + return (DWORD_PTR)pHook->pTrampoline + pHook->newIPs[i]; + } + + return 0; +} + +//------------------------------------------------------------------------- +static VOID ProcessThreadIPs(HANDLE hThread, UINT pos, UINT action) +{ + // If the thread suspended in the overwritten area, + // move IP to the proper address. + + CONTEXT c; +#if defined(_M_X64) || defined(__x86_64__) + DWORD64 *pIP = &c.Rip; +#else + DWORD *pIP = &c.Eip; +#endif + UINT count; + + c.ContextFlags = CONTEXT_CONTROL; + if (!GetThreadContext(hThread, &c)) + return; + + if (pos == ALL_HOOKS_POS) + { + pos = 0; + count = g_hooks.size; + } + else + { + count = pos + 1; + } + + for (; pos < count; ++pos) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + BOOL enable; + DWORD_PTR ip; + + switch (action) + { + case ACTION_DISABLE: + enable = FALSE; + break; + + case ACTION_ENABLE: + enable = TRUE; + break; + + default: // ACTION_APPLY_QUEUED + enable = pHook->queueEnable; + break; + } + if (pHook->isEnabled == enable) + continue; + + if (enable) + ip = FindNewIP(pHook, *pIP); + else + ip = FindOldIP(pHook, *pIP); + + if (ip != 0) + { + *pIP = ip; + SetThreadContext(hThread, &c); + } + } +} + +//------------------------------------------------------------------------- +static BOOL EnumerateThreads(PFROZEN_THREADS pThreads) +{ + BOOL succeeded = FALSE; + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (hSnapshot != INVALID_HANDLE_VALUE) + { + THREADENTRY32 te; + te.dwSize = sizeof(THREADENTRY32); + if (Thread32First(hSnapshot, &te)) + { + succeeded = TRUE; + do + { + if (te.dwSize >= (FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(DWORD)) + && te.th32OwnerProcessID == GetCurrentProcessId() + && te.th32ThreadID != GetCurrentThreadId()) + { + if (pThreads->pItems == NULL) + { + pThreads->capacity = INITIAL_THREAD_CAPACITY; + pThreads->pItems + = (LPDWORD)HeapAlloc(g_hHeap, 0, pThreads->capacity * sizeof(DWORD)); + if (pThreads->pItems == NULL) + { + succeeded = FALSE; + break; + } + } + else if (pThreads->size >= pThreads->capacity) + { + LPDWORD p; + pThreads->capacity *= 2; + p = (LPDWORD)HeapReAlloc( + g_hHeap, 0, pThreads->pItems, pThreads->capacity * sizeof(DWORD)); + if (p == NULL) + { + succeeded = FALSE; + break; + } + + pThreads->pItems = p; + } + pThreads->pItems[pThreads->size++] = te.th32ThreadID; + } + + te.dwSize = sizeof(THREADENTRY32); + } while (Thread32Next(hSnapshot, &te)); + + if (succeeded && GetLastError() != ERROR_NO_MORE_FILES) + succeeded = FALSE; + + if (!succeeded && pThreads->pItems != NULL) + { + HeapFree(g_hHeap, 0, pThreads->pItems); + pThreads->pItems = NULL; + } + } + CloseHandle(hSnapshot); + } + + return succeeded; +} + +//------------------------------------------------------------------------- +static MH_STATUS Freeze(PFROZEN_THREADS pThreads, UINT pos, UINT action) +{ + MH_STATUS status = MH_OK; + + pThreads->pItems = NULL; + pThreads->capacity = 0; + pThreads->size = 0; + if (!EnumerateThreads(pThreads)) + { + status = MH_ERROR_MEMORY_ALLOC; + } + else if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, pThreads->pItems[i]); + BOOL suspended = FALSE; + if (hThread != NULL) + { + DWORD result = SuspendThread(hThread); + if (result != 0xFFFFFFFF) + { + suspended = TRUE; + ProcessThreadIPs(hThread, pos, action); + } + CloseHandle(hThread); + } + + if (!suspended) + { + // Mark thread as not suspended, so it's not resumed later on. + pThreads->pItems[i] = 0; + } + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID Unfreeze(PFROZEN_THREADS pThreads) +{ + if (pThreads->pItems != NULL) + { + UINT i; + for (i = 0; i < pThreads->size; ++i) + { + DWORD threadId = pThreads->pItems[i]; + if (threadId != 0) + { + HANDLE hThread = OpenThread(THREAD_ACCESS, FALSE, threadId); + if (hThread != NULL) + { + ResumeThread(hThread); + CloseHandle(hThread); + } + } + } + + HeapFree(g_hHeap, 0, pThreads->pItems); + } +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHookLL(UINT pos, BOOL enable) +{ + PHOOK_ENTRY pHook = &g_hooks.pItems[pos]; + DWORD oldProtect; + SIZE_T patchSize = sizeof(JMP_REL); + LPBYTE pPatchTarget = (LPBYTE)pHook->pTarget; + + if (pHook->patchAbove) + { + pPatchTarget -= sizeof(JMP_REL); + patchSize += sizeof(JMP_REL_SHORT); + } + + if (!VirtualProtect(pPatchTarget, patchSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + return MH_ERROR_MEMORY_PROTECT; + + if (enable) + { + PJMP_REL pJmp = (PJMP_REL)pPatchTarget; + pJmp->opcode = 0xE9; + pJmp->operand = (UINT32)((LPBYTE)pHook->pDetour - (pPatchTarget + sizeof(JMP_REL))); + + if (pHook->patchAbove) + { + PJMP_REL_SHORT pShortJmp = (PJMP_REL_SHORT)pHook->pTarget; + pShortJmp->opcode = 0xEB; + pShortJmp->operand = (UINT8)(0 - (sizeof(JMP_REL_SHORT) + sizeof(JMP_REL))); + } + } + else + { + if (pHook->patchAbove) + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + else + memcpy(pPatchTarget, pHook->backup, sizeof(JMP_REL)); + } + + VirtualProtect(pPatchTarget, patchSize, oldProtect, &oldProtect); + + // Just-in-case measure. + FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize); + + pHook->isEnabled = enable; + pHook->queueEnable = enable; + + return MH_OK; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableAllHooksLL(BOOL enable) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, enable ? ACTION_ENABLE : ACTION_DISABLE); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != enable) + { + status = EnableHookLL(i, enable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + + return status; +} + +//------------------------------------------------------------------------- +static VOID EnterSpinLock(VOID) +{ + SIZE_T spinCount = 0; + + // Wait until the flag is FALSE. + while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE) + { + // No need to generate a memory barrier here, since InterlockedCompareExchange() + // generates a full memory barrier itself. + + // Prevent the loop from being too busy. + if (spinCount < 32) + Sleep(0); + else + Sleep(1); + + spinCount++; + } +} + +//------------------------------------------------------------------------- +static VOID LeaveSpinLock(VOID) +{ + // No need to generate a memory barrier here, since InterlockedExchange() + // generates a full memory barrier itself. + + InterlockedExchange(&g_isLocked, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Initialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap == NULL) + { + g_hHeap = HeapCreate(0, 0, 0); + if (g_hHeap != NULL) + { + // Initialize the internal function buffer. + InitializeBuffer(); + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_Uninitialize(VOID) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + status = EnableAllHooksLL(FALSE); + if (status == MH_OK) + { + // Free the internal function buffer. + + // HeapFree is actually not required, but some tools detect a false + // memory leak without HeapFree. + + UninitializeBuffer(); + + HeapFree(g_hHeap, 0, g_hooks.pItems); + HeapDestroy(g_hHeap); + + g_hHeap = NULL; + + g_hooks.pItems = NULL; + g_hooks.capacity = 0; + g_hooks.size = 0; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (IsExecutableAddress(pTarget) && IsExecutableAddress(pDetour)) + { + UINT pos = FindHookEntry(pTarget); + if (pos == INVALID_HOOK_POS) + { + LPVOID pBuffer = AllocateBuffer(pTarget); + if (pBuffer != NULL) + { + TRAMPOLINE ct; + + ct.pTarget = pTarget; + ct.pDetour = pDetour; + ct.pTrampoline = pBuffer; + if (CreateTrampolineFunction(&ct)) + { + PHOOK_ENTRY pHook = AddHookEntry(); + if (pHook != NULL) + { + pHook->pTarget = ct.pTarget; +#if defined(_M_X64) || defined(__x86_64__) + pHook->pDetour = ct.pRelay; +#else + pHook->pDetour = ct.pDetour; +#endif + pHook->pTrampoline = ct.pTrampoline; + pHook->patchAbove = ct.patchAbove; + pHook->isEnabled = FALSE; + pHook->queueEnable = FALSE; + pHook->nIP = ct.nIP; + memcpy(pHook->oldIPs, ct.oldIPs, ARRAYSIZE(ct.oldIPs)); + memcpy(pHook->newIPs, ct.newIPs, ARRAYSIZE(ct.newIPs)); + + // Back up the target function. + + if (ct.patchAbove) + { + memcpy( + pHook->backup, + (LPBYTE)pTarget - sizeof(JMP_REL), + sizeof(JMP_REL) + sizeof(JMP_REL_SHORT)); + } + else + { + memcpy(pHook->backup, pTarget, sizeof(JMP_REL)); + } + + if (ppOriginal != NULL) + *ppOriginal = pHook->pTrampoline; + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_UNSUPPORTED_FUNCTION; + } + + if (status != MH_OK) + { + FreeBuffer(pBuffer); + } + } + else + { + status = MH_ERROR_MEMORY_ALLOC; + } + } + else + { + status = MH_ERROR_ALREADY_CREATED; + } + } + else + { + status = MH_ERROR_NOT_EXECUTABLE; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_DISABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, FALSE); + + Unfreeze(&threads); + } + } + + if (status == MH_OK) + { + FreeBuffer(g_hooks.pItems[pos].pTrampoline); + DeleteHookEntry(pos); + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +static MH_STATUS EnableHook(LPVOID pTarget, BOOL enable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + status = EnableAllHooksLL(enable); + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + if (g_hooks.pItems[pos].isEnabled != enable) + { + FROZEN_THREADS threads; + status = Freeze(&threads, pos, ACTION_ENABLE); + if (status == MH_OK) + { + status = EnableHookLL(pos, enable); + + Unfreeze(&threads); + } + } + else + { + status = enable ? MH_ERROR_ENABLED : MH_ERROR_DISABLED; + } + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget) +{ + return EnableHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +static MH_STATUS QueueHook(LPVOID pTarget, BOOL queueEnable) +{ + MH_STATUS status = MH_OK; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + if (pTarget == MH_ALL_HOOKS) + { + UINT i; + for (i = 0; i < g_hooks.size; ++i) + g_hooks.pItems[i].queueEnable = queueEnable; + } + else + { + UINT pos = FindHookEntry(pTarget); + if (pos != INVALID_HOOK_POS) + { + g_hooks.pItems[pos].queueEnable = queueEnable; + } + else + { + status = MH_ERROR_NOT_CREATED; + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, TRUE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget) +{ + return QueueHook(pTarget, FALSE); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_ApplyQueued(VOID) +{ + MH_STATUS status = MH_OK; + UINT i, first = INVALID_HOOK_POS; + + EnterSpinLock(); + + if (g_hHeap != NULL) + { + for (i = 0; i < g_hooks.size; ++i) + { + if (g_hooks.pItems[i].isEnabled != g_hooks.pItems[i].queueEnable) + { + first = i; + break; + } + } + + if (first != INVALID_HOOK_POS) + { + FROZEN_THREADS threads; + status = Freeze(&threads, ALL_HOOKS_POS, ACTION_APPLY_QUEUED); + if (status == MH_OK) + { + for (i = first; i < g_hooks.size; ++i) + { + PHOOK_ENTRY pHook = &g_hooks.pItems[i]; + if (pHook->isEnabled != pHook->queueEnable) + { + status = EnableHookLL(i, pHook->queueEnable); + if (status != MH_OK) + break; + } + } + + Unfreeze(&threads); + } + } + } + else + { + status = MH_ERROR_NOT_INITIALIZED; + } + + LeaveSpinLock(); + + return status; +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, + LPVOID *ppOriginal, LPVOID *ppTarget) +{ + HMODULE hModule; + LPVOID pTarget; + + hModule = GetModuleHandleW(pszModule); + if (hModule == NULL) + return MH_ERROR_MODULE_NOT_FOUND; + + pTarget = (LPVOID)GetProcAddress(hModule, pszProcName); + if (pTarget == NULL) + return MH_ERROR_FUNCTION_NOT_FOUND; + + if (ppTarget != NULL) + *ppTarget = pTarget; + + return MH_CreateHook(pTarget, pDetour, ppOriginal); +} + +//------------------------------------------------------------------------- +MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal) +{ + return MH_CreateHookApiEx(pszModule, pszProcName, pDetour, ppOriginal, NULL); +} + +//------------------------------------------------------------------------- +const char *WINAPI MH_StatusToString(MH_STATUS status) +{ +#define MH_ST2STR(x) \ + case x: \ + return #x; + + switch (status) { + MH_ST2STR(MH_UNKNOWN) + MH_ST2STR(MH_OK) + MH_ST2STR(MH_ERROR_ALREADY_INITIALIZED) + MH_ST2STR(MH_ERROR_NOT_INITIALIZED) + MH_ST2STR(MH_ERROR_ALREADY_CREATED) + MH_ST2STR(MH_ERROR_NOT_CREATED) + MH_ST2STR(MH_ERROR_ENABLED) + MH_ST2STR(MH_ERROR_DISABLED) + MH_ST2STR(MH_ERROR_NOT_EXECUTABLE) + MH_ST2STR(MH_ERROR_UNSUPPORTED_FUNCTION) + MH_ST2STR(MH_ERROR_MEMORY_ALLOC) + MH_ST2STR(MH_ERROR_MEMORY_PROTECT) + MH_ST2STR(MH_ERROR_MODULE_NOT_FOUND) + MH_ST2STR(MH_ERROR_FUNCTION_NOT_FOUND) + } + +#undef MH_ST2STR + + return "(unknown)"; +} diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.c b/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.c new file mode 100644 index 0000000..d4d6f99 --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.c @@ -0,0 +1,320 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#if defined(_MSC_VER) && !defined(MINHOOK_DISABLE_INTRINSICS) + #define ALLOW_INTRINSICS + #include +#endif + +#ifndef ARRAYSIZE + #define ARRAYSIZE(A) (sizeof(A)/sizeof((A)[0])) +#endif + +#if defined(_M_X64) || defined(__x86_64__) + #include "./hde/hde64.h" + typedef hde64s HDE; + #define HDE_DISASM(code, hs) hde64_disasm(code, hs) +#else + #include "./hde/hde32.h" + typedef hde32s HDE; + #define HDE_DISASM(code, hs) hde32_disasm(code, hs) +#endif + +#include "trampoline.h" +#include "buffer.h" + +// Maximum size of a trampoline function. +#if defined(_M_X64) || defined(__x86_64__) + #define TRAMPOLINE_MAX_SIZE (MEMORY_SLOT_SIZE - sizeof(JMP_ABS)) +#else + #define TRAMPOLINE_MAX_SIZE MEMORY_SLOT_SIZE +#endif + +//------------------------------------------------------------------------- +static BOOL IsCodePadding(LPBYTE pInst, UINT size) +{ + UINT i; + + if (pInst[0] != 0x00 && pInst[0] != 0x90 && pInst[0] != 0xCC) + return FALSE; + + for (i = 1; i < size; ++i) + { + if (pInst[i] != pInst[0]) + return FALSE; + } + return TRUE; +} + +//------------------------------------------------------------------------- +BOOL CreateTrampolineFunction(PTRAMPOLINE ct) +{ +#if defined(_M_X64) || defined(__x86_64__) + CALL_ABS call = { + 0xFF, 0x15, 0x00000002, // FF15 00000002: CALL [RIP+8] + 0xEB, 0x08, // EB 08: JMP +10 + 0x0000000000000000ULL // Absolute destination address + }; + JMP_ABS jmp = { + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; + JCC_ABS jcc = { + 0x70, 0x0E, // 7* 0E: J** +16 + 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] + 0x0000000000000000ULL // Absolute destination address + }; +#else + CALL_REL call = { + 0xE8, // E8 xxxxxxxx: CALL +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JMP_REL jmp = { + 0xE9, // E9 xxxxxxxx: JMP +5+xxxxxxxx + 0x00000000 // Relative destination address + }; + JCC_REL jcc = { + 0x0F, 0x80, // 0F8* xxxxxxxx: J** +6+xxxxxxxx + 0x00000000 // Relative destination address + }; +#endif + + UINT8 oldPos = 0; + UINT8 newPos = 0; + ULONG_PTR jmpDest = 0; // Destination address of an internal jump. + BOOL finished = FALSE; // Is the function completed? +#if defined(_M_X64) || defined(__x86_64__) + UINT8 instBuf[16]; +#endif + + ct->patchAbove = FALSE; + ct->nIP = 0; + + do + { + HDE hs; + UINT copySize; + LPVOID pCopySrc; + ULONG_PTR pOldInst = (ULONG_PTR)ct->pTarget + oldPos; + ULONG_PTR pNewInst = (ULONG_PTR)ct->pTrampoline + newPos; + + copySize = HDE_DISASM((LPVOID)pOldInst, &hs); + if (hs.flags & F_ERROR) + return FALSE; + + pCopySrc = (LPVOID)pOldInst; + if (oldPos >= sizeof(JMP_REL)) + { + // The trampoline function is long enough. + // Complete the function with the jump to the target function. +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = pOldInst; +#else + jmp.operand = (UINT32)(pOldInst - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + finished = TRUE; + } +#if defined(_M_X64) || defined(__x86_64__) + else if ((hs.modrm & 0xC7) == 0x05) + { + // Instructions using RIP relative addressing. (ModR/M = 00???101B) + + // Modify the RIP relative address. + PUINT32 pRelAddr; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy(instBuf, (LPBYTE)pOldInst, copySize); +#else + __movsb(instBuf, (LPBYTE)pOldInst, copySize); +#endif + pCopySrc = instBuf; + + // Relative address is stored at (instruction length - immediate value length - 4). + pRelAddr = (PUINT32)(instBuf + hs.len - ((hs.flags & 0x3C) >> 2) - 4); + *pRelAddr + = (UINT32)((pOldInst + hs.len + (INT32)hs.disp.disp32) - (pNewInst + hs.len)); + + // Complete the function if JMP (FF /4). + if (hs.opcode == 0xFF && hs.modrm_reg == 4) + finished = TRUE; + } +#endif + else if (hs.opcode == 0xE8) + { + // Direct relative CALL + ULONG_PTR dest = pOldInst + hs.len + (INT32)hs.imm.imm32; +#if defined(_M_X64) || defined(__x86_64__) + call.address = dest; +#else + call.operand = (UINT32)(dest - (pNewInst + sizeof(call))); +#endif + pCopySrc = &call; + copySize = sizeof(call); + } + else if ((hs.opcode & 0xFD) == 0xE9) + { + // Direct relative JMP (EB or E9) + ULONG_PTR dest = pOldInst + hs.len; + + if (hs.opcode == 0xEB) // isShort jmp + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else + { +#if defined(_M_X64) || defined(__x86_64__) + jmp.address = dest; +#else + jmp.operand = (UINT32)(dest - (pNewInst + sizeof(jmp))); +#endif + pCopySrc = &jmp; + copySize = sizeof(jmp); + + // Exit the function if it is not in the branch. + finished = (pOldInst >= jmpDest); + } + } + else if ((hs.opcode & 0xF0) == 0x70 + || (hs.opcode & 0xFC) == 0xE0 + || (hs.opcode2 & 0xF0) == 0x80) + { + // Direct relative Jcc + ULONG_PTR dest = pOldInst + hs.len; + + if ((hs.opcode & 0xF0) == 0x70 // Jcc + || (hs.opcode & 0xFC) == 0xE0) // LOOPNZ/LOOPZ/LOOP/JECXZ + dest += (INT8)hs.imm.imm8; + else + dest += (INT32)hs.imm.imm32; + + // Simply copy an internal jump. + if ((ULONG_PTR)ct->pTarget <= dest + && dest < ((ULONG_PTR)ct->pTarget + sizeof(JMP_REL))) + { + if (jmpDest < dest) + jmpDest = dest; + } + else if ((hs.opcode & 0xFC) == 0xE0) + { + // LOOPNZ/LOOPZ/LOOP/JCXZ/JECXZ to the outside are not supported. + return FALSE; + } + else + { + UINT8 cond = ((hs.opcode != 0x0F ? hs.opcode : hs.opcode2) & 0x0F); +#if defined(_M_X64) || defined(__x86_64__) + // Invert the condition in x64 mode to simplify the conditional jump logic. + jcc.opcode = 0x71 ^ cond; + jcc.address = dest; +#else + jcc.opcode1 = 0x80 | cond; + jcc.operand = (UINT32)(dest - (pNewInst + sizeof(jcc))); +#endif + pCopySrc = &jcc; + copySize = sizeof(jcc); + } + } + else if ((hs.opcode & 0xFE) == 0xC2) + { + // RET (C2 or C3) + + // Complete the function if not in a branch. + finished = (pOldInst >= jmpDest); + } + + // Can't alter the instruction length in a branch. + if (pOldInst < jmpDest && copySize != hs.len) + return FALSE; + + // Trampoline function is too large. + if ((newPos + copySize) > TRAMPOLINE_MAX_SIZE) + return FALSE; + + // Trampoline function has too many instructions. + if (ct->nIP >= ARRAYSIZE(ct->oldIPs)) + return FALSE; + + ct->oldIPs[ct->nIP] = oldPos; + ct->newIPs[ct->nIP] = newPos; + ct->nIP++; + + // Avoid using memcpy to reduce the footprint. +#ifndef ALLOW_INTRINSICS + memcpy((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize); +#else + __movsb((LPBYTE)ct->pTrampoline + newPos, (LPBYTE)pCopySrc, copySize); +#endif + newPos += copySize; + oldPos += hs.len; + } while (!finished); + + // Is there enough place for a long jump? + if (oldPos < sizeof(JMP_REL) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL) - oldPos)) + { + // Is there enough place for a short jump? + if (oldPos < sizeof(JMP_REL_SHORT) + && !IsCodePadding((LPBYTE)ct->pTarget + oldPos, sizeof(JMP_REL_SHORT) - oldPos)) + { + return FALSE; + } + + // Can we place the long jump above the function? + if (!IsExecutableAddress((LPBYTE)ct->pTarget - sizeof(JMP_REL))) + return FALSE; + + if (!IsCodePadding((LPBYTE)ct->pTarget - sizeof(JMP_REL), sizeof(JMP_REL))) + return FALSE; + + ct->patchAbove = TRUE; + } + +#if defined(_M_X64) || defined(__x86_64__) + // Create a relay function. + jmp.address = (ULONG_PTR)ct->pDetour; + + ct->pRelay = (LPBYTE)ct->pTrampoline + newPos; + memcpy(ct->pRelay, &jmp, sizeof(jmp)); +#endif + + return TRUE; +} diff --git a/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.h b/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.h new file mode 100644 index 0000000..bdffdac --- /dev/null +++ b/src/MultiSeat.SteamIsolation/third_party/minhook/src/trampoline.h @@ -0,0 +1,105 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#pragma pack(push, 1) + +// Structs for writing x86/x64 instructions. + +// 8-bit relative jump. +typedef struct _JMP_REL_SHORT +{ + UINT8 opcode; // EB xx: JMP +2+xx + UINT8 operand; +} JMP_REL_SHORT, *PJMP_REL_SHORT; + +// 32-bit direct relative jump/call. +typedef struct _JMP_REL +{ + UINT8 opcode; // E9/E8 xxxxxxxx: JMP/CALL +5+xxxxxxxx + UINT32 operand; // Relative destination address +} JMP_REL, *PJMP_REL, CALL_REL; + +// 64-bit indirect absolute jump. +typedef struct _JMP_ABS +{ + UINT8 opcode0; // FF25 00000000: JMP [+6] + UINT8 opcode1; + UINT32 dummy; + UINT64 address; // Absolute destination address +} JMP_ABS, *PJMP_ABS; + +// 64-bit indirect absolute call. +typedef struct _CALL_ABS +{ + UINT8 opcode0; // FF15 00000002: CALL [+6] + UINT8 opcode1; + UINT32 dummy0; + UINT8 dummy1; // EB 08: JMP +10 + UINT8 dummy2; + UINT64 address; // Absolute destination address +} CALL_ABS; + +// 32-bit direct relative conditional jumps. +typedef struct _JCC_REL +{ + UINT8 opcode0; // 0F8* xxxxxxxx: J** +6+xxxxxxxx + UINT8 opcode1; + UINT32 operand; // Relative destination address +} JCC_REL; + +// 64bit indirect absolute conditional jumps that x64 lacks. +typedef struct _JCC_ABS +{ + UINT8 opcode; // 7* 0E: J** +16 + UINT8 dummy0; + UINT8 dummy1; // FF25 00000000: JMP [+6] + UINT8 dummy2; + UINT32 dummy3; + UINT64 address; // Absolute destination address +} JCC_ABS; + +#pragma pack(pop) + +typedef struct _TRAMPOLINE +{ + LPVOID pTarget; // [In] Address of the target function. + LPVOID pDetour; // [In] Address of the detour function. + LPVOID pTrampoline; // [In] Buffer address for the trampoline and relay function. + +#if defined(_M_X64) || defined(__x86_64__) + LPVOID pRelay; // [Out] Address of the relay function. +#endif + BOOL patchAbove; // [Out] Should use the hot patch area? + UINT nIP; // [Out] Number of the instruction boundaries. + UINT8 oldIPs[8]; // [Out] Instruction boundaries of the target function. + UINT8 newIPs[8]; // [Out] Instruction boundaries of the trampoline function. +} TRAMPOLINE, *PTRAMPOLINE; + +BOOL CreateTrampolineFunction(PTRAMPOLINE ct); diff --git a/src/MultiSeat.Tests/Api/HttpStatusSemanticsTests.cs b/src/MultiSeat.Tests/Api/HttpStatusSemanticsTests.cs index baa5256..bb40b28 100644 --- a/src/MultiSeat.Tests/Api/HttpStatusSemanticsTests.cs +++ b/src/MultiSeat.Tests/Api/HttpStatusSemanticsTests.cs @@ -47,6 +47,35 @@ public void SeatNotFound_Is404() Assert.Equal(StatusCodes.Status404NotFound, ApiErrors.StatusFor(new SeatNotFoundException())); } + // A Steam launch that never reports ready, or whose helper dies, is not a malformed request. + // The timeout used to be a bare TimeoutException (not an InvalidOperationException), which the + // launch endpoint does not catch, so the caller got an unexplained 500. + [Fact] + public void LaunchTimedOut_Is504_NotBadRequest() + { + var ex = new LaunchTimedOutException("Steam isolation initialization did not signal readiness."); + + Assert.Equal(StatusCodes.Status504GatewayTimeout, ApiErrors.StatusFor(ex)); + Assert.NotEqual(StatusCodes.Status400BadRequest, ApiErrors.StatusFor(ex)); + } + + [Fact] + public void LaunchFailed_Is500_NotBadRequest() + { + var ex = new LaunchFailedException("Steam isolation launcher exited before initialization succeeded (code 2)."); + + Assert.Equal(StatusCodes.Status500InternalServerError, ApiErrors.StatusFor(ex)); + Assert.NotEqual(StatusCodes.Status400BadRequest, ApiErrors.StatusFor(ex)); + } + + [Fact] + public void LaunchErrorsAreStillInvalidOperations_SoExistingCallersKeepCatchingThem() + { + // OnConnectAppLauncher and SeatManager catch InvalidOperationException/Exception today. + Assert.IsAssignableFrom(new LaunchTimedOutException("x")); + Assert.IsAssignableFrom(new LaunchFailedException("x")); + } + [Fact] public void APlainInvalidOperation_StaysBadRequest() { diff --git a/src/MultiSeat.Tests/Scripts/InstallLibTests.cs b/src/MultiSeat.Tests/Scripts/InstallLibTests.cs index 64a1b93..cde43ff 100644 --- a/src/MultiSeat.Tests/Scripts/InstallLibTests.cs +++ b/src/MultiSeat.Tests/Scripts/InstallLibTests.cs @@ -656,6 +656,134 @@ public void Keeps_only_the_newest_full_folder_backups(string engine) Assert.Equal(new[] { "20260103-000000", "20260104-000000", "20261009-120000" }, left); } + // -- Steam isolation helpers (PR #110) -------------------------------------------- + // + // The installer wipes the install folder on every deploy. SteamIsolation\ holds native + // helpers the service needs when MultiSeat:SteamIsolationEnabled is on, so an upgrade has to + // either bring a new set with it or leave the old one alone. Deleting it silently is the bug. + + private static readonly string[] SteamFiles = + { + "MultiSeat.SteamLauncher.exe", "MultiSeat.SteamLauncher32.exe", + "MultiSeat.SteamHook.dll", "MultiSeat.SteamHook32.dll", + }; + + private static void PutSteamIsolation(string root, string tag) + { + foreach (var f in SteamFiles) Put(root, "SteamIsolation/" + f, tag + ":" + f); + } + + [Theory, MemberData(nameof(Engines))] + public void Names_every_steam_isolation_file_a_folder_lacks(string engine) + { + using var t = new Temp(); + var dir = t.Sub("steam"); + Put(dir, "MultiSeat.SteamLauncher.exe", "x"); + var j = J(Ps(engine, "$r = @(Get-MissingSteamIsolationFiles -Dir " + Q(dir) + ")")); + Assert.Equal(SteamFiles.Skip(1).OrderBy(x => x), Strings(j).OrderBy(x => x)); + + // A folder holding all four, and one that does not exist, are told apart. + var full = t.Sub("full"); + foreach (var f in SteamFiles) Put(full, f, "x"); + Assert.Empty(Strings(J(Ps(engine, "$r = @(Get-MissingSteamIsolationFiles -Dir " + Q(full) + ")")))); + Assert.Equal(SteamFiles.Length, + Strings(J(Ps(engine, "$r = @(Get-MissingSteamIsolationFiles -Dir " + Q(Path.Combine(t.Root, "nope")) + ")"))).Length); + } + + [Theory, MemberData(nameof(Engines))] + public void Keeps_an_existing_steam_isolation_folder_when_the_new_payload_ships_none(string engine) + { + using var t = new Temp(); + var install = PopulatedSelfContainedInstall(t); + PutSteamIsolation(install, "old"); + Put(install, "SteamIsolation/extra-note.txt", "placed by hand"); + var stage = SelfContained(t.Sub("stage"), "new"); + + var res = Ps(engine, InstallCall(stage, install, t.Root) + "$r = 1"); + Assert.Equal(0, res.ExitCode); + foreach (var f in SteamFiles) + Assert.Equal("old:" + f, File.ReadAllText(Path.Combine(install, "SteamIsolation", f))); + Assert.Equal("placed by hand", File.ReadAllText(Path.Combine(install, "SteamIsolation", "extra-note.txt"))); + Assert.Contains("Kept the existing SteamIsolation folder", res.Stdout); + } + + [Theory, MemberData(nameof(Engines))] + public void A_payload_that_ships_steam_isolation_replaces_the_old_copy(string engine) + { + using var t = new Temp(); + var install = PopulatedSelfContainedInstall(t); + PutSteamIsolation(install, "old"); + Put(install, "SteamIsolation/old-only.dll", "gone after the upgrade"); + var stage = SelfContained(t.Sub("stage"), "new"); + PutSteamIsolation(stage, "new"); + + var res = Ps(engine, InstallCall(stage, install, t.Root) + "$r = 1"); + Assert.Equal(0, res.ExitCode); + foreach (var f in SteamFiles) + Assert.Equal("new:" + f, File.ReadAllText(Path.Combine(install, "SteamIsolation", f))); + Assert.False(File.Exists(Path.Combine(install, "SteamIsolation", "old-only.dll"))); + Assert.DoesNotContain("Kept the existing SteamIsolation folder", res.Stdout); + } + + [Theory, MemberData(nameof(Engines))] + public void A_payload_without_steam_isolation_does_not_invent_a_folder(string engine) + { + using var t = new Temp(); + var install = PopulatedSelfContainedInstall(t); + var stage = SelfContained(t.Sub("stage"), "new"); + + Assert.Equal(0, Ps(engine, InstallCall(stage, install, t.Root) + "$r = 1").ExitCode); + Assert.False(Directory.Exists(Path.Combine(install, "SteamIsolation"))); + } + + [Theory, MemberData(nameof(Engines))] + public void A_partial_steam_isolation_set_in_the_stage_is_refused_before_anything_is_touched(string engine) + { + using var t = new Temp(); + var install = PopulatedSelfContainedInstall(t); + PutSteamIsolation(install, "old"); + var before = Hashes(install); + var stage = SelfContained(t.Sub("stage"), "new"); + PutSteamIsolation(stage, "new"); + File.Delete(Path.Combine(stage, "SteamIsolation", "MultiSeat.SteamHook32.dll")); + + var res = Ps(engine, InstallCall(stage, install, t.Root) + "$r = 'unreachable'"); + Assert.NotEqual(0, res.ExitCode); + Assert.Contains("staged SteamIsolation folder is missing", res.Stdout + res.Stderr); + Assert.Contains("MultiSeat.SteamHook32.dll", res.Stdout + res.Stderr); + Assert.Equal(before, Hashes(install)); + Assert.False(Directory.Exists(Path.Combine(t.Root, "install-backups")), "no backup should be made for a refused stage"); + } + + // -- Wiring: where the helpers come from ------------------------------------------- + + [Fact] + public void The_source_build_builds_the_helpers_only_when_asked_and_never_reuses_old_output() + { + var text = string.Join("\n", InstallerLines()); + Assert.Contains("[switch]$BuildSteamIsolation", text); + var iGate = text.IndexOf("if ($BuildSteamIsolation)", StringComparison.Ordinal); + Assert.True(iGate > 0, "the build must sit behind the switch"); + var iBuild = text.IndexOf("build.ps1", iGate, StringComparison.Ordinal); + var iStaged = text.IndexOf("Get-MissingSteamIsolationFiles", iGate, StringComparison.Ordinal); + Assert.True(iBuild > iGate && iStaged > iBuild, "build first, then check all four files arrived"); + // Built straight into the stage: no artifacts\ folder that an earlier run could have left. + Assert.DoesNotContain("artifacts", text); + } + + [Fact] + public void The_release_workflow_ships_all_four_helpers_in_the_asset() + { + var yml = File.ReadAllText(Path.Combine(RepoRoot, ".github", "workflows", "release.yml")); + Assert.Contains("needs: steam-isolation", yml); + // Uploaded by one job and downloaded by the next, under one name. + Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(yml, "name: steam-isolation-binaries").Count); + Assert.Contains("path: publish/MultiSeat/SteamIsolation", yml); + // The payload check is what stops a release that would delete the helpers on upgrade. + foreach (var f in SteamFiles) Assert.Contains(f, yml); + Assert.Contains("an upgrade would remove the Steam isolation helpers", yml); + } + // -- Wiring of install-service.ps1 itself ---------------------------------------- private static string[] InstallerLines() => File.ReadAllLines(Path.Combine(RepoRoot, "scripts", "install-service.ps1")); diff --git a/src/MultiSeat.Tests/Sessions/SteamIsolationLaunchPlanTests.cs b/src/MultiSeat.Tests/Sessions/SteamIsolationLaunchPlanTests.cs new file mode 100644 index 0000000..d4cba11 --- /dev/null +++ b/src/MultiSeat.Tests/Sessions/SteamIsolationLaunchPlanTests.cs @@ -0,0 +1,249 @@ +using System.Reflection.PortableExecutable; +using MultiSeat.Service.Configuration; +using MultiSeat.Service.Sessions; +using Xunit; + +namespace MultiSeat.Tests.Sessions; + +public sealed class SteamIsolationLaunchPlanTests : IDisposable +{ + private readonly string _directory = Path.Combine(Path.GetTempPath(), "MultiSeat Steam " + Guid.NewGuid()); + + public SteamIsolationLaunchPlanTests() => Directory.CreateDirectory(_directory); + public void Dispose() => Directory.Delete(_directory, recursive: true); + + [Fact] + public void DisabledPreservesSteamCommandWithoutRequiringComponents() + { + var plan = SteamIsolationLaunchPlan.Create("steam.exe", "-bigpicture", new()); + Assert.Equal("steam.exe", plan.ExecutablePath); + Assert.Equal("-bigpicture", plan.Arguments); + Assert.False(plan.Supervised); + } + + [Theory] + [InlineData("steamwebhelper.exe")] + [InlineData("my-steam.exe")] + [InlineData("sunshine.exe")] + public void EnabledDoesNotWrapOtherExecutables(string executable) + { + var plan = SteamIsolationLaunchPlan.Create(executable, "--unchanged", Enabled()); + Assert.Equal(executable, plan.ExecutablePath); + Assert.Equal("--unchanged", plan.Arguments); + Assert.False(plan.Supervised); + } + + [Theory] + [InlineData("steam.exe")] + [InlineData(".\\steam.exe")] + public void EnabledRefusesRelativeSteamPath(string executable) => + Assert.Throws(() => SteamIsolationLaunchPlan.Create(executable, null, Enabled())); + + // Windows drops trailing dots and spaces from the last path segment and reads ":stream" as a + // stream of the same file, so each of these starts the real steam.exe. They must be wrapped + // like the plain name, and the launcher must be told the name Windows resolves to. + [Theory] + [InlineData("steam.exe.", "steam.exe")] + [InlineData("steam.exe ", "steam.exe")] + [InlineData("steam.exe. .", "steam.exe")] + [InlineData("STEAM.EXE...", "STEAM.EXE")] + [InlineData("steam.exe::$DATA", "steam.exe")] + public void WindowsEquivalentNamesOfSteamAreStillWrapped(string given, string resolved) + { + Fixture("steam.exe", Machine.Amd64); + var options = CompleteComponents(Machine.Amd64); + var plan = SteamIsolationLaunchPlan.Create(Path.Combine(_directory, given), "-bigpicture", options); + Assert.True(plan.Supervised); + Assert.Equal(options.SteamIsolationLauncherPath, plan.ExecutablePath); + Assert.Equal($"--steam \"{Path.Combine(_directory, resolved)}\" -- -bigpicture", plan.Arguments); + } + + [Fact] + public void ForwardSlashPathsAreRecognizedToo() + { + Fixture("steam.exe", Machine.Amd64); + var plan = SteamIsolationLaunchPlan.Create( + _directory.Replace('\\', '/') + "/steam.exe.", null, CompleteComponents(Machine.Amd64)); + Assert.True(plan.Supervised); + } + + // A name that only Windows would normalize into steam.exe must not slip past as "not Steam" + // when the path is unusable: it is refused like any other bad Steam path. + [Theory] + [InlineData("steam.exe.")] + [InlineData(".\\steam.exe ")] + [InlineData("C:steam.exe.")] + public void EnabledRefusesRelativeSteamPathWithTrailingDotsOrSpaces(string executable) => + Assert.Throws(() => SteamIsolationLaunchPlan.Create(executable, null, Enabled())); + + [Theory] + [InlineData("steam.exe.bak")] + [InlineData("steam.exe.exe")] + [InlineData("steam .exe")] + [InlineData("xsteam.exe.")] + [InlineData("steamwebhelper.exe.")] + [InlineData("steam.exe:alt.exe")] + public void LookalikeNamesAreNotWrapped(string name) + { + var executable = Path.Combine(_directory, name); + var plan = SteamIsolationLaunchPlan.Create(executable, "--unchanged", Enabled()); + Assert.Equal(executable, plan.ExecutablePath); + Assert.Equal("--unchanged", plan.Arguments); + Assert.False(plan.Supervised); + } + + [Theory] + [InlineData(@"C:\Steam\steam.exe", @"C:\Steam\steam.exe")] + [InlineData(@"C:\Steam\Steam.EXE. ", @"C:\Steam\Steam.EXE")] + [InlineData(@"C:\Program Files (x86)\Steam\steam.exe::$DATA", @"C:\Program Files (x86)\Steam\steam.exe")] + [InlineData(@"steam.exe", @"steam.exe")] + [InlineData(@"C:steam.exe.", @"C:steam.exe")] + public void NormalizationOnlyRewritesTheLastSegment(string given, string expected) + { + Assert.True(SteamIsolationLaunchPlan.TryNormalizeSteamPath(given, out var normalized)); + Assert.Equal(expected, normalized); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(@"C:\Steam.exe\game.exe")] + [InlineData(@"C:\Steam\steam.exe\readme.txt")] + public void NormalizationRejectsEverythingThatIsNotASteamExecutable(string given) => + Assert.False(SteamIsolationLaunchPlan.TryNormalizeSteamPath(given, out _)); + + [Theory] + [InlineData(Machine.Amd64)] + [InlineData(Machine.I386)] + public void WrapsBothArchitecturesAndPreservesArgumentTail(Machine machine) + { + var steam = Fixture("StEaM.exe", machine); + var options = CompleteComponents(machine); + const string arguments = "-bigpicture -applaunch 123 \"save game\""; + var plan = SteamIsolationLaunchPlan.Create(steam, arguments, options); + Assert.Equal(options.SteamIsolationLauncherPath, plan.ExecutablePath); + Assert.Equal($"--steam \"{steam}\" -- {arguments}", plan.Arguments); + Assert.True(plan.Supervised); + } + + [Fact] + public void MissingHelperFailsBeforeLaunchingSteam() + { + var steam = Fixture("steam.exe", Machine.Amd64); + var error = Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + Assert.Contains("launcher", error.Message); + } + + [Fact] + public void MissingHookFailsBeforeLaunchingSteam() + { + var steam = Fixture("steam.exe", Machine.Amd64); + Fixture("MultiSeat.SteamLauncher.exe", Machine.Amd64); + var error = Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + Assert.Contains("hook", error.Message); + } + + [Fact] + public void X86SteamRequiresX86Helper() + { + var steam = Fixture("steam.exe", Machine.I386); + Fixture("MultiSeat.SteamLauncher.exe", Machine.Amd64); + Fixture("MultiSeat.SteamHook32.dll", Machine.I386); + var error = Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + Assert.Contains("32-bit", error.Message); + } + + [Fact] + public void WrongHookArchitectureFails() + { + var steam = Fixture("steam.exe", Machine.Amd64); + CompleteComponents(Machine.Amd64); + Fixture("MultiSeat.SteamHook.dll", Machine.I386); + var error = Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + Assert.Contains("wrong architecture", error.Message); + } + + [Fact] + public void CorruptSteamExecutableFails() + { + var steam = Path.Combine(_directory, "steam.exe"); + File.WriteAllText(steam, "not an executable"); + Fixture("MultiSeat.SteamLauncher.exe", Machine.Amd64); + Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + } + + [Fact] + public void WrongPrimaryLauncherArchitectureFails() + { + var steam = Fixture("steam.exe", Machine.I386); + CompleteComponents(Machine.I386); + Fixture("MultiSeat.SteamLauncher.exe", Machine.I386); + var error = Assert.Throws(() => SteamIsolationLaunchPlan.Create(steam, null, Enabled())); + Assert.Contains("wrong architecture", error.Message); + } + + [Fact] + public void NoSteamArgumentsStillHasSeparator() + { + var steam = Fixture("steam.exe", Machine.Amd64); + var plan = SteamIsolationLaunchPlan.Create(steam, null, CompleteComponents(Machine.Amd64)); + Assert.Equal($"--steam \"{steam}\" --", plan.Arguments); + } + + [Fact] + public void ReadyEventPrecedesSeparatorAndPreservesSteamArgumentTail() + { + var steam = Fixture("steam.exe", Machine.Amd64); + const string arguments = "-bigpicture --literal \"hello -- world\""; + var eventName = @"Global\MultiSeatSteamReady-" + Guid.NewGuid().ToString("N"); + var plan = SteamIsolationLaunchPlan.Create(steam, arguments, CompleteComponents(Machine.Amd64)) + .WithReadyEvent(eventName); + Assert.Equal($"--steam \"{steam}\" --ready-event {eventName} -- {arguments}", plan.Arguments); + } + + [Theory] + [InlineData("arbitrary-event")] + [InlineData("Global\\MultiSeatSteamReady-x --evil")] + public void ReadyEventRejectsInvalidNames(string eventName) + { + var steam = Fixture("steam.exe", Machine.Amd64); + var plan = SteamIsolationLaunchPlan.Create(steam, null, CompleteComponents(Machine.Amd64)); + Assert.Throws(() => plan.WithReadyEvent(eventName)); + } + + private MultiSeatOptions Enabled() => new() + { + SteamIsolationEnabled = true, + SteamIsolationLauncherPath = Path.Combine(_directory, "MultiSeat.SteamLauncher.exe") + }; + + private MultiSeatOptions CompleteComponents(Machine steamMachine) + { + Fixture("MultiSeat.SteamLauncher.exe", Machine.Amd64); + Fixture(steamMachine == Machine.I386 ? "MultiSeat.SteamHook32.dll" : "MultiSeat.SteamHook.dll", steamMachine); + if (steamMachine == Machine.I386) Fixture("MultiSeat.SteamLauncher32.exe", Machine.I386); + return Enabled(); + } + + // Minimal PE fixtures are never executed. The architecture preflight parses their headers. + private string Fixture(string name, Machine machine) + { + var path = Path.Combine(_directory, name); + var bytes = new byte[1024]; + using (var writer = new BinaryWriter(new MemoryStream(bytes))) + { + writer.Write((ushort)0x5a4d); + writer.BaseStream.Position = 0x3c; + writer.Write(0x80); + writer.BaseStream.Position = 0x80; + writer.Write(0x00004550); + writer.Write((ushort)machine); + writer.BaseStream.Position = 0x94; + writer.Write((ushort)(machine == Machine.Amd64 ? 240 : 224)); + writer.Write((ushort)0x0002); + writer.Write((ushort)(machine == Machine.Amd64 ? 0x20b : 0x10b)); + } + File.WriteAllBytes(path, bytes); + return path; + } +} diff --git a/src/MultiSeat.Tests/Sessions/SteamIsolationOptionsTests.cs b/src/MultiSeat.Tests/Sessions/SteamIsolationOptionsTests.cs new file mode 100644 index 0000000..64230f6 --- /dev/null +++ b/src/MultiSeat.Tests/Sessions/SteamIsolationOptionsTests.cs @@ -0,0 +1,72 @@ +using MultiSeat.Service.Configuration; +using MultiSeat.Service.Interop; +using MultiSeat.Service.Sessions; +using Xunit; + +namespace MultiSeat.Tests.Sessions; + +public sealed class SteamIsolationOptionsTests +{ + [Fact] + public void ReadyTimeoutDefaultsToThreeMinutes() => + Assert.Equal(TimeSpan.FromSeconds(180), new MultiSeatOptions().UsableSteamIsolationReadyTimeout); + + // A typo must not make every isolated launch fail at once (0, negative) or hold a seat's + // lifecycle gate for hours (very large), so the value is kept in 10 s .. 15 min. + [Theory] + [InlineData(-5, 10)] + [InlineData(0, 10)] + [InlineData(9, 10)] + [InlineData(10, 10)] + [InlineData(45, 45)] + [InlineData(900, 900)] + [InlineData(901, 900)] + [InlineData(int.MaxValue, 900)] + public void ReadyTimeoutIsClampedToAUsableRange(int configured, int expectedSeconds) + { + var options = new MultiSeatOptions { SteamIsolationReadyTimeoutSeconds = configured }; + Assert.Equal(TimeSpan.FromSeconds(expectedSeconds), options.UsableSteamIsolationReadyTimeout); + } + + [Fact] + public void ShippedSettingsFileCarriesTheSameDefault() + { + // The default in code and the default in the shipped appsettings.json are two copies of + // one number; if they drift, a host that never touched the file behaves differently from + // what the code's own default says. + var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + // The shipped file carries comments, which the default JSON reader rejects. + using var document = System.Text.Json.JsonDocument.Parse(File.ReadAllText(path), + new System.Text.Json.JsonDocumentOptions + { + CommentHandling = System.Text.Json.JsonCommentHandling.Skip, + AllowTrailingCommas = true + }); + var shipped = document.RootElement.GetProperty("MultiSeat") + .GetProperty("SteamIsolationReadyTimeoutSeconds").GetInt32(); + Assert.Equal(new MultiSeatOptions().SteamIsolationReadyTimeoutSeconds, shipped); + } + + // The supervised launcher must not get a console window in the seat; every other launch + // keeps the new console it always had. + [Fact] + public void SupervisedLaunchGetsNoConsoleWindow() + { + var flags = ProcessInjector.CreationFlagsFor(supervised: true); + Assert.True((flags & Kernel32.CREATE_NO_WINDOW) != 0); + Assert.True((flags & Kernel32.CREATE_NEW_CONSOLE) == 0); + Assert.True((flags & Kernel32.CREATE_UNICODE_ENVIRONMENT) != 0); + Assert.True((flags & Kernel32.NORMAL_PRIORITY_CLASS) != 0); + } + + [Fact] + public void OrdinaryLaunchKeepsItsNewConsole() + { + var flags = ProcessInjector.CreationFlagsFor(supervised: false); + Assert.True((flags & Kernel32.CREATE_NEW_CONSOLE) != 0); + Assert.True((flags & Kernel32.CREATE_NO_WINDOW) == 0); + Assert.Equal( + Kernel32.CREATE_UNICODE_ENVIRONMENT | Kernel32.CREATE_NEW_CONSOLE | Kernel32.NORMAL_PRIORITY_CLASS, + flags); + } +} diff --git a/src/MultiSeat.Tests/Sessions/SteamIsolationReadinessTests.cs b/src/MultiSeat.Tests/Sessions/SteamIsolationReadinessTests.cs new file mode 100644 index 0000000..de4d5c3 --- /dev/null +++ b/src/MultiSeat.Tests/Sessions/SteamIsolationReadinessTests.cs @@ -0,0 +1,77 @@ +using System.Security.AccessControl; +using System.Security.Principal; +using MultiSeat.Service; +using MultiSeat.Service.Interop; +using MultiSeat.Service.Sessions; +using Xunit; + +namespace MultiSeat.Tests.Sessions; + +public sealed class SteamIsolationReadinessTests +{ + [Fact] + public void SecurityGrantsOnlyHostAdminsAndExactSeatMinimalRights() + { + var seat = new SecurityIdentifier("S-1-5-21-100-200-300-1001"); + var security = new RawSecurityDescriptor(SteamIsolationReadiness.BuildSddl(seat)); + Assert.True(security.ControlFlags.HasFlag(ControlFlags.DiscretionaryAclProtected)); + Assert.Equal(3, security.DiscretionaryAcl!.Count); + var rule = Assert.IsType(security.DiscretionaryAcl[2]); + Assert.Equal(seat, rule.SecurityIdentifier); + Assert.Equal(0x00100002, rule.AccessMask); + Assert.Equal(AceQualifier.AccessAllowed, rule.AceQualifier); + } + + [Fact] + public async Task RealNamedEventCanBeSignaledByItsIntendedAccount() + { + using var identity = WindowsIdentity.GetCurrent(); + using var ready = new SteamIsolationReadiness(identity.User!); + using var opened = EventWaitHandle.OpenExisting(ready.Name); + Assert.False(opened.WaitOne(0)); + opened.Set(); + await ready.WaitAsync(Kernel32.GetCurrentProcess(), TimeSpan.FromSeconds(1), default); + Assert.True(opened.WaitOne(0)); // manual reset + } + + [Fact] + public async Task WaitsForInitializationInsteadOfAssumingRunningMeansReady() + { + var checks = 0; + await SteamIsolationReadiness.WaitCoreAsync(() => ++checks >= 3, () => null, + TimeSpan.FromSeconds(2), default); + Assert.True(checks >= 3); + } + + [Theory] + [InlineData(0u)] + [InlineData(9u)] + public async Task ExitWithoutReadinessFailsEvenWithZeroExitCode(uint code) + { + var error = await Assert.ThrowsAsync(() => + SteamIsolationReadiness.WaitCoreAsync(() => false, () => code, TimeSpan.FromSeconds(1), default)); + Assert.Contains($"code {code}", error.Message); + } + + [Fact] + public async Task MissingReadinessTimesOut() => + await Assert.ThrowsAsync(() => SteamIsolationReadiness.WaitCoreAsync( + () => false, () => null, TimeSpan.Zero, default)); + + [Fact] + public async Task TimeoutMessageNamesTheLimitAndTheSettingThatChangesIt() + { + var error = await Assert.ThrowsAsync(() => SteamIsolationReadiness.WaitCoreAsync( + () => false, () => null, TimeSpan.Zero, default)); + Assert.Contains("within 0 s", error.Message); + Assert.Contains("SteamIsolationReadyTimeoutSeconds", error.Message); + } + + [Fact] + public async Task CancellationInterruptsReadinessWait() + { + using var cancellation = new CancellationTokenSource(TimeSpan.FromMilliseconds(80)); + await Assert.ThrowsAnyAsync(() => SteamIsolationReadiness.WaitCoreAsync( + () => false, () => null, TimeSpan.FromSeconds(30), cancellation.Token)); + } +}