v1.90.0.0 feat: make browser cookie imports explicit and safe (#2964)

* fix(browse): prepare reliable cookie import wave for validation

* ci: sequence quality and behavior for validation branch

* fix(browse): isolate Windows qualification and preserve native diagnostics

* test(browse): cover cookie workflow quality and isolate Windows user paths

* test(browse): trace native member startup and initialize fresh folders

* fix(browse): keep Windows member stdin alive through EOF

* fix(browse): latch native timeouts and compare contained Edge startup

* test(browse): verify native version metadata and actual Windows argv

* test(browse): qualify Dia import on isolated macOS CI

* fix(browse): require picker origin for session mutations

* fix(browse): bound credential reads through stream completion

* test(browse): inspect owned Windows process arguments natively

* test(evals): preserve passing coverage during cookie repair reruns

* test(browse): isolate Dia qualification in a fresh macOS account

* test(browse): pass bounded integer timeouts to native Mac probes

* test(browse): distinguish Windows profile initialization from containment

* test(browse): await descendant pipe readiness before parent exit

* test(browse): initialize and restore isolated macOS Keychain state

* test(browse): initialize Windows fixture folders before qualification

* test(ci): pin the same Node runtime across Windows checks

* test(browse): distinguish native macOS browser preflight stages

* test(browse): isolate Windows descendant console lifetime

* test(browse): preserve native receipts and identify fixture lock holders

* test(browse): prepare dependency resolution before native Mac worker startup

* test(ci): include lock and close checks in native diagnostics

* test(browse): preserve native owner probe stages and subprocess deadlines

* fix(browse): classify Chromium profile-in-use exit precisely

* test(browse): retain Mac qualification evidence through cleanup failures

* test(browse): bound Mac fixture paths and retire its owned user domain

* test(browse): accept vanished fixture entries without weakening cleanup

* test(browse): identify probe-created macOS user domains safely

* test(browse): observe Mac user domains without targeting them first

* test(browse): use passive fresh-user ownership throughout Mac qualification

* test(browse): distinguish profile and registered-home Keychain lookups

* test(browse): qualify Dia under one registered account home

* test(browse): identify Dia startup and owned process-group failures

* test(browse): classify bounded Dia startup diagnostics without leaking output

* fix(test): preserve native Mac sandboxing and reap owned browser children

* fix(browse): preserve Chromium sandboxing for native profile imports

* test(browse): inspect signed Mach-O architecture without launching Xcode tools

* test(browse): sample pending Dia startup and reap on all cleanup paths

* test(browse): compare protected Dia launches in fresh Bun and Node accounts

* test(browse): inspect isolated Mac GUI readiness without browser access

* v1.90.0.0 fix: bind cookie picker actions to their document

* test: validate cookie guards and fit nested launch fixtures

* ci: configure the bundled Chromium sandbox helper

* fix(browse): classify Playwright authentication timeouts

* test: retain bounded Windows lifecycle diagnostics

* test(cso): reuse bounded NTFS precision candidates

* test(review): handle explicit preservation choices safely

* test(browse): remove owned fixture directories with explicit primitives

* test(review): distinguish descriptive reuse from edit commitments

* test: admit only the approved unscored cookie workflow refusal

* test: keep the Office Hours judge mock export-complete

* fix: keep dependency-free CI planners independent of the model SDK

* test: observe the exact holder after a native fixture unlink failure

* fix: start seeded PTY observations at owned readiness

* test: acquire identity-bound Windows deletion admission before profile resets

* test: preserve qualified Git index bits without authorizing mutations
This commit is contained in:
Garry Tan
2026-09-25 12:06:45 -04:00
committed by GitHub
parent 730a1017d1
commit a84b0b5b6d
111 changed files with 14996 additions and 1057 deletions
+125
View File
@@ -0,0 +1,125 @@
import { lstatSync, unlinkSync } from 'node:fs';
import { toNamespacedPath } from 'node:path';
import { dlopen, FFIType, ptr } from 'bun:ffi';
type Identity = { dev: bigint; ino: bigint; mode: bigint };
type Handle = number | bigint;
type Stage = 'admission' | 'identity' | 'disposition' | 'close' | 'absence';
export class FixtureDeleteError extends Error {
readonly code: string;
readonly syscall: string;
constructor(public stage: Stage, public path: string, public win32Error?: number, public deadlineExceeded = false) {
super(`Owned fixture deletion failed at ${stage}${deadlineExceeded ? ' (deadline)' : win32Error === undefined ? '' : ` (Windows ${win32Error})`}`);
this.name = 'FixtureDeleteError';
this.code = deadlineExceeded ? 'ETIMEDOUT' : win32Error === 32 ? 'EBUSY' : win32Error === 2 || win32Error === 3 ? 'ENOENT' : win32Error === 5 ? 'EACCES' : 'EIO';
this.syscall = { admission: 'open', identity: 'fstat', disposition: 'unlink', close: 'close', absence: 'lstat' }[stage];
}
}
export interface FixtureDeleteBackend {
open(file: string): Handle;
identity(handle: Handle, file: string): { dev: bigint; ino: bigint; attributes: number; filesystem: string };
dispose(handle: Handle, file: string): void;
close(handle: Handle, file: string): void;
absent(file: string): boolean;
}
type LeaseClock = { now(): number; wait(ms: number): void; onSharing?(): void };
const leaseClock: LeaseClock = { now: () => performance.now(), wait: ms => { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } };
export function deleteWithFixtureLease(file: string, expected: Identity, deadline: number, verify: () => void,
backend: FixtureDeleteBackend, clock: LeaseClock = leaseClock): { admissionProbes: number; waitedMs: number } {
if (!Number.isFinite(deadline)) throw new Error('Invalid fixture deletion deadline');
let handle: Handle | undefined;
let firstSharing: FixtureDeleteError | undefined;
let admissionProbes = 0;
let waitedMs = 0;
while (handle === undefined) {
if (clock.now() >= deadline) throw firstSharing ?? new FixtureDeleteError('admission', file, undefined, true);
verify();
if (clock.now() >= deadline) throw firstSharing ?? new FixtureDeleteError('admission', file, undefined, true);
try { admissionProbes++; handle = backend.open(file); }
catch (error) {
if (!(error instanceof FixtureDeleteError) || error.stage !== 'admission' || error.win32Error !== 32) throw error;
if (!firstSharing) { firstSharing = error; clock.onSharing?.(); }
const remaining = deadline - clock.now();
if (remaining <= 0) throw firstSharing;
const before = clock.now();
clock.wait(Math.min(20, remaining));
waitedMs += Math.max(0, clock.now() - before);
}
}
let failed = false;
let failure: unknown;
try {
const current = backend.identity(handle, file);
if (current.filesystem !== 'NTFS' || current.dev !== expected.dev || current.ino !== expected.ino
|| (current.attributes & (0x1 | 0x10 | 0x400)) !== 0) throw new FixtureDeleteError('identity', file);
verify();
if (clock.now() >= deadline) throw firstSharing ?? new FixtureDeleteError('admission', file, undefined, true);
backend.dispose(handle, file);
} catch (error) { failed = true; failure = error; }
try { backend.close(handle, file); }
catch (error) {
if (failed) console.error(JSON.stringify({ nativeFixtureDeleteCloseFailure: {
stage: error instanceof FixtureDeleteError ? error.stage : 'close',
win32Error: error instanceof FixtureDeleteError ? error.win32Error : undefined,
} }));
else { failed = true; failure = error; }
}
if (failed) throw failure;
if (!backend.absent(file)) throw new FixtureDeleteError('absence', file);
return { admissionProbes, waitedMs };
}
export function createFixtureDeleteLease(deadline: number, onSharing?: () => void) {
if (process.platform !== 'win32') return { unlink: (file: string, _identity: Identity, _verify: () => void) => unlinkSync(file), close: () => {} };
const kernel = dlopen('kernel32.dll', {
CreateFileW: { args: [FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.u64], returns: FFIType.u64 },
GetFileInformationByHandle: { args: [FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
GetVolumeInformationByHandleW: { args: [FFIType.u64, FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
SetFileInformationByHandle: { args: [FFIType.u64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
GetLastError: { args: [], returns: FFIType.u32 },
});
const backend: FixtureDeleteBackend = {
open(file) {
const name = Buffer.from(toNamespacedPath(file) + '\0', 'utf16le');
const handle = kernel.symbols.CreateFileW(ptr(name), 0x10080, 3, null, 3, 0x00200000, 0);
const error = kernel.symbols.GetLastError();
if (BigInt(handle) === 0xffffffffffffffffn || BigInt(handle) === 0n) throw new FixtureDeleteError('admission', file, error);
return handle;
},
identity(handle, file) {
const info = Buffer.alloc(52);
if (!kernel.symbols.GetFileInformationByHandle(handle, ptr(info))) throw new FixtureDeleteError('identity', file, kernel.symbols.GetLastError());
const filesystem = Buffer.alloc(128);
if (!kernel.symbols.GetVolumeInformationByHandleW(handle, null, 0, null, null, null, ptr(filesystem), 64)) throw new FixtureDeleteError('identity', file, kernel.symbols.GetLastError());
return { dev: BigInt(info.readUInt32LE(28)), ino: (BigInt(info.readUInt32LE(44)) << 32n) | BigInt(info.readUInt32LE(48)),
attributes: info.readUInt32LE(0), filesystem: filesystem.toString('utf16le').split('\0')[0] };
},
dispose(handle, file) {
const flags = Buffer.alloc(4);
flags.writeUInt32LE(3);
if (!kernel.symbols.SetFileInformationByHandle(handle, 21, ptr(flags), 4)) throw new FixtureDeleteError('disposition', file, kernel.symbols.GetLastError());
},
close(handle, file) {
if (!kernel.symbols.CloseHandle(handle)) throw new FixtureDeleteError('close', file, kernel.symbols.GetLastError());
},
absent(file) {
try { lstatSync(file); return false; }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true; throw error; }
},
};
return {
unlink(file: string, identity: Identity, verify: () => void) {
if ((identity.mode & 0o170000n) !== 0o100000n) { verify(); unlinkSync(file); return; }
const receipt = deleteWithFixtureLease(file, identity, deadline, verify, backend, { ...leaseClock, onSharing });
if (receipt.admissionProbes > 1) console.log(JSON.stringify({ nativeFixtureSharingAdmission: {
objectDev: identity.dev.toString(), objectIno: identity.ino.toString(), ...receipt, dispositionCalls: 1,
} }));
},
close() { kernel.close(); },
};
}
+132
View File
@@ -0,0 +1,132 @@
import { lstatSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { dlopen, FFIType, ptr } from 'bun:ffi';
let stage = 'platform';
class FileOwnerProbeError extends Error {
errorCode?: string;
constructor(public stage: string, error: unknown) {
super('owner_query_failed');
const code = (error as NodeJS.ErrnoException | undefined)?.code;
if (['EPERM', 'EACCES', 'EBUSY', 'ENOENT', 'EINVAL', 'ENOTDIR', 'ENAMETOOLONG', 'ETIMEDOUT'].includes(code || '')) this.errorCode = code;
}
}
function inspect() {
if (process.platform !== 'win32' || !['x64', 'arm64'].includes(process.arch)) return { available: false, reason: 'not_windows' };
stage = 'input_decode';
const input = JSON.parse(Buffer.from(process.argv[2], 'base64').toString('utf8'));
if (typeof input.root !== 'string' || typeof input.file !== 'string' || !Number.isSafeInteger(input.testPid)) return { available: false, reason: 'invalid_input' };
stage = 'resolve_root';
const root = realpathSync(input.root);
stage = 'resolve_file';
const file = realpathSync(input.file);
const relative = path.relative(root, file);
stage = 'file_stat';
const initial = lstatSync(input.file, { bigint: true });
if (relative.startsWith('..') || path.isAbsolute(relative) || !initial.isFile() || initial.isSymbolicLink()
|| relative !== path.relative(root, path.resolve(input.file))) return { available: false, reason: 'outside_owned_fixture' };
if (input.expectedIdentity !== undefined && (input.expectedIdentity?.dev !== initial.dev.toString()
|| input.expectedIdentity?.ino !== initial.ino.toString())) return { available: false, reason: 'failed_object_identity_changed' };
const unchanged = () => {
stage = 'file_recheck';
const current = lstatSync(input.file, { bigint: true });
return current.isFile() && !current.isSymbolicLink() && current.dev === initial.dev && current.ino === initial.ino
&& realpathSync(input.file) === file;
};
stage = 'load_restart_manager';
const restart = dlopen(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'rstrtmgr.dll'), {
RmStartSession: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
RmRegisterResources: { args: [FFIType.u32, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
RmGetList: { args: [FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.u32 },
RmEndSession: { args: [FFIType.u32], returns: FFIType.u32 },
});
stage = 'load_kernel';
const kernel = dlopen('kernel32.dll', {
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
GetProcessTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
QueryFullProcessImageNameW: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
});
let session: number | undefined;
try {
const sessionBuffer = Buffer.alloc(4);
const key = Buffer.alloc(66);
stage = 'session_start';
let status = restart.symbols.RmStartSession(ptr(sessionBuffer), 0, ptr(key));
if (status !== 0) return { available: false, reason: 'session_start', status };
session = sessionBuffer.readUInt32LE(0);
const wideFile = Buffer.from(file + '\0', 'utf16le');
const names = Buffer.alloc(8);
names.writeBigUInt64LE(BigInt(ptr(wideFile)));
stage = 'register_file';
status = restart.symbols.RmRegisterResources(session, 1, ptr(names), 0, null, 0, null);
if (status !== 0) return { available: false, reason: 'register_file', status };
const needed = Buffer.alloc(4);
const count = Buffer.alloc(4);
const rebootReasons = Buffer.alloc(4);
stage = 'owner_count';
status = restart.symbols.RmGetList(session, ptr(needed), ptr(count), null, ptr(rebootReasons));
if (status === 0 && needed.readUInt32LE(0) === 0) return unchanged()
? { available: true, owners: [], rebootReasons: rebootReasons.readUInt32LE(0) }
: { available: false, reason: 'failed_object_identity_changed' };
const entries = needed.readUInt32LE(0);
if (status !== 234 || entries < 1 || entries > 64) return { available: false, reason: 'owner_count', status, entries };
const information = Buffer.alloc(entries * 668);
count.writeUInt32LE(entries);
stage = 'owner_list';
status = restart.symbols.RmGetList(session, ptr(needed), ptr(count), ptr(information), ptr(rebootReasons));
const returned = count.readUInt32LE(0);
if (status !== 0 || returned > entries) return { available: false, reason: 'owner_list', status };
const owners = [];
stage = 'owner_identity';
for (let index = 0; index < returned; index++) {
const offset = index * 668;
const pid = information.readUInt32LE(offset);
const recordedStart = information.readBigUInt64LE(offset + 4);
let image = 'unavailable';
let creationMatched = false;
const handle = kernel.symbols.OpenProcess(0x1000, 0, pid);
if (handle) {
try {
const times = Buffer.alloc(32);
const timeAddress = ptr(times);
if (kernel.symbols.GetProcessTimes(handle, timeAddress, timeAddress + 8, timeAddress + 16, timeAddress + 24)) {
creationMatched = times.readBigUInt64LE(0) === recordedStart;
}
if (creationMatched) {
const imageBuffer = Buffer.alloc(65536);
const imageLength = Buffer.alloc(4);
imageLength.writeUInt32LE(32768);
if (kernel.symbols.QueryFullProcessImageNameW(handle, 0, ptr(imageBuffer), ptr(imageLength))) {
const chars = imageLength.readUInt32LE(0);
const name = chars <= 32768 ? path.basename(imageBuffer.subarray(0, chars * 2).toString('utf16le')).toLowerCase() : '';
image = ['bun.exe', 'node.exe', 'msedge.exe', 'msmpeng.exe', 'mssense.exe', 'dllhost.exe', 'explorer.exe', 'powershell.exe', 'pwsh.exe', 'svchost.exe', 'conhost.exe'].includes(name) ? name : 'other';
}
}
} finally {
kernel.symbols.CloseHandle(handle);
}
}
owners.push({ pid, image, creationMatched, isTestHost: creationMatched && pid === input.testPid, applicationType: information.readUInt32LE(offset + 652) });
}
return unchanged() ? { available: true, owners, rebootReasons: rebootReasons.readUInt32LE(0) }
: { available: false, reason: 'failed_object_identity_changed' };
} catch (error) {
throw new FileOwnerProbeError(stage, error);
} finally {
stage = 'session_end';
if (session !== undefined) restart.symbols.RmEndSession(session);
stage = 'library_close';
kernel.close(); restart.close();
}
}
let result: object;
try { result = inspect(); }
catch (error) {
const failure = error instanceof FileOwnerProbeError ? error : new FileOwnerProbeError(stage, error);
result = { available: false, reason: 'owner_query_failed', stage: failure.stage, errorCode: failure.errorCode };
}
process.stdout.write(JSON.stringify(result) + '\n', () => process.exit(0));
+131
View File
@@ -0,0 +1,131 @@
const fs = require('node:fs');
const cp = require('node:child_process');
const { createHash } = require('node:crypto');
const path = require('node:path');
module.exports = ({ observation, playwrightEntry, mode = 'normal-close', inspectCommandLine = false, observerExecutable, seedCookie = { name: 'synthetic', value: 'synthetic', domain: 'example.test', path: '/' } }) => {
if (inspectCommandLine && process.platform === 'win32' && typeof observerExecutable !== 'string') throw new Error('Native observer executable is required');
const originalSpawn = cp.spawn;
let inspected = Promise.resolve();
let folderEvidence;
const directoryState = (env, root) => ({
requestedProfile: fs.existsSync(root),
localEnvironment: fs.existsSync(env.LOCALAPPDATA || ''),
roamingEnvironment: fs.existsSync(env.APPDATA || ''),
localUnderProfile: fs.existsSync(path.join(env.USERPROFILE || '', 'AppData', 'Local')),
roamingUnderProfile: fs.existsSync(path.join(env.USERPROFILE || '', 'AppData', 'Roaming')),
});
const safeFolders = value => Object.fromEntries(['local', 'roaming'].map(name => [name,
Object.fromEntries(['verified', 'dontVerify'].map(kind => {
const item = value?.[name]?.[kind];
return [kind, {
hresult: Number.isInteger(item?.hresult) ? item.hresult : null,
pathHash: /^[a-f0-9]{64}$/.test(item?.pathHash) ? item.pathHash : null,
exists: typeof item?.exists === 'boolean' ? item.exists : null,
matchesEnvironment: typeof item?.matchesEnvironment === 'boolean' ? item.matchesEnvironment : null,
underUserProfile: typeof item?.underUserProfile === 'boolean' ? item.underUserProfile : null,
}];
})),
]));
const runProbe = (input, env) => new Promise(resolve => {
const probe = originalSpawn(observerExecutable, [
'--no-env-file', '--no-install', '--no-macros', '--config=NUL', path.join(__dirname, 'native-cookie-process-observer.ts'),
Buffer.from(JSON.stringify(input)).toString('base64'),
], { env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
let output = '';
let stderrBytes = 0;
let spawnFailed = false;
const timer = setTimeout(() => probe.kill(), 5_000);
probe.stdout.on('data', chunk => { output += chunk.toString('utf8'); if (output.length > 16384) probe.kill(); });
probe.stderr.on('data', chunk => { stderrBytes += chunk.length; });
probe.once('error', () => { spawnFailed = true; });
probe.once('close', code => {
clearTimeout(timer);
try { resolve({ measured: JSON.parse(output), code, stderrBytes }); }
catch { resolve({ measured: { available: false, reason: spawnFailed ? 'probe_spawn_failed' : 'probe_no_receipt' }, code, stderrBytes }); }
});
});
cp.spawn = function(command, args, options) {
if (args.some(arg => /^--(?:no-sandbox|disable-setuid-sandbox)(?:=|$)/.test(arg))) throw new Error('Native fixture refuses a sandbox-disabled browser');
const child = originalSpawn.call(this, command, args, options);
const evidence = {
command, args, pid: child.pid,
argsHash: createHash('sha256').update(JSON.stringify(args)).digest('hex'),
envHash: createHash('sha256').update(JSON.stringify(Object.entries(options.env || {}).sort(([a], [b]) => a.localeCompare(b)))).digest('hex'),
stderrBytes: 0,
reasons: [],
runtime: { node: process.version, bun: process.versions.bun || null, architecture: process.arch },
folderEvidence,
};
const publish = () => fs.writeFileSync(observation, JSON.stringify(evidence));
publish();
let tail = '';
child.stderr?.on('data', chunk => {
evidence.stderrBytes += chunk.length;
if (evidence.stderrBytes > 65536) return;
const text = tail + chunk.toString('utf8');
const patterns = {
job_assignment_failed: /AssignProcessToJobObject|failed to (?:assign|create).*job object/i,
sandbox_failed: /sandbox.*(?:failed|error)|SBOX_FATAL/i,
profile_locked: /ProcessSingleton|profile.*in use/i,
default_profile_policy: /remote debugging requires a non-default data directory/i,
permission_denied: /access is denied|ERROR_ACCESS_DENIED|permission denied/i,
crashpad_failed: /crashpad.*(?:failed|error)/i,
missing_dependency: /specified module could not be found|0xc0000135/i,
};
for (const [reason, pattern] of Object.entries(patterns)) {
if (pattern.test(text) && !evidence.reasons.includes(reason)) evidence.reasons.push(reason);
}
tail = text.slice(-512);
publish();
});
child.once('exit', (code, signal) => { evidence.exitCode = code; evidence.signal = signal; publish(); });
child.once('error', error => {
evidence.spawnError = ['ENOENT', 'EACCES', 'EPERM', 'EINVAL'].includes(error.code) ? error.code : 'spawn_failed';
publish();
});
if (inspectCommandLine && process.platform === 'win32' && Number.isInteger(child.pid)) {
inspected = runProbe({ pid: child.pid, owner: process.pid, image: command }, options.env).then(({ measured, code, stderrBytes }) => {
const expectedHashes = args.map(arg => createHash('sha256').update(arg).digest('hex'));
const dataDir = args.find(arg => arg.startsWith('--user-data-dir='))?.slice(16);
evidence.observedCommandLine = {
available: measured.available === true,
parentMatched: measured.parentMatched === true,
imageMatched: measured.imageMatched === true,
reason: ['not_windows', 'invalid_input', 'process_open', 'process_identity', 'owned_process_unavailable', 'command_line', 'command_line_length', 'command_line_bounds', 'argument_parse', 'argument_bounds', 'job_query', 'observer_initialize', 'probe_spawn_failed', 'probe_no_receipt'].includes(measured.reason) ? measured.reason : undefined,
win32Error: Number.isInteger(measured.win32Error) ? measured.win32Error : undefined,
ntStatus: Number.isInteger(measured.ntStatus) ? measured.ntStatus : undefined,
commandLineHash: /^[a-f0-9]{64}$/.test(measured.commandLineHash) ? measured.commandLineHash : undefined,
argumentsMatchRequested: measured.available === true && JSON.stringify(measured.argumentHashes) === JSON.stringify(expectedHashes),
userDataDirCount: Number.isInteger(measured.userDataDirCount) && measured.userDataDirCount >= 0 && measured.userDataDirCount <= 128 ? measured.userDataDirCount : undefined,
userDataDirMatchesRequested: typeof dataDir === 'string' && measured.userDataDirHash === createHash('sha256').update(dataDir).digest('hex'),
pipePresent: measured.pipePresent === true,
browserInJob: typeof measured.browserInJob === 'boolean' ? measured.browserInJob : undefined,
observerJobLimitFlags: Number.isInteger(measured.observerJobLimitFlags) ? measured.observerJobLimitFlags : undefined,
observerJobQueryError: Number.isInteger(measured.observerJobQueryError) ? measured.observerJobQueryError : undefined,
exitCode: code, stderrBytes,
};
evidence.folderEvidence.afterLaunch = safeFolders(measured.knownFolders);
evidence.folderEvidence.directoriesAfterLaunch = directoryState(options.env, args.find(arg => arg.startsWith('--user-data-dir='))?.slice(16) || '');
publish();
});
}
return child;
};
const { chromium } = require(playwrightEntry);
return { chromium: { async launchPersistentContext(root, options) {
if (options.chromiumSandbox !== true) throw new Error('Native fixture requires the browser sandbox');
const started = Date.now();
if (inspectCommandLine && process.platform === 'win32') {
const directoriesBefore = directoryState(options.env, root);
const before = await runProbe({ mode: 'known-folders' }, options.env);
folderEvidence = { beforeLaunch: safeFolders(before.measured.knownFolders), directoriesBefore, directoriesAfterProbe: directoryState(options.env, root) };
}
const context = await chromium.launchPersistentContext(root, inspectCommandLine && process.platform === 'win32'
? { ...options, timeout: Math.max(1, options.timeout - (Date.now() - started)) } : options);
await inspected;
await context.addCookies([seedCookie]);
if (mode === 'stalled-close') context.close = () => { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0); };
return context;
} } };
};
+150
View File
@@ -0,0 +1,150 @@
import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { dlopen, FFIType, ptr, toArrayBuffer } from 'bun:ffi';
const hash = (text: string) => createHash('sha256').update(text).digest('hex');
export function decodeNativeCommandLine(buffer: Buffer, address: number | bigint): string | null {
if (buffer.length < 16) return null;
const length = buffer.readUInt16LE(0);
const offset = Number(buffer.readBigUInt64LE(8) - BigInt(address));
if (!Number.isSafeInteger(offset) || offset < 16 || offset + length > buffer.length || length % 2 !== 0) return null;
return buffer.subarray(offset, offset + length).toString('utf16le');
}
function knownFolders() {
const shell = dlopen('shell32.dll', {
SHGetFolderPathW: { args: [FFIType.u64, FFIType.i32, FFIType.u64, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
});
const normalized = (value: string) => path.win32.normalize(value).toLowerCase();
try {
return Object.fromEntries([['local', 0x1c, 'LOCALAPPDATA'], ['roaming', 0x1a, 'APPDATA']].map(([name, id, env]) => {
const calls = Object.fromEntries([['verified', 0], ['dontVerify', 0x4000]].map(([kind, flag]) => {
const output = Buffer.alloc(520);
const status = shell.symbols.SHGetFolderPathW(0, Number(id) | Number(flag), 0, 0, ptr(output));
let end = 0;
while (end + 2 <= output.length && output.readUInt16LE(end) !== 0) end += 2;
const folder = status >= 0 && end > 0 && end + 2 <= output.length ? output.subarray(0, end).toString('utf16le') : null;
return [kind, {
hresult: status,
pathHash: folder ? hash(normalized(folder)) : null,
exists: folder ? existsSync(folder) : null,
matchesEnvironment: folder ? normalized(folder) === normalized(process.env[String(env)] || '') : null,
underUserProfile: folder ? normalized(folder).startsWith(normalized(process.env.USERPROFILE || '') + '\\') : null,
}];
}));
return [name, calls];
}));
} finally {
shell.close();
}
}
function observe() {
if (process.platform !== 'win32' || !['x64', 'arm64'].includes(process.arch)) return { available: false, reason: 'not_windows' };
const input = JSON.parse(Buffer.from(process.argv[2], 'base64').toString('utf8'));
if (input.mode === 'known-folders') return { available: true, knownFolders: knownFolders() };
if (!Number.isSafeInteger(input.pid) || input.pid <= 0 || input.pid > 0xffffffff || !Number.isSafeInteger(input.owner) || input.owner <= 0 || input.owner > 0xffffffff || typeof input.image !== 'string' || input.image.length > 32768) {
return { available: false, reason: 'invalid_input' };
}
const kernel = dlopen('kernel32.dll', {
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
QueryFullProcessImageNameW: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
QueryInformationJobObject: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
IsProcessInJob: { args: [FFIType.u64, FFIType.u64, FFIType.ptr], returns: FFIType.i32 },
LocalFree: { args: [FFIType.ptr], returns: FFIType.ptr },
LocalSize: { args: [FFIType.ptr], returns: FFIType.u64 },
GetLastError: { args: [], returns: FFIType.u32 },
});
const nt = dlopen('ntdll.dll', {
NtQueryInformationProcess: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
});
const shell = dlopen('shell32.dll', {
CommandLineToArgvW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.ptr },
});
let processHandle: number | bigint = 0;
let argumentMemory: ReturnType<typeof shell.symbols.CommandLineToArgvW> = null;
let stage = 'process_open';
try {
processHandle = kernel.symbols.OpenProcess(0x1000, 0, input.pid);
if (!processHandle) return { available: false, reason: stage, win32Error: kernel.symbols.GetLastError() };
stage = 'process_identity';
const basic = Buffer.alloc(48);
const returned = Buffer.alloc(4);
let status = nt.symbols.NtQueryInformationProcess(processHandle, 0, ptr(basic), basic.length, ptr(returned));
if (status !== 0) return { available: false, reason: stage, ntStatus: status };
const parentMatched = basic.readBigUInt64LE(40) === BigInt(input.owner);
const pidMatched = basic.readBigUInt64LE(32) === BigInt(input.pid);
const imageBuffer = Buffer.alloc(65536);
const imageLength = Buffer.alloc(4);
imageLength.writeUInt32LE(32768);
if (!kernel.symbols.QueryFullProcessImageNameW(processHandle, 0, ptr(imageBuffer), ptr(imageLength))) {
return { available: false, reason: stage, win32Error: kernel.symbols.GetLastError() };
}
const imageChars = imageLength.readUInt32LE(0);
const imageMatched = imageChars <= 32768 && imageBuffer.subarray(0, imageChars * 2).toString('utf16le').toLowerCase() === input.image.toLowerCase();
if (!parentMatched || !pidMatched || !imageMatched) return { available: false, reason: 'owned_process_unavailable', parentMatched, imageMatched };
stage = 'command_line';
status = nt.symbols.NtQueryInformationProcess(processHandle, 60, null, 0, ptr(returned));
const length = returned.readUInt32LE(0);
if (length < 16 || length > 131072) return { available: false, reason: 'command_line_length', ntStatus: status };
const commandBuffer = Buffer.alloc(length);
const commandAddress = ptr(commandBuffer);
status = nt.symbols.NtQueryInformationProcess(processHandle, 60, commandAddress, length, ptr(returned));
if (status !== 0) return { available: false, reason: stage, ntStatus: status };
const commandLine = decodeNativeCommandLine(commandBuffer, commandAddress);
if (commandLine === null) return { available: false, reason: 'command_line_bounds' };
stage = 'argument_parse';
const wideCommand = Buffer.from(commandLine + '\0', 'utf16le');
const count = Buffer.alloc(4);
argumentMemory = shell.symbols.CommandLineToArgvW(ptr(wideCommand), ptr(count));
const argumentCount = count.readInt32LE(0);
if (!argumentMemory || argumentCount < 1 || argumentCount > 4096) return { available: false, reason: stage };
const size = Number(kernel.symbols.LocalSize(argumentMemory));
if (!Number.isSafeInteger(size) || size < argumentCount * 8 || size > 1048576) return { available: false, reason: 'argument_bounds' };
const argumentsBuffer = Buffer.from(toArrayBuffer(argumentMemory, 0, size));
const args: string[] = [];
for (let index = 1; index < argumentCount; index++) {
const start = Number(argumentsBuffer.readBigUInt64LE(index * 8) - BigInt(argumentMemory));
if (!Number.isSafeInteger(start) || start < argumentCount * 8 || start % 2 !== 0 || start >= size) return { available: false, reason: 'argument_bounds' };
let end = start;
while (end + 2 <= size && argumentsBuffer.readUInt16LE(end) !== 0) end += 2;
if (end + 2 > size) return { available: false, reason: 'argument_bounds' };
args.push(argumentsBuffer.subarray(start, end).toString('utf16le'));
}
stage = 'job_query';
const limits = Buffer.alloc(144);
const jobKnown = kernel.symbols.QueryInformationJobObject(0, 9, ptr(limits), limits.length, ptr(returned));
const jobError = jobKnown ? undefined : kernel.symbols.GetLastError();
const inJob = Buffer.alloc(4);
const membershipKnown = kernel.symbols.IsProcessInJob(processHandle, 0, ptr(inJob));
const dataArgs = args.filter(arg => arg.startsWith('--user-data-dir='));
return {
available: true, parentMatched, imageMatched,
commandLineHash: hash(commandLine), argumentHashes: args.map(hash),
userDataDirCount: dataArgs.length,
userDataDirHash: dataArgs.length === 1 ? hash(dataArgs[0].slice(16)) : null,
pipePresent: args.includes('--remote-debugging-pipe'),
browserInJob: membershipKnown ? inJob.readInt32LE(0) !== 0 : null,
observerJobLimitFlags: jobKnown ? limits.readUInt32LE(16) : null,
observerJobQueryError: jobError,
knownFolders: knownFolders(),
};
} catch {
return { available: false, reason: stage };
} finally {
if (argumentMemory) kernel.symbols.LocalFree(argumentMemory);
if (processHandle) kernel.symbols.CloseHandle(processHandle);
shell.close(); nt.close(); kernel.close();
}
}
if (import.meta.main) {
let result: object;
let exitCode = 0;
try { result = observe(); }
catch { result = { available: false, reason: 'observer_initialize' }; exitCode = 1; }
process.stdout.write(JSON.stringify(result) + '\n', () => process.exit(exitCode));
}
+13
View File
@@ -0,0 +1,13 @@
module.exports = ({ pidsFile, mode }) => ({
chromium: {
async launchPersistentContext() {
const child = require('node:child_process').spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
stdio: 'ignore',
detached: true,
});
require('node:fs').writeFileSync(pidsFile, JSON.stringify([process.pid, child.pid]));
if (mode === 'worker-crash') setTimeout(() => process.exit(3), 100);
await new Promise(() => {});
},
},
});
+42
View File
@@ -0,0 +1,42 @@
const fs = require('node:fs');
const path = require('node:path');
let stage = 'input';
let root;
const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
try {
const input = JSON.parse(Buffer.from(process.argv[2], 'base64').toString('utf8'));
root = input.root;
if (typeof root !== 'string' || typeof input.realpath !== 'string' || typeof input.dev !== 'string' || typeof input.ino !== 'string') throw new Error('invalid_input');
stage = 'identity';
const state = fs.lstatSync(root, { bigint: true });
const identity = {
directory: state.isDirectory() && !state.isSymbolicLink(),
pathMatches: samePath(fs.realpathSync(root), input.realpath),
deviceMatches: state.dev.toString() === input.dev,
inodeMatches: state.ino.toString() === input.ino,
};
if (Object.values(identity).some(value => !value)) {
console.log(JSON.stringify({ removed: false, reason: 'identity_mismatch', identity }));
process.exitCode = 1;
} else {
stage = 'remove';
fs.rmSync(root, { recursive: true, force: true, maxRetries: 0 });
console.log(JSON.stringify({ removed: !fs.existsSync(root), identity, retries: 0 }));
}
} catch (error) {
const code = ['EPERM', 'EACCES', 'EBUSY', 'ENOENT', 'EINVAL', 'ENOTEMPTY', 'ENOTDIR'].includes(error.code) ? error.code : 'filesystem_error';
let failingPath = null;
let metadata = null;
if (typeof root === 'string' && typeof error.path === 'string') {
const relative = path.relative(root, error.path);
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
failingPath = relative || '.';
try {
const state = fs.lstatSync(error.path);
metadata = { mode: state.mode, directory: state.isDirectory(), link: state.isSymbolicLink() };
} catch {}
}
}
console.log(JSON.stringify({ removed: false, stage, code, failingPath, metadata }));
process.exitCode = 1;
}