refactor: adapter 구현중..
This commit is contained in:
@@ -0,0 +1,763 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, open, type FileHandle } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
decodeProviderGuardianPublished,
|
||||
decodeProviderGuardianReady,
|
||||
encodeProviderGuardianCommit,
|
||||
encodeProviderGuardianGuard,
|
||||
encodeProviderGuardianPublish,
|
||||
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
|
||||
MAX_PROVIDER_GUARDIAN_LEASE_MS,
|
||||
MAX_PROVIDER_SEALED_BYTES,
|
||||
providerGuardianRawStagingLeaf,
|
||||
providerGuardianSealedTempLeaf,
|
||||
type ProviderGuardianKind,
|
||||
} from "./provider-guardian-protocol.ts";
|
||||
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
|
||||
|
||||
const RESPONSE_TIMEOUT_MS = 5_000;
|
||||
const CLOSE_TIMEOUT_MS = 5_000;
|
||||
const MAX_CONTROL_OUTPUT_BYTES = 4_096;
|
||||
|
||||
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
|
||||
type RecoveryAuthority = Readonly<{
|
||||
rawDirectoryHandle: FileHandle;
|
||||
evidenceDirectoryHandle: FileHandle;
|
||||
rawStagingHandle: FileHandle;
|
||||
sealedTempHandle: FileHandle;
|
||||
rawIdentity: OwnedIdentity;
|
||||
sealedIdentity: OwnedIdentity;
|
||||
rawStagingPinnedPath: string;
|
||||
rawPinnedPath: string;
|
||||
sealedTempPinnedPath: string;
|
||||
sealedPinnedPath: string;
|
||||
}>;
|
||||
|
||||
export type ProviderGuardianLease = Readonly<{
|
||||
pid: number;
|
||||
rawPath: string;
|
||||
rawIdentity: OwnedIdentity;
|
||||
sealedPath: string;
|
||||
sealedTempPath: string;
|
||||
sealedIdentity: OwnedIdentity;
|
||||
prematureExit: Promise<Error>;
|
||||
publish(bytes: Buffer): Promise<void>;
|
||||
commit(): Promise<void>;
|
||||
abort(): Promise<void>;
|
||||
}>;
|
||||
|
||||
export type StartProviderGuardianInput = Readonly<{
|
||||
kind: ProviderGuardianKind;
|
||||
workspaceRoot: string;
|
||||
leaseMs: number;
|
||||
guardianScript: string;
|
||||
}>;
|
||||
|
||||
export type ProviderScopeGuardianLatch = Readonly<{
|
||||
activeFailure: Promise<Error>;
|
||||
close(): Promise<void>;
|
||||
failure(): Error | undefined;
|
||||
}>;
|
||||
|
||||
export function createProviderScopeGuardianLatch(
|
||||
guardianExit: Promise<Error>,
|
||||
): ProviderScopeGuardianLatch {
|
||||
let active = true;
|
||||
let closing: Promise<void> | undefined;
|
||||
let observedFailure: Error | undefined;
|
||||
let signalActiveFailure!: (error: Error) => void;
|
||||
const activeFailure = new Promise<Error>((resolve) => { signalActiveFailure = resolve; });
|
||||
void guardianExit.then((error) => {
|
||||
observedFailure = error;
|
||||
if (active) signalActiveFailure(error);
|
||||
});
|
||||
return Object.freeze({
|
||||
activeFailure,
|
||||
close: () => {
|
||||
closing ??= Promise.resolve().then(() => { active = false; });
|
||||
return closing;
|
||||
},
|
||||
failure: () => observedFailure,
|
||||
});
|
||||
}
|
||||
|
||||
export function assertProviderGuardianLeasePaths(
|
||||
lease: Readonly<{ rawPath: string; sealedPath: string }>,
|
||||
expected: Readonly<{ rawPath: string; sealedPath: string }>,
|
||||
): void {
|
||||
if (lease.rawPath !== expected.rawPath) {
|
||||
throw new Error("provider guardian returned a noncanonical raw path");
|
||||
}
|
||||
if (lease.sealedPath !== expected.sealedPath) {
|
||||
throw new Error("provider guardian returned a noncanonical sealed path");
|
||||
}
|
||||
}
|
||||
|
||||
type GuardianResult = Readonly<{
|
||||
code: number | null;
|
||||
error?: Error;
|
||||
signal: NodeJS.Signals | null;
|
||||
}>;
|
||||
|
||||
export async function startProviderGuardian(
|
||||
input: StartProviderGuardianInput,
|
||||
): Promise<ProviderGuardianLease> {
|
||||
if (
|
||||
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
|
||||
!path.isAbsolute(input.workspaceRoot) || !path.isAbsolute(input.guardianScript) ||
|
||||
!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0 ||
|
||||
input.leaseMs > MAX_PROVIDER_GUARDIAN_LEASE_MS
|
||||
) {
|
||||
throw new TypeError("provider guardian client input is invalid");
|
||||
}
|
||||
const rawLeaf = input.kind === "vulnerability"
|
||||
? "vulnerability-report.json"
|
||||
: "provenance-attestation.json";
|
||||
const evidenceRoot = path.resolve(input.workspaceRoot, "provider-evidence");
|
||||
const rawDirectory = path.join(evidenceRoot, "untrusted");
|
||||
const rawPath = path.join(evidenceRoot, "untrusted", rawLeaf);
|
||||
const sealedPath = path.join(evidenceRoot, rawLeaf);
|
||||
const nonce = randomBytes(32);
|
||||
const rawStagingLeaf = providerGuardianRawStagingLeaf(input.kind, nonce);
|
||||
const sealedTempLeaf = providerGuardianSealedTempLeaf(input.kind, nonce);
|
||||
const sealedTempPath = path.join(evidenceRoot, sealedTempLeaf);
|
||||
const recovery = await openRecoveryAuthority({
|
||||
rawDirectory,
|
||||
evidenceRoot,
|
||||
rawLeaf,
|
||||
rawStagingLeaf,
|
||||
sealedLeaf: rawLeaf,
|
||||
sealedTempLeaf,
|
||||
});
|
||||
let child: ReturnType<typeof spawn>;
|
||||
try {
|
||||
await assertRecoveryLeavesMissing(recovery);
|
||||
child = spawn(process.execPath, [input.guardianScript], {
|
||||
cwd: input.workspaceRoot,
|
||||
env: {},
|
||||
stdio: [
|
||||
"pipe",
|
||||
"pipe",
|
||||
"pipe",
|
||||
recovery.rawDirectoryHandle.fd,
|
||||
recovery.evidenceDirectoryHandle.fd,
|
||||
recovery.rawStagingHandle.fd,
|
||||
recovery.sealedTempHandle.fd,
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
return await closeRecoveryAndThrow(recovery, error);
|
||||
}
|
||||
if (!child.pid || !child.stdin || !child.stdout || !child.stderr) {
|
||||
child.kill("SIGKILL");
|
||||
return await closeRecoveryAndThrow(
|
||||
recovery,
|
||||
new Error("provider guardian process pipes are unavailable"),
|
||||
);
|
||||
}
|
||||
|
||||
let state: "starting" | "guarding" | "publishing" | "published" |
|
||||
"committing" | "aborting" | "terminated" = "starting";
|
||||
let stderr = Buffer.alloc(0);
|
||||
let inputError: Error | undefined;
|
||||
child.stdin.once("error", (error) => { inputError = error; });
|
||||
child.stderr.on("data", (chunk: Buffer | string) => {
|
||||
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
if (stderr.byteLength < MAX_CONTROL_OUTPUT_BYTES) {
|
||||
stderr = Buffer.concat([stderr, bytes.subarray(0, MAX_CONTROL_OUTPUT_BYTES - stderr.byteLength)]);
|
||||
}
|
||||
});
|
||||
const completion = guardianCompletion(child);
|
||||
let signalPrematureExit!: (error: Error) => void;
|
||||
const prematureExit = new Promise<Error>((resolve) => { signalPrematureExit = resolve; });
|
||||
void completion.then((result) => {
|
||||
if (state === "guarding" || state === "publishing" || state === "published") {
|
||||
signalPrematureExit(guardianCloseError(result, stderr));
|
||||
}
|
||||
});
|
||||
|
||||
let ready: ReturnType<typeof decodeProviderGuardianReady>;
|
||||
try {
|
||||
const readyResponse = waitForFrame(child.stdout, completion, "READY");
|
||||
child.stdin.write(encodeProviderGuardianGuard({
|
||||
kind: input.kind,
|
||||
nonce,
|
||||
deadlineEpochMs: Date.now() + input.leaseMs,
|
||||
}));
|
||||
ready = decodeProviderGuardianReady(await readyResponse, nonce);
|
||||
if (
|
||||
ready.sealedTempLeaf !== sealedTempLeaf ||
|
||||
ready.rawDev !== recovery.rawIdentity.dev ||
|
||||
ready.rawIno !== recovery.rawIdentity.ino ||
|
||||
ready.sealedDev !== recovery.sealedIdentity.dev ||
|
||||
ready.sealedIno !== recovery.sealedIdentity.ino
|
||||
) {
|
||||
throw new TypeError("provider guardian READY identity is invalid for its allocation");
|
||||
}
|
||||
await assertPinnedLeafIdentity(recovery.rawPinnedPath, {
|
||||
dev: ready.rawDev,
|
||||
ino: ready.rawIno,
|
||||
}, 0o600);
|
||||
await assertPinnedLeafIdentity(recovery.sealedTempPinnedPath, {
|
||||
dev: ready.sealedDev,
|
||||
ino: ready.sealedIno,
|
||||
}, 0o600);
|
||||
await assertPinnedLeafMissing(recovery.rawStagingPinnedPath);
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
throw guardianCloseError(await completion, stderr);
|
||||
}
|
||||
state = "guarding";
|
||||
} catch (error) {
|
||||
state = "aborting";
|
||||
child.stdin.end();
|
||||
const failures = [toError(error)];
|
||||
try {
|
||||
await waitForClose(completion, child);
|
||||
} catch (closeError) {
|
||||
failures.push(toError(closeError));
|
||||
}
|
||||
try {
|
||||
await cleanupStartupRecovery(recovery);
|
||||
} catch (cleanupError) {
|
||||
failures.push(toError(cleanupError));
|
||||
}
|
||||
try {
|
||||
await closeRecoveryAuthority(recovery);
|
||||
} catch (closeError) {
|
||||
failures.push(toError(closeError));
|
||||
}
|
||||
state = "terminated";
|
||||
if (failures.length > 1) {
|
||||
throw new AggregateError(failures, "provider guardian startup failed", { cause: error });
|
||||
}
|
||||
throw failures[0]!;
|
||||
}
|
||||
|
||||
const rawIdentity = Object.freeze({ dev: ready.rawDev, ino: ready.rawIno });
|
||||
const sealedIdentity = Object.freeze({ dev: ready.sealedDev, ino: ready.sealedIno });
|
||||
const fallback = Object.freeze({
|
||||
rawStagingPath: recovery.rawStagingPinnedPath,
|
||||
rawPath: recovery.rawPinnedPath,
|
||||
rawIdentity,
|
||||
sealedPath: recovery.sealedPinnedPath,
|
||||
sealedTempPath: recovery.sealedTempPinnedPath,
|
||||
sealedIdentity,
|
||||
});
|
||||
|
||||
const publish = async (bytes: Buffer): Promise<void> => {
|
||||
if (state !== "guarding") throw new Error("provider guardian lease is not ready to publish");
|
||||
if (!Buffer.isBuffer(bytes) || bytes.byteLength <= 0 || bytes.byteLength > MAX_PROVIDER_SEALED_BYTES) {
|
||||
throw new TypeError("provider guardian sealed bytes are invalid");
|
||||
}
|
||||
state = "publishing";
|
||||
try {
|
||||
await writePinnedSealedBytes(recovery.sealedTempHandle, sealedIdentity, bytes);
|
||||
const publishedResponse = waitForFrame(child.stdout!, completion, "PUBLISHED");
|
||||
child.stdin!.write(encodeProviderGuardianPublish({
|
||||
nonce,
|
||||
sealedDev: sealedIdentity.dev,
|
||||
sealedIno: sealedIdentity.ino,
|
||||
size: bytes.byteLength,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
}));
|
||||
decodeProviderGuardianPublished(
|
||||
await publishedResponse,
|
||||
nonce,
|
||||
sealedIdentity,
|
||||
);
|
||||
if (inputError) throw inputError;
|
||||
state = "published";
|
||||
} catch (error) {
|
||||
state = "guarding";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const commit = async (): Promise<void> => {
|
||||
if (state !== "published") throw new Error("provider guardian lease is not ready to commit");
|
||||
state = "committing";
|
||||
child.stdin!.write(encodeProviderGuardianCommit(nonce));
|
||||
child.stdin!.end();
|
||||
let result: GuardianResult;
|
||||
try {
|
||||
result = await waitForClose(completion, child);
|
||||
} catch (error) {
|
||||
state = "terminated";
|
||||
return await cleanupFallbackCloseAndThrow(fallback, recovery, error);
|
||||
}
|
||||
state = "terminated";
|
||||
if (inputError) return await cleanupFallbackCloseAndThrow(fallback, recovery, inputError);
|
||||
if (result.error || result.code !== 0 || result.signal !== null) {
|
||||
return await cleanupFallbackCloseAndThrow(
|
||||
fallback,
|
||||
recovery,
|
||||
guardianCloseError(result, stderr),
|
||||
);
|
||||
}
|
||||
await closeRecoveryAuthority(recovery);
|
||||
};
|
||||
|
||||
const abort = async (): Promise<void> => {
|
||||
if (state !== "guarding" && state !== "published") {
|
||||
throw new Error("provider guardian lease already terminated");
|
||||
}
|
||||
state = "aborting";
|
||||
child.stdin!.end();
|
||||
let closeError: unknown;
|
||||
try {
|
||||
await waitForClose(completion, child);
|
||||
} catch (error) {
|
||||
closeError = error;
|
||||
}
|
||||
state = "terminated";
|
||||
if (closeError) return await cleanupFallbackCloseAndThrow(fallback, recovery, closeError);
|
||||
await cleanupFallbackAndClose(fallback, recovery);
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
pid: child.pid,
|
||||
rawPath,
|
||||
rawIdentity,
|
||||
sealedPath,
|
||||
sealedTempPath,
|
||||
sealedIdentity,
|
||||
prematureExit,
|
||||
publish,
|
||||
commit,
|
||||
abort,
|
||||
});
|
||||
}
|
||||
|
||||
async function writePinnedSealedBytes(
|
||||
handle: FileHandle,
|
||||
identity: OwnedIdentity,
|
||||
bytes: Buffer,
|
||||
): Promise<void> {
|
||||
assertPinnedMetadata(await handle.stat(), identity, 0o600, 0);
|
||||
await handle.truncate(0);
|
||||
await handle.writeFile(bytes);
|
||||
await handle.chmod(0o400);
|
||||
await handle.sync();
|
||||
assertPinnedMetadata(await handle.stat(), identity, 0o400, bytes.byteLength);
|
||||
}
|
||||
|
||||
function assertPinnedMetadata(
|
||||
metadata: Awaited<ReturnType<Awaited<ReturnType<typeof open>>["stat"]>>,
|
||||
identity: OwnedIdentity,
|
||||
mode: number,
|
||||
size: number,
|
||||
): void {
|
||||
if (
|
||||
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
|
||||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
|
||||
(Number(metadata.mode) & 0o777) !== mode || Number(metadata.size) !== size
|
||||
) {
|
||||
throw new TypeError("provider guardian sealed temp identity changed");
|
||||
}
|
||||
}
|
||||
|
||||
function guardianCompletion(child: ReturnType<typeof spawn>): Promise<GuardianResult> {
|
||||
return new Promise((resolve) => {
|
||||
child.once("error", (error) => resolve({ code: null, error, signal: null }));
|
||||
child.once("close", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForFrame(
|
||||
stdout: NodeJS.ReadableStream,
|
||||
completion: Promise<GuardianResult>,
|
||||
label: string,
|
||||
): Promise<Buffer> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
let pending = Buffer.alloc(0);
|
||||
const response = new Promise<Buffer>((resolve, reject) => {
|
||||
const onData = (chunk: Buffer | string): void => {
|
||||
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
||||
if (pending.byteLength > MAX_CONTROL_OUTPUT_BYTES) {
|
||||
reject(new Error(`provider guardian ${label} output exceeded its bound`));
|
||||
return;
|
||||
}
|
||||
if (pending.byteLength < 4) return;
|
||||
const payloadBytes = pending.readUInt32BE(0);
|
||||
if (payloadBytes <= 0 || payloadBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
|
||||
reject(new Error(`provider guardian ${label} frame length is invalid`));
|
||||
return;
|
||||
}
|
||||
if (pending.byteLength < payloadBytes + 4) return;
|
||||
if (pending.byteLength !== payloadBytes + 4) {
|
||||
reject(new Error(`provider guardian ${label} output has trailing bytes`));
|
||||
return;
|
||||
}
|
||||
resolve(pending.subarray(4));
|
||||
};
|
||||
stdout.on("data", onData);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([
|
||||
response,
|
||||
completion.then((result) => { throw guardianCloseError(result, Buffer.alloc(0)); }),
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error(`provider guardian ${label} timed out`)),
|
||||
RESPONSE_TIMEOUT_MS,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
stdout.removeAllListeners("data");
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForClose(
|
||||
completion: Promise<GuardianResult>,
|
||||
child: ReturnType<typeof spawn>,
|
||||
): Promise<GuardianResult> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
completion,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error("provider guardian did not close within its bound"));
|
||||
}, CLOSE_TIMEOUT_MS);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
type FallbackIdentity = Readonly<{
|
||||
rawStagingPath: string;
|
||||
rawPath: string;
|
||||
rawIdentity: OwnedIdentity;
|
||||
sealedPath: string;
|
||||
sealedTempPath: string;
|
||||
sealedIdentity: OwnedIdentity;
|
||||
}>;
|
||||
|
||||
async function cleanupFallback(input: FallbackIdentity): Promise<void> {
|
||||
const failures: Error[] = [];
|
||||
for (const target of [
|
||||
{ path: input.rawStagingPath, identity: input.rawIdentity },
|
||||
{ path: input.rawPath, identity: input.rawIdentity },
|
||||
{ path: input.sealedTempPath, identity: input.sealedIdentity },
|
||||
{ path: input.sealedPath, identity: input.sealedIdentity },
|
||||
]) {
|
||||
try {
|
||||
await cleanupOwnedProviderReport({
|
||||
reportPath: target.path,
|
||||
reportDev: target.identity.dev,
|
||||
reportIno: target.identity.ino,
|
||||
});
|
||||
} catch (error) {
|
||||
failures.push(toError(error));
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new AggregateError(failures, "provider guardian fallback cleanup failed", {
|
||||
cause: failures[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupFallbackCloseAndThrow(
|
||||
fallback: FallbackIdentity,
|
||||
recovery: RecoveryAuthority,
|
||||
primaryError: unknown,
|
||||
): Promise<never> {
|
||||
const failures = [toError(primaryError)];
|
||||
try {
|
||||
await cleanupFallback(fallback);
|
||||
} catch (cleanupError) {
|
||||
failures.push(toError(cleanupError));
|
||||
}
|
||||
try {
|
||||
await closeRecoveryAuthority(recovery);
|
||||
} catch (closeError) {
|
||||
failures.push(toError(closeError));
|
||||
}
|
||||
if (failures.length > 1) {
|
||||
throw new AggregateError(failures, "provider guardian failure and recovery failed", {
|
||||
cause: failures[0],
|
||||
});
|
||||
}
|
||||
throw failures[0]!;
|
||||
}
|
||||
|
||||
async function cleanupFallbackAndClose(
|
||||
fallback: FallbackIdentity,
|
||||
recovery: RecoveryAuthority,
|
||||
): Promise<void> {
|
||||
const failures: Error[] = [];
|
||||
try {
|
||||
await cleanupFallback(fallback);
|
||||
} catch (cleanupError) {
|
||||
failures.push(toError(cleanupError));
|
||||
}
|
||||
try {
|
||||
await closeRecoveryAuthority(recovery);
|
||||
} catch (closeError) {
|
||||
failures.push(toError(closeError));
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new AggregateError(failures, "provider guardian abort recovery failed", {
|
||||
cause: failures[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function openRecoveryAuthority(input: Readonly<{
|
||||
rawDirectory: string;
|
||||
evidenceRoot: string;
|
||||
rawLeaf: string;
|
||||
rawStagingLeaf: string;
|
||||
sealedLeaf: string;
|
||||
sealedTempLeaf: string;
|
||||
}>): Promise<RecoveryAuthority> {
|
||||
let rawDirectoryHandle: FileHandle | undefined;
|
||||
let evidenceDirectoryHandle: FileHandle | undefined;
|
||||
let rawStagingHandle: FileHandle | undefined;
|
||||
let sealedTempHandle: FileHandle | undefined;
|
||||
let rawIdentity: OwnedIdentity | undefined;
|
||||
let sealedIdentity: OwnedIdentity | undefined;
|
||||
let rawStagingPinnedPath: string | undefined;
|
||||
let rawPinnedPath: string | undefined;
|
||||
let sealedTempPinnedPath: string | undefined;
|
||||
let sealedPinnedPath: string | undefined;
|
||||
try {
|
||||
rawDirectoryHandle = await open(
|
||||
input.rawDirectory,
|
||||
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
||||
);
|
||||
await assertPinnedDirectory(rawDirectoryHandle, input.rawDirectory, "raw");
|
||||
evidenceDirectoryHandle = await open(
|
||||
input.evidenceRoot,
|
||||
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
||||
);
|
||||
await assertPinnedDirectory(evidenceDirectoryHandle, input.evidenceRoot, "evidence");
|
||||
rawStagingPinnedPath =
|
||||
`/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawStagingLeaf}`;
|
||||
rawPinnedPath = `/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawLeaf}`;
|
||||
sealedTempPinnedPath =
|
||||
`/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedTempLeaf}`;
|
||||
sealedPinnedPath = `/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedLeaf}`;
|
||||
rawStagingHandle = await open(
|
||||
rawStagingPinnedPath,
|
||||
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
const rawMetadata = await rawStagingHandle.stat();
|
||||
rawIdentity = Object.freeze({ dev: rawMetadata.dev, ino: rawMetadata.ino });
|
||||
assertAllocatedPrivateMetadata(rawMetadata, rawIdentity, "raw staging");
|
||||
await assertPinnedLeafIdentity(rawStagingPinnedPath, rawIdentity, 0o600);
|
||||
sealedTempHandle = await open(
|
||||
sealedTempPinnedPath,
|
||||
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
const sealedMetadata = await sealedTempHandle.stat();
|
||||
sealedIdentity = Object.freeze({ dev: sealedMetadata.dev, ino: sealedMetadata.ino });
|
||||
assertAllocatedPrivateMetadata(sealedMetadata, sealedIdentity, "sealed temp");
|
||||
await assertPinnedLeafIdentity(sealedTempPinnedPath, sealedIdentity, 0o600);
|
||||
return Object.freeze({
|
||||
rawDirectoryHandle,
|
||||
evidenceDirectoryHandle,
|
||||
rawStagingHandle,
|
||||
sealedTempHandle,
|
||||
rawIdentity,
|
||||
sealedIdentity,
|
||||
rawStagingPinnedPath,
|
||||
rawPinnedPath,
|
||||
sealedTempPinnedPath,
|
||||
sealedPinnedPath,
|
||||
});
|
||||
} catch (error) {
|
||||
const failures = [toError(error)];
|
||||
for (const target of [
|
||||
{ path: rawStagingPinnedPath, identity: rawIdentity },
|
||||
{ path: rawPinnedPath, identity: rawIdentity },
|
||||
{ path: sealedTempPinnedPath, identity: sealedIdentity },
|
||||
{ path: sealedPinnedPath, identity: sealedIdentity },
|
||||
]) {
|
||||
if (!target.path || !target.identity) continue;
|
||||
try {
|
||||
await cleanupOwnedProviderReport({
|
||||
reportPath: target.path,
|
||||
reportDev: target.identity.dev,
|
||||
reportIno: target.identity.ino,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
failures.push(toError(cleanupError));
|
||||
}
|
||||
}
|
||||
for (const handle of [
|
||||
sealedTempHandle,
|
||||
rawStagingHandle,
|
||||
evidenceDirectoryHandle,
|
||||
rawDirectoryHandle,
|
||||
]) {
|
||||
if (!handle) continue;
|
||||
try { await handle.close(); } catch (closeError) { failures.push(toError(closeError)); }
|
||||
}
|
||||
if (failures.length > 1) {
|
||||
throw new AggregateError(failures, "provider guardian recovery setup failed", {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
throw failures[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
function assertAllocatedPrivateMetadata(
|
||||
metadata: Awaited<ReturnType<FileHandle["stat"]>>,
|
||||
identity: OwnedIdentity,
|
||||
label: string,
|
||||
): void {
|
||||
if (
|
||||
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
|
||||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
|
||||
(Number(metadata.mode) & 0o777) !== 0o600 || Number(metadata.size) !== 0
|
||||
) {
|
||||
throw new TypeError(`provider guardian ${label} allocation is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertPinnedDirectory(
|
||||
handle: FileHandle,
|
||||
canonicalPath: string,
|
||||
label: string,
|
||||
): Promise<void> {
|
||||
const [descriptorMetadata, pathMetadata] = await Promise.all([
|
||||
handle.stat(),
|
||||
lstat(canonicalPath),
|
||||
]);
|
||||
if (
|
||||
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
|
||||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
|
||||
descriptorMetadata.ino !== pathMetadata.ino
|
||||
) {
|
||||
throw new TypeError(`provider guardian ${label} recovery directory identity changed`);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertRecoveryLeavesMissing(recovery: RecoveryAuthority): Promise<void> {
|
||||
await assertPinnedLeafMissing(recovery.rawPinnedPath);
|
||||
await assertPinnedLeafMissing(recovery.sealedPinnedPath);
|
||||
assertAllocatedPrivateMetadata(
|
||||
await recovery.rawStagingHandle.stat(),
|
||||
recovery.rawIdentity,
|
||||
"raw staging",
|
||||
);
|
||||
assertAllocatedPrivateMetadata(
|
||||
await recovery.sealedTempHandle.stat(),
|
||||
recovery.sealedIdentity,
|
||||
"sealed temp",
|
||||
);
|
||||
await assertPinnedLeafIdentity(
|
||||
recovery.rawStagingPinnedPath,
|
||||
recovery.rawIdentity,
|
||||
0o600,
|
||||
);
|
||||
await assertPinnedLeafIdentity(
|
||||
recovery.sealedTempPinnedPath,
|
||||
recovery.sealedIdentity,
|
||||
0o600,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertPinnedLeafMissing(target: string): Promise<void> {
|
||||
try {
|
||||
await lstat(target);
|
||||
throw new Error("provider guardian transaction leaf already exists");
|
||||
} catch (error) {
|
||||
if (!hasErrorCode(error, "ENOENT")) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertPinnedLeafIdentity(
|
||||
target: string,
|
||||
identity: OwnedIdentity,
|
||||
mode: number,
|
||||
): Promise<void> {
|
||||
const metadata = await lstat(target);
|
||||
if (
|
||||
!metadata.isFile() || metadata.isSymbolicLink() || metadata.dev !== identity.dev ||
|
||||
metadata.ino !== identity.ino || metadata.nlink !== 1 ||
|
||||
(metadata.mode & 0o777) !== mode || metadata.size !== 0
|
||||
) {
|
||||
throw new TypeError("provider guardian READY identity changed");
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupStartupRecovery(recovery: RecoveryAuthority): Promise<void> {
|
||||
await cleanupFallback({
|
||||
rawStagingPath: recovery.rawStagingPinnedPath,
|
||||
rawPath: recovery.rawPinnedPath,
|
||||
rawIdentity: recovery.rawIdentity,
|
||||
sealedTempPath: recovery.sealedTempPinnedPath,
|
||||
sealedPath: recovery.sealedPinnedPath,
|
||||
sealedIdentity: recovery.sealedIdentity,
|
||||
});
|
||||
}
|
||||
|
||||
async function closeRecoveryAuthority(recovery: RecoveryAuthority): Promise<void> {
|
||||
const failures: Error[] = [];
|
||||
for (const handle of [
|
||||
recovery.rawStagingHandle,
|
||||
recovery.sealedTempHandle,
|
||||
recovery.rawDirectoryHandle,
|
||||
recovery.evidenceDirectoryHandle,
|
||||
]) {
|
||||
try { await handle.close(); } catch (error) { failures.push(toError(error)); }
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new AggregateError(failures, "provider guardian recovery directory close failed", {
|
||||
cause: failures[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function closeRecoveryAndThrow(
|
||||
recovery: RecoveryAuthority,
|
||||
primaryError: unknown,
|
||||
): Promise<never> {
|
||||
const failures = [toError(primaryError)];
|
||||
try {
|
||||
await cleanupStartupRecovery(recovery);
|
||||
} catch (cleanupError) {
|
||||
failures.push(toError(cleanupError));
|
||||
}
|
||||
try {
|
||||
await closeRecoveryAuthority(recovery);
|
||||
} catch (closeError) {
|
||||
failures.push(toError(closeError));
|
||||
}
|
||||
if (failures.length > 1) {
|
||||
throw new AggregateError(failures,
|
||||
"provider guardian failure and recovery close failed", { cause: failures[0] });
|
||||
}
|
||||
throw failures[0]!;
|
||||
}
|
||||
|
||||
function hasErrorCode(error: unknown, code: string): boolean {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
||||
}
|
||||
|
||||
function guardianCloseError(result: GuardianResult, stderr: Buffer): Error {
|
||||
if (result.error) return result.error;
|
||||
const detail = stderr.toString("utf8").trim();
|
||||
return new Error(
|
||||
`provider guardian failed: exit=${result.code ?? "none"}, signal=${result.signal ?? "none"}${detail ? `, output=${detail}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
function toError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
Reference in New Issue
Block a user