mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-28 07:32:14 +02:00
* 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
133 lines
7.0 KiB
TypeScript
133 lines
7.0 KiB
TypeScript
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));
|