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
3 changes: 2 additions & 1 deletion docs/phase1-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,8 @@ 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` | 171,179 | `55e9cf065e2dc7cc656c6aa8cc9ea53542259d3d7eee55c368c6cf0fc6356ab9` |
| `scripts/windows-job-supervisor.test.ps1` | 172,760 | `cecc4c4a88ddceff68ab941798a700d2f60e18048be6373044e6c115b08bfcfe` |
| `scripts/windows-status-acl-probe.cs` | 6,559 | `aeb7fec2d8becf63b5e94e93d2f8b56cf761ea76d4a714a33f6457a3c65dabe7` |

The table above is the SDK-facing subset; `phase1-conformance.lock.json`'s
`harnessAuthority.files` also tracks `.github/workflows/ci.yml`, which does
Expand Down
38 changes: 38 additions & 0 deletions scripts/windows-job-supervisor.test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,16 @@ public static class ScmDenialProbe
'@,
[Text.UTF8Encoding]::new($false)
)
$statusAclProbeSource = Join-Path $root 'windows-status-acl-probe.cs'
[IO.File]::Copy(
(Join-Path $PSScriptRoot 'windows-status-acl-probe.cs'),
$statusAclProbeSource
)
Add-Type -Path $statusAclProbeSource
$statusAclControl = [StatusAclProbe]::RunControl([IO.Path]::GetTempPath())
if ($statusAclControl -cne "combined:success`nowner-only:success`ndacl-only:success") {
throw "Ordinary-directory status ACL control failed: $statusAclControl"
}
$accessProbeScript = Join-Path $root 'job-access-probe.ps1'
[IO.File]::WriteAllText(
$accessProbeScript,
Expand All @@ -1554,6 +1564,13 @@ Add-Type -TypeDefinition ([IO.File]::ReadAllText('$($accessProbeSource.Replace("
[long]`$env:OPENCOVEN_WINDOWS_SUPERVISOR_JOB_HANDLE
)
[JobAccessProbe]::Run(`$env:OPENCOVEN_ACCESS_PROBE_JOB)
Add-Type -Path '$($statusAclProbeSource.Replace("'", "''"))'
`$statusAclResult = [StatusAclProbe]::Run(
`$env:TEMP,
`$env:OPENCOVEN_STATUS_ACL_SUPERVISOR_SID
)
Write-Output "status-acl-probe:`n`$statusAclResult"

`$root = [IO.Path]::GetFullPath(`$env:OPENCOVEN_WINDOWS_BOOTSTRAP_ROOT)
`$profile = [IO.Path]::GetFullPath(`$env:USERPROFILE)
`$temp = [IO.Path]::GetFullPath(`$env:TEMP)
Expand Down Expand Up @@ -1645,6 +1662,13 @@ if (-not `$wmiDenied) {
"Local\OpenCoven.Chat.SupervisorTest.$([Guid]::NewGuid().ToString('N'))"
$accessEnvironment = $childEnvironment.Clone()
$accessEnvironment.OPENCOVEN_ACCESS_PROBE_JOB = $accessJobName
$statusAclIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
try {
$accessEnvironment.OPENCOVEN_STATUS_ACL_SUPERVISOR_SID = $statusAclIdentity.User.Value
} finally {
$statusAclIdentity.Dispose()
}

$accessEnvironment.OPENCOVEN_DENIAL_SERVICE_NAME =
"OpenCovenSupervisorTest$([Guid]::NewGuid().ToString('N'))"
$accessEnvironment.OPENCOVEN_DENIAL_WMI_FILTER_NAME =
Expand All @@ -1664,6 +1688,20 @@ if (-not `$wmiDenied) {
if ($accessResult.ExitCode -ne 0) {
throw "Protected Job Object DACL runtime probe failed: $($accessResult.Stderr)"
}
$statusAclLines = $accessResult.Stdout.Trim() -split '\r?\n'
if ($statusAclLines.Count -ne 4 -or $statusAclLines[0] -cne 'status-acl-probe:') {
throw 'Status ACL probe result shape was invalid.'
}
$statusAclLabels = @('combined', 'owner-only', 'dacl-only')
for ($index = 0; $index -lt 3; $index++) {
$pattern = '^' + $statusAclLabels[$index] +
':(success|access-denied|invalid-owner|privilege-not-held|unclassified)$'
if ($statusAclLines[$index + 1] -cnotmatch $pattern) {
throw 'Status ACL probe result was outside the bounded vocabulary.'
}
Write-Host "status-acl.$($statusAclLines[$index + 1])"
}

} finally {
$accessJob.Dispose()
}
Expand Down
134 changes: 134 additions & 0 deletions scripts/windows-status-acl-probe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.IO;
using System.Security.AccessControl;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security.Principal;

// Run only inside a disposable directory supplied by the existing supervisor
// test context. No ACL or token privilege changes are made to that directory.
public static class StatusAclProbe
{
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW(
string text, uint revision, out IntPtr descriptor, out uint size);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool GetSecurityDescriptorDacl(IntPtr descriptor,
out bool present, out IntPtr dacl, out bool defaulted);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
private static extern uint SetNamedSecurityInfoW(string name, uint type,
uint information, IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr memory);

private static string Classify(uint status)
{
if (status == 0) return "success";
if (status == 5) return "access-denied";
if (status == 1307) return "invalid-owner";
if (status == 1314) return "privilege-not-held";
return "unclassified";
}

private static void VerifyInheritedSecurity(string path, string supervisorSid)
{
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
if (identity.User == null) throw new InvalidOperationException("token-user-missing");
FileSecurity security = new FileInfo(path).GetAccessControl();
if (!identity.User.Equals(security.GetOwner(typeof(SecurityIdentifier))))
throw new InvalidOperationException("created-file-owner-mismatch");
var expected = new Dictionary<string, int> {
{ "S-1-5-18", 0x001f01ff },
{ "S-1-5-32-544", 0x001f01ff },
{ supervisorSid, 0x001f01ff },
{ identity.User.Value, 0x001301bf },
{ "S-1-3-4", 0x00020000 }
};
var descriptor = new RawSecurityDescriptor(security.GetSecurityDescriptorBinaryForm(), 0);
if (descriptor.DiscretionaryAcl == null || descriptor.DiscretionaryAcl.Count != expected.Count)
throw new InvalidOperationException("created-file-acl-count-mismatch");
foreach (GenericAce entry in descriptor.DiscretionaryAcl)
{
var ace = entry as CommonAce;
int mask;
if (ace == null || ace.IsCallback || ace.AceQualifier != AceQualifier.AccessAllowed
|| (ace.AceFlags & AceFlags.Inherited) == 0
|| (ace.AceFlags & AceFlags.InheritOnly) != 0
|| !expected.TryGetValue(ace.SecurityIdentifier.Value, out mask)
|| ace.AccessMask != mask)
throw new InvalidOperationException("created-file-acl-mismatch");
expected.Remove(ace.SecurityIdentifier.Value);
}
if (expected.Count != 0) throw new InvalidOperationException("created-file-acl-missing");
}
}

public static string RunControl(string scratchDirectory)
{
return RunInternal(scratchDirectory, null);
}

public static string Run(string scratchDirectory, string supervisorSid)
{
if (string.IsNullOrEmpty(supervisorSid))
throw new InvalidOperationException("supervisor-identity-required");
return RunInternal(scratchDirectory, supervisorSid);
}

private static string RunInternal(string scratchDirectory, string supervisorSid)
{
if (!OperatingSystem.IsWindows()) throw new InvalidOperationException("windows-required");
if (!Directory.Exists(scratchDirectory)) throw new InvalidOperationException("scratch-required");
IntPtr descriptor = IntPtr.Zero;
IntPtr owner = IntPtr.Zero;
string root = Path.Combine(scratchDirectory, "status-acl-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
uint size;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
"D:P(A;;GA;;;OW)", 1, out descriptor, out size))
throw new InvalidOperationException("descriptor-conversion-failed");
bool present, defaulted;
IntPtr dacl;
if (!GetSecurityDescriptorDacl(descriptor, out present, out dacl, out defaulted)
|| !present || dacl == IntPtr.Zero)
throw new InvalidOperationException("descriptor-dacl-failed");
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
if (identity.User == null) throw new InvalidOperationException("token-user-missing");
byte[] sid = new byte[identity.User.BinaryLength];
identity.User.GetBinaryForm(sid, 0);
owner = Marshal.AllocHGlobal(sid.Length);
Marshal.Copy(sid, 0, owner, sid.Length);
}
string[] labels = { "combined", "owner-only", "dacl-only" };
uint[] flags = { 0x80000005u, 1u, 0x80000004u };
string[] results = new string[3];
for (int i = 0; i < labels.Length; i++)
{
string path = Path.Combine(root, labels[i]);
// Exclusive creation, write, sync, and close mirror the writer.
using (FileStream file = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
{
file.WriteByte(10);
file.Flush(true);
}
if (supervisorSid != null) VerifyInheritedSecurity(path, supervisorSid);
uint status = SetNamedSecurityInfoW(path, 1, flags[i],
i == 2 ? IntPtr.Zero : owner, IntPtr.Zero,
i == 1 ? IntPtr.Zero : dacl, IntPtr.Zero);
results[i] = labels[i] + ":" + Classify(status);
}
return string.Join("\n", results);
}
finally
{
if (owner != IntPtr.Zero) Marshal.FreeHGlobal(owner);
if (descriptor != IntPtr.Zero) LocalFree(descriptor);
// File deletion uses existing modify rights, never an ACL override.
Directory.Delete(root, true);
}
}
}
1 change: 1 addition & 0 deletions src/client-v1-conformance-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,6 +2307,7 @@ ${source.slice(start, end)}
'scripts/phase1-windows-supervisor-install.ps1',
'scripts/windows-job-supervisor.cs',
'scripts/windows-job-supervisor.test.ps1',
'scripts/windows-status-acl-probe.cs',
];
for (const relativePath of [...new Set(metadataPaths)]) {
const bytes = readFileSync(resolve(projectRoot, relativePath));
Expand Down
Loading