Skip to content
Draft
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: 16 additions & 5 deletions src/codex/user-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { isAbsolute, join, resolve } from "node:path";

import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
import { resolveWindowsLocalAppData } from "./windows-local-appdata";

import type {
ResolveCodexCoordinatorDatabasePath,
Expand Down Expand Up @@ -209,10 +210,11 @@ export function decodeWindowsIdentityPowerShellOutputForTests(output: Uint8Array
/**
* Per-process memo for the Windows lookups.
*
* Both values -- the effective token's SID and its known-folder local AppData --
* Both helper values -- the effective token's SID and its known-folder local AppData --
* are fixed for the lifetime of a process: neither can change without a new logon
* token, and the lookups deliberately ignore the environment, so nothing a caller
* does between two calls can alter the answer. Each call otherwise spawns a fresh
* does between two calls can alter the answer. The native x64 known-folder path
* avoids this helper entirely. Each fallback call otherwise spawns a fresh
* PowerShell, roughly 150ms for the SID and 310ms for the folder, and the
* coordinator asks for both on every config write and lock acquisition. That cost
* pushed real multi-process injection tests past their budget while proving
Expand Down Expand Up @@ -265,6 +267,15 @@ function resolveWindowsSid(): string {
return sid.toUpperCase();
}

function windowsLocalAppData(): string {
try {
return resolveWindowsLocalAppData(() => powershellValue(windowsLocalAppDataExpression()));
} catch (cause) {
if (cause instanceof CodexUserIdentityRefusal) throw cause;
refuse("Windows LocalAppData resolution failed.", cause);
}
}

export const resolveEffectiveUserIdentity: ResolveEffectiveUserIdentity = () => {
if (process.platform === "win32") {
return { platform: "win32", sid: resolveWindowsSid() };
Expand Down Expand Up @@ -374,7 +385,7 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
}

if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
const localAppData = powershellValue(windowsLocalAppDataExpression());
const localAppData = windowsLocalAppData();
if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
let entry;
Expand Down Expand Up @@ -404,10 +415,10 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina

function resolveWindowsRuntimeRoot(identity: Extract<UserIdentity, { platform: "win32" }>): string {
if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
const localAppData = powershellValue(windowsLocalAppDataExpression());
const localAppData = windowsLocalAppData();
if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");

// The SID and known-folder values come from the effective token/.NET OS APIs,
// The SID and known-folder values come from effective-token OS APIs,
// never USERPROFILE or LOCALAPPDATA. WP11 adds descriptor/reparse/ACL checks at
// the stable-database open boundary where those checks can cover SQLite too.
const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
Expand Down
84 changes: 84 additions & 0 deletions src/codex/windows-local-appdata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { dlopen, ptr, read, type Pointer } from "bun:ffi";
import { win32 } from "node:path";
import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation";

// FOLDERID_LocalAppData: GUID fields Data1/Data2/Data3 are little-endian.
const LOCAL_APPDATA_ID = Buffer.from("8527b3f1ba6fcf4f9d557b8e7f157091", "hex");
const KF_FLAG_DEFAULT_PATH = 0x400;
const MAX_PATH_CODE_UNITS = 32_768;

export interface WindowsKnownFolderApi {
getPath(id: Uint8Array, flags: number, token: null, output: BigUint64Array): number;
readCodeUnit(address: number, byteOffset: number): number;
free(address: number): void;
}

/** Same current-token/default-path API as the PowerShell fallback, without Add-Type. */
export function readWindowsLocalAppData(api: WindowsKnownFolderApi): string {
const output = new BigUint64Array(1);
let address = 0;
try {
const hr = api.getPath(LOCAL_APPDATA_ID, KF_FLAG_DEFAULT_PATH, null, output);
address = Number(output[0]);
if (hr !== 0 || !Number.isSafeInteger(address) || address <= 0) {
throw new Error("Windows LocalAppData known-folder lookup failed.");
}
const units: number[] = [];
for (let index = 0; index < MAX_PATH_CODE_UNITS; index += 1) {
const unit = api.readCodeUnit(address, index * 2);
if (unit === 0) {
const path = String.fromCharCode(...units);
if (!win32.isAbsolute(path) || !/^[a-z]:\\|^\\\\/i.test(path)) {
throw new Error("Windows LocalAppData known-folder lookup returned an invalid path.");
}
return path;
}
units.push(unit);
}
throw new Error("Windows LocalAppData known-folder lookup returned an unterminated path.");
} finally {
// SHGetKnownFolderPath requires CoTaskMemFree even on a failed HRESULT.
if (address > 0 && Number.isSafeInteger(address)) api.free(address);
}
}

let nativeLookup: (() => string) | null | undefined;

function loadNativeLookup(): (() => string) | null {
if (nativeLookup !== undefined) return nativeLookup;
if (process.platform !== "win32" || process.arch !== "x64") return null;
try {
const systemDirectory = resolveTrustedWindowsSystemDirectory();
const shell = dlopen(win32.join(systemDirectory, "shell32.dll"), {
SHGetKnownFolderPath: { args: ["ptr", "u32", "ptr", "ptr"], returns: "i32" },
});
let ole;
try {
ole = dlopen(win32.join(systemDirectory, "ole32.dll"), {
CoTaskMemFree: { args: ["ptr"], returns: "void" },
});
} catch (error) {
shell.close();
throw error;
}
const api: WindowsKnownFolderApi = {
getPath: (id, flags, token, output) => shell.symbols.SHGetKnownFolderPath(
ptr(id), flags, token, ptr(output),
),
readCodeUnit: (address, offset) => read.u16(address as Pointer, offset),
free: address => { ole.symbols.CoTaskMemFree(address as Pointer); },
};
nativeLookup = () => readWindowsLocalAppData(api);
} catch {
nativeLookup = null;
}
return nativeLookup;
}

/** Unsupported FFI keeps the bounded trusted helper; an OS API failure still refuses. */
export function resolveWindowsLocalAppData(
powershellLookup: () => string,
lookup: (() => string) | null = loadNativeLookup(),
): string {
return lookup ? lookup() : powershellLookup();
}
2 changes: 1 addition & 1 deletion structure/codex-home.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ separate role. It is not a kernel lock, a cross-host lock, or protection against
writer between byte verification and rename. A rolling mix of old writers that do not
reserve takeover has the same cooperation limit.

Windows secret-file hardening resolves the effective token SID through an absolute, trusted
Effective-user roots use `src/codex/windows-local-appdata.ts` ([known-folder contract](ops/test-sandbox-cleanup.md#windows-runtime-namespace)). Windows secret-file hardening resolves the effective token SID through an absolute, trusted
PowerShell path before granting the owner and removing inherited broad ACL entries. The normal
path obtains System32 from `GetSystemDirectoryW`. Windows ARM64 Bun builds that cannot execute
`bun:ffi` use a narrower ACL-only fallback to the fixed protected default installation path
Expand Down
12 changes: 12 additions & 0 deletions structure/ops/test-sandbox-cleanup.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Test Sandbox Cleanup

## Windows runtime namespace

`src/codex/user-identity.ts` keys runtime namespaces on the effective SID and environment-independent
LocalAppData. `src/codex/windows-local-appdata.ts` calls `SHGetKnownFolderPath` in-process on Windows
x64, loading Shell32/Ole32 from the OS-resolved system directory. It uses `KF_FLAG_DEFAULT_PATH`
and a null token, bounds UTF-16 decoding, and frees the allocation even when the API fails.
Unsupported FFI retains the bounded trusted PowerShell helper; native API failures refuse.
`USERPROFILE` and `LOCALAPPDATA` never choose this root. Runtime canonicalization/reparse checks
and the Bun test lock's required ACL hardening remain separate admission checks.

## Sandbox lifecycle

The batch runner's per-process home isolation and live-home/service-manager guards remain active
because the preload installs them before the lock boundary. `tests/preload.ts` resolves cleanup
dependencies after home/lock admission and before test cases. Teardown awaits native-main startup
Expand Down
94 changes: 94 additions & 0 deletions tests/codex-integration/codex-user-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,104 @@ import {
} from "../../src/codex/user-identity";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { repoPath } from "../helpers/repo-root";
import {
readWindowsLocalAppData,
resolveWindowsLocalAppData,
type WindowsKnownFolderApi,
} from "../../src/codex/windows-local-appdata";

let codexHome = "";
let previousHome: string | undefined;

function knownFolderFixture(path = "C:\\Users\\한글😀\\AppData\\Local", hr = 0) {
const freed: number[] = [];
const api: WindowsKnownFolderApi = {
getPath(id, flags, token, output) {
expect(Buffer.from(id).toString("hex")).toBe("8527b3f1ba6fcf4f9d557b8e7f157091");
expect(flags).toBe(0x400);
expect(token).toBeNull();
expect(output[0]).toBe(0n);
output[0] = 4096n;
return hr;
},
readCodeUnit(address, offset) {
expect(address).toBe(4096);
return path.charCodeAt(offset / 2) || 0;
},
free(address) { freed.push(address); },
};
return { api, freed, path };
}

test("native known-folder resolution avoids a timed-out PowerShell identity probe", () => {
const fixture = knownFolderFixture();
let helperCalls = 0;
const timedOutHelper = (): never => {
helperCalls += 1;
throw new Error("Windows effective-account lookup timed out.");
};
// Toggle only native availability: the failing helper models the 30s Add-Type
// timeout without a wall-clock sleep, on every platform.
expect(() => resolveWindowsLocalAppData(timedOutHelper, null)).toThrow("timed out");
expect(helperCalls).toBe(1);
expect(resolveWindowsLocalAppData(timedOutHelper, () => readWindowsLocalAppData(fixture.api)))
.toBe(fixture.path);
expect(helperCalls).toBe(1);
expect(fixture.freed).toEqual([4096]);
});

test("native known-folder failures refuse and free the OS allocation without helper fallback", () => {
for (const fixture of [knownFolderFixture(undefined, -2147467259), knownFolderFixture("relative")]) {
let fallbackCalls = 0;
expect(() => resolveWindowsLocalAppData(
() => { fallbackCalls += 1; return "C:\\forged-environment"; },
() => readWindowsLocalAppData(fixture.api),
)).toThrow("known-folder lookup");
expect(fixture.freed).toEqual([4096]);
expect(fallbackCalls).toBe(0);
}
});

test("native known-folder resolution refuses a null result and frees on decoding failure", () => {
const fixture = knownFolderFixture();
fixture.api.getPath = () => 0;
expect(() => readWindowsLocalAppData(fixture.api)).toThrow("lookup failed");
expect(fixture.freed).toEqual([]);

const broken = knownFolderFixture();
broken.api.readCodeUnit = () => { throw new Error("read failed"); };
expect(() => readWindowsLocalAppData(broken.api)).toThrow("read failed");
expect(broken.freed).toEqual([4096]);
});

test("native known-folder resolution bounds an unterminated response and frees it", () => {
const fixture = knownFolderFixture();
let reads = 0;
fixture.api.readCodeUnit = () => { reads += 1; return 65; };
expect(() => readWindowsLocalAppData(fixture.api)).toThrow("unterminated path");
expect(reads).toBe(32_768);
expect(fixture.freed).toEqual([4096]);
});

test.if(process.platform === "win32" && process.arch === "x64")(
"Windows x64 obtains the real known folder in-process even when the helper cannot start",
() => {
const previous = { USERPROFILE: process.env.USERPROFILE, LOCALAPPDATA: process.env.LOCALAPPDATA };
try {
process.env.USERPROFILE = "C:\\forged-known-folder-profile";
process.env.LOCALAPPDATA = "C:\\forged-known-folder-appdata";
const path = resolveWindowsLocalAppData(() => { throw new Error("PowerShell must not start"); });
expect(isAbsolute(path)).toBe(true);
expect(path).not.toContain("forged-known-folder");
} finally {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
},
);

const CHILD_TIMEOUT_MS = 10_000;
const userIdentityModuleUrl = pathToFileURL(
repoPath("src", "codex", "user-identity.ts"),
Expand Down
Loading