From f6106b55a40fd0a49682e2bca713cab1a7ec53e3 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 3 Sep 2026 16:57:49 -0500 Subject: [PATCH 1/5] fix(ci): stop the Windows supervisor readiness waits failing for machine speed The windows-supervisor-behavior job is red on main. It is not red because of anything this suite exists to catch. Every wait this touches polls for a marker that a spawned process writes, and what it asserts is that the marker APPEARS -- never that it appears quickly. The per-site deadlines (5-30s) were tuned on a runner that could start a secondary-user session in a second or two. On the hosted windows-2025 image that now regularly takes far longer: creating the logon session and loading a fresh profile is most of the cost, and it is all paid before the spawned script runs its first statement. A control branch -- origin/main plus one comment, with windows-job-supervisor.{cs,test.ps1} byte-identical to main -- fails the same step, so this predates and is independent of any feature work. The seven in-process readiness waits now share one budget, and the one that runs inside a spawned child script (which cannot see script scope) carries the same number literally. This bounds patience, not behaviour: a marker that never arrives still fails, and the job's own 20-minute timeout still bounds the run. The two 2s loops that observe a scheduler engine are left alone -- they expire tolerantly rather than throwing -- as is the 6s churn workload, where the duration IS the workload. Also name the cause when something throws. The supervisor wraps a quarantine failure as "Terminal producer identity quarantine failed" with the real fault as InnerException, and as an AggregateException when the producer failed too; PowerShell printed only the outer message, so the CI failure on #84 said which stage failed and never why. A script-scope trap now prints the whole chain, including AggregateException members, before the exception continues to terminate the run -- so the next occurrence is diagnosable instead of opaque. Verified with pwsh 7.6.3: the file parses clean, and both helpers were exercised against the exact exception shape the supervisor throws. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie --- scripts/windows-job-supervisor.test.ps1 | 74 ++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/scripts/windows-job-supervisor.test.ps1 b/scripts/windows-job-supervisor.test.ps1 index 635bf65c..1876f870 100644 --- a/scripts/windows-job-supervisor.test.ps1 +++ b/scripts/windows-job-supervisor.test.ps1 @@ -1,6 +1,64 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest +# How long a readiness wait will wait. +# +# Every wait that uses this polls for a marker some spawned process writes, and +# what it asserts is that the marker APPEARS -- never that it appears quickly. +# The old per-site values (5-30s) were tuned on a runner that could start a +# secondary-user session in a second or two. Starting one now regularly takes +# far longer on the hosted windows-2025 image, where creating the logon session +# and loading a fresh profile is most of the cost and is paid before the +# spawned script runs its first statement. Those deadlines began firing for +# machine speed rather than for anything this suite exists to catch. +# +# This bounds patience, not behaviour: a marker that never arrives still fails, +# and the job's own 20-minute timeout still bounds the run. +$script:ReadinessTimeout = [TimeSpan]::FromSeconds(60) + +function Get-ReadinessDeadline { + return [DateTime]::UtcNow.Add($script:ReadinessTimeout) +} + +# Name the cause when something throws. +# +# The supervisor wraps a quarantine failure as "Terminal producer identity +# quarantine failed" with the real fault as InnerException, and as an +# AggregateException when the producer failed too. PowerShell prints only the +# outer message, so a CI failure said which stage failed and never why. This +# prints the whole chain before the exception continues to terminate the run. +function Write-ExceptionChain { + param([Parameter(Mandatory)][AllowNull()][object]$Failure) + + $exception = if ($Failure -is [Management.Automation.ErrorRecord]) { + $Failure.Exception + } else { + $Failure + } + $depth = 0 + while ($null -ne $exception -and $depth -lt 12) { + Write-Host "cause[$depth] $($exception.GetType().FullName): $($exception.Message)" + if ($exception -is [AggregateException]) { + $index = 0 + foreach ($inner in $exception.InnerExceptions) { + Write-Host " aggregate[$index] $($inner.GetType().FullName): $($inner.Message)" + $index++ + } + } + $exception = $exception.InnerException + $depth++ + } +} + +trap { + Write-Host '--- windows-job-supervisor.test.ps1 failure ---' + Write-ExceptionChain -Failure $_ + if ($null -ne $_.ScriptStackTrace) { + Write-Host $_.ScriptStackTrace + } + break +} + if (-not $IsWindows -or [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'X64') { throw 'Windows Job Object runtime tests require Windows x64.' } @@ -513,7 +571,7 @@ if ( function Assert-ProcessExited { param([Parameter(Mandatory)][int]$ProcessId) - $deadline = [DateTime]::UtcNow.AddSeconds(10) + $deadline = Get-ReadinessDeadline do { try { $process = [Diagnostics.Process]::GetProcessById($ProcessId) @@ -532,7 +590,7 @@ function Assert-BoundedTextMarker { [Parameter(Mandatory)][string]$Expected ) - $deadline = [DateTime]::UtcNow.AddSeconds(5) + $deadline = Get-ReadinessDeadline do { try { if ([IO.File]::Exists($Path)) { @@ -1875,7 +1933,7 @@ $attack = Start-Process ` -RedirectStandardError $env:OPENCOVEN_ROOT_ATTACK_STDERR ` -PassThru try { - $deadline = [DateTime]::UtcNow.AddSeconds(10) + $deadline = Get-ReadinessDeadline while (-not [IO.File]::Exists($env:OPENCOVEN_ROOT_ATTACK_COMPLETE)) { if ($attack.HasExited) { throw "Root process attack failed: $( @@ -2428,7 +2486,7 @@ while (-not [IO.File]::Exists($env:OPENCOVEN_HANDOFF_RACE_STOP)) { -RedirectStandardError (Join-Path $operatorPrivateRoot 'handoff-race.stderr') ` -PassThru try { - $raceDeadline = [DateTime]::UtcNow.AddSeconds(10) + $raceDeadline = Get-ReadinessDeadline while (-not [IO.File]::Exists($raceReady)) { if ($raceProcess.HasExited) { throw 'Artifact replacement race exited before replacing the record.' @@ -3262,7 +3320,7 @@ public static class UnsupervisedLogonProcess "-NoLogo -NoProfile -NonInteractive -File `"$lateRegistrarScript`" -UserName `"$($serviceEscapeContext.User.UserName)`"", $serviceEscapeContext.User.RootPath ) - $lateReadyDeadline = [DateTime]::UtcNow.AddSeconds(20) + $lateReadyDeadline = Get-ReadinessDeadline while (-not [IO.File]::Exists($lateRegistrarReady)) { try { $lateRegistrar = [Diagnostics.Process]::GetProcessById($lateRegistrarPid) @@ -3783,7 +3841,7 @@ exit 23 "-NoLogo -NoProfile -NonInteractive -File `"$failureSleeperScript`"", $failureEscapeContext.User.RootPath ) - $failureSleeperDeadline = [DateTime]::UtcNow.AddSeconds(20) + $failureSleeperDeadline = Get-ReadinessDeadline while (-not [IO.File]::Exists($failureSleeperReady)) { try { $failureSleeper = [Diagnostics.Process]::GetProcessById( @@ -4039,7 +4097,7 @@ Start-Sleep -Seconds 300 "-NoLogo -NoProfile -NonInteractive -File `"$setupPath`"", $Context.User.RootPath ) - $readyDeadline = [DateTime]::UtcNow.AddSeconds(30) + $readyDeadline = Get-ReadinessDeadline while (-not [IO.File]::Exists($readyPath)) { try { $setupProcess = [Diagnostics.Process]::GetProcessById($setupPid) @@ -4533,7 +4591,7 @@ Add-Type -TypeDefinition ([IO.File]::ReadAllText('$($retainedHandleSource.Replac $retainedRootScript, @" `$descendant = Start-Process -FilePath '$($trustedPwsh.Replace("'", "''"))' -ArgumentList @('-NoLogo','-NoProfile','-NonInteractive','-File','$($retainedHandleScript.Replace("'", "''"))') -RedirectStandardOutput '$((Join-Path $root 'retained-stdout.txt').Replace("'", "''"))' -RedirectStandardError '$((Join-Path $root 'retained-stderr.txt').Replace("'", "''"))' -PassThru -`$deadline = [DateTime]::UtcNow.AddSeconds(10) +`$deadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists('$($retainedPidPath.Replace("'", "''"))')) { if (`$descendant.HasExited) { throw 'Retained Job handle descendant exited before reporting readiness.' From d17d3bfbdc423bfdf6690d991f9259c7296f1d51 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 3 Sep 2026 17:17:17 -0500 Subject: [PATCH 2/5] fix(ci): use a scope-independent readiness literal The shared Get-ReadinessDeadline helper was wrong, and CI said so: the protected-root-process test failed with Protected root process did not execute normally: Get-ReadinessDeadline: C:\Users\runneradmin\AppData\Local\Temp\opencoven... Several of these readiness waits do not run in this script. They live inside here-strings that are written out and executed as their own child scripts, in a fresh process where a function defined in this file does not exist. The helper read better and failed at runtime in exactly the places that matter. Each site now carries the 60-second literal, which works the same in-process and in a spawned child. The reasoning that was in the helper's comment stays at the top of the file, including why the literal is deliberate. Found by the exception-chain diagnostics added in the previous commit, which surfaced the child's stderr instead of a bare outer message. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie --- scripts/windows-job-supervisor.test.ps1 | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/windows-job-supervisor.test.ps1 b/scripts/windows-job-supervisor.test.ps1 index 1876f870..1f50a83a 100644 --- a/scripts/windows-job-supervisor.test.ps1 +++ b/scripts/windows-job-supervisor.test.ps1 @@ -1,24 +1,24 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest -# How long a readiness wait will wait. +# Readiness waits are 60 seconds, written out at each site. # -# Every wait that uses this polls for a marker some spawned process writes, and -# what it asserts is that the marker APPEARS -- never that it appears quickly. -# The old per-site values (5-30s) were tuned on a runner that could start a +# Every one of them polls for a marker some spawned process writes, and what it +# asserts is that the marker APPEARS -- never that it appears quickly. The old +# per-site values (5-30s) were tuned on a runner that could start a # secondary-user session in a second or two. Starting one now regularly takes # far longer on the hosted windows-2025 image, where creating the logon session # and loading a fresh profile is most of the cost and is paid before the # spawned script runs its first statement. Those deadlines began firing for # machine speed rather than for anything this suite exists to catch. # +# Deliberately a literal at each site rather than one shared helper: several of +# these waits live inside here-strings that are written out and run as their +# own child scripts, where a function defined in this file does not exist. A +# helper reads better and fails at runtime in exactly the places that matter. +# # This bounds patience, not behaviour: a marker that never arrives still fails, # and the job's own 20-minute timeout still bounds the run. -$script:ReadinessTimeout = [TimeSpan]::FromSeconds(60) - -function Get-ReadinessDeadline { - return [DateTime]::UtcNow.Add($script:ReadinessTimeout) -} # Name the cause when something throws. # @@ -571,7 +571,7 @@ if ( function Assert-ProcessExited { param([Parameter(Mandatory)][int]$ProcessId) - $deadline = Get-ReadinessDeadline + $deadline = [DateTime]::UtcNow.AddSeconds(60) do { try { $process = [Diagnostics.Process]::GetProcessById($ProcessId) @@ -590,7 +590,7 @@ function Assert-BoundedTextMarker { [Parameter(Mandatory)][string]$Expected ) - $deadline = Get-ReadinessDeadline + $deadline = [DateTime]::UtcNow.AddSeconds(60) do { try { if ([IO.File]::Exists($Path)) { @@ -1933,7 +1933,7 @@ $attack = Start-Process ` -RedirectStandardError $env:OPENCOVEN_ROOT_ATTACK_STDERR ` -PassThru try { - $deadline = Get-ReadinessDeadline + $deadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists($env:OPENCOVEN_ROOT_ATTACK_COMPLETE)) { if ($attack.HasExited) { throw "Root process attack failed: $( @@ -2486,7 +2486,7 @@ while (-not [IO.File]::Exists($env:OPENCOVEN_HANDOFF_RACE_STOP)) { -RedirectStandardError (Join-Path $operatorPrivateRoot 'handoff-race.stderr') ` -PassThru try { - $raceDeadline = Get-ReadinessDeadline + $raceDeadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists($raceReady)) { if ($raceProcess.HasExited) { throw 'Artifact replacement race exited before replacing the record.' @@ -3320,7 +3320,7 @@ public static class UnsupervisedLogonProcess "-NoLogo -NoProfile -NonInteractive -File `"$lateRegistrarScript`" -UserName `"$($serviceEscapeContext.User.UserName)`"", $serviceEscapeContext.User.RootPath ) - $lateReadyDeadline = Get-ReadinessDeadline + $lateReadyDeadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists($lateRegistrarReady)) { try { $lateRegistrar = [Diagnostics.Process]::GetProcessById($lateRegistrarPid) @@ -3841,7 +3841,7 @@ exit 23 "-NoLogo -NoProfile -NonInteractive -File `"$failureSleeperScript`"", $failureEscapeContext.User.RootPath ) - $failureSleeperDeadline = Get-ReadinessDeadline + $failureSleeperDeadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists($failureSleeperReady)) { try { $failureSleeper = [Diagnostics.Process]::GetProcessById( @@ -4097,7 +4097,7 @@ Start-Sleep -Seconds 300 "-NoLogo -NoProfile -NonInteractive -File `"$setupPath`"", $Context.User.RootPath ) - $readyDeadline = Get-ReadinessDeadline + $readyDeadline = [DateTime]::UtcNow.AddSeconds(60) while (-not [IO.File]::Exists($readyPath)) { try { $setupProcess = [Diagnostics.Process]::GetProcessById($setupPid) From 82b013add7845be0a2b36ec7a5d0b0daf74f63e7 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 3 Sep 2026 17:41:59 -0500 Subject: [PATCH 3/5] fix(ci): re-pin the documented supervisor test digest Editing scripts/windows-job-supervisor.test.ps1 invalidated the size and SHA-256 recorded for it in docs/phase1-conformance.md, which client-v1-conformance-workflow.test.ts asserts row by row. That is the guard working: the harness metadata is documented so a change to a supervised script cannot land unnoticed. Only the PowerShell row moves. The earlier C# diagnostic -- naming the process whose primary-token SID could not be read -- is reverted: windows-job-supervisor.cs is embedded byte-for-byte in the conformance workflow and pinned by blob in phase1-conformance.lock.json's harnessAuthority, so touching it means re-embedding a 290 KB copy and re-pinning the harness authority to a new revision. That ceremony is not worth a better error message, and it is not needed: the PowerShell exception-chain diagnostics already named the cause. For the record, they named it precisely: Terminal producer identity quarantine failed. -> Terminal isolated identity quarantine failed. -> AggregateException: WTS process primary token SID query was ambiguous. So the quarantine is not timing out. EnumerateProcessPrimaryTokenSids fails closed when any process reports a null primary-token SID, excusing only PID 0, and on the current windows-2025 image some other process now reports null. The readiness budgets here do not address that, and deliberately do not pretend to. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie --- docs/phase1-conformance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index fbb2035f..8d341c33 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1024,7 +1024,7 @@ The later SDK validator repin must use these exact committed file bytes: | `scripts/phase1-windows-supervisor-build.sh` | 4,646 | `713a9e0282887ade3e243b5ba175794d74cdb02c28c38dcd41491c9505812770` | | `scripts/phase1-windows-supervisor-install.ps1` | 1,743 | `2baab275f0bb6789884cded5f6185d00bfa5348b9e7c3ad1e5575353639101d5` | | `scripts/windows-job-supervisor.cs` | 289,604 | `20ac2be79c6ec6ebaccdf78543b68540fe2645e37e96c6715d477f19229b943f` | -| `scripts/windows-job-supervisor.test.ps1` | 168,772 | `74df69934cf26c7d6c083ce74abe840f472613b4df39d93584f5cb6391b951bf` | +| `scripts/windows-job-supervisor.test.ps1` | 171,179 | `55e9cf065e2dc7cc656c6aa8cc9ea53542259d3d7eee55c368c6cf0fc6356ab9` | Before parsing or executing SDK authority, the harness queries the verified checkout with `git rev-parse --show-object-format`, accepts only `sha1` or From 6d4c3057df6b85aaa5ca3f95dda261afbdab6390 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 3 Sep 2026 21:50:38 -0500 Subject: [PATCH 4/5] fix(conformance): name the process whose primary token SID cannot be read The quarantine refusal that is failing CI says only that a WTS primary token SID query was ambiguous. That is unactionable: a process that exited between enumeration and read and a permanently unreadable system process are opposite faults with opposite fixes, and the message named neither. EnumerateProcessPrimaryTokenSids now reports the process id and session id it could not resolve. The refusal itself is unchanged and still fails closed -- an owner this enumeration cannot read is an owner it cannot rule out -- so this decides no policy. It only makes the next failure diagnosable, which is what the choice between skipping unreadable entries, excusing session-0 system processes, and retrying has to be made on. windows-job-supervisor.cs is embedded byte-for-byte in the conformance workflow and pinned by digest in the harness authority, so the same change lands in four places at once: the source, the workflow's inline copy, the documented size and SHA-256 for both files, and their blob and SHA-256 in phase1-conformance.lock.json. The authority's revision and tree still point at the current pinned commit; those are repinned to the merge commit afterwards, as every earlier harness change in this repository has been. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie --- .github/workflows/client-v1-conformance.yml | 15 +++++++++++++-- docs/phase1-conformance.md | 4 ++-- phase1-conformance.lock.json | 8 ++++---- scripts/windows-job-supervisor.cs | 15 +++++++++++++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/client-v1-conformance.yml b/.github/workflows/client-v1-conformance.yml index 9f31084b..96acdb27 100644 --- a/.github/workflows/client-v1-conformance.yml +++ b/.github/workflows/client-v1-conformance.yml @@ -4530,8 +4530,19 @@ jobs: { continue; } - throw new InvalidOperationException( - "WTS process primary token SID query was ambiguous."); + // Fails closed: an owner this enumeration cannot read + // is an owner it cannot rule out, so the refusal + // stands. It names the process because the refusal is + // otherwise unactionable -- a process that exited + // between enumeration and read and a permanently + // unreadable system process demand opposite fixes, and + // the message carried neither. + throw new InvalidOperationException(String.Format( + CultureInfo.InvariantCulture, + "WTS process primary token SID query was ambiguous " + + "for process {0} in session {1}.", + information.ProcessId, + information.SessionId)); } string sid = ConvertNativeSidToString( information.pUserSid, diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index 8d341c33..e6cbb2ec 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -998,7 +998,7 @@ The later SDK validator repin must use these exact committed file bytes: | File | Bytes | SHA-256 | | --- | ---: | --- | -| `.github/workflows/client-v1-conformance.yml` | 457,825 | `1904746089bfa3fe079efdc686a9a12dd0f836f4bcfdc4a0df214f9e7e6c52a4` | +| `.github/workflows/client-v1-conformance.yml` | 458,695 | `ba8896d38ad2a02c865abb1eab7d0ae9ab05ddb74d33244aca6d660ecd3937b4` | | `scripts/contract-canary.mjs` | 38,191 | `4eb4d9b693187f110343a4c1efd92e59a9705e25790845bf04b05cb5bac6cbb5` | | `scripts/executable-resolution.mjs` | 9,154 | `31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430` | | `scripts/owned-temp-directory.mjs` | 6,965 | `a9c55c85cf2b7d70310d278bafd2c8e7695d66f4ae38b9c3f1f12fce0b442095` | @@ -1023,7 +1023,7 @@ The later SDK validator repin must use these exact committed file bytes: | `scripts/unix-producer-supervisor.test.sh` | 8,083 | `c7d2d023d4c1f1ba3cdb3da1e95b30af1763602932a91592388bb117de6bd397` | | `scripts/phase1-windows-supervisor-build.sh` | 4,646 | `713a9e0282887ade3e243b5ba175794d74cdb02c28c38dcd41491c9505812770` | | `scripts/phase1-windows-supervisor-install.ps1` | 1,743 | `2baab275f0bb6789884cded5f6185d00bfa5348b9e7c3ad1e5575353639101d5` | -| `scripts/windows-job-supervisor.cs` | 289,604 | `20ac2be79c6ec6ebaccdf78543b68540fe2645e37e96c6715d477f19229b943f` | +| `scripts/windows-job-supervisor.cs` | 290,364 | `69b70a392b92e934491eb75c374a5a24bae7fb1285e1aa387a515513c54d6610` | | `scripts/windows-job-supervisor.test.ps1` | 171,179 | `55e9cf065e2dc7cc656c6aa8cc9ea53542259d3d7eee55c368c6cf0fc6356ab9` | Before parsing or executing SDK authority, the harness queries the verified diff --git a/phase1-conformance.lock.json b/phase1-conformance.lock.json index d6847c61..da176527 100644 --- a/phase1-conformance.lock.json +++ b/phase1-conformance.lock.json @@ -116,8 +116,8 @@ }, { "path": "scripts/windows-job-supervisor.cs", - "blob": "521a3d44e619ce9e3da385d5632e3333c0c63198", - "sha256": "20ac2be79c6ec6ebaccdf78543b68540fe2645e37e96c6715d477f19229b943f" + "blob": "e3c525de275f9d70c319c2a5a0effe6c3d5ed1b3", + "sha256": "69b70a392b92e934491eb75c374a5a24bae7fb1285e1aa387a515513c54d6610" }, { "path": "scripts/contract-canary.mjs", @@ -146,8 +146,8 @@ }, { "path": ".github/workflows/client-v1-conformance.yml", - "blob": "9f31084b5fdd0b9546f3f2010f7c66cf912aeb25", - "sha256": "1904746089bfa3fe079efdc686a9a12dd0f836f4bcfdc4a0df214f9e7e6c52a4" + "blob": "96acdb27dfba14ca82eb446e700785f040529668", + "sha256": "ba8896d38ad2a02c865abb1eab7d0ae9ab05ddb74d33244aca6d660ecd3937b4" } ], "productionDeltas": [ diff --git a/scripts/windows-job-supervisor.cs b/scripts/windows-job-supervisor.cs index 521a3d44..e3c525de 100644 --- a/scripts/windows-job-supervisor.cs +++ b/scripts/windows-job-supervisor.cs @@ -4090,8 +4090,19 @@ private static Dictionary { continue; } - throw new InvalidOperationException( - "WTS process primary token SID query was ambiguous."); + // Fails closed: an owner this enumeration cannot read + // is an owner it cannot rule out, so the refusal + // stands. It names the process because the refusal is + // otherwise unactionable -- a process that exited + // between enumeration and read and a permanently + // unreadable system process demand opposite fixes, and + // the message carried neither. + throw new InvalidOperationException(String.Format( + CultureInfo.InvariantCulture, + "WTS process primary token SID query was ambiguous " + + "for process {0} in session {1}.", + information.ProcessId, + information.SessionId)); } string sid = ConvertNativeSidToString( information.pUserSid, From e737763182a35718a2d56cd599f4d8585db5c35b Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 4 Sep 2026 00:39:48 -0500 Subject: [PATCH 5/5] fix(conformance): skip unreadable session-0 processes in the quarantine The identity quarantine has been failing on every current Windows image: Terminal producer identity quarantine failed. -> Terminal isolated identity quarantine failed. -> AggregateException: WTS process primary token SID query was ambiguous. EnumerateProcessPrimaryTokenSids refused whenever any process reported a null primary token SID, excusing only the Idle process. The protected system processes that virtualization-based security introduces -- Secure System, Registry, and their kin -- expose no primary token SID to any caller, however privileged, and they live in session 0. So the drain became unrunnable on hosts that enable VBS, which is now every hosted runner image. Unreadable session-0 processes are skipped. Anywhere else an unreadable owner is still an owner that cannot be ruled out, so the refusal stands, and it names the process and session. This does not widen what the quarantine proves. The supervised identity is a local account this process creates and logs on with CreateProcessWithLogonW, so its processes hold a readable token and appear in the enumeration with a SID to compare against. A process whose SID cannot be read at all is not one of them. The final zero proof -- no job processes, account disabled, no scheduled tasks, no BITS jobs, no processes owned by the supervised SID -- is unchanged. windows-job-supervisor.cs is embedded byte-for-byte in the conformance workflow and pinned by digest, so the source, the workflow's inline copy, the documented sizes and SHA-256s, and the harness authority blobs all move together. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KacEJmkX5GhkViPUhx9Mie --- .github/workflows/client-v1-conformance.yml | 30 +++++++++++++++------ docs/phase1-conformance.md | 4 +-- phase1-conformance.lock.json | 8 +++--- scripts/windows-job-supervisor.cs | 30 +++++++++++++++------ 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/.github/workflows/client-v1-conformance.yml b/.github/workflows/client-v1-conformance.yml index 96acdb27..28bad32e 100644 --- a/.github/workflows/client-v1-conformance.yml +++ b/.github/workflows/client-v1-conformance.yml @@ -4526,17 +4526,31 @@ jobs: typeof(WTS_PROCESS_INFO_EXW)); if (information.pUserSid == IntPtr.Zero) { - if (information.ProcessId == 0) + // The Idle process, and the protected system processes + // that live in session 0 -- Secure System, Registry, + // and their kin -- expose no primary token SID to any + // caller, however privileged. Refusing on their + // account made the drain unrunnable on hosts that + // enable virtualization-based security, which is every + // current Windows image. + // + // Skipping them does not widen what this proves. The + // supervised identity is a local account this process + // created and logged on with CreateProcessWithLogonW, + // so its processes hold a readable token and appear in + // this enumeration with a SID to compare. A process + // whose SID cannot be read AT ALL is not one of them. + // + // Anywhere else, an owner this enumeration cannot read + // is an owner it cannot rule out, so the refusal + // stands -- and names the process, because a process + // that exited between enumeration and read and a + // permanently unreadable one demand opposite fixes. + if (information.ProcessId == 0 || + information.SessionId == 0) { continue; } - // Fails closed: an owner this enumeration cannot read - // is an owner it cannot rule out, so the refusal - // stands. It names the process because the refusal is - // otherwise unactionable -- a process that exited - // between enumeration and read and a permanently - // unreadable system process demand opposite fixes, and - // the message carried neither. throw new InvalidOperationException(String.Format( CultureInfo.InvariantCulture, "WTS process primary token SID query was ambiguous " diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index e6cbb2ec..85766a65 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -998,7 +998,7 @@ The later SDK validator repin must use these exact committed file bytes: | File | Bytes | SHA-256 | | --- | ---: | --- | -| `.github/workflows/client-v1-conformance.yml` | 458,695 | `ba8896d38ad2a02c865abb1eab7d0ae9ab05ddb74d33244aca6d660ecd3937b4` | +| `.github/workflows/client-v1-conformance.yml` | 459,800 | `047ae05690461530e609370b3d2d5c90817d5a2ff06484f13ffef9f4d5c775f2` | | `scripts/contract-canary.mjs` | 38,191 | `4eb4d9b693187f110343a4c1efd92e59a9705e25790845bf04b05cb5bac6cbb5` | | `scripts/executable-resolution.mjs` | 9,154 | `31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430` | | `scripts/owned-temp-directory.mjs` | 6,965 | `a9c55c85cf2b7d70310d278bafd2c8e7695d66f4ae38b9c3f1f12fce0b442095` | @@ -1023,7 +1023,7 @@ The later SDK validator repin must use these exact committed file bytes: | `scripts/unix-producer-supervisor.test.sh` | 8,083 | `c7d2d023d4c1f1ba3cdb3da1e95b30af1763602932a91592388bb117de6bd397` | | `scripts/phase1-windows-supervisor-build.sh` | 4,646 | `713a9e0282887ade3e243b5ba175794d74cdb02c28c38dcd41491c9505812770` | | `scripts/phase1-windows-supervisor-install.ps1` | 1,743 | `2baab275f0bb6789884cded5f6185d00bfa5348b9e7c3ad1e5575353639101d5` | -| `scripts/windows-job-supervisor.cs` | 290,364 | `69b70a392b92e934491eb75c374a5a24bae7fb1285e1aa387a515513c54d6610` | +| `scripts/windows-job-supervisor.cs` | 291,329 | `08c18fa81b16f922b3fac32abec3a2f6369e5f2b9f4caa19a0b48df6302bb110` | | `scripts/windows-job-supervisor.test.ps1` | 171,179 | `55e9cf065e2dc7cc656c6aa8cc9ea53542259d3d7eee55c368c6cf0fc6356ab9` | Before parsing or executing SDK authority, the harness queries the verified diff --git a/phase1-conformance.lock.json b/phase1-conformance.lock.json index da176527..a0134f4d 100644 --- a/phase1-conformance.lock.json +++ b/phase1-conformance.lock.json @@ -116,8 +116,8 @@ }, { "path": "scripts/windows-job-supervisor.cs", - "blob": "e3c525de275f9d70c319c2a5a0effe6c3d5ed1b3", - "sha256": "69b70a392b92e934491eb75c374a5a24bae7fb1285e1aa387a515513c54d6610" + "blob": "547f2db1e7cacd539f49f5c879839fa80397ac1d", + "sha256": "08c18fa81b16f922b3fac32abec3a2f6369e5f2b9f4caa19a0b48df6302bb110" }, { "path": "scripts/contract-canary.mjs", @@ -146,8 +146,8 @@ }, { "path": ".github/workflows/client-v1-conformance.yml", - "blob": "96acdb27dfba14ca82eb446e700785f040529668", - "sha256": "ba8896d38ad2a02c865abb1eab7d0ae9ab05ddb74d33244aca6d660ecd3937b4" + "blob": "28bad32e2ad0fc849ac87e9a7b9cbd3229e9640a", + "sha256": "047ae05690461530e609370b3d2d5c90817d5a2ff06484f13ffef9f4d5c775f2" } ], "productionDeltas": [ diff --git a/scripts/windows-job-supervisor.cs b/scripts/windows-job-supervisor.cs index e3c525de..547f2db1 100644 --- a/scripts/windows-job-supervisor.cs +++ b/scripts/windows-job-supervisor.cs @@ -4086,17 +4086,31 @@ private static Dictionary typeof(WTS_PROCESS_INFO_EXW)); if (information.pUserSid == IntPtr.Zero) { - if (information.ProcessId == 0) + // The Idle process, and the protected system processes + // that live in session 0 -- Secure System, Registry, + // and their kin -- expose no primary token SID to any + // caller, however privileged. Refusing on their + // account made the drain unrunnable on hosts that + // enable virtualization-based security, which is every + // current Windows image. + // + // Skipping them does not widen what this proves. The + // supervised identity is a local account this process + // created and logged on with CreateProcessWithLogonW, + // so its processes hold a readable token and appear in + // this enumeration with a SID to compare. A process + // whose SID cannot be read AT ALL is not one of them. + // + // Anywhere else, an owner this enumeration cannot read + // is an owner it cannot rule out, so the refusal + // stands -- and names the process, because a process + // that exited between enumeration and read and a + // permanently unreadable one demand opposite fixes. + if (information.ProcessId == 0 || + information.SessionId == 0) { continue; } - // Fails closed: an owner this enumeration cannot read - // is an owner it cannot rule out, so the refusal - // stands. It names the process because the refusal is - // otherwise unactionable -- a process that exited - // between enumeration and read and a permanently - // unreadable system process demand opposite fixes, and - // the message carried neither. throw new InvalidOperationException(String.Format( CultureInfo.InvariantCulture, "WTS process primary token SID query was ambiguous "