Compare commits

...
Author SHA1 Message Date
gensokyo-dev a606f2ae52 Merge remote-tracking branch 'origin/main' into fix/windows-acl-observer-overhead-20261022 2026-10-22 07:15:43 +00:00
gensokyo-dev b1c7c27d3c fix(ci): remove utility loading from ACL regression setup
Use language-defined ACL fixtures, strict member access and command traps
instead of utility-based object construction and module unloading. Retain
the same six observations and 15-second subprocess deadline.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-22 07:15:43 +00:00
gensokyo-dev e7c90802e9 fix(ci): load the ACL observer before allocating its workspace
Fail before creating a temporary directory if the verifier helper cannot be read.
2026-10-22 06:52:46 +00:00
gensokyo-dev d85e0fffe4 fix(ci): select the Windows lane for ACL observer inputs
Register the shared projection and its regression with the package scope,
and exercise both single-file changes through the real planner.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-22 06:31:19 +00:00
gensokyo-dev 37e61613d9 fix(ci): avoid utility loading in the Windows ACL observer
Keep independent SID and readonly verification within the existing deadline,
and move account-lookup traps into the shared projection regression.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-22 06:20:16 +00:00
7 changed files with 232 additions and 24 deletions

No files matched your search

+1
View File
@@ -875,6 +875,7 @@ jobs:
tests/boundary/process/packageNpmInvocation.test.ts
tests/boundary/process/packedBridgeVerification.test.ts
tests/boundary/process/windowsOutputCapture.test.ts
tests/boundary/process/windowsPrivateRuntimeAclObserver.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
tests/boundary/cli/referenceSourceImportStatus.test.ts
+10 -3
View File
@@ -708,9 +708,16 @@ artifacts, dependency installs, package archives, and runtime verification remai
host-local. Keep platform-dependent outputs out of this shared cache.
The Windows native verifier's private-runtime ACL observer reads SID rules once
for both trustee validation and counting. It deliberately rejects the convenience
`Access` property, which resolves account names. Its `private-runtime-acl` stderr
markers report completed phases and elapsed time; inspect these on a timeout
for both trustee validation and counting, using .NET APIs and a fixed JSON
projection of booleans and a count. Module autoloading is disabled; the observer
does not need PowerShell utility cmdlets or account-name resolution. A separate
PowerShell regression checks the shared projection with utility cmdlet traps
and strict property access on a language-defined ACL fixture that has no `Access`
convenience property. Fixture setup also avoids utility cmdlets. It covers
foreign trustees, different owners, an unprotected DACL, and empty/single rules;
these substitutes do not establish real Windows ACL behavior. The observer's
`private-runtime-acl` stderr markers report completed phases and elapsed time;
inspect these on a timeout
before attributing it to startup, ACL reads, or identity lookup. The original
15-second deadline and all ownership, DACL, and readonly assertions remain in
force. A passing retry alone does not explain a previous timeout.
+2
View File
@@ -118,6 +118,7 @@ export const SCOPES = {
"tests/boundary/process/releaseArtifactCanary.test.ts",
"src/windows/**",
"scripts/lib/windows-owned-command.mjs",
"scripts/lib/windows-private-runtime-acl.ps1",
"scripts/run-exclusive-command.mjs",
"tests/process-global/runnerCancellation.test.ts",
"tests/process-global/windowsOwnedCommand.test.ts",
@@ -170,6 +171,7 @@ export const SCOPES = {
"tests/boundary/process/packageNpmInvocation.test.ts",
"tests/boundary/process/packedBridgeVerification.test.ts",
"tests/boundary/process/windowsOutputCapture.test.ts",
"tests/boundary/process/windowsPrivateRuntimeAclObserver.test.ts",
"tests/acceptance/webModuleCli.test.ts",
"tests/acceptance/webSourceLocationCli.test.ts",
"tests/boundary/cli/referenceSourceImportStatus.test.ts",
@@ -0,0 +1,33 @@
param($Acl, [string]$UserSid, [string]$Path, [Diagnostics.Stopwatch]$Clock)
# Keep this independent observer on .NET SID APIs. PowerShell convenience
# properties and utility cmdlets can introduce account lookup/module loading.
$PSModuleAutoLoadingPreference = 'None'
function Trace-AclPhase([string]$phase) {
[Console]::Error.WriteLine('private-runtime-acl: ' + $phase + ' ' + $Clock.ElapsedMilliseconds + 'ms')
}
$owner = $Acl.GetOwner([Security.Principal.SecurityIdentifier])
Trace-AclPhase 'owner-sid-read'
$rules = @($Acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]))
Trace-AclPhase 'rule-sids-read'
$trusteesOnlyUserAndSystem = $true
foreach ($rule in $rules) {
$sid = $rule.IdentityReference.Value
if ($sid -ne $UserSid -and $sid -ne 'S-1-5-18') {
$trusteesOnlyUserAndSystem = $false
}
}
[IO.File]::SetAttributes($Path, [IO.FileAttributes]([int][IO.File]::GetAttributes($Path) -bor 1))
Trace-AclPhase 'readonly-set'
# The wire values are exclusively booleans and a collection count, so no
# arbitrary strings need JSON escaping or a general-purpose serializer.
$values = @(
([bool]$Acl.AreAccessRulesProtected).ToString().ToLowerInvariant(),
($owner.Value -eq $UserSid).ToString().ToLowerInvariant(),
$trusteesOnlyUserAndSystem.ToString().ToLowerInvariant(),
$rules.Count.ToString([Globalization.CultureInfo]::InvariantCulture),
(([int][IO.File]::GetAttributes($Path) -band 1) -ne 0).ToString().ToLowerInvariant()
)
[Console]::WriteLine(('{{"protected":{0},"ownerMatchesUser":{1},"trusteesOnlyUserAndSystem":{2},"count":{3},"rootReadonly":{4}}}' -f $values))
Trace-AclPhase 'complete'
+7 -21
View File
@@ -48,6 +48,10 @@ const { PrivateRuntimeRootUnavailableError } = await import(
join(packageRoot, "dist/process/PrivateRuntimeRootUnavailableError.js"),
)
);
const privateRuntimeAclObserver = await readFile(
new URL("./lib/windows-private-runtime-acl.ps1", import.meta.url),
"utf8",
);
const workspace = await mkdtemp(join(tmpdir(), "rea-native-conformance-"));
// A pwsh caller can supply module paths incompatible with Windows PowerShell.
// Each independent observer must discover its own module path.
@@ -371,35 +375,17 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"-Command",
String.raw`
$ErrorActionPreference = 'Stop'
$PSModuleAutoLoadingPreference = 'None'
$clock = [Diagnostics.Stopwatch]::StartNew()
function Trace-AclPhase([string]$phase) {
[Console]::Error.WriteLine('private-runtime-acl: ' + $phase + ' ' + $clock.ElapsedMilliseconds + 'ms')
}
Trace-AclPhase 'started'
$p = $env:REA_TEST_PRIVATE_RUNTIME_PATH
$a = Get-Acl -LiteralPath $p
$a = ([IO.DirectoryInfo]::new($p)).GetAccessControl()
Trace-AclPhase 'acl-read'
$u = [Security.Principal.WindowsIdentity]::GetCurrent().User
$owner = $a.GetOwner([Security.Principal.SecurityIdentifier])
Trace-AclPhase 'owner-sid-read'
# Guard the independent observer against accidental account-name resolution.
# Access is a PowerShell convenience property that requests NTAccount rules.
Update-TypeData -TypeName ($a.GetType().FullName) -MemberType ScriptProperty -MemberName Access -Value { throw 'ACL observation must use SID rules' } -Force
if ($a.PSObject.Properties['Access'].MemberType -ne [Management.Automation.PSMemberTypes]::ScriptProperty) {
throw 'ACL observation Access guard was not installed'
}
$rules = @($a.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]))
Trace-AclPhase 'rule-sids-read'
[IO.File]::SetAttributes($p, [IO.FileAttributes]([int][IO.File]::GetAttributes($p) -bor 1))
Trace-AclPhase 'readonly-set'
@{
protected = $a.AreAccessRulesProtected
ownerMatchesUser = ($owner.Value -eq $u.Value)
trusteesOnlyUserAndSystem = (@($rules | Where-Object { $_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18' }).Count -eq 0)
count = $rules.Count
rootReadonly = (([int][IO.File]::GetAttributes($p) -band 1) -ne 0)
} | ConvertTo-Json -Compress
Trace-AclPhase 'complete'
& { ${privateRuntimeAclObserver} } -Acl $a -UserSid $u.Value -Path $p -Clock $clock
`,
],
{
@@ -761,6 +761,14 @@ it.each([
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
{ code: true, package: true, runtime: true, apple: false },
],
[
"scripts/lib/windows-private-runtime-acl.ps1",
{ code: true, package: true, runtime: true, apple: false },
],
[
"tests/boundary/process/windowsPrivateRuntimeAclObserver.test.ts",
{ code: true, package: true, runtime: true, apple: false },
],
[
"tests/boundary/filesystem/clientConfigurationEnvironment.test.ts",
{ code: true, package: true, runtime: true, apple: false },
@@ -0,0 +1,171 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
const shell = process.platform === "win32" ? "powershell.exe" : "pwsh";
const shellError =
process.platform === "win32"
? undefined
: spawnSync(
shell,
["-NoProfile", "-NonInteractive", "-Command", "exit 0"],
{
timeout: 15_000,
},
).error;
const missingShell =
shellError !== undefined &&
"code" in shellError &&
shellError.code === "ENOENT";
it.skipIf(missingShell)(
"observes ACL facts without account lookup or PowerShell utility cmdlets",
async () => {
const observer = await readFile(
new URL(
"../../../scripts/lib/windows-private-runtime-acl.ps1",
import.meta.url,
),
"utf8",
);
const user = "S-1-5-21-123";
const system = "S-1-5-18";
const cases = [
{ owner: user, protected: true, trustees: [user, system] },
{ owner: system, protected: true, trustees: [user, system] },
{ owner: user, protected: false, trustees: [user, system] },
{
owner: user,
protected: true,
trustees: [user, system, "S-1-5-32-544"],
},
{ owner: user, protected: true, trustees: [user] },
{ owner: user, protected: true, trustees: [] },
];
const quotePowerShell = (value: string) =>
`'${value.replaceAll("'", "''")}'`;
const powerShellCases = cases
.map(
({ owner, protected: protectedAcl, trustees }) =>
`[ReaAclFixture]::new(${quotePowerShell(owner)}, $${protectedAcl}, [string[]]@(${trustees.map(quotePowerShell).join(", ")}))`,
)
.join("\n");
const path = await mkdtemp(join(tmpdir(), "rea acl 観測-"));
try {
const output = execFileSync(
shell,
[
"-NoProfile",
"-NonInteractive",
"-Command",
String.raw`
$ErrorActionPreference = 'Stop'
$PSModuleAutoLoadingPreference = 'None'
Set-StrictMode -Version Latest
function Update-TypeData { throw 'Utility cmdlet invocation is forbidden' }
function ConvertTo-Json { throw 'Utility cmdlet invocation is forbidden' }
function ConvertFrom-Json { throw 'Utility cmdlet invocation is forbidden' }
function Add-Member { throw 'Utility cmdlet invocation is forbidden' }
class ReaAclFixture {
[string]$OwnerSid
[bool]$AreAccessRulesProtected
[object[]]$Rules
ReaAclFixture([string]$owner, [bool]$protected, [string[]]$trustees) {
$this.OwnerSid = $owner
$this.AreAccessRulesProtected = $protected
$this.Rules = @(foreach ($sid in $trustees) {
[PSCustomObject]@{ IdentityReference = [PSCustomObject]@{ Value = $sid } }
})
}
[object] GetOwner([type]$type) {
if ($type -ne [Security.Principal.SecurityIdentifier]) { throw 'Owner must request SID form' }
return [PSCustomObject]@{ Value = $this.OwnerSid }
}
[object[]] GetAccessRules([bool]$explicit, [bool]$inherited, [type]$type) {
if (!$explicit -or !$inherited -or $type -ne [Security.Principal.SecurityIdentifier]) {
throw 'Inspect all rules in SID form'
}
return $this.Rules
}
}
$acls = @(
${powerShellCases}
)
foreach ($acl in $acls) {
[IO.File]::SetAttributes($env:REA_ACL_PATH, [IO.FileAttributes]::Directory)
& { ${observer} } -Acl $acl -UserSid 'S-1-5-21-123' -Path $env:REA_ACL_PATH -Clock ([Diagnostics.Stopwatch]::StartNew())
if (([int][IO.File]::GetAttributes($env:REA_ACL_PATH) -band 1) -eq 0) { throw 'Observer did not set readonly' }
}
`,
],
{
encoding: "utf8",
timeout: 15_000,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => name.toLowerCase() !== "psmodulepath",
),
),
REA_ACL_PATH: path,
},
},
);
expect(
output
.trim()
.split(/\r?\n/u)
.map((line) => JSON.parse(line)),
).toEqual([
{
protected: true,
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: true,
count: 2,
rootReadonly: true,
},
{
protected: true,
ownerMatchesUser: false,
trusteesOnlyUserAndSystem: true,
count: 2,
rootReadonly: true,
},
{
protected: false,
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: true,
count: 2,
rootReadonly: true,
},
{
protected: true,
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: false,
count: 3,
rootReadonly: true,
},
{
protected: true,
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: true,
count: 1,
rootReadonly: true,
},
{
protected: true,
ownerMatchesUser: true,
trusteesOnlyUserAndSystem: true,
count: 0,
rootReadonly: true,
},
]);
} finally {
await rm(path, { recursive: true, force: true });
}
},
25_000,
);