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
7 changes: 6 additions & 1 deletion sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,12 @@ async function secureWindowsCredentialHome(path: string): Promise<void> {
" foreach ($entry in $entries) {",
" if (($entry.Attributes -band 1024) -and ($entry.LinkType -in @('SymbolicLink', 'Junction'))) { throw 'Windows credential home contains a symbolic link or junction' }",
" if ($entry.PSObject.TypeNames -notcontains 'System.IO.DirectoryInfo' -and $entry.PSObject.TypeNames -notcontains 'System.IO.FileInfo') { throw 'Windows credential home contains an unsafe entry' }",
" try { Write-CredentialAcl $entry.FullName } catch { if ($_.FullyQualifiedErrorId -like 'GetAcl_PathNotFound,*') { continue }; throw }",
" try { Write-CredentialAcl $entry.FullName } catch {",
// A descendant can disappear inside Get-Acl after it was enumerated.
` if ($_.FullyQualifiedErrorId -eq 'System.IO.FileNotFoundException,Microsoft.PowerShell.Commands.GetAclCommand') { exit ${WINDOWS_CREDENTIAL_DESCENDANTS_CHANGED_EXIT_CODE} }`,
" if ($_.FullyQualifiedErrorId -like 'GetAcl_PathNotFound,*') { continue }",
" throw",
" }",
" if ($entry.PSIsContainer) { Read-CredentialDescendants $entry.FullName }",
" }",
"}",
Expand Down
94 changes: 94 additions & 0 deletions sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2831,6 +2831,100 @@ describe("runtime directories and plugin Python boundary", () => {
});
});

test
.skipIf(process.platform !== "win32")
.each([
"transient missing descendant",
"persistent missing descendant",
"access denied",
"unexpected error",
"missing home",
] as const)("replays Windows credential ACL %s", async (kind) => {
if (
runTestInSubprocess(
import.meta.path,
`replays Windows credential ACL ${kind}`,
)
) {
return;
}
const root = await temporaryDirectory();
const home = join(root, "home");
await mkdir(home);
await requireSecureCredentialHome(home);
const descendant = join(home, ".auth-replay.tmp");
await writeFile(descendant, "synthetic credential\n", { mode: 0o600 });
const missing = kind.includes("missing");
const exception = missing
? "System.IO.FileNotFoundException"
: kind === "access denied"
? "System.UnauthorizedAccessException"
: "System.InvalidOperationException";
const errorId = missing
? "System.IO.FileNotFoundException,Microsoft.PowerShell.Commands.GetAclCommand"
: kind === "access denied"
? "System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetAclCommand"
: "SyntheticCredentialAclFailure";
const category =
kind === "access denied"
? "PermissionDenied"
: missing
? "NotSpecified"
: "ObjectNotFound";
const failurePath = kind === "missing home" ? home : descendant;
// Replay the native error without relying on racing a filesystem deletion.
const injection = `if ($path -eq '${failurePath.replaceAll("'", "''")}') { throw [System.Management.Automation.ErrorRecord]::new([${exception}]::new('synthetic credential ACL error'), '${errorId}', [System.Management.Automation.ErrorCategory]::${category}, $path) };`;
const marker = "function Write-CredentialAcl($path) {";
const originalSpawn = childProcess.spawn;
let attempts = 0;
mock.module("node:child_process", () => ({
...childProcess,
spawn: (...spawnArgs: Parameters<typeof childProcess.spawn>) => {
const [command, args, options] = spawnArgs;
if (
options?.env?.["CODEX_SECURITY_CREDENTIAL_ACL_PATH"] !== home ||
!Array.isArray(args)
) {
return originalSpawn(...spawnArgs);
}
const scriptIndex = args.indexOf("-Command") + 1;
const script = args[scriptIndex];
if (typeof script !== "string" || !script.includes(marker)) {
return originalSpawn(...spawnArgs);
}
attempts += 1;
if (kind === "transient missing descendant" && attempts > 1) {
return originalSpawn(...spawnArgs);
}
expect(script.split(marker)).toHaveLength(2);
const replayArgs = [...args];
replayArgs[scriptIndex] = script.replace(
marker,
`${marker} ${injection}`,
);
return originalSpawn(command, replayArgs, options);
},
}));
try {
if (kind === "transient missing descendant") {
await requireSecureCredentialHome(home);
expect(attempts).toBe(2);
} else {
await expect(requireSecureCredentialHome(home)).rejects.toThrow(
kind === "persistent missing descendant"
? "Windows credential descendants could not be verified"
: "synthetic credential ACL error",
);
expect(attempts).toBe(kind === "persistent missing descendant" ? 3 : 1);
}
} finally {
mock.module("node:child_process", () => ({
...childProcess,
spawn: originalSpawn,
}));
}
});

test("retries interrupted Windows credential ACL enumeration", async () => {
const root = await temporaryDirectory();
const home = join(root, "home");
Expand Down
Loading