Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/phase1-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,23 @@ toolchain component hashes from that release manifest. The workflow then
requires the exact Git, Node, pnpm, rustup, Rust, and Tauri versions before
conformance.

When the ordinary Windows test suite reports a non-system null WTS SID, its
failure reporter makes one bounded observation without changing the failure.
The test-only `scripts/windows-process-sid-diagnostics.cs` opens one
query/synchronize handle, performs at most two zero-time waits and one token
query, and closes the handle. Fixed labels and numeric OS codes distinguish
open-not-found, open failure, exited, live readable/unreadable token, invalid
token, and wait failure without emitting a SID. The observation describes the
newly opened handle; it cannot establish continuity or reuse relative to the
original WTS row. Chat #206 remains open until native evidence explains the
ambiguity. Standalone `scripts/windows-process-sid-diagnostics.test.ps1` tests
exercise states and call/cleanup bounds and also run in the native suite.
The native suite additionally checks one real self-process observation through a
nested failure report; terminal-attempt wrapping preserves the inner exception
chain so a real WTS failure reaches the same reporter. The reporter traverses
aggregate children within a twelve-exception total bound and shares one probe
budget across all branches, including producer-plus-quarantine failures.

`scripts/windows-job-supervisor.test.ps1` is also run by the ordinary elevated
`windows-2025` supervisor behavior CI job. It creates a real ephemeral standard
user and scoped profile/temp/workspace ACLs, launches every supervised probe as
Expand Down Expand Up @@ -1260,7 +1277,9 @@ 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` | 291,329 | `08c18fa81b16f922b3fac32abec3a2f6369e5f2b9f4caa19a0b48df6302bb110` |
| `scripts/windows-job-supervisor.test.ps1` | 172,760 | `cecc4c4a88ddceff68ab941798a700d2f60e18048be6373044e6c115b08bfcfe` |
| `scripts/windows-job-supervisor.test.ps1` | 175,090 | `8d4ae0914a65f4648523c161c3a212e7d8926bc878ce8e54eeaab102c29b25d7` |
| `scripts/windows-process-sid-diagnostics.cs` | 4,054 | `cd4b1c16a759ce4e63b87c82c4be0dbee9c0b48e9bfd3851eb966c303918e1a2` |
| `scripts/windows-process-sid-diagnostics.test.ps1` | 7,316 | `c83e2d63355fb95c8220045115a3b8106b7507b7132d235ad74eb0283f6c481f` |
| `scripts/windows-status-acl-probe.cs` | 6,559 | `aeb7fec2d8becf63b5e94e93d2f8b56cf761ea76d4a714a33f6457a3c65dabe7` |

The table above is the SDK-facing subset; `phase1-conformance.lock.json`'s
Expand Down
60 changes: 53 additions & 7 deletions scripts/windows-job-supervisor.test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,38 @@ function Write-ExceptionChain {
$Failure
}
$depth = 0
while ($null -ne $exception -and $depth -lt 12) {
$sidProbeAttempted = $false
$pending = [Collections.Generic.Queue[Exception]]::new()
if ($null -ne $exception) { $pending.Enqueue($exception) }
while ($pending.Count -gt 0 -and $depth -lt 12) {
$exception = $pending.Dequeue()
Write-Host "cause[$depth] $($exception.GetType().FullName): $($exception.Message)"
if (-not $sidProbeAttempted -and
$exception.Message -match '^WTS process primary token SID query was ambiguous for process ([0-9]+) in session ([0-9]+)\.$') {
$sidProbeAttempted = $true
# One observation only, after failure; no SID output or acceptance change.
try {
$processId = [uint32]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture)
$queryMethod = [OpenCoven.WindowsJobSupervisor].GetMethod(
'QueryProcessPrimaryTokenSid', [Reflection.BindingFlags]'NonPublic,Static'
)
$querySid = [Delegate]::CreateDelegate([Func[IntPtr, string]], $queryMethod)
$observation = [OpenCoven.WindowsProcessSidDiagnostics]::Describe($processId, $querySid)
Write-Host "wts-null-sid-observation: $observation"
} catch {
Write-Host 'wts-null-sid-observation: probe-failed'
}
}
$depth++
if ($exception -is [AggregateException]) {
$index = 0
foreach ($inner in $exception.InnerExceptions) {
Write-Host " aggregate[$index] $($inner.GetType().FullName): $($inner.Message)"
$index++
if ($pending.Count -ge (12 - $depth)) { break }
$pending.Enqueue($inner)
}
} elseif ($null -ne $exception.InnerException -and
$pending.Count -lt (12 - $depth)) {
$pending.Enqueue($exception.InnerException)
}
$exception = $exception.InnerException
$depth++
}
}

Expand All @@ -72,6 +93,29 @@ if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
throw 'Reviewed Windows Job Object supervisor source is missing.'
}
Add-Type -TypeDefinition ([IO.File]::ReadAllText($sourcePath)) -Language CSharp
& (Join-Path $PSScriptRoot 'windows-process-sid-diagnostics.test.ps1')

# Exercise the real diagnostic through a nested failure report without failing
# the suite or changing any process. The observed handle is this test process.
$diagnosticFailure = [InvalidOperationException]::new(
'Synthetic terminal quarantine failure.',
[AggregateException]::new([Exception[]]@(
[InvalidOperationException]::new('Synthetic producer failure.'),
[InvalidOperationException]::new(
"WTS process primary token SID query was ambiguous for process $PID in session 1."
),
[InvalidOperationException]::new(
"WTS process primary token SID query was ambiguous for process $PID in session 1."
)
))
)
$diagnosticOutput = @(Write-ExceptionChain -Failure $diagnosticFailure 6>&1)
$observations = @($diagnosticOutput | ForEach-Object { $_.ToString() } |
Where-Object { $_.StartsWith('wts-null-sid-observation:') })
if ($observations.Count -ne 1 -or
$observations[0] -cne 'wts-null-sid-observation: live-token-readable') {
throw 'Nested WTS failure reporting did not observe the live test process exactly once.'
}

$createProcessWithLogon = [OpenCoven.WindowsJobSupervisor].GetMethod(
'CreateProcessWithLogonW',
Expand Down Expand Up @@ -4309,7 +4353,9 @@ Start-Sleep -Seconds 300
$directoryQuotas
)
} catch {
throw "Terminal failure '$Label' producer attempt failed: $($_.Exception.ToString())"
throw [InvalidOperationException]::new(
"Terminal failure '$Label' producer attempt failed.", $_.Exception
)
}
if (
$Mode -eq 'stdout-overflow' -and
Expand Down
104 changes: 104 additions & 0 deletions scripts/windows-process-sid-diagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;

namespace OpenCoven
{
// Test-only observation: never used to decide quarantine acceptance.
public static class WindowsProcessSidDiagnostics
{
private const int ERROR_INVALID_PARAMETER = 87;
private const int ERROR_NOT_FOUND = 1168;
private const uint WAIT_OBJECT_0 = 0;
private const uint WAIT_TIMEOUT = 258;
private const uint SYNCHRONIZE = 0x00100000;
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;

public static string Describe(uint processId, Func<IntPtr, string> querySid)
{
try
{
return ObserveAmbiguousProcessSid(
processId,
delegate(uint id)
{
return OpenProcess(
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, false, id);
},
Marshal.GetLastWin32Error,
delegate(IntPtr process) { return WaitForSingleObject(process, 0); },
querySid,
delegate(IntPtr process) { CloseHandle(process); });
}
catch (Exception)
{
return "probe-failed";
}
}

// Observations describe only the newly opened handle, not identity continuity
// with the earlier WTS row. They never authorize skipping an ambiguous row.
private static string ObserveAmbiguousProcessSid(
uint processId,
Func<uint, IntPtr> openProcess,
Func<int> lastError,
Func<IntPtr, uint> waitProcess,
Func<IntPtr, string> querySid,
Action<IntPtr> closeProcess)
{
IntPtr process = openProcess(processId);
if (process == IntPtr.Zero)
{
int error = lastError();
return ((error == ERROR_INVALID_PARAMETER || error == ERROR_NOT_FOUND)
? "open-not-found:" : "open-failed:")
+ error.ToString(CultureInfo.InvariantCulture);
}
try
{
uint wait = waitProcess(process);
if (wait == WAIT_OBJECT_0) return "exited";
if (wait != WAIT_TIMEOUT)
{
return "wait-failed:"
+ lastError().ToString(CultureInfo.InvariantCulture);
}
string tokenState;
try
{
tokenState = String.IsNullOrEmpty(querySid(process))
? "live-token-invalid" : "live-token-readable";
}
catch (Win32Exception error)
{
tokenState = "live-token-unreadable:"
+ error.NativeErrorCode.ToString(CultureInfo.InvariantCulture);
}
catch (InvalidOperationException)
{
tokenState = "live-token-invalid";
}
wait = waitProcess(process);
if (wait == WAIT_OBJECT_0) return "exited-during-query";
if (wait != WAIT_TIMEOUT)
{
return "wait-failed:"
+ lastError().ToString(CultureInfo.InvariantCulture);
}
return tokenState;
}
finally
{
closeProcess(process);
}
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(IntPtr process, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr process);
}
}
166 changes: 166 additions & 0 deletions scripts/windows-process-sid-diagnostics.test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

if (-not ('OpenCoven.WindowsProcessSidDiagnostics' -as [type])) {
Add-Type -TypeDefinition ([IO.File]::ReadAllText(
(Join-Path $PSScriptRoot 'windows-process-sid-diagnostics.cs')
)) -Language CSharp
}
$observe = [OpenCoven.WindowsProcessSidDiagnostics].GetMethod(
'ObserveAmbiguousProcessSid',
[Reflection.BindingFlags]'NonPublic,Static'
)
if ($null -eq $observe) {
throw 'Bounded null-SID observation is missing.'
}

# Throw from a managed delegate directly: a PowerShell scriptblock wraps native
# exceptions and would test PowerShell's adapter instead of the C# boundary.
Add-Type -TypeDefinition @'
public sealed class SidDiagnosticDeniedQuery
{
public int Calls;
public bool Unexpected;
public string Query(System.IntPtr handle)
{
if (handle != new System.IntPtr(1)) throw new System.Exception("Wrong query handle.");
Calls++;
if (Unexpected) throw new System.Exception("untrusted diagnostic detail");
throw new System.ComponentModel.Win32Exception(5);
}
}
'@

foreach ($case in @(
@{ Name = 'missing'; Open = 0; Error = 87; Wait = 258; Token = 'readable'; Expected = 'open-not-found:87'; Queries = 0; Waits = 0 },
@{ Name = 'denied'; Open = 0; Error = 5; Wait = 258; Token = 'readable'; Expected = 'open-failed:5'; Queries = 0; Waits = 0 },
@{ Name = 'exited'; Open = 1; Error = 0; Wait = 0; Token = 'readable'; Expected = 'exited'; Queries = 0; Waits = 1 },
@{ Name = 'live-readable'; Open = 1; Error = 0; Wait = 258; Token = 'S-1-5-21-private'; Expected = 'live-token-readable'; Queries = 1; Waits = 2 },
@{ Name = 'live-unreadable'; Open = 1; Error = 5; Wait = 258; Token = 'throw'; Expected = 'live-token-unreadable:5'; Queries = 1; Waits = 2 },
@{ Name = 'live-empty'; Open = 1; Error = 0; Wait = 258; Token = ''; Expected = 'live-token-invalid'; Queries = 1; Waits = 2 },
@{ Name = 'wait-failed'; Open = 1; Error = 6; Wait = [uint32]::MaxValue; Token = 'readable'; Expected = 'wait-failed:6'; Queries = 0; Waits = 1 },
@{ Name = 'second-wait-failed'; Open = 1; Error = 6; Wait = 258; SecondWait = [uint32]::MaxValue; Token = 'readable'; Expected = 'wait-failed:6'; Queries = 1; Waits = 2 },
@{ Name = 'exit-during-query'; Open = 1; Error = 5; Wait = 258; Token = 'throw'; SecondWait = 0; Expected = 'exited-during-query'; Queries = 1; Waits = 2 }
)) {
$calls = @{ Open = 0; Wait = 0; Query = 0; Close = 0 }
$arguments = [object[]]::new(6)
$arguments[0] = [uint32]123
$arguments[1] = [Func[uint32, IntPtr]]{
param($processId)
if ($processId -ne 123) { throw 'Wrong diagnostic PID.' }
$calls.Open++
return [IntPtr]$case.Open
}
$arguments[2] = [Func[int]]{ return [int]$case.Error }
$arguments[3] = [Func[IntPtr, uint32]]{
param($handle)
if ($handle -ne [IntPtr]1) { throw 'Wrong wait handle.' }
$calls.Wait++
if ($calls.Wait -eq 2 -and $case.ContainsKey('SecondWait')) {
return [uint32]$case.SecondWait
}
return [uint32]$case.Wait
}
$arguments[4] = [Func[IntPtr, string]]{
param($handle)
if ($handle -ne [IntPtr]1) { throw 'Wrong token handle.' }
$calls.Query++
if ($case.Token -ceq 'throw') {
throw [ComponentModel.Win32Exception]::new([int]$case.Error)
}
return [string]$case.Token
}
$arguments[5] = [Action[IntPtr]]{
param($handle)
if ($handle -ne [IntPtr]1) { throw 'Wrong close handle.' }
$calls.Close++
}
$deniedQuery = [SidDiagnosticDeniedQuery]::new()
if ($case.Token -ceq 'throw') {
$arguments[4] = [Delegate]::CreateDelegate(
[Func[IntPtr, string]], $deniedQuery, 'Query'
)
}
$actual = $observe.Invoke($null, $arguments)
$calls.Query += $deniedQuery.Calls
if ($actual -cne $case.Expected -or $calls.Open -ne 1 -or
$calls.Wait -ne $case.Waits -or $calls.Query -ne $case.Queries -or
$calls.Close -ne $case.Open) {
throw "Bounded SID diagnostic case $($case.Name) failed: $actual; $($calls | ConvertTo-Json -Compress)"
}
}
# Unexpected query failures propagate to the native call site's fixed fallback,
# but must still release the only opened handle. This fixture is independent of
# the matrix case order and its captured delegates.
$unexpectedCalls = @{ Open = 0; Wait = 0; Close = 0 }
$unexpectedQuery = [SidDiagnosticDeniedQuery]::new()
$unexpectedQuery.Unexpected = $true
$unexpectedArguments = [object[]]::new(6)
$unexpectedArguments[0] = [uint32]123
$unexpectedArguments[1] = [Func[uint32, IntPtr]]{
param($processId)
if ($processId -ne 123) { throw 'Wrong unexpected-query PID.' }
$unexpectedCalls.Open++
return [IntPtr]1
}
$unexpectedArguments[2] = [Func[int]]{ return 0 }
$unexpectedArguments[3] = [Func[IntPtr, uint32]]{
param($handle)
if ($handle -ne [IntPtr]1) { throw 'Wrong unexpected-query wait handle.' }
$unexpectedCalls.Wait++
return [uint32]258
}
$unexpectedArguments[4] = [Delegate]::CreateDelegate(
[Func[IntPtr, string]], $unexpectedQuery, 'Query'
)
$unexpectedArguments[5] = [Action[IntPtr]]{
param($handle)
if ($handle -ne [IntPtr]1) { throw 'Wrong unexpected-query close handle.' }
$unexpectedCalls.Close++
}
$unexpectedFailed = $false
try {
$null = $observe.Invoke($null, $unexpectedArguments)
} catch [Management.Automation.MethodInvocationException] {
$underlying = $_.Exception.InnerException
if ($underlying -is [Reflection.TargetInvocationException]) {
$underlying = $underlying.InnerException
}
$unexpectedFailed = $underlying.GetType() -eq [Exception]
}
if (-not $unexpectedFailed -or $unexpectedCalls.Open -ne 1 -or
$unexpectedCalls.Wait -ne 1 -or $unexpectedQuery.Calls -ne 1 -or
$unexpectedCalls.Close -ne 1) {
throw 'Unexpected query failure did not preserve bounded cleanup.'
}
# Execute only the checked-in reporter function, without starting the native
# suite. This verifies aggregate traversal and its shared probe budget on any OS.
$parseTokens = $null
$parseErrors = $null
$reporterFile = [Management.Automation.Language.Parser]::ParseFile(
(Join-Path $PSScriptRoot 'windows-job-supervisor.test.ps1'),
[ref]$parseTokens, [ref]$parseErrors
)
if ($parseErrors.Count -ne 0) { throw 'Native suite parse failed.' }
$reporter = $reporterFile.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -ceq 'Write-ExceptionChain'
}, $false)
if ($null -eq $reporter) { throw 'Failure reporter definition is missing.' }
Invoke-Expression $reporter.Extent.Text
$children = [Collections.Generic.List[Exception]]::new()
$children.Add([Exception]::new('Synthetic producer failure.'))
foreach ($index in 1..2) {
$children.Add([Exception]::new(
"WTS process primary token SID query was ambiguous for process $PID in session 1."
))
}
foreach ($index in 3..20) { $children.Add([Exception]::new('Synthetic extra failure.')) }
$report = @(Write-ExceptionChain -Failure ([AggregateException]::new($children)) 6>&1 |
ForEach-Object { $_.ToString() })
if (@($report | Where-Object { $_.StartsWith('cause[') }).Count -ne 12 -or
@($report | Where-Object { $_.StartsWith('wts-null-sid-observation:') }).Count -ne 1) {
throw 'Aggregate reporting exceeded its bounds or missed the sibling ambiguity.'
}
Write-Host 'Bounded null-SID diagnostic cases passed.'
Loading
Loading