From 6262438b47a7b833a7396bb767f0a1e8a20c0257 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 11 Sep 2026 10:21:35 -0500 Subject: [PATCH 1/5] test(windows): characterize owner-directory quota access --- scripts/windows-job-supervisor.test.ps1 | 1 + .../windows-owner-directory-quota.test.ps1 | 151 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 scripts/windows-owner-directory-quota.test.ps1 diff --git a/scripts/windows-job-supervisor.test.ps1 b/scripts/windows-job-supervisor.test.ps1 index 96a905f3..c79bfe9d 100644 --- a/scripts/windows-job-supervisor.test.ps1 +++ b/scripts/windows-job-supervisor.test.ps1 @@ -95,6 +95,7 @@ if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { Add-Type -TypeDefinition ([IO.File]::ReadAllText($sourcePath)) -Language CSharp & (Join-Path $PSScriptRoot 'windows-process-sid-diagnostics.test.ps1') & (Join-Path $PSScriptRoot 'windows-quota-diagnostics.test.ps1') +& (Join-Path $PSScriptRoot 'windows-owner-directory-quota.test.ps1') & (Join-Path $PSScriptRoot 'windows-identity-cleanup-diagnostics.test.ps1') & (Join-Path $PSScriptRoot 'windows-staging-binding.test.ps1') & (Join-Path $PSScriptRoot 'windows-status-acl-probe.test.ps1') diff --git a/scripts/windows-owner-directory-quota.test.ps1 b/scripts/windows-owner-directory-quota.test.ps1 new file mode 100644 index 00000000..af777c73 --- /dev/null +++ b/scripts/windows-owner-directory-quota.test.ps1 @@ -0,0 +1,151 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +if (-not $IsWindows) { throw 'Owner-directory quota reproduction requires native Windows.' } +if (-not ('OpenCoven.WindowsJobSupervisor' -as [type])) { + Add-Type -TypeDefinition ([IO.File]::ReadAllText((Join-Path $PSScriptRoot 'windows-job-supervisor.cs'))) -Language CSharp +} + +# Characterize the access boundary without changing production accounting. +# This is a controlled owner-only directory, not proof of the protected run's +# denied descendant. All filesystem reads below use the real quota methods. +$staticFlags = [Reflection.BindingFlags]'NonPublic,Static' +$instanceFlags = [Reflection.BindingFlags]'NonPublic,Instance' +$terminal = [OpenCoven.WindowsJobSupervisor].GetMethod('ApplyTerminalDirectoryQuotaCheck', $staticFlags) +$monitor = [OpenCoven.WindowsJobSupervisor].GetMethod('MonitorDirectoryQuotasAsync', $staticFlags) +$stateType = [OpenCoven.WindowsJobSupervisor].GetNestedType('DirectoryQuotaFailureState', [Reflection.BindingFlags]'NonPublic') +$secureDirectory = [OpenCoven.WindowsJobSupervisor].GetMethod( + 'SecureIsolatedDirectory', $staticFlags, $null, + [type[]]@([string], [string], [string]), $null +) +$enablePrivilege = [OpenCoven.WindowsJobSupervisor].GetMethod('EnablePrivilege', $staticFlags) +foreach ($method in @($terminal, $monitor, $stateType, $secureDirectory, $enablePrivilege)) { + if ($null -eq $method) { throw 'Native owner-directory fixture contract is missing.' } +} + +if (-not ('OpenCoven.Tests.OwnerDirectoryQuotaFixture' -as [type])) { + Add-Type -Language CSharp -TypeDefinition @' +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +namespace OpenCoven.Tests +{ + public static class OwnerDirectoryQuotaFixture + { + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetFileSecurityW(string path, uint information, byte[] descriptor); + + public static void Protect(string path, string ownerSid) + { + // Match Coven's owner-only directory DACL, with the fixture owner + // explicitly bound to the real isolated account provisioned below. + var descriptor = new RawSecurityDescriptor("O:" + ownerSid + "D:P(A;OICI;GA;;;OW)"); + var bytes = new byte[descriptor.BinaryLength]; + descriptor.GetBinaryForm(bytes, 0); + if (!SetFileSecurityW(path, 0x80000005u, bytes)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Fixture owner-only directory setup failed."); + } + } +} +'@ +} + +$fixtureRoot = Join-Path ([IO.Path]::GetTempPath()) ('opencoven-owner-directory-' + [Guid]::NewGuid().ToString('N')) +$identity = $null +$directory = $null +$directoryCreated = $false +$state = $null +$cancellation = $null +$monitorTask = $null +$failures = [Collections.Generic.List[Exception]]::new() +try { + $identity = [OpenCoven.WindowsIsolatedUser]::Create($fixtureRoot) + $supervisorSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + if ($identity.Sid -ceq $supervisorSid) { throw 'Fixture identities must differ.' } + $directory = Join-Path $identity.TempPath 'phase1-conformance-run-fixture' + [IO.Directory]::CreateDirectory($directory) | Out-Null + $directoryCreated = $true + [IO.File]::WriteAllBytes((Join-Path $directory 'payload.bin'), [byte[]]::new(1024)) + $quota = [OpenCoven.WindowsDirectoryQuota[]]@( + [OpenCoven.WindowsDirectoryQuota]::new('harness execution aggregate', $directory, 2048) + ) + $baseline = [OpenCoven.WindowsJobRunResult]::new() + $terminal.Invoke($null, [object[]]@($baseline, $quota)) + if ($baseline.ResourceQuotaExceeded -or $baseline.ResourceQuotaMonitorError) { + throw 'Readable owner-directory control failed quota accounting.' + } + + $enablePrivilege.Invoke($null, [object[]]@('SeRestorePrivilege')) + [OpenCoven.Tests.OwnerDirectoryQuotaFixture]::Protect($directory, $identity.Sid) + $denied = [OpenCoven.WindowsJobRunResult]::new() + $terminal.Invoke($null, [object[]]@($denied, $quota)) + if (-not $denied.ResourceQuotaExceeded -or -not $denied.ResourceQuotaMonitorError -or + $denied.ExitCode -eq 0 -or $denied.ResourceQuotaMonitorCategory -cne 'access-denied' -or + $denied.ResourceQuotaMonitorRoot -cne 'harness-execution-aggregate' -or + $denied.ResourceQuotaMonitorOperation -cne 'directory-enumeration') { + throw 'Owner-only directory did not reproduce the protected terminal quota signature.' + } + $state = [Activator]::CreateInstance($stateType, $true) + $cancellation = [Threading.CancellationTokenSource]::new(5000) + $monitorTask = $monitor.Invoke($null, [object[]]@($quota, $state, $cancellation.Token)) + if (-not $monitorTask.Wait(6000)) { throw 'Owner-directory monitor did not terminate.' } + foreach ($pair in @( + @('MonitorErrorCategory', 'access-denied'), + @('MonitorErrorRoot', 'harness-execution-aggregate'), + @('MonitorErrorOperation', 'directory-enumeration'))) { + if ($stateType.GetProperty($pair[0], $instanceFlags).GetValue($state) -cne $pair[1]) { + throw 'Owner-only directory did not reproduce the protected background quota signature.' + } + } + if ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -cne $supervisorSid) { + throw 'Quota fixture changed supervisor identity.' + } + + # Restore only this test fixture. Production private ACLs remain untouched. + $secureDirectory.Invoke($null, [object[]]@($directory, $identity.Sid, $supervisorSid)) + $overflow = [OpenCoven.WindowsJobRunResult]::new() + $terminal.Invoke($null, [object[]]@($overflow, [OpenCoven.WindowsDirectoryQuota[]]@( + [OpenCoven.WindowsDirectoryQuota]::new('harness execution aggregate', $directory, 512) + ))) + if (-not $overflow.ResourceQuotaExceeded -or $overflow.ResourceQuotaMonitorError -or + $overflow.ResourceQuotaLabel -cne 'harness execution aggregate' -or $overflow.ExitCode -eq 0) { + throw 'Readable control did not enforce the actual byte quota.' + } + Write-Host 'Native isolated-owner directory reproduces bounded terminal/background quota denial; readable and overflow controls passed.' +} catch { + $failures.Add($_.Exception) +} finally { + try { if ($null -ne $cancellation) { $cancellation.Cancel() } } + catch { $failures.Add($_.Exception) } + try { + if ($null -ne $monitorTask -and -not $monitorTask.Wait(6000)) { + throw [TimeoutException]::new('Owner-directory monitor survived cancellation.') + } + } catch { $failures.Add($_.Exception) } + # Do not dispose state underneath a worker that failed to stop. The fixture + # fails in that case, while still attempting ACL and account cleanup below. + if ($null -eq $monitorTask -or $monitorTask.IsCompleted) { + try { if ($null -ne $state) { $state.Dispose() } } + catch { $failures.Add($_.Exception) } + try { if ($null -ne $cancellation) { $cancellation.Dispose() } } + catch { $failures.Add($_.Exception) } + } + if ($null -ne $identity) { + try { + # Existence queries can hide access denial; creation state owns teardown. + if ($directoryCreated) { + $secureDirectory.Invoke($null, [object[]]@($directory, $identity.Sid, $supervisorSid)) + } + } catch { $failures.Add($_.Exception) } + try { $identity.Dispose() } + catch { $failures.Add($_.Exception) } + } +} +if ([IO.Directory]::Exists($fixtureRoot)) { + $failures.Add([IO.IOException]::new('Owner-directory fixture survived cleanup.')) +} +if ($failures.Count -gt 0) { + throw [AggregateException]::new('Native owner-directory reproduction failed.', $failures.ToArray()) +} +Write-Host 'Native owner-directory fixture cleanup passed.' From b9e25c296e18a76f11abdfc57c9c03382e31bced Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 11 Sep 2026 10:22:36 -0500 Subject: [PATCH 2/5] docs(windows): explain quota reproduction evidence limits --- docs/windows-quota-reproduction.md | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/windows-quota-reproduction.md diff --git a/docs/windows-quota-reproduction.md b/docs/windows-quota-reproduction.md new file mode 100644 index 00000000..9096a8a8 --- /dev/null +++ b/docs/windows-quota-reproduction.md @@ -0,0 +1,53 @@ +# Windows quota directory reproduction + +[Issue #219](https://github.com/OpenCoven/chat/issues/219) tracks the protected +Windows failure from run +[34611963297](https://github.com/OpenCoven/chat/actions/runs/34611963297): + +```text +access-denied; root=harness-execution-aggregate; operation=directory-enumeration +``` + +The bounded diagnostic identifies the selected quota and filesystem operation. +It does not identify the denied descendant or establish a byte-quota breach. + +## Native fixture + +The existing Windows supervisor CI job invokes +`scripts/windows-owner-directory-quota.test.ps1` through the full supervisor +test suite. The fixture requires native Windows and the same account-provisioning +privileges as that suite. It can also run directly from the repository root: + +```powershell +pwsh -NoLogo -NoProfile -NonInteractive -File scripts/windows-owner-directory-quota.test.ps1 +``` + +The fixture creates a temporary isolated account and a directory containing a +known-size file. It checks readable-directory accounting, assigns the isolated +account as owner with Coven's protected owner-only directory DACL, and calls the +real terminal and background quota paths. The expected characterization is the +same bounded access-denial signature above. After restoring the fixture ACL, a +smaller quota must report an actual byte breach. Teardown restores fixture access +and removes the account and root, retaining any failures. + +A matching result proves that this ACL shape can cause the observed monitor +failure. It does not prove that Coven, or any specific descendant, caused the +protected-run failure. A different result rejects this reproduction hypothesis +and must be investigated before selecting a repair. + +## Separate cleanup result + +The same protected job reported: + +```text +root-delete:win32-3,root-survived:invalid-operation +``` + +This is a separate cleanup failure. The directory fixture does not reproduce it. +Investigate the native removal path, including path handling and missing or +replaced descendants, while preserving reparse safeguards and the requirement +that the owned root is actually removed. + +The reproduction changes no production quota limits, private ACL rules, frozen +source bindings or validator scopes. A repair still requires native regression +proof, reviewed source and SDK binding updates, and fresh protected acceptance. From 35922497da89ef918a27c5224d857e10d5db0623 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 11 Sep 2026 10:26:48 -0500 Subject: [PATCH 3/5] docs(windows): bind native quota fixture metadata --- docs/phase1-conformance.md | 3 ++- src/client-v1-conformance-workflow.test.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index 04cb6e1e..e94df1c0 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1366,8 +1366,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` | 305,876 | `b034c6dd3c7af0724733259a1257cf6b2885c97ee08b3f9b1e2bba0abca9c3a1` | -| `scripts/windows-job-supervisor.test.ps1` | 177,716 | `a3b67a5b6130bc695ebd15cbf55718b40bd594409106e106b70b50c8840b3ffe` | +| `scripts/windows-job-supervisor.test.ps1` | 177,785 | `c0044f2ba955ff901fcbee3568a3dc081e590a5f880c0bb00fc4d2eedf2a3cf1` | | `scripts/windows-quota-diagnostics.test.ps1` | 13,409 | `2594ddf573f7642eea7e050382dcc523daa5b477852d3db774b570328502a2e8` | +| `scripts/windows-owner-directory-quota.test.ps1` | 7,698 | `a9003cbc15a7ba84e625c3a1367735534a1c2d3db706f65d32d38480af47cd50` | | `scripts/windows-identity-cleanup-diagnostics.test.ps1` | 5,324 | `f0dd69a9aadc6ca662fc7d33091986f40771d6ce3c10a57cda2e73f70c0e0417` | | `scripts/windows-process-sid-diagnostics.cs` | 4,054 | `cd4b1c16a759ce4e63b87c82c4be0dbee9c0b48e9bfd3851eb966c303918e1a2` | | `scripts/windows-process-sid-diagnostics.test.ps1` | 7,316 | `c83e2d63355fb95c8220045115a3b8106b7507b7132d235ad74eb0283f6c481f` | diff --git a/src/client-v1-conformance-workflow.test.ts b/src/client-v1-conformance-workflow.test.ts index 516e66f9..f59d24eb 100644 --- a/src/client-v1-conformance-workflow.test.ts +++ b/src/client-v1-conformance-workflow.test.ts @@ -2366,6 +2366,7 @@ ${source.slice(start, end)} 'scripts/windows-job-supervisor.cs', 'scripts/windows-job-supervisor.test.ps1', 'scripts/windows-quota-diagnostics.test.ps1', + 'scripts/windows-owner-directory-quota.test.ps1', 'scripts/windows-identity-cleanup-diagnostics.test.ps1', 'scripts/windows-staging-binding.test.ps1', 'scripts/windows-status-acl-probe.test.ps1', From 47115587a4eb9135f49da1618c73c3a8fb739e4e Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 11 Sep 2026 10:35:02 -0500 Subject: [PATCH 4/5] test(windows): bind fixture restoration through typed delegate --- docs/phase1-conformance.md | 2 +- docs/roadmap.md | 51 ++++++++++++++++++- .../windows-owner-directory-quota.test.ps1 | 7 ++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index e94df1c0..d5ffac45 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1368,7 +1368,7 @@ The later SDK validator repin must use these exact committed file bytes: | `scripts/windows-job-supervisor.cs` | 305,876 | `b034c6dd3c7af0724733259a1257cf6b2885c97ee08b3f9b1e2bba0abca9c3a1` | | `scripts/windows-job-supervisor.test.ps1` | 177,785 | `c0044f2ba955ff901fcbee3568a3dc081e590a5f880c0bb00fc4d2eedf2a3cf1` | | `scripts/windows-quota-diagnostics.test.ps1` | 13,409 | `2594ddf573f7642eea7e050382dcc523daa5b477852d3db774b570328502a2e8` | -| `scripts/windows-owner-directory-quota.test.ps1` | 7,698 | `a9003cbc15a7ba84e625c3a1367735534a1c2d3db706f65d32d38480af47cd50` | +| `scripts/windows-owner-directory-quota.test.ps1` | 7,902 | `cc4ebc6d2ccaca482b590b5addce39165a962bfe2cc5bef482668e9c473abe7f` | | `scripts/windows-identity-cleanup-diagnostics.test.ps1` | 5,324 | `f0dd69a9aadc6ca662fc7d33091986f40771d6ce3c10a57cda2e73f70c0e0417` | | `scripts/windows-process-sid-diagnostics.cs` | 4,054 | `cd4b1c16a759ce4e63b87c82c4be0dbee9c0b48e9bfd3851eb966c303918e1a2` | | `scripts/windows-process-sid-diagnostics.test.ps1` | 7,316 | `c83e2d63355fb95c8220045115a3b8106b7507b7132d235ad74eb0283f6c481f` | diff --git a/docs/roadmap.md b/docs/roadmap.md index 1ec78b53..87b4da03 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,55 @@ # Delivery roadmap and consolidation audit -## Consolidation checkpoint, 2026-09-10 UTC +## Consolidation checkpoint, 2026-09-11 UTC + +This checkpoint supersedes the pending landing and validation claims in the +historical snapshots below. The latest terminal protected attempt is +[34611963297](https://github.com/OpenCoven/chat/actions/runs/34611963297), using +Chat producer `37e6984d83835f8994392d35d28fc98813746716` and SDK validator +`7f53b74c1c2be2719c87a1c0592d1c13a6a641cf`. Both validator scopes matched that +revision. Linux and macOS passed; each retained record passed independent ZIP +digest, SDK parser/scanner, exact source identity, Cave timing, and ordered +110 Cave / 46 SDK / 41 Chat assertion checks. + +Windows failed with bounded quota-monitor context: + +```text +access-denied; root=harness-execution-aggregate; operation=directory-enumeration +``` + +Cleanup separately reported +`root-delete:win32-3,root-survived:invalid-operation`. Artifact validation, +attestation, and aggregation were skipped. This establishes neither quota +exhaustion nor the identity of the denied descendant. + +| Work | Delivered evidence | Remaining work | +| --- | --- | --- | +| Frozen Linux backport adoption | Chat #210 and the prepared #207–#209 follow-ups landed. The production backport remains frozen at `0da8c4749f57e63601b29d66032f80c9bbac1cb5`; subsequent harness bindings retain the reviewed source ancestry. | [#188](https://github.com/OpenCoven/chat/issues/188) remains open for complete protected acceptance and advisory reconciliation. | +| Windows staging and diagnostics | Coven #988 and Chat #211 landed. Chat #216 adds bounded cleanup categories; #218 adds quota-root/operation context. All ten final #218 CI jobs passed, including native Windows tests. | Protected status-staging success remains unproven. [#219](https://github.com/OpenCoven/chat/issues/219) owns the quota enumeration repair and separate cleanup investigation. #211 review-thread disposition remains unverified. | +| Native quota reproduction | [Draft #220](https://github.com/OpenCoven/chat/pull/220) adds an isolated-owner directory fixture with readable and byte-overflow controls. See [the reproduction contract](windows-quota-reproduction.md). | Inspect native execution before selecting a repair. A matching fixture signature does not identify the protected run's denied descendant. | +| SDK binding | [SDK #204](https://github.com/OpenCoven/sdk/pull/204) landed at `7f53b74c1c2be2719c87a1c0592d1c13a6a641cf`, binding the exact #218 producer/workflow/bootstrap bytes. Both scopes were rotated and read back before the protected attempt. | Rebind any subsequent governed source change. [SDK #38](https://github.com/OpenCoven/sdk/issues/38) and the final release gate remain open. | +| Process-identity investigation | Chat #207's bounded test-only diagnostics landed. | [#206](https://github.com/OpenCoven/chat/issues/206) still requires causal evidence; later successful jobs do not classify its original failure. | + +Diagnostic [#217](https://github.com/OpenCoven/chat/issues/217) and Bead +`cave-k0aqq.1` are complete on verified protected root/operation disclosure. +Repair Bead `cave-k0aqq.2` is active. The root Bead `cave-k0aqq`, final gate +`cave-ilh1h`, and authorized Teamwork root retain the outstanding release work. + +The branch audit inspected 32 previously unattached Chat/SDK branches: 21 have +exact trees retained by merged commits in fetched main. Two additional branches +have no unmatched non-merge patches; that is weaker than complete delivery +proof. SDK PR #90 already delivered the canonicalize change; its local repair +branch's older dependency lock is not missing implementation to restore. +Alternate and unmatched histories still need individual disposition. + +No branches or worktrees were removed in this checkpoint. Exact-tree retention +does not establish retirement of active or reserved ownership. Preserve dirty, +active, and ambiguous work, including Chat #214 and parked #86. The minimum +working set remains unestablished. Machine-readable delivery, branch, and +protected-run receipts remain in the external +`chat-consolidation-20260907/audit` directory. + +## Historical consolidation checkpoint, 2026-09-10 UTC This checkpoint supersedes the pending repair and validation claims below. The latest verified protected attempt is diff --git a/scripts/windows-owner-directory-quota.test.ps1 b/scripts/windows-owner-directory-quota.test.ps1 index af777c73..3f28116d 100644 --- a/scripts/windows-owner-directory-quota.test.ps1 +++ b/scripts/windows-owner-directory-quota.test.ps1 @@ -21,6 +21,9 @@ $enablePrivilege = [OpenCoven.WindowsJobSupervisor].GetMethod('EnablePrivilege', foreach ($method in @($terminal, $monitor, $stateType, $secureDirectory, $enablePrivilege)) { if ($null -eq $method) { throw 'Native owner-directory fixture contract is missing.' } } +# PowerShell path/SID values can retain PSObject wrappers. A typed delegate +# binds those strings before entering the native restoration method. +$restoreDirectory = [Delegate]::CreateDelegate([Action[string,string,string]], $secureDirectory) if (-not ('OpenCoven.Tests.OwnerDirectoryQuotaFixture' -as [type])) { Add-Type -Language CSharp -TypeDefinition @' @@ -103,7 +106,7 @@ try { } # Restore only this test fixture. Production private ACLs remain untouched. - $secureDirectory.Invoke($null, [object[]]@($directory, $identity.Sid, $supervisorSid)) + $restoreDirectory.Invoke($directory, $identity.Sid, $supervisorSid) $overflow = [OpenCoven.WindowsJobRunResult]::new() $terminal.Invoke($null, [object[]]@($overflow, [OpenCoven.WindowsDirectoryQuota[]]@( [OpenCoven.WindowsDirectoryQuota]::new('harness execution aggregate', $directory, 512) @@ -135,7 +138,7 @@ try { try { # Existence queries can hide access denial; creation state owns teardown. if ($directoryCreated) { - $secureDirectory.Invoke($null, [object[]]@($directory, $identity.Sid, $supervisorSid)) + $restoreDirectory.Invoke($directory, $identity.Sid, $supervisorSid) } } catch { $failures.Add($_.Exception) } try { $identity.Dispose() } From b40ee09985c1995d5c4a64ae78230077190c5ac1 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 11 Sep 2026 10:55:11 -0500 Subject: [PATCH 5/5] test(windows): retain fixture ACL restoration handle --- docs/phase1-conformance.md | 2 +- docs/windows-quota-reproduction.md | 4 +- .../windows-owner-directory-quota.test.ps1 | 102 +++++++++++++++--- 3 files changed, 89 insertions(+), 19 deletions(-) diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index d5ffac45..d7e3447f 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1368,7 +1368,7 @@ The later SDK validator repin must use these exact committed file bytes: | `scripts/windows-job-supervisor.cs` | 305,876 | `b034c6dd3c7af0724733259a1257cf6b2885c97ee08b3f9b1e2bba0abca9c3a1` | | `scripts/windows-job-supervisor.test.ps1` | 177,785 | `c0044f2ba955ff901fcbee3568a3dc081e590a5f880c0bb00fc4d2eedf2a3cf1` | | `scripts/windows-quota-diagnostics.test.ps1` | 13,409 | `2594ddf573f7642eea7e050382dcc523daa5b477852d3db774b570328502a2e8` | -| `scripts/windows-owner-directory-quota.test.ps1` | 7,902 | `cc4ebc6d2ccaca482b590b5addce39165a962bfe2cc5bef482668e9c473abe7f` | +| `scripts/windows-owner-directory-quota.test.ps1` | 11,176 | `23b0b5106c5d50676622bf74238e465a80c5a6a9d017275d067263673d9ceecb` | | `scripts/windows-identity-cleanup-diagnostics.test.ps1` | 5,324 | `f0dd69a9aadc6ca662fc7d33091986f40771d6ce3c10a57cda2e73f70c0e0417` | | `scripts/windows-process-sid-diagnostics.cs` | 4,054 | `cd4b1c16a759ce4e63b87c82c4be0dbee9c0b48e9bfd3851eb966c303918e1a2` | | `scripts/windows-process-sid-diagnostics.test.ps1` | 7,316 | `c83e2d63355fb95c8220045115a3b8106b7507b7132d235ad74eb0283f6c481f` | diff --git a/docs/windows-quota-reproduction.md b/docs/windows-quota-reproduction.md index 9096a8a8..fbcbd708 100644 --- a/docs/windows-quota-reproduction.md +++ b/docs/windows-quota-reproduction.md @@ -27,7 +27,9 @@ known-size file. It checks readable-directory accounting, assigns the isolated account as owner with Coven's protected owner-only directory DACL, and calls the real terminal and background quota paths. The expected characterization is the same bounded access-denial signature above. After restoring the fixture ACL, a -smaller quota must report an actual byte breach. Teardown restores fixture access +smaller quota must report an actual byte breach. A noninheritable directory handle +opened before ACL restriction retains restoration access and the original owner, +DACL and inheritance protection. Teardown restores fixture access and removes the account and root, retaining any failures. A matching result proves that this ACL shape can cause the observed monitor diff --git a/scripts/windows-owner-directory-quota.test.ps1 b/scripts/windows-owner-directory-quota.test.ps1 index 3f28116d..ed8c5e48 100644 --- a/scripts/windows-owner-directory-quota.test.ps1 +++ b/scripts/windows-owner-directory-quota.test.ps1 @@ -13,18 +13,10 @@ $instanceFlags = [Reflection.BindingFlags]'NonPublic,Instance' $terminal = [OpenCoven.WindowsJobSupervisor].GetMethod('ApplyTerminalDirectoryQuotaCheck', $staticFlags) $monitor = [OpenCoven.WindowsJobSupervisor].GetMethod('MonitorDirectoryQuotasAsync', $staticFlags) $stateType = [OpenCoven.WindowsJobSupervisor].GetNestedType('DirectoryQuotaFailureState', [Reflection.BindingFlags]'NonPublic') -$secureDirectory = [OpenCoven.WindowsJobSupervisor].GetMethod( - 'SecureIsolatedDirectory', $staticFlags, $null, - [type[]]@([string], [string], [string]), $null -) $enablePrivilege = [OpenCoven.WindowsJobSupervisor].GetMethod('EnablePrivilege', $staticFlags) -foreach ($method in @($terminal, $monitor, $stateType, $secureDirectory, $enablePrivilege)) { +foreach ($method in @($terminal, $monitor, $stateType, $enablePrivilege)) { if ($null -eq $method) { throw 'Native owner-directory fixture contract is missing.' } } -# PowerShell path/SID values can retain PSObject wrappers. A typed delegate -# binds those strings before entering the native restoration method. -$restoreDirectory = [Delegate]::CreateDelegate([Action[string,string,string]], $secureDirectory) - if (-not ('OpenCoven.Tests.OwnerDirectoryQuotaFixture' -as [type])) { Add-Type -Language CSharp -TypeDefinition @' using System; @@ -33,12 +25,91 @@ using System.Runtime.InteropServices; using System.Security.AccessControl; namespace OpenCoven.Tests { - public static class OwnerDirectoryQuotaFixture + public sealed class OwnerDirectoryQuotaFixture : IDisposable { [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetFileSecurityW(string path, uint information, byte[] descriptor); + // Retain access before revoking the supervisor's directory permissions. + // A null SECURITY_ATTRIBUTES pointer makes this handle noninheritable. + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateFileW(string path, uint access, uint share, + IntPtr attributes, uint creation, uint flags, IntPtr template); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); + [DllImport("kernel32.dll")] + private static extern IntPtr LocalFree(IntPtr memory); + [DllImport("advapi32.dll")] + private static extern uint GetSecurityInfo(IntPtr handle, uint type, uint information, + out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr descriptor); + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetSecurityDescriptorControl(IntPtr descriptor, out ushort control, out uint revision); + [DllImport("advapi32.dll")] + private static extern uint SetSecurityInfo(IntPtr handle, uint type, uint information, + IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl); + + private IntPtr handle; + private IntPtr descriptor; + private IntPtr owner; + private IntPtr dacl; + private uint restoreInformation; + private bool restored; + private bool disposed; + + public OwnerDirectoryQuotaFixture(string path) + { + // READ_CONTROL | WRITE_DAC | WRITE_OWNER; share read/write/delete; + // OPEN_EXISTING; BACKUP_SEMANTICS | OPEN_REPARSE_POINT. + handle = CreateFileW(path, 0x000E0000u, 7, IntPtr.Zero, 3, 0x02200000u, IntPtr.Zero); + if (handle == new IntPtr(-1)) + { + handle = IntPtr.Zero; + throw new Win32Exception(Marshal.GetLastWin32Error(), "Fixture restoration handle could not be opened."); + } + try + { + IntPtr group, sacl; + uint error = GetSecurityInfo(handle, 1, 5, out owner, out group, out dacl, out sacl, out descriptor); + if (error != 0) throw new Win32Exception((int)error, "Fixture security could not be captured."); + ushort control; + uint revision; + if (!GetSecurityDescriptorControl(descriptor, out control, out revision)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Fixture security control could not be read."); + // OWNER | DACL and the original DACL inheritance protection. + restoreInformation = 5u | ((control & 0x1000) != 0 ? 0x80000000u : 0x20000000u); + } + catch + { + Release(); + throw; + } + } + + public void Restore() + { + if (disposed) throw new ObjectDisposedException(nameof(OwnerDirectoryQuotaFixture)); + if (restored) return; + uint error = SetSecurityInfo(handle, 1, restoreInformation, owner, IntPtr.Zero, dacl, IntPtr.Zero); + if (error != 0) throw new Win32Exception((int)error, "Fixture security restoration failed."); + restored = true; + } + + private void Release() + { + if (descriptor != IntPtr.Zero) { LocalFree(descriptor); descriptor = IntPtr.Zero; } + if (handle != IntPtr.Zero) { CloseHandle(handle); handle = IntPtr.Zero; } + } + + public void Dispose() + { + if (disposed) return; + try { Restore(); } + finally { disposed = true; Release(); } + } + public static void Protect(string path, string ownerSid) { // Match Coven's owner-only directory DACL, with the fixture owner @@ -57,7 +128,7 @@ namespace OpenCoven.Tests $fixtureRoot = Join-Path ([IO.Path]::GetTempPath()) ('opencoven-owner-directory-' + [Guid]::NewGuid().ToString('N')) $identity = $null $directory = $null -$directoryCreated = $false +$securityLease = $null $state = $null $cancellation = $null $monitorTask = $null @@ -68,7 +139,7 @@ try { if ($identity.Sid -ceq $supervisorSid) { throw 'Fixture identities must differ.' } $directory = Join-Path $identity.TempPath 'phase1-conformance-run-fixture' [IO.Directory]::CreateDirectory($directory) | Out-Null - $directoryCreated = $true + $securityLease = [OpenCoven.Tests.OwnerDirectoryQuotaFixture]::new($directory) [IO.File]::WriteAllBytes((Join-Path $directory 'payload.bin'), [byte[]]::new(1024)) $quota = [OpenCoven.WindowsDirectoryQuota[]]@( [OpenCoven.WindowsDirectoryQuota]::new('harness execution aggregate', $directory, 2048) @@ -106,7 +177,7 @@ try { } # Restore only this test fixture. Production private ACLs remain untouched. - $restoreDirectory.Invoke($directory, $identity.Sid, $supervisorSid) + $securityLease.Restore() $overflow = [OpenCoven.WindowsJobRunResult]::new() $terminal.Invoke($null, [object[]]@($overflow, [OpenCoven.WindowsDirectoryQuota[]]@( [OpenCoven.WindowsDirectoryQuota]::new('harness execution aggregate', $directory, 512) @@ -136,10 +207,7 @@ try { } if ($null -ne $identity) { try { - # Existence queries can hide access denial; creation state owns teardown. - if ($directoryCreated) { - $restoreDirectory.Invoke($directory, $identity.Sid, $supervisorSid) - } + if ($null -ne $securityLease) { $securityLease.Dispose() } } catch { $failures.Add($_.Exception) } try { $identity.Dispose() } catch { $failures.Add($_.Exception) }