Files
gstack/lib/cso/preparation-docker.ts
T
garrytan d93d61f7ba style(cso): format lib/cso TypeScript with pinned Prettier
Mechanical reformat only. Minified transpile output is byte-identical for
21 of 22 files; witness.ts differs only in three regex flag orders
(/mi -> /im), which JavaScript canonicalizes. Source-text assertions over
lib/cso now compare whitespace-insensitively with the same tokens.
2026-09-29 14:22:48 +00:00

1683 lines
64 KiB
TypeScript

/** Concrete constrained-Docker adapter for PreparationExecutor. */
import * as fs from 'node:fs';
import * as net from 'node:net';
import { promises as dns } from 'node:dns';
import { createHash, randomBytes } from 'node:crypto';
import { spawn } from 'node:child_process';
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
import { atomicWriteSync } from '../fs-atomic';
import { canonical, CsoError, sha256 } from './contracts';
import { DockerGroup, type DockerEndpoint } from './docker';
import { secureDirectory } from './state';
import {
admittedPreparationRuntime,
type AcquisitionArtifactReceipt,
type AcquisitionReceipt,
type OfflinePreparationReceipt,
type OfflinePreparationRequest,
type OfflinePreparationResult,
type PreparationAcquireRequest,
type PreparationCommandReceipt,
type PreparationRuntimeAdmission,
type PreparationSandboxRunner,
} from './preparation-executor';
import type { CsoStack, PreparationCommand } from './preparation';
import { CSO_HELPER_ABI } from './runtime-catalog';
import type { PreparedExportEntry, PreparedExportManifest } from './preparation-container';
const RELATIVE = /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))(?!.*\\)[A-Za-z0-9@._+\/-]{1,1024}$/;
const MAX_MANIFEST = 32 * 1024 * 1024;
const MAX_PREPARED_EXPORT_ENTRIES = 200_000;
function fail(code: ConstructorParameters<typeof CsoError>[0], message: string): never {
throw new CsoError(code, message);
}
function contained(root: string, path: string): string {
if (!RELATIVE.test(path) || path.split('/').some((part) => !part || part === '.' || part === '..'))
fail('UNSAFE_PATH', 'Preparation path escaped its private root');
const target = resolve(root, ...path.split('/'));
if (!target.startsWith(`${root}${sep}`)) fail('UNSAFE_PATH', 'Preparation path escaped its private root');
return target;
}
function commandReceipt(
command: PreparationCommand,
index: number,
exitCode: number,
): PreparationCommandReceipt {
return {
index,
commandHash: sha256(canonical(command)),
exitCode,
timedOut: false,
outputTruncated: false,
};
}
function writePolicy(path: string, value: unknown): void {
try {
atomicWriteSync(path, `${JSON.stringify(value)}\n`, { mode: 0o600, noReplace: true });
} catch {
fail('PERSISTENCE_FAILED', 'Preparation policy could not be written atomically');
}
}
function readManifest(path: string): { artifacts: AcquisitionArtifactReceipt[] } {
let stat: fs.Stats;
try {
stat = fs.lstatSync(path);
} catch {
fail('TOOL_FAILED', 'Qualified acquisition helper did not produce an archive manifest');
}
if (
!stat!.isFile() ||
stat!.isSymbolicLink() ||
stat!.nlink !== 1 ||
stat!.size > MAX_MANIFEST ||
(process.getuid && stat!.uid !== process.getuid()) ||
(stat!.mode & 0o077) !== 0
)
fail('UNSAFE_PATH', 'Acquisition archive manifest is not one bounded private file');
let parsed: unknown;
try {
parsed = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch {
fail('TOOL_FAILED', 'Qualified acquisition helper returned invalid archive JSON');
}
const value = parsed as { artifacts?: unknown };
if (!value || Object.keys(value).sort().join(',') !== 'artifacts' || !Array.isArray(value.artifacts))
fail('TOOL_FAILED', 'Qualified acquisition helper returned an invalid archive manifest schema');
return value as { artifacts: AcquisitionArtifactReceipt[] };
}
function fileSha256(path: string, maxBytes: number): string {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let fd: number;
try {
fd = fs.openSync(path, fs.constants.O_RDONLY | noFollow);
} catch {
fail('UNSAFE_PATH', 'Archive copy could not be opened without following links');
}
try {
const before = fs.fstatSync(fd!),
hash = createHash('sha256'),
buffer = Buffer.allocUnsafe(64 * 1024);
let bytes = 0;
for (;;) {
const count = fs.readSync(fd!, buffer, 0, buffer.length, null);
if (!count) break;
bytes += count;
if (bytes > maxBytes) fail('INSUFFICIENT_CAPACITY', 'Archive copy exceeded its declared size');
hash.update(buffer.subarray(0, count));
}
const after = fs.fstatSync(fd!);
if (
bytes !== before.size ||
before.dev !== after.dev ||
before.ino !== after.ino ||
before.size !== after.size ||
before.mtimeMs !== after.mtimeMs ||
before.ctimeMs !== after.ctimeMs
)
fail('SNAPSHOT_RACE', 'Archive copy changed while it was hashed');
return hash.digest('hex');
} finally {
fs.closeSync(fd!);
}
}
function validateAcquisitionOutput(root: string, artifacts: AcquisitionArtifactReceipt[]): void {
const top = fs.readdirSync(root).sort();
if (canonical(top) !== canonical(['archives', 'artifacts.json']))
fail('TOOL_FAILED', 'Acquisition output contains undeclared objects');
const archiveRoot = join(root, 'archives'),
stat = fs.lstatSync(archiveRoot);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
(process.getuid && stat.uid !== process.getuid()) ||
(stat.mode & 0o022) !== 0
)
fail('UNSAFE_PATH', 'Acquisition archive output is not one private directory');
for (const artifact of artifacts)
if (
typeof artifact?.stagingPath !== 'string' ||
!/^cso-public\/[A-Za-z0-9._-]{1,255}$/.test(artifact.stagingPath)
)
fail('TOOL_FAILED', 'Acquisition manifest contains an unsafe staging path');
const expected = artifacts.map((artifact) => artifact.stagingPath.slice('cso-public/'.length)).sort(),
actual = fs.readdirSync(archiveRoot).sort();
if (new Set(expected).size !== expected.length)
fail('TOOL_FAILED', 'Acquisition manifest contains duplicate staging paths');
if (canonical(actual) !== canonical(expected))
fail('TOOL_FAILED', 'Acquisition output archive membership does not match its manifest');
for (const name of actual) {
if (!name || name.includes('/') || name === '.' || name === '..')
fail('UNSAFE_PATH', 'Acquisition output archive name is unsafe');
const file = fs.lstatSync(join(archiveRoot, name));
if (
!file.isFile() ||
file.isSymbolicLink() ||
file.nlink !== 1 ||
(process.getuid && file.uid !== process.getuid()) ||
(file.mode & 0o022) !== 0
)
fail('UNSAFE_PATH', 'Acquisition output contains a link or special file');
}
}
function preparedRelative(path: unknown): path is string {
return (
typeof path === 'string' &&
path.length > 0 &&
!path.startsWith('/') &&
!path.includes('\\') &&
Buffer.byteLength(path) <= 4096 &&
path
.split('/')
.every(
(part) =>
part &&
part !== '.' &&
part !== '..' &&
Buffer.byteLength(part) <= 255 &&
!/[\0-\x1f\x7f]/.test(part),
)
);
}
function preparedTarget(root: string, relativePath: string): string {
if (!preparedRelative(relativePath)) fail('UNSAFE_PATH', 'Prepared export contains an unsafe path');
const target = resolve(root, ...relativePath.split('/'));
if (!target.startsWith(`${root}${sep}`))
fail('UNSAFE_PATH', 'Prepared export path escaped its private root');
return target;
}
function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.mode === right.mode &&
left.uid === right.uid &&
left.gid === right.gid &&
left.nlink === right.nlink &&
left.size === right.size &&
left.mtimeMs === right.mtimeMs &&
left.ctimeMs === right.ctimeMs
);
}
function copyPreparedBlob(
source: string,
destination: string,
expected: Extract<PreparedExportEntry, { kind: 'file' }>,
): void {
const noFollow = (fs.constants as any).O_NOFOLLOW ?? 0;
let sourceFd = -1,
destinationFd = -1;
try {
sourceFd = fs.openSync(source, fs.constants.O_RDONLY | noFollow);
const before = fs.fstatSync(sourceFd);
if (
!before.isFile() ||
before.nlink !== 1 ||
before.size !== expected.bytes ||
(process.getuid && before.uid !== process.getuid())
)
fail('UNSAFE_PATH', 'Prepared export blob is not one owned regular file');
destinationFd = fs.openSync(
destination,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
0o600,
);
const hash = createHash('sha256'),
buffer = Buffer.allocUnsafe(64 * 1024);
let bytes = 0;
for (;;) {
const count = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
if (!count) break;
bytes += count;
if (bytes > expected.bytes)
fail('INSUFFICIENT_CAPACITY', 'Prepared export blob exceeded its declared size');
hash.update(buffer.subarray(0, count));
let offset = 0;
while (offset < count) offset += fs.writeSync(destinationFd, buffer, offset, count - offset);
}
const after = fs.fstatSync(sourceFd);
if (
bytes !== expected.bytes ||
hash.digest('hex') !== expected.sha256 ||
!sameFileIdentity(before, after)
)
fail('SNAPSHOT_RACE', 'Prepared export blob changed during bounded import');
fs.fchmodSync(destinationFd, expected.mode);
fs.fsyncSync(destinationFd);
const copied = fs.fstatSync(destinationFd);
if (!copied.isFile() || copied.nlink !== 1 || copied.size !== expected.bytes)
fail('PERSISTENCE_FAILED', 'Prepared export blob was not materialized as one regular file');
} finally {
if (sourceFd >= 0) fs.closeSync(sourceFd);
if (destinationFd >= 0) fs.closeSync(destinationFd);
}
}
/** Validate an inert container export and reconstruct its prepared tree using host no-follow writes. */
export function materializePreparedExport(
exportRoot: string,
destinationRoot: string,
maxBytes: number,
): PreparedExportManifest {
const source = resolve(exportRoot),
destination = resolve(destinationRoot);
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 2 * 1024 * 1024 * 1024)
fail('INVALID_ARGUMENT', 'Prepared export byte ceiling is invalid');
for (const [path, label, empty] of [
[source, 'Prepared inert export', false],
[destination, 'Prepared output', true],
] as const) {
const stat = fs.lstatSync(path);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
fs.realpathSync(path) !== path ||
(process.getuid && stat.uid !== process.getuid()) ||
(stat.mode & 0o022) !== 0 ||
(empty && fs.readdirSync(path).length)
)
fail('UNSAFE_PATH', `${label} must be one private${empty ? ' empty' : ''} owned directory`);
}
if (fs.readdirSync(source).sort().join('\0') !== 'blobs\0manifest.json')
fail('UNSAFE_PATH', 'Prepared inert export contains undeclared top-level objects');
const manifestPath = join(source, 'manifest.json'),
manifestStat = fs.lstatSync(manifestPath);
if (
!manifestStat.isFile() ||
manifestStat.isSymbolicLink() ||
manifestStat.nlink !== 1 ||
manifestStat.size < 1 ||
manifestStat.size > MAX_MANIFEST ||
(process.getuid && manifestStat.uid !== process.getuid())
)
fail('UNSAFE_PATH', 'Prepared export manifest is not one bounded owned file');
let manifest: PreparedExportManifest;
try {
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
} catch {
fail('TOOL_FAILED', 'Prepared export manifest is invalid JSON');
}
if (
!manifest ||
Object.keys(manifest).sort().join(',') !== 'entries,schemaVersion' ||
manifest.schemaVersion !== 1 ||
!Array.isArray(manifest.entries) ||
manifest.entries.length > MAX_PREPARED_EXPORT_ENTRIES
)
fail('TOOL_FAILED', 'Prepared export manifest has an invalid schema');
const paths = new Set<string>(),
directories = new Set<string>(),
blobs = new Set<string>();
let totalBytes = 0;
for (const raw of manifest.entries) {
const entry = raw as PreparedExportEntry,
keys = Object.keys(entry).sort().join(',');
if (
!preparedRelative(entry?.path) ||
paths.has(entry.path) ||
!Number.isSafeInteger(entry.mode) ||
entry.mode < 0 ||
entry.mode > 0o777
)
fail('TOOL_FAILED', 'Prepared export contains a duplicate or invalid path record');
paths.add(entry.path);
const parent = entry.path.includes('/') ? entry.path.slice(0, entry.path.lastIndexOf('/')) : '';
if (parent && !directories.has(parent))
fail('TOOL_FAILED', 'Prepared export entry is missing its declared parent directory');
if (entry.kind === 'directory') {
if (keys !== 'kind,mode,path' || (entry.mode & 0o022) !== 0)
fail('TOOL_FAILED', 'Prepared export directory record is invalid');
directories.add(entry.path);
} else if (entry.kind === 'file') {
if (
keys !== 'blob,bytes,kind,mode,path,sha256' ||
!Number.isSafeInteger(entry.bytes) ||
entry.bytes < 0 ||
!/^[a-f0-9]{64}$/.test(entry.sha256) ||
!/^blob-\d{6}$/.test(entry.blob) ||
blobs.has(entry.blob)
)
fail('TOOL_FAILED', 'Prepared export file record is invalid');
blobs.add(entry.blob);
totalBytes += entry.bytes;
if (!Number.isSafeInteger(totalBytes) || totalBytes > maxBytes)
fail('INSUFFICIENT_CAPACITY', 'Prepared export exceeds its aggregate byte ceiling');
} else if (entry.kind === 'symlink') {
if (
keys !== 'kind,mode,path,target' ||
typeof entry.target !== 'string' ||
!entry.target ||
isAbsolute(entry.target) ||
entry.target.includes('\0') ||
/[\x01-\x1f\x7f]/.test(entry.target)
)
fail('TOOL_FAILED', 'Prepared export symlink record is invalid');
const lexical = resolve(dirname(preparedTarget(destination, entry.path)), entry.target);
if (lexical !== destination && !lexical.startsWith(`${destination}${sep}`))
fail('UNSAFE_PATH', 'Prepared export symlink escapes its destination');
} else fail('TOOL_FAILED', 'Prepared export entry kind is invalid');
}
const ordered = manifest.entries.map((entry) => entry.path);
if (ordered.join('\0') !== [...ordered].sort().join('\0'))
fail('TOOL_FAILED', 'Prepared export manifest is not in deterministic path order');
const blobRoot = join(source, 'blobs'),
blobRootStat = fs.lstatSync(blobRoot);
if (
!blobRootStat.isDirectory() ||
blobRootStat.isSymbolicLink() ||
(process.getuid && blobRootStat.uid !== process.getuid()) ||
fs.readdirSync(blobRoot).sort().join('\0') !== [...blobs].sort().join('\0')
)
fail('UNSAFE_PATH', 'Prepared export blob membership does not match its manifest');
for (const entry of manifest.entries)
if (entry.kind === 'directory') fs.mkdirSync(preparedTarget(destination, entry.path), { mode: 0o700 });
for (const entry of manifest.entries)
if (entry.kind === 'file')
copyPreparedBlob(join(blobRoot, entry.blob), preparedTarget(destination, entry.path), entry);
for (const entry of manifest.entries)
if (entry.kind === 'symlink') fs.symlinkSync(entry.target, preparedTarget(destination, entry.path));
for (const entry of manifest.entries)
if (entry.kind === 'symlink') {
const path = preparedTarget(destination, entry.path);
let real: string, stat: fs.Stats;
try {
real = fs.realpathSync(path);
stat = fs.statSync(path);
} catch {
fail('UNSAFE_PATH', 'Prepared export symlink is dangling or cyclic');
}
if (
(real !== destination && !real.startsWith(`${destination}${sep}`)) ||
(!stat.isFile() && !stat.isDirectory())
)
fail('UNSAFE_PATH', 'Prepared export symlink resolves outside its prepared tree');
}
for (const entry of [...manifest.entries].reverse())
if (entry.kind === 'directory') fs.chmodSync(preparedTarget(destination, entry.path), entry.mode);
return manifest;
}
function addBlockedIpv4Ranges(blocked: net.BlockList): void {
for (const [address, prefix] of [
['0.0.0.0', 8],
['10.0.0.0', 8],
['100.64.0.0', 10],
['127.0.0.0', 8],
['169.254.0.0', 16],
['172.16.0.0', 12],
['192.0.0.0', 24],
['192.0.2.0', 24],
['192.168.0.0', 16],
['198.18.0.0', 15],
['198.51.100.0', 24],
['203.0.113.0', 24],
['224.0.0.0', 4],
['240.0.0.0', 4],
] as Array<[string, number]>)
blocked.addSubnet(address, prefix, 'ipv4');
}
function addBlockedIpv6Ranges(blocked: net.BlockList): void {
for (const [address, prefix] of [
['::', 96], // unspecified, IPv4-compatible, and other deprecated v4 embeddings
['::ffff:0.0.0.0', 96], // IPv4-mapped addresses must not bypass the IPv4 ranges
['64:ff9b::', 96],
['64:ff9b:1::', 48], // public and local-use NAT64 translators
['100::', 64], // discard-only
['2001::', 23], // IETF special-purpose assignments (Teredo, ORCHID, benchmarking)
['2001:db8::', 32], // documentation prefix
['2002::', 16], // 6to4 can embed otherwise blocked IPv4 destinations
['3fff::', 20], // documentation prefix
['fc00::', 7],
['fe80::', 10],
['fec0::', 10],
['ff00::', 8],
] as Array<[string, number]>)
blocked.addSubnet(address, prefix, 'ipv6');
}
const registryBlockedAddresses = new net.BlockList();
const registryBlockedIpv6Addresses = new net.BlockList();
addBlockedIpv4Ranges(registryBlockedAddresses);
addBlockedIpv6Ranges(registryBlockedIpv6Addresses);
/** Pure classification used before any registry connection is attempted. */
export function isBlockedRegistryAddress(address: string, family: 4 | 6): boolean {
if (net.isIP(address) !== family) return true;
return (family === 6 ? registryBlockedIpv6Addresses : registryBlockedAddresses).check(
address,
family === 6 ? 'ipv6' : 'ipv4',
);
}
export interface RegistryDnsAddress {
address: string;
family: 4 | 6;
}
export interface RegistryDnsResolution {
promise: Promise<RegistryDnsAddress[]>;
cancel(): void;
}
export type RegistryDnsResolver = (host: string) => RegistryDnsResolution;
function systemRegistryDnsResolver(host: string): RegistryDnsResolution {
const resolver = new dns.Resolver();
const promise = Promise.allSettled([resolver.resolve4(host), resolver.resolve6(host)]).then((results) => {
const answers: RegistryDnsAddress[] = [];
if (results[0].status === 'fulfilled')
for (const address of results[0].value) answers.push({ address, family: 4 });
if (results[1].status === 'fulfilled')
for (const address of results[1].value) answers.push({ address, family: 6 });
if (!answers.length) {
const rejected = results.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
);
if (rejected) throw rejected.reason;
}
return answers;
});
return { promise, cancel: () => resolver.cancel() };
}
/**
* Host-side CONNECT broker. The acquisition container remains in Docker's
* network-none namespace and reaches this broker only through a bind-mounted
* Unix socket and its qualified loopback forwarder.
*/
export class RegistryEgressBroker {
readonly contactedHosts = new Set<string>();
readonly deniedHosts = new Set<string>();
private readonly server = net.createServer((socket) => this.accept(socket));
private readonly sockets = new Set<net.Socket>();
private readonly tasks = new Set<Promise<void>>();
private readonly resolutions = new Set<{ cancel(error: CsoError): void }>();
private readonly pinned = new Map<string, string>();
private transferred = 0;
private violation: string | undefined;
private started = false;
private closing = false;
constructor(
readonly socketPath: string,
readonly allowedHosts: string[],
private readonly deadline: number,
private readonly maxBytes: number,
private readonly resolveDns: RegistryDnsResolver = systemRegistryDnsResolver,
) {
if (
!socketPath.startsWith('/') ||
!allowedHosts.length ||
new Set(allowedHosts).size !== allowedHosts.length ||
allowedHosts.some((host) => host !== host.toLowerCase() || !/^[a-z0-9.-]{1,253}$/.test(host)) ||
!Number.isSafeInteger(deadline) ||
deadline <= Date.now() ||
!Number.isSafeInteger(maxBytes) ||
maxBytes <= 0
)
fail('INVALID_ARGUMENT', 'Registry broker requires a bounded allowlist, deadline, and byte ceiling');
}
async start(): Promise<void> {
if (this.started || this.closing)
fail('INVALID_ARGUMENT', 'Registry broker cannot be started more than once');
if (fs.existsSync(this.socketPath)) fail('UNSAFE_PATH', 'Registry broker socket path already exists');
await new Promise<void>((resolveStart, reject) => {
const onError = () =>
reject(new CsoError('ISOLATION_FAILED', 'Registry broker could not bind its private Unix socket'));
this.server.once('error', onError);
this.server.listen(this.socketPath, () => {
this.server.off('error', onError);
resolveStart();
});
});
this.started = true;
this.server.on('error', () => {
this.violation ??= 'Registry broker listener failed';
});
fs.chmodSync(this.socketPath, 0o600);
const stat = fs.lstatSync(this.socketPath);
if (!stat.isSocket() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
fail('UNSAFE_PATH', 'Registry broker did not create an owned Unix socket');
}
private deny(socket: net.Socket, message: string, host?: string): void {
this.violation ??= message;
if (host) this.deniedHosts.add(host);
try {
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
} catch {
socket.destroy();
}
}
private async lookup(host: string, lookupDeadline: number): Promise<RegistryDnsAddress[]> {
const resolution = this.resolveDns(host);
if (
!resolution ||
typeof resolution.cancel !== 'function' ||
!resolution.promise ||
typeof resolution.promise.then !== 'function'
)
fail('ISOLATION_FAILED', 'Registry DNS resolver returned an invalid operation');
let rejectCancellation!: (error: CsoError) => void,
settled = false;
const cancelled = new Promise<never>((_resolve, reject) => {
rejectCancellation = reject;
});
const active = {
cancel: (error: CsoError) => {
if (settled) return;
try {
resolution.cancel();
} catch {}
rejectCancellation(error);
},
};
this.resolutions.add(active);
const remaining = lookupDeadline - Date.now();
if (remaining <= 0) active.cancel(new CsoError('DEADLINE', 'Registry DNS lookup deadline elapsed'));
const timer =
remaining > 0
? setTimeout(
() => active.cancel(new CsoError('DEADLINE', 'Registry DNS lookup deadline elapsed')),
remaining,
)
: undefined;
try {
const answers = await Promise.race([resolution.promise, cancelled]);
if (
!Array.isArray(answers) ||
answers.some(
(answer) =>
!answer || typeof answer.address !== 'string' || (answer.family !== 4 && answer.family !== 6),
)
)
fail('ISOLATION_FAILED', 'Registry DNS resolver returned invalid addresses');
return answers;
} finally {
settled = true;
if (timer) clearTimeout(timer);
this.resolutions.delete(active);
}
}
private async openTunnel(
socket: net.Socket,
host: string,
remainder: Buffer,
connectionDeadline: number,
): Promise<void> {
try {
const answers = (await this.lookup(host, connectionDeadline)).filter(
(answer) => !isBlockedRegistryAddress(answer.address, answer.family),
);
if (this.closing || socket.destroyed) return;
if (Date.now() >= this.deadline || Date.now() >= connectionDeadline) {
this.deny(socket, 'Registry acquisition deadline elapsed', host);
return;
}
if (!answers.length) {
this.deny(socket, 'Registry DNS resolved only to blocked or invalid addresses', host);
return;
}
const identity = answers
.map((answer) => `${answer.family}:${answer.address}`)
.sort()
.join(',');
const prior = this.pinned.get(host);
if (prior && prior !== identity) {
this.deny(socket, 'Registry DNS answers changed during one acquisition', host);
return;
}
this.pinned.set(host, identity);
const selected = answers.sort((a, b) => a.address.localeCompare(b.address))[0];
// No await is permitted between the closing/deadline check and socket
// registration: close() must either prevent this dial or destroy it.
if (this.closing || socket.destroyed || Date.now() >= this.deadline) return;
const upstream = net.connect({ host: selected.address, port: 443, family: selected.family });
this.sockets.add(upstream);
upstream.setTimeout(Math.max(1, Math.min(30_000, this.deadline - Date.now())));
upstream.once('close', () => this.sockets.delete(upstream));
upstream.once('error', () => {
if (!this.closing) this.violation ??= 'Registry connection failed after DNS pinning';
socket.destroy();
});
upstream.once('connect', () => {
if (this.closing || socket.destroyed || Date.now() >= this.deadline) {
upstream.destroy();
socket.destroy();
return;
}
const addBytes = (bytes: number) => {
this.transferred += bytes;
if (this.transferred <= this.maxBytes) return true;
this.violation = 'Registry transfer exceeded its byte ceiling';
socket.destroy();
upstream.destroy();
return false;
};
const count = (chunk: Buffer) => {
addBytes(chunk.length);
};
socket.on('data', count);
upstream.on('data', count);
socket.pipe(upstream);
upstream.pipe(socket);
this.contactedHosts.add(host);
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
if (remainder.length && addBytes(remainder.length)) upstream.write(remainder);
});
} catch {
if (this.closing || socket.destroyed) return;
this.deny(socket, 'Registry DNS lookup or pinned connection failed', host);
}
}
private accept(socket: net.Socket): void {
if (this.closing || Date.now() >= this.deadline) {
socket.destroy();
return;
}
if (this.sockets.size >= 64) {
this.deny(socket, 'Registry broker connection limit exceeded');
return;
}
const connectionDeadline = Math.min(this.deadline, Date.now() + 30_000);
this.sockets.add(socket);
socket.setTimeout(Math.max(1, connectionDeadline - Date.now()));
socket.once('close', () => this.sockets.delete(socket));
let pending = Buffer.alloc(0),
handled = false;
const first = (chunk: Buffer) => {
if (handled) return;
pending = Buffer.concat([pending, chunk]);
if (pending.length > 16 * 1024) {
handled = true;
this.deny(socket, 'Registry proxy request exceeded its header limit');
return;
}
const end = pending.indexOf('\r\n\r\n');
if (end < 0) return;
handled = true;
socket.off('data', first);
const header = pending.subarray(0, end + 4).toString('ascii'),
remainder = pending.subarray(end + 4);
const line = header.slice(0, header.indexOf('\r\n'));
const match = line.match(/^CONNECT ([a-zA-Z0-9.-]+):443 HTTP\/1\.[01]$/);
const host = match?.[1].toLowerCase();
if (
!host ||
header.toLowerCase().includes('\r\nproxy-authorization:') ||
!this.allowedHosts.includes(host)
) {
this.deny(socket, 'Registry proxy rejected a non-allowlisted CONNECT request', host);
return;
}
const task = this.openTunnel(socket, host, remainder, connectionDeadline);
this.tasks.add(task);
void task.then(
() => this.tasks.delete(task),
() => this.tasks.delete(task),
);
};
socket.on('data', first);
socket.once('timeout', () => this.deny(socket, 'Registry proxy connection timed out'));
socket.once('error', () => {});
}
assertClean(): void {
if (this.violation) fail('ISOLATION_FAILED', this.violation);
}
async close(): Promise<void> {
this.closing = true;
for (const resolution of this.resolutions)
resolution.cancel(new CsoError('ISOLATION_FAILED', 'Registry broker closed during DNS resolution'));
for (const socket of this.sockets) socket.destroy();
if (this.started && this.server.listening)
await new Promise<void>((resolveClose) => this.server.close(() => resolveClose()));
await Promise.allSettled([...this.tasks]);
this.started = false;
try {
const stat = fs.lstatSync(this.socketPath);
if (!stat.isSocket() || stat.isSymbolicLink())
fail('UNSAFE_PATH', 'Registry broker socket changed before cleanup');
fs.unlinkSync(this.socketPath);
} catch (error: any) {
if (error?.code !== 'ENOENT') throw error;
}
}
}
export interface DockerPreparationRunnerOptions {
endpoint: DockerEndpoint;
watchdogPath: string;
controlRoot: string;
runRoot?: string;
admission: PreparationRuntimeAdmission;
}
export interface PreparedCallGuard {
callRoot: string;
controlRoot: string;
dispose(): Promise<void>;
}
export interface SupervisedRegistrySocket {
root: string;
socketPath: string;
dispose(): Promise<void>;
}
/** Darwin's sockaddr_un.sun_path is 104 bytes including its terminator. */
export const REGISTRY_SOCKET_PATH_MAX_BYTES = 90;
function sameDirectory(left: fs.Stats, right: fs.Stats): boolean {
return left.dev === right.dev && left.ino === right.ino && left.uid === right.uid;
}
function ownedDirectory(path: string, label: string): fs.Stats {
let stat: fs.Stats;
try {
stat = fs.lstatSync(path);
} catch {
fail('PERSISTENCE_FAILED', `${label} disappeared`);
}
if (!stat!.isDirectory() || stat!.isSymbolicLink() || (process.getuid && stat!.uid !== process.getuid()))
fail('UNSAFE_PATH', `${label} is not one owned directory`);
return stat!;
}
/** Detached guard for a successful retained preparation copy. Exported for fault-injection qualification. */
export async function supervisePreparedCall(options: {
watchdogPath: string;
ownerPid: number;
deadline: number;
runRoot: string;
callRoot: string;
controlRoot: string;
}): Promise<PreparedCallGuard> {
const run = fs.realpathSync(options.runRoot),
call = fs.realpathSync(options.callRoot),
control = fs.realpathSync(options.controlRoot);
if (
!Number.isSafeInteger(options.ownerPid) ||
options.ownerPid <= 1 ||
!Number.isSafeInteger(options.deadline) ||
options.deadline <= Date.now() ||
!call.startsWith(`${run}${sep}`) ||
!control.startsWith(`${run}${sep}`) ||
call === control
)
fail('ISOLATION_FAILED', 'Prepared-copy supervision paths or deadline are invalid');
const callIdentity = ownedDirectory(call, 'Prepared call root'),
controlIdentity = ownedDirectory(control, 'Prepared supervision control');
if (!fs.existsSync(options.watchdogPath) || fs.lstatSync(options.watchdogPath).isSymbolicLink())
fail('ISOLATION_FAILED', 'Prepared-copy watchdog is missing from the trusted helper distribution');
const ready = join(control, 'attempt.ready'),
terminal = join(control, 'attempt.terminal'),
stopped = join(control, 'attempt.stopped'),
event = join(control, 'attempt.event');
const child = spawn(
options.watchdogPath,
[
'--attempt-owner',
String(options.ownerPid),
'--deadline',
String(Math.ceil(options.deadline / 1000)),
'--control-dir',
control,
'--work-root',
call,
'--run-root',
run,
],
{ cwd: control, env: { PATH: '/usr/bin:/bin' }, detached: true, stdio: 'ignore' },
);
let failed = false;
child.once('error', () => {
failed = true;
});
child.unref();
for (let attempt = 0; attempt < 100 && !failed && !fs.existsSync(ready); attempt++)
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
let alive = false;
try {
if (child.pid) {
process.kill(child.pid, 0);
alive = true;
}
} catch {}
if (failed || !alive || !fs.existsSync(ready)) {
try {
if (child.pid) process.kill(child.pid, 'SIGKILL');
} catch {}
fail('ISOLATION_FAILED', 'Prepared-copy watchdog failed its startup handshake');
}
let disposed = false;
return {
callRoot: call,
controlRoot: control,
dispose: async () => {
if (disposed) fail('PERSISTENCE_FAILED', 'Prepared-copy guard was already disposed');
disposed = true;
let supervised = false;
try {
const current = fs.lstatSync(call);
if (!sameDirectory(callIdentity, current) || !current.isDirectory() || current.isSymbolicLink())
fail('SNAPSHOT_RACE', 'Prepared call root changed before cleanup');
fs.rmSync(call, { recursive: true, force: false });
supervised = true;
} catch (error: any) {
if (error instanceof CsoError) throw error;
if (error?.code !== 'ENOENT' || !fs.existsSync(event))
fail('PERSISTENCE_FAILED', 'Prepared execution copy could not be removed exactly');
}
if (supervised) {
fs.writeFileSync(terminal, 'normal cleanup complete\n', { mode: 0o600, flag: 'wx' });
for (let attempt = 0; attempt < 100 && !fs.existsSync(stopped); attempt++)
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
if (!fs.existsSync(stopped))
fail('ISOLATION_FAILED', 'Prepared-copy watchdog did not acknowledge exact cleanup');
}
const currentControl = ownedDirectory(control, 'Prepared supervision control');
if (!sameDirectory(controlIdentity, currentControl))
fail('SNAPSHOT_RACE', 'Prepared supervision control changed before cleanup');
fs.rmSync(control, { recursive: true, force: false });
},
};
}
function removeExactDirectory(path: string, identity: fs.Stats, label: string): void {
let current: fs.Stats;
try {
current = fs.lstatSync(path);
} catch (error: any) {
if (error?.code === 'ENOENT') return;
fail('PERSISTENCE_FAILED', `${label} disappeared before exact cleanup`);
}
if (!current!.isDirectory() || current!.isSymbolicLink() || !sameDirectory(identity, current!))
fail('SNAPSHOT_RACE', `${label} changed before exact cleanup`);
try {
fs.rmSync(path, { recursive: true, force: false });
} catch {
fail('PERSISTENCE_FAILED', `${label} could not be removed exactly`);
}
if (fs.existsSync(path)) fail('PERSISTENCE_FAILED', `${label} cleanup could not be proven`);
}
function registrySocketBase(): { path: string; uid: number } {
const getuid = process.getuid;
if (process.platform === 'win32' || !getuid)
fail('PREREQUISITE', 'Registry acquisition requires local Unix sockets');
const uid = getuid();
let temporary: string;
try {
temporary = fs.realpathSync('/tmp');
} catch {
fail('PREREQUISITE', 'A canonical local temporary directory is required for registry acquisition');
}
const stat = fs.lstatSync(temporary!);
if (
temporary === '/' ||
!stat.isDirectory() ||
stat.isSymbolicLink() ||
(stat.uid !== 0 && stat.uid !== uid) ||
((stat.mode & 0o022) !== 0 && (stat.mode & 0o1000) === 0)
)
fail('UNSAFE_PATH', 'The local temporary directory is not a trusted sticky directory');
return { path: temporary!, uid };
}
/**
* Create a collision-resistant, Darwin-safe registry socket path and place its
* entire per-call root under an independent owner/deadline watchdog. The
* watchdog control directory is nested deliberately, so normal termination,
* owner death, and deadline expiry all remove the same exact root without a
* caller-only cleanup interval.
*/
export async function superviseRegistrySocket(options: {
watchdogPath: string;
ownerPid: number;
deadline: number;
}): Promise<SupervisedRegistrySocket> {
if (
!Number.isSafeInteger(options.ownerPid) ||
options.ownerPid <= 1 ||
!Number.isSafeInteger(options.deadline) ||
options.deadline <= Date.now()
)
fail('ISOLATION_FAILED', 'Registry socket supervision owner or deadline is invalid');
let watchdog = '',
watchdogStat: fs.Stats;
try {
watchdog = fs.realpathSync(options.watchdogPath);
watchdogStat = fs.lstatSync(options.watchdogPath);
} catch {
fail('ISOLATION_FAILED', 'Registry socket watchdog is missing from the trusted helper distribution');
}
if (
!isAbsolute(options.watchdogPath) ||
watchdog !== options.watchdogPath ||
!watchdogStat!.isFile() ||
watchdogStat!.isSymbolicLink() ||
(watchdogStat!.mode & 0o111) === 0 ||
(process.getuid && watchdogStat!.uid !== 0 && watchdogStat!.uid !== process.getuid())
)
fail('ISOLATION_FAILED', 'Registry socket watchdog must be one canonical owned executable');
const { path: base, uid } = registrySocketBase();
let root = '';
for (let attempt = 0; attempt < 4 && !root; attempt++) {
const candidate = join(base, `gscso-${uid}-${randomBytes(16).toString('hex')}`);
try {
fs.mkdirSync(candidate, { mode: 0o700 });
root = candidate;
} catch (error: any) {
if (error?.code !== 'EEXIST') throw error;
}
}
if (!root)
fail('INSUFFICIENT_CAPACITY', 'A unique private registry socket directory could not be allocated');
fs.chmodSync(root, 0o700);
const rootIdentity = ownedDirectory(root, 'Registry socket root'),
socketPath = join(root, 'r.sock');
if (fs.realpathSync(root) !== root || Buffer.byteLength(socketPath) > REGISTRY_SOCKET_PATH_MAX_BYTES) {
removeExactDirectory(root, rootIdentity, 'Registry socket root');
fail(
'PREREQUISITE',
`The canonical registry socket path exceeds ${REGISTRY_SOCKET_PATH_MAX_BYTES} bytes`,
);
}
let control = '';
let child: ReturnType<typeof spawn> | undefined;
let failed = false;
let exitCode: number | null | undefined;
try {
control = secureDirectory(join(root, 'control'));
const ready = join(control, 'attempt.ready'),
terminal = join(control, 'attempt.terminal');
child = spawn(
watchdog,
[
'--ephemeral-owner',
String(options.ownerPid),
'--deadline',
String(Math.ceil(options.deadline / 1000)),
'--control-dir',
control,
'--work-root',
root,
'--run-root',
base,
],
{ cwd: control, env: { PATH: '/usr/bin:/bin' }, detached: true, stdio: 'ignore' },
);
const exited = new Promise<number | null>((resolveExit) =>
child!.once('close', (code) => {
exitCode = code;
resolveExit(code);
}),
);
child.once('error', () => {
failed = true;
});
child.unref();
for (let attempt = 0; attempt < 100 && !failed && !fs.existsSync(ready); attempt++)
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
let alive = false;
try {
if (child.pid) {
process.kill(child.pid, 0);
alive = true;
}
} catch {}
if (failed || !alive || !fs.existsSync(ready)) {
try {
if (child.pid) process.kill(child.pid, 'SIGKILL');
} catch {}
await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, 1000))]);
fail('ISOLATION_FAILED', 'Registry socket watchdog failed its startup handshake');
}
let disposed = false;
return {
root,
socketPath,
dispose: async () => {
if (disposed) fail('PERSISTENCE_FAILED', 'Registry socket guard was already disposed');
disposed = true;
let current: fs.Stats;
try {
current = fs.lstatSync(root);
} catch (error: any) {
if (error?.code !== 'ENOENT')
fail('PERSISTENCE_FAILED', 'Registry socket root disappeared during cleanup');
const code =
exitCode === undefined
? await Promise.race([
exited,
new Promise<undefined>((resolveWait) => setTimeout(() => resolveWait(undefined), 5000)),
])
: exitCode;
if (code !== 0)
fail('ISOLATION_FAILED', 'Registry socket watchdog did not complete abnormal exact cleanup');
return;
}
if (!current!.isDirectory() || current!.isSymbolicLink() || !sameDirectory(rootIdentity, current!))
fail('SNAPSHOT_RACE', 'Registry socket root changed before cleanup');
try {
fs.writeFileSync(terminal, 'normal cleanup complete\n', { mode: 0o600, flag: 'wx' });
} catch (error: any) {
if (error?.code !== 'ENOENT') throw error;
}
for (let attempt = 0; attempt < 500 && fs.existsSync(root); attempt++)
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
if (fs.existsSync(root))
fail('ISOLATION_FAILED', 'Registry socket watchdog did not remove the exact private root');
const code =
exitCode === undefined
? await Promise.race([
exited,
new Promise<undefined>((resolveWait) => setTimeout(() => resolveWait(undefined), 5000)),
])
: exitCode;
if (code !== 0)
fail('ISOLATION_FAILED', 'Registry socket watchdog exited without completing exact cleanup');
},
};
} catch (error) {
try {
if (child?.pid) process.kill(child.pid, 'SIGKILL');
} catch {}
let cleanupError: unknown;
try {
removeExactDirectory(root, rootIdentity, 'Registry socket root');
} catch (failure) {
cleanupError = failure;
}
if (cleanupError) throw cleanupError;
throw error;
}
}
/**
* Requires qualified runtime images to contain the fixed, compiled
* /opt/cso/preparation@1.0.0 helper contract.
*/
export class DockerPreparationSandboxRunner implements PreparationSandboxRunner {
readonly qualification;
private readonly runtime;
private readonly controlRoot: string;
private readonly preparedRoots = new Map<
string,
{ guard: PreparedCallGuard; callDir: string; callIdentity: fs.Stats }
>();
constructor(private readonly options: DockerPreparationRunnerOptions) {
this.runtime = admittedPreparationRuntime(options.admission);
if (
!['node', 'bun', 'python', 'rails'].includes(this.runtime.stack) ||
this.runtime.versions['cso-preparation'] !== '1.0.0'
)
fail('PREREQUISITE', 'Qualified runtime lacks the cso-preparation=1.0.0 container helper contract');
this.controlRoot = secureDirectory(resolve(options.controlRoot));
this.qualification = Object.freeze({
schemaVersion: 1 as const,
helperAbi: CSO_HELPER_ABI,
runnerId: 'docker-registry-broker-v1',
policyVersion: 'cso-preparation-v1' as const,
supportedStacks: [this.runtime.stack as CsoStack],
registryRestrictionQualified: true as const,
dnsRebindingTestsPassed: true as const,
acquisitionExcludesSource: true as const,
offlineContainmentQualified: true as const,
immutableArchiveMounts: true as const,
resourceLimitsEnforced: true as const,
});
}
private assertRuntime(request: PreparationAcquireRequest | OfflinePreparationRequest): void {
if (
request.runtime.id !== this.runtime.id ||
request.runtime.image !== this.runtime.image ||
request.runtime.platform !== this.runtime.platform ||
request.stack !== this.runtime.stack
)
fail('INCOMPATIBLE_INPUT', 'Docker preparation request does not match its admitted runtime');
}
private callDirectory(prefix: string): string {
return secureDirectory(
join(this.controlRoot, `${prefix}-${Date.now()}-${randomBytes(8).toString('hex')}`),
);
}
private removeCallDirectory(path: string, identity: fs.Stats): void {
let current: fs.Stats;
try {
current = fs.lstatSync(path);
} catch {
fail('PERSISTENCE_FAILED', 'Preparation call directory disappeared before exact cleanup');
}
if (
!current!.isDirectory() ||
current!.isSymbolicLink() ||
current!.dev !== identity.dev ||
current!.ino !== identity.ino
)
fail('SNAPSHOT_RACE', 'Preparation call directory changed before cleanup');
try {
fs.rmSync(path, { recursive: true, force: false });
} catch {
fail('PERSISTENCE_FAILED', 'Preparation call directory could not be removed');
}
if (fs.existsSync(path))
fail('PERSISTENCE_FAILED', 'Preparation call directory cleanup could not be proven');
}
private publishArtifacts(
artifacts: AcquisitionArtifactReceipt[],
output: string,
stagingRoot: string,
maxBytes: number,
): void {
const created: string[] = [];
try {
for (const artifact of artifacts) {
if (
typeof artifact.stagingPath !== 'string' ||
!artifact.stagingPath.startsWith('cso-public/') ||
artifact.stagingPath.slice('cso-public/'.length).includes('/')
)
fail('TOOL_FAILED', 'Qualified helper returned an invalid staging artifact path');
const name = artifact.stagingPath.slice('cso-public/'.length),
source = contained(join(output, 'archives'), name),
target = contained(stagingRoot, artifact.stagingPath);
const stat = fs.lstatSync(source);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.nlink !== 1 ||
stat.size !== artifact.bytes ||
stat.size > maxBytes ||
(process.getuid && stat.uid !== process.getuid()) ||
(stat.mode & 0o022) !== 0 ||
fileSha256(source, maxBytes) !== artifact.sha256
)
fail('TOOL_FAILED', 'Qualified helper output did not match its archive receipt');
secureDirectory(resolve(target, '..'));
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
fs.chmodSync(target, 0o600);
created.push(target);
const copied = fs.lstatSync(target);
if (
!copied.isFile() ||
copied.isSymbolicLink() ||
copied.nlink !== 1 ||
copied.size !== artifact.bytes ||
fileSha256(target, maxBytes) !== artifact.sha256
)
fail('SNAPSHOT_RACE', 'Staged acquisition artifact changed during publication');
}
} catch (error) {
for (const path of created.reverse()) {
try {
const stat = fs.lstatSync(path);
if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1) fs.unlinkSync(path);
} catch {}
}
throw error;
}
}
private execClean(
group: DockerGroup,
id: string,
command: string[],
options: { workdir?: string; env?: Record<string, string> } = {},
) {
const forbidden = new Set([
'BUN_OPTIONS',
'BUN_BE_BUN',
'NODE_OPTIONS',
'RUBYOPT',
'RUBYLIB',
'PYTHONPATH',
'PYTHONHOME',
'LD_PRELOAD',
'LD_LIBRARY_PATH',
'ENV',
'BASH_ENV',
'CDPATH',
]);
if (Object.keys(options.env ?? {}).some((key) => forbidden.has(key)))
fail('ISOLATION_FAILED', 'Preparation command attempted to restore a runtime injection variable');
const clean = { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/work/.cso-home', ...(options.env ?? {}) };
const argv = [
'/usr/bin/env',
'-i',
...Object.entries(clean)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`),
...command,
];
return group.execCapture(id, argv, { workdir: options.workdir });
}
async acquire(request: PreparationAcquireRequest): Promise<AcquisitionReceipt> {
this.assertRuntime(request);
const dir = this.callDirectory('acquire'),
callIdentity = fs.lstatSync(dir);
let group: DockerGroup | undefined,
broker: RegistryEgressBroker | undefined,
guard: PreparedCallGuard | undefined,
registrySocket: SupervisedRegistrySocket | undefined;
const commandResults: PreparationCommandReceipt[] = [];
try {
const executionCopies = secureDirectory(join(dir, 'execution-copies')),
metadata = secureDirectory(join(executionCopies, 'metadata')),
policyDir = secureDirectory(join(executionCopies, 'policy')),
output = secureDirectory(join(executionCopies, 'output')),
runRoot = secureDirectory(resolve(this.options.runRoot ?? this.controlRoot)),
supervision = secureDirectory(join(runRoot, 'supervision')),
guardControl = secureDirectory(join(supervision, `acquisition-${randomBytes(12).toString('hex')}`));
guard = await supervisePreparedCall({
watchdogPath: this.options.watchdogPath,
ownerPid: process.pid,
deadline: request.deadline,
runRoot,
callRoot: executionCopies,
controlRoot: guardControl,
});
for (const item of request.metadata) {
if (sha256(item.content) !== item.sha256)
fail('INCOMPATIBLE_INPUT', `Acquisition metadata hash changed: ${item.path}`);
const file = contained(metadata, item.path);
secureDirectory(resolve(file, '..'));
fs.writeFileSync(file, item.content, { mode: 0o600, flag: 'wx' });
}
const policyFile = join(policyDir, 'acquisition.json');
writePolicy(policyFile, {
schemaVersion: 1,
planHash: request.planHash,
stack: request.stack,
inputs: request.inputs,
commands: request.commands,
allowedHosts: request.network.allowedHosts,
archiveRoot: '/archives',
limits: request.limits,
});
group = await DockerGroup.create(
this.options.endpoint,
`prep-a-${randomBytes(10).toString('hex')}`,
dir,
request.deadline,
this.runtime.image,
this.options.watchdogPath,
);
registrySocket = await superviseRegistrySocket({
watchdogPath: this.options.watchdogPath,
ownerPid: process.pid,
deadline: request.deadline,
});
broker = new RegistryEgressBroker(
registrySocket.socketPath,
request.network.allowedHosts,
request.deadline,
request.limits.maxTotalArchiveBytes + Math.min(64 * 1024 * 1024, request.limits.maxTotalArchiveBytes),
);
await broker.start();
const container = await group.createContainer({
role: 'app',
image: this.runtime.image,
command: ['/opt/cso/preparation', 'forwarder', '/run/cso-registry.sock', '127.0.0.1:18443'],
readonlyFiles: [{ host: policyFile, container: '/policy/acquisition.json' }],
readonlyInputMetadata: metadata,
workTmpfsBytes: 64 * 1024 * 1024,
temporaryTmpfsBytes: 64 * 1024 * 1024,
metadataTmpfsBytes: (request.stack === 'node' || request.stack === 'bun' ? 1024 : 64) * 1024 * 1024,
archiveTmpfsBytes: request.limits.maxTotalArchiveBytes,
registrySocket: broker.socketPath,
});
await group.start(container);
let ready = false;
for (let attempt = 0; attempt < 20 && !ready; attempt++) {
const health = await this.execClean(group, container, [
'/opt/cso/preparation',
'health',
'127.0.0.1',
'18443',
]);
ready = health.code === 0;
if (!ready) await new Promise((resolveWait) => setTimeout(resolveWait, 25));
}
if (!ready) fail('ISOLATION_FAILED', 'Qualified registry forwarder did not become ready');
const proxy = {
HTTP_PROXY: 'http://127.0.0.1:18443',
HTTPS_PROXY: 'http://127.0.0.1:18443',
ALL_PROXY: 'http://127.0.0.1:18443',
NO_PROXY: '',
};
for (let index = 0; index < request.commands.length; index++) {
const command = request.commands[index],
result = await this.execClean(group, container, [command.executable, ...command.args], {
workdir: command.cwd,
env: { ...command.env, ...proxy },
});
broker.assertClean();
commandResults.push(commandReceipt(command, index, result.code));
if (result.code !== 0) fail('TOOL_FAILED', `Dependency acquisition command ${index + 1} failed`);
}
await group.assertOnlyInitProcess(container);
const containerExport = `/archives/.gstack-cso-acquisition-export-${randomBytes(12).toString('hex')}`;
const manifest = await this.execClean(
group,
container,
[
'/opt/cso/preparation',
'manifest',
'/policy/acquisition.json',
'/archives',
`${containerExport}/artifacts.json`,
],
{ workdir: '/archives', env: proxy },
);
if (manifest.code !== 0) fail('TOOL_FAILED', 'Qualified archive manifest helper failed');
broker.assertClean();
await group.assertOnlyInitProcess(container);
await group.pause(container);
await group.copyAcquisitionExport(container, containerExport, output);
const artifacts = readManifest(join(output, 'artifacts.json')).artifacts;
validateAcquisitionOutput(output, artifacts);
this.publishArtifacts(artifacts, output, request.stagingRoot, request.limits.maxArchiveBytes);
return {
schemaVersion: 1,
planHash: request.planHash,
runtimeId: request.runtime.id,
runtimeImage: request.runtime.image,
platform: request.runtime.platform,
deadlineEnforced: true,
network: {
mode: 'registry-restricted',
allowedHosts: [...request.network.allowedHosts],
contactedHosts: [...broker.contactedHosts].sort(),
redirectVisibility: 'opaque-tls',
dnsRebindingBlocked: true,
credentialsMounted: false,
sourceMounted: false,
dockerSocketMounted: false,
},
lifecycleScriptsExecuted: false,
targetCodeExecuted: false,
commands: commandResults,
artifacts,
};
} finally {
let cleanupError: unknown;
try {
if (broker) await broker.close();
} catch (error) {
cleanupError = error;
}
try {
if (group) await group.cleanup();
} catch (error) {
cleanupError ??= error;
}
try {
if (registrySocket) await registrySocket.dispose();
} catch (error) {
cleanupError ??= error;
}
try {
if (guard) await guard.dispose();
} catch (error) {
cleanupError ??= error;
}
if (cleanupError) throw cleanupError;
this.removeCallDirectory(dir, callIdentity);
}
}
private materializeArchives(request: OfflinePreparationRequest, root: string): void {
for (const archive of request.archives) {
if (!archive.containerPath.startsWith('/archives/'))
fail('UNSAFE_PATH', 'Offline archive mount escaped /archives');
const relativePath = archive.containerPath.slice('/archives/'.length),
target = contained(root, relativePath),
source = resolve(archive.hostPath);
const stat = fs.lstatSync(source);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.nlink !== 1 ||
stat.size !== archive.bytes ||
(stat.mode & 0o222) !== 0 ||
(process.getuid && stat.uid !== process.getuid())
)
fail('UNSAFE_PATH', 'Offline archive is not an immutable cache file');
secureDirectory(resolve(target, '..'));
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
fs.chmodSync(target, 0o400);
const hash = fileSha256(target, archive.bytes);
if (hash !== archive.sha256)
fail('INCOMPATIBLE_INPUT', 'Offline archive changed while its execution view was materialized');
}
}
async prepareOffline(request: OfflinePreparationRequest): Promise<OfflinePreparationResult> {
this.assertRuntime(request);
const dir = this.callDirectory('offline'),
callIdentity = fs.lstatSync(dir);
let preparedRoot = '',
group: DockerGroup | undefined,
success = false,
guard: PreparedCallGuard | undefined;
try {
// Docker supervision owns `dir`; the retained-copy watchdog owns this
// child. An owner death can therefore remove every execution input without
// deleting the Docker watchdog's journal or control files.
const executionCopies = secureDirectory(join(dir, 'execution-copies')),
source = secureDirectory(join(executionCopies, 'source')),
metadata = secureDirectory(join(executionCopies, 'metadata')),
archives = secureDirectory(join(executionCopies, 'archives')),
policyDir = secureDirectory(join(executionCopies, 'policy'));
preparedRoot = secureDirectory(join(executionCopies, 'prepared'));
const runRoot = secureDirectory(resolve(this.options.runRoot ?? this.controlRoot)),
supervision = secureDirectory(join(runRoot, 'supervision')),
guardControl = secureDirectory(join(supervision, `prepared-${randomBytes(12).toString('hex')}`));
// Supervision is acknowledged before the first potentially large source
// or dependency copy. It remains the retained-copy guard after Docker
// teardown, closing the owner-death handoff window.
guard = await supervisePreparedCall({
watchdogPath: this.options.watchdogPath,
ownerPid: process.pid,
deadline: request.deadline,
runRoot,
callRoot: executionCopies,
controlRoot: guardControl,
});
fs.cpSync(request.sourceRoot, source, {
recursive: true,
force: false,
errorOnExist: false,
preserveTimestamps: false,
});
for (const transformation of request.transformations) {
if (sha256(transformation.content) !== transformation.sha256)
fail('INCOMPATIBLE_INPUT', 'Offline transformation content hash changed');
const file = contained(source, transformation.path);
secureDirectory(resolve(file, '..'));
fs.writeFileSync(file, transformation.content, { mode: 0o600 });
}
for (const item of request.metadata) {
if (sha256(item.content) !== item.sha256)
fail('INCOMPATIBLE_INPUT', `Offline metadata hash changed: ${item.path}`);
const file = contained(metadata, item.path);
secureDirectory(resolve(file, '..'));
fs.writeFileSync(file, item.content, { mode: 0o600, flag: 'wx' });
}
this.materializeArchives(request, archives);
const offlinePolicy = join(policyDir, 'offline.json');
writePolicy(offlinePolicy, {
schemaVersion: 1,
planHash: request.planHash,
stack: request.stack,
inputs: request.archives.map((archive) => ({
index: archive.inputIndex,
input: {
kind: 'public',
name: archive.name,
version: archive.version,
url: archive.requestedUrl,
integrity: archive.declaredIntegrity,
integritySource:
archive.declaredIntegrity === 'registry-on-acquisition' ? 'registry-on-acquisition' : 'lock',
},
})),
archives: request.archives.map((archive) => ({
inputIndex: archive.inputIndex,
name: archive.name,
version: archive.version,
declaredIntegrity: archive.declaredIntegrity,
requestedUrl: archive.requestedUrl,
resolvedUrl: archive.resolvedUrl,
containerPath: archive.containerPath,
sha256: archive.sha256,
bytes: archive.bytes,
})),
});
const commands: PreparationCommandReceipt[] = [];
group = await DockerGroup.create(
this.options.endpoint,
`prep-o-${randomBytes(10).toString('hex')}`,
dir,
request.deadline,
this.runtime.image,
this.options.watchdogPath,
);
const app = await group.createContainer({
role: 'app',
image: this.runtime.image,
source,
readonlyFiles: [{ host: offlinePolicy, container: '/policy/offline.json' }],
readonlyMetadata: metadata,
readonlyArchiveDirectory: archives,
command: ['/opt/cso/run-app', '/opt/cso/preparation', 'seed', '/policy/offline.json'],
});
await group.start(app);
let ready = false;
for (let attempt = 0; attempt < 100 && !ready; attempt++) {
const result = await this.execClean(group, app, ['/opt/cso/preparation', 'ready']);
ready = result.code === 0;
if (!ready) await new Promise((resolveWait) => setTimeout(resolveWait, 25));
}
if (!ready) fail('TOOL_FAILED', 'Offline cache seeding did not become ready');
for (let index = 0; index < request.commands.length; index++) {
const command = request.commands[index],
result = await this.execClean(group, app, [command.executable, ...command.args], {
workdir: command.cwd,
env: command.env,
});
commands.push(commandReceipt(command, index, result.code));
if (result.code !== 0)
fail('TOOL_FAILED', `Offline dependency preparation command ${index + 1} failed`);
}
const finalized = await this.execClean(group, app, ['/opt/cso/preparation', 'finalize']);
if (finalized.code !== 0) fail('TOOL_FAILED', 'Offline preparation scratch cleanup failed');
await group.assertOnlyInitProcess(app);
const containerExport = `/work/.gstack-cso-export-${randomBytes(12).toString('hex')}`;
const exported = await this.execClean(group, app, [
'/opt/cso/preparation',
'export-prepared',
'/work',
containerExport,
String(request.limits.writableBytes),
]);
if (exported.code !== 0)
fail('TOOL_FAILED', 'Qualified prepared-tree export rejected offline application output');
await group.assertOnlyInitProcess(app);
await group.pause(app);
const inertExport = secureDirectory(join(executionCopies, 'prepared-export')),
inertIdentity = fs.lstatSync(inertExport);
await group.copyPreparedExport(app, containerExport, inertExport);
materializePreparedExport(inertExport, preparedRoot, request.limits.writableBytes);
this.removeCallDirectory(inertExport, inertIdentity);
const services: Array<'application' | 'postgresql'> = ['application'];
const receipt: OfflinePreparationReceipt = {
schemaVersion: 1,
planHash: request.planHash,
runtimeId: request.runtime.id,
runtimeImage: request.runtime.image,
platform: request.runtime.platform,
sourceHash: request.sourceHash,
dependencyClosureHash: request.dependencyClosureHash,
configurationHash: request.configurationHash,
databaseHash: request.databaseHash,
deadlineEnforced: true,
network: {
mode: 'none',
namespaceAnchor: group.anchor,
externalEgress: false,
dnsAvailable: false,
publishedPorts: false,
services,
},
commands,
inputSourceReadOnly: true,
preparedCopySeparate: true,
archivesReadOnly: true,
applicationCodeExecutedOnlyOffline: true,
};
// The guard is live before Docker teardown starts. Remove redundant
// source, metadata, archives, and policy now; only the prepared tree is
// retained. Docker's independent watchdog keeps its parent control root.
for (const current of [source, metadata, archives, policyDir])
this.removeCallDirectory(current, fs.lstatSync(current));
const completedGroup = group;
await completedGroup.cleanup();
group = undefined;
this.preparedRoots.set(resolve(preparedRoot), { guard, callDir: dir, callIdentity });
success = true;
return { preparedRoot, receipt };
} finally {
let cleanupError: unknown;
if (group) {
try {
await group.cleanup();
} catch (error) {
cleanupError = error;
}
}
if (cleanupError) {
if (preparedRoot) this.preparedRoots.delete(resolve(preparedRoot));
if (guard)
try {
await guard.dispose();
} catch {}
throw cleanupError;
}
if (!success) {
if (preparedRoot) this.preparedRoots.delete(resolve(preparedRoot));
// Once the retained-copy watchdog has acknowledged supervision it is
// the only actor allowed to consume its call root. Stop and
// acknowledge it before removing the Docker call directory.
if (guard) await guard.dispose();
this.removeCallDirectory(dir, callIdentity);
}
}
}
async disposePrepared(preparedRoot: string): Promise<void> {
const root = resolve(preparedRoot),
owned = this.preparedRoots.get(root),
guard = owned?.guard,
call = guard?.callRoot;
if (
!guard ||
!call ||
!owned ||
root !== join(call, 'prepared') ||
!owned.callDir.startsWith(`${this.controlRoot}${sep}`)
)
fail('UNSAFE_PATH', 'Prepared execution copy is not owned by this runner');
this.preparedRoots.delete(root);
await guard.dispose();
this.removeCallDirectory(owned.callDir, owned.callIdentity);
}
}