fix(kuna): bind version probes to stable executable identity

Compare executable fingerprints before and after the owned version probe, and capture the script prefix from the final hash stream. Preserve partial facts and failed-close ownership without claiming process interruption from a snapshot.

Closes #5611
This commit is contained in:
morluto committed 2026-10-21 00:52:53 +08:00
1 parent 5b676ba9f3
commit 04ae07ab8c
9 files changed
+661 -142

No files matched your search

+51 -2
View File
@@ -19,6 +19,20 @@ export interface StableFileHashOptions {
readonly retainFailedClose: (owner: OwnedFileHandle) => void;
}
export interface StableFileObservation {
readonly sha256: string;
readonly bytes: number;
/** The first two bytes, read from the same descriptor and stream as the hash. */
readonly prefix: Buffer;
}
const STABLE_FILE_PREFIX_BYTES = 2;
interface StableFileFingerprint {
readonly sha256: string;
readonly bytes: number;
}
/** Physical hash admission/read failure with no outstanding descriptor cleanup. */
export class StableFileHashFailure extends Error {
readonly _tag = "StableFileHashFailure";
@@ -43,6 +57,30 @@ export const hashStableFile = async (
path: string,
options: StableFileHashOptions,
): Promise<{ readonly sha256: string; readonly bytes: number }> => {
return await readStableFile(path, options, false);
};
/** Hash a stable file and capture exactly its first two bytes from that read. */
export const observeStableFile = (
path: string,
options: StableFileHashOptions,
): Promise<StableFileObservation> => readStableFile(path, options, true);
function readStableFile(
path: string,
options: StableFileHashOptions,
includePrefix: false,
): Promise<StableFileFingerprint>;
function readStableFile(
path: string,
options: StableFileHashOptions,
includePrefix: true,
): Promise<StableFileObservation>;
async function readStableFile(
path: string,
options: StableFileHashOptions,
includePrefix: boolean,
): Promise<StableFileFingerprint | StableFileObservation> {
try {
return await withOwnedRegularFile(path, options, async (handle) => {
const initial = await handle.stat();
@@ -52,6 +90,9 @@ export const hashStableFile = async (
);
const hash = createHash("sha256");
const chunk = Buffer.allocUnsafe(64 * 1024);
const prefix = Buffer.alloc(
includePrefix ? Math.min(STABLE_FILE_PREFIX_BYTES, initial.size) : 0,
);
let position = 0;
while (position <= initial.size) {
assertRegularFileActive(options.signal);
@@ -65,6 +106,13 @@ export const hashStableFile = async (
throw new RegularFileChangedError(path);
assertRegularFileActive(options.signal);
if (read.bytesRead === 0) break;
if (position < prefix.length) {
const prefixBytes = Math.min(
read.bytesRead,
prefix.length - position,
);
chunk.copy(prefix, position, 0, prefixBytes);
}
hash.update(chunk.subarray(0, read.bytesRead));
position += read.bytesRead;
}
@@ -80,7 +128,8 @@ export const hashStableFile = async (
)
throw new RegularFileChangedError(path);
assertRegularFileActive(options.signal);
return { sha256: hash.digest("hex"), bytes: position };
const fingerprint = { sha256: hash.digest("hex"), bytes: position };
return includePrefix ? { ...fingerprint, prefix } : fingerprint;
});
} catch (cause: unknown) {
if (
@@ -91,4 +140,4 @@ export const hashStableFile = async (
throw cause;
throw new StableFileHashFailure(path, cause);
}
};
}
+197 -69
View File
@@ -17,6 +17,7 @@ import {
} from "../artifacts/AnalysisCleanupLease.js";
import {
hashStableFile,
observeStableFile,
stableFileHashCause,
} from "../filesystem/StableFileHash.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
@@ -24,7 +25,6 @@ import {
RegularFileCleanupFailure,
RegularFileCancelled,
regularFileCancellation,
withOwnedRegularFile,
} from "../filesystem/OwnedRegularFile.js";
import {
OwnedCommandFailure,
@@ -251,6 +251,103 @@ const failure = (
const message = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);
interface InstallationFileObservation {
readonly phase: "fingerprint" | "identity";
readonly completedHash?: { readonly sha256: string; readonly bytes: number };
readonly script?: boolean;
readonly beforeProbeFingerprint?: {
readonly sha256: string;
readonly bytes: number;
};
readonly probe?: {
readonly deadlineMs: number;
readonly output: ReturnType<typeof versionProbeOutput>;
};
}
/** Keep completed file facts attached when cancellation or close failure interrupts inspection. */
const recordPartialInstallationFileOperation = (
cause: unknown,
facts: {
readonly command: string;
readonly executable: string;
readonly version?: string;
readonly observed: InstallationFileObservation;
},
): boolean => {
const cancellation = regularFileCancellation(cause);
if (
!(cause instanceof Error) ||
(!(cause instanceof RegularFileCleanupFailure) &&
cancellation === undefined)
)
return false;
const outcome =
cause instanceof RegularFileCancelled ||
cause instanceof RegularFileCleanupFailure
? cause.outcome
: undefined;
const completed = outcome?.kind === "completed" ? outcome.value : undefined;
const completedRecord =
completed !== null &&
typeof completed === "object" &&
!Array.isArray(completed)
? (completed as Record<string, unknown>)
: undefined;
const completedFingerprint =
facts.observed.phase === "fingerprint"
? completed
: completedRecord === undefined
? undefined
: {
sha256: completedRecord["sha256"],
bytes: completedRecord["bytes"],
};
const completedHash =
facts.observed.completedHash ??
(outcome?.kind === "completed" && completedFingerprint !== undefined
? jsonValueSchema.parse(completedFingerprint)
: undefined);
const completedScript =
facts.observed.script ??
(facts.observed.phase === "identity" &&
Buffer.isBuffer(completedRecord?.["prefix"])
? completedRecord["prefix"].toString("latin1") === "#!"
: undefined);
installationFailureDiagnostics.set(cause, {
kuna_installation: {
status: "partial",
command: facts.command,
executable: facts.executable,
...(facts.version === undefined
? {}
: {
version: facts.version,
verified: facts.version === KUNA_VERIFIED_VERSION,
}),
...(completedHash === undefined ? {} : { completed_hash: completedHash }),
...(completedScript === undefined ? {} : { script: completedScript }),
...(facts.observed.beforeProbeFingerprint === undefined
? {}
: {
before_probe_sha256: facts.observed.beforeProbeFingerprint.sha256,
before_probe_bytes: facts.observed.beforeProbeFingerprint.bytes,
}),
},
...(facts.observed.probe === undefined
? {}
: {
kuna_version_probe: {
status: "completed",
deadline_ms: facts.observed.probe.deadlineMs,
...versionProbeOutputDiagnostics(facts.observed.probe.output),
},
}),
});
return true;
};
/** Resolve, version-probe and fingerprint the configured executable. */
export const inspectKunaInstallation = async (
environment: Readonly<NodeJS.ProcessEnv>,
@@ -287,6 +384,38 @@ export const inspectKunaInstallation = async (
assertRegularFileActive(options.signal);
let beforeProbeFingerprint: {
readonly sha256: string;
readonly bytes: number;
};
const beforeProbeObservation: InstallationFileObservation = {
phase: "fingerprint",
};
try {
beforeProbeFingerprint = await options.resourceScope.run(() =>
hashStableFile(executable, {
...(options.signal === undefined ? {} : { signal: options.signal }),
retainFailedClose: (owner) =>
options.resourceScope.retain(owner, executable),
}),
);
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure) throw cause;
if (
recordPartialInstallationFileOperation(cause, {
command,
executable,
observed: beforeProbeObservation,
})
)
throw cause;
return failure(
"executable_missing",
`Cannot fingerprint ${executable}: ${message(stableFileHashCause(cause))}`,
{ command, executable },
);
}
const deadline = options.probeTimeoutMs ?? VERSION_PROBE_TIMEOUT_MS;
let snapshot: ProviderProcessSnapshot;
try {
@@ -335,7 +464,6 @@ export const inspectKunaInstallation = async (
deadline_ms: deadline,
...versionProbeOutputDiagnostics(output),
probe_failure_reason: cause.reason,
stop_requested: cause.snapshot !== null,
cleanup_incomplete: cleanupIncomplete,
},
});
@@ -348,7 +476,6 @@ export const inspectKunaInstallation = async (
command,
executable,
probe_failure_reason: cause.reason,
stop_requested: cause.snapshot !== null,
cleanup_incomplete: cleanupIncomplete,
deadline_ms: deadline,
...versionProbeOutputDiagnostics(output),
@@ -392,74 +519,94 @@ export const inspectKunaInstallation = async (
let fingerprint: { readonly sha256: string; readonly bytes: number };
let script: boolean;
const observed: {
phase: "fingerprint" | "shebang";
completed_hash?: { readonly sha256: string; readonly bytes: number };
phase: "fingerprint" | "identity";
completedHash?: { readonly sha256: string; readonly bytes: number };
script?: boolean;
} = { phase: "fingerprint" };
beforeProbeFingerprint?: {
readonly sha256: string;
readonly bytes: number;
};
probe?: {
readonly deadlineMs: number;
readonly output: ReturnType<typeof versionProbeOutput>;
};
} = {
phase: "fingerprint",
beforeProbeFingerprint,
probe: { deadlineMs: deadline, output: probeOutput },
};
try {
({ fingerprint, script } = await options.resourceScope.run(async () => {
const fingerprint = await hashStableFile(executable, {
observed.phase = "identity";
const observation = await observeStableFile(executable, {
...(options.signal === undefined ? {} : { signal: options.signal }),
retainFailedClose: (owner) =>
options.resourceScope.retain(owner, executable),
});
observed.completed_hash = fingerprint;
assertRegularFileActive(options.signal);
observed.phase = "shebang";
const script = await startsWithShebang(
executable,
options.resourceScope,
options.signal,
);
const fingerprint = {
sha256: observation.sha256,
bytes: observation.bytes,
};
const script =
observation.prefix.length === 2 &&
observation.prefix.toString("latin1") === "#!";
observed.completedHash = fingerprint;
observed.script = script;
assertRegularFileActive(options.signal);
return { fingerprint, script };
}));
} catch (cause: unknown) {
const cancellation = regularFileCancellation(cause);
if (cause instanceof ArtifactReaderFailure) throw cause;
if (
cause instanceof Error &&
(cause instanceof RegularFileCleanupFailure || cancellation !== undefined)
recordPartialInstallationFileOperation(cause, {
command,
executable,
version,
observed,
})
) {
const outcome =
cause instanceof RegularFileCancelled ||
cause instanceof RegularFileCleanupFailure
? cause.outcome
: undefined;
const completed =
outcome?.kind === "completed" ? outcome.value : undefined;
const completedHash =
observed.completed_hash ??
(observed.phase === "fingerprint" && outcome?.kind === "completed"
? jsonValueSchema.parse(completed)
: undefined);
const completedScript =
observed.script ??
(observed.phase === "shebang" && typeof completed === "boolean"
? completed
: undefined);
installationFailureDiagnostics.set(cause, {
kuna_installation: {
status: "partial",
command,
executable,
version,
verified: version === KUNA_VERIFIED_VERSION,
...(completedHash === undefined
? {}
: { completed_hash: completedHash }),
...(completedScript === undefined ? {} : { script: completedScript }),
},
});
throw cause;
}
return failure(
"executable_missing",
`Cannot fingerprint ${executable}: ${message(stableFileHashCause(cause))}`,
{ command, executable },
`Cannot observe ${executable} after its --version probe: ${message(stableFileHashCause(cause))}`,
{
command,
executable,
version,
verified: version === KUNA_VERIFIED_VERSION,
identity_observation_phase: "post_probe",
identity_observation_failure: message(stableFileHashCause(cause)),
before_probe_sha256: beforeProbeFingerprint.sha256,
before_probe_bytes: beforeProbeFingerprint.bytes,
deadline_ms: deadline,
...versionProbeOutputDiagnostics(probeOutput),
},
);
}
if (
beforeProbeFingerprint.sha256 !== fingerprint.sha256 ||
beforeProbeFingerprint.bytes !== fingerprint.bytes
)
return failure(
"version_unresolved",
`${executable} changed during the --version probe. ${KUNA_VERSION_PROBE_REMEDIATION}`,
{
command,
executable,
version,
verified: version === KUNA_VERIFIED_VERSION,
installation_identity_changed: true,
before_sha256: beforeProbeFingerprint.sha256,
before_bytes: beforeProbeFingerprint.bytes,
after_sha256: fingerprint.sha256,
after_bytes: fingerprint.bytes,
deadline_ms: deadline,
...versionProbeOutputDiagnostics(probeOutput),
},
);
return ok({
configuration: configuration.value,
executable,
@@ -469,22 +616,3 @@ export const inspectKunaInstallation = async (
script,
});
};
const startsWithShebang = (
path: string,
resourceScope: RetainedResourceScope,
signal?: AbortSignal,
): Promise<boolean> =>
withOwnedRegularFile(
path,
{
symlinks: "follow",
...(signal === undefined ? {} : { signal }),
retainFailedClose: (owner) => resourceScope.retain(owner, path),
},
async (handle) => {
const prefix = Buffer.alloc(2);
const { bytesRead } = await handle.read(prefix, 0, 2, 0);
return bytesRead === 2 && prefix.toString("latin1") === "#!";
},
);
@@ -106,7 +106,6 @@ process.exit(0);
code: "version_unresolved",
details: expect.objectContaining({
probe_failure_reason: "timeout",
stop_requested: true,
exit_code: 0,
signal: null,
stdout: "kuna 1.833\n",
@@ -0,0 +1,95 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest
.skipIf(process.platform === "win32")
.for(["stable", "replaced"] as const)(
"doctor binds Kuna version output to a %s executable identity",
async (identity, { cli }) => {
const root = await createTestTempDirectory("rea-kuna-doctor-identity-");
const home = join(root, "home");
const executable = join(root, "kuna");
await mkdir(home);
const replacement = `#!${process.execPath}\nconsole.log("kuna 1.900");\n`;
const mutation =
identity === "replaced"
? `const fs = require("node:fs");
fs.writeFileSync(process.argv[1] + ".new", ${JSON.stringify(replacement)}, { mode: 0o700 });
fs.renameSync(process.argv[1] + ".new", process.argv[1]);`
: "";
const source = `#!${process.execPath}
console.log("kuna 1.833");
${mutation}
`;
await writeFile(executable, source, { mode: 0o700 });
const beforeHash = createHash("sha256").update(source).digest("hex");
const result = await cli.run({
arguments: ["doctor", "--provider", "kuna", "--json"],
environment: {
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(root, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(root, "missing-ghidra"),
REA_KUNA_COMMAND: executable,
REA_KUNA_CACHE_DIR: join(root, "cache"),
REA_LOG_LEVEL: "silent",
},
timeoutMs: 20_000,
});
const after = await readFile(executable);
const afterHash = createHash("sha256").update(after).digest("hex");
if (identity === "stable") {
expect(afterHash).toBe(beforeHash);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.json).toMatchObject({
providerInspections: expect.arrayContaining([
expect.objectContaining({
id: "kuna",
available: true,
providerVersion: "1.833",
checks: expect.arrayContaining([
expect.objectContaining({ name: "installation", ok: true }),
]),
}),
]),
});
} else {
expect(afterHash).not.toBe(beforeHash);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
providerInspections: expect.arrayContaining([
expect.objectContaining({
id: "kuna",
available: false,
providerVersion: null,
checks: expect.arrayContaining([
expect.objectContaining({
name: "installation",
ok: false,
code: "version_unresolved",
details: expect.objectContaining({
installation_identity_changed: true,
version: "1.833",
before_sha256: beforeHash,
before_bytes: Buffer.byteLength(source),
after_sha256: afterHash,
after_bytes: after.byteLength,
stdout: "kuna 1.833\n",
exit_code: 0,
}),
}),
]),
}),
]),
});
}
},
);
@@ -8,7 +8,10 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import * as regularFile from "../../../src/filesystem/RegularFile.js";
import { RegularFileAdmissionFailure } from "../../../src/filesystem/RegularFile.js";
import { hashStableFile } from "../../../src/filesystem/StableFileHash.js";
import {
hashStableFile,
observeStableFile,
} from "../../../src/filesystem/StableFileHash.js";
import { LinuxHopperPublisher } from "../../../src/application/LinuxHopperPublisher.js";
import {
RegularFileCancelled,
@@ -17,6 +20,53 @@ import {
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
import type { VerifiedLinuxHopperPackage } from "../../../src/application/LinuxHopperPublisher.js";
it("captures a two-byte prefix across short reads and buffer reuse", async () => {
const root = await createTestTempDirectory("rea-stable-observation-prefix-");
const path = join(root, "script");
const bytes = Buffer.from("#!rest-of-file");
await writeFile(path, bytes);
const retained: OwnedFileHandle[] = [];
const originalOpen = regularFile.openRegularFile;
const open = vi
.spyOn(regularFile, "openRegularFile")
.mockImplementation(async (...args) => {
const handle = await originalOpen(...args);
return {
get fd() {
return handle.fd;
},
stat: (...statArgs: Parameters<typeof handle.stat>) =>
handle.stat(...statArgs),
read: (
buffer: Buffer,
offset: number,
length: number,
position: number,
) =>
handle.read(
buffer,
offset,
position === 0 ? Math.min(length, 1) : length,
position,
),
close: () => handle.close(),
} as unknown as Awaited<ReturnType<typeof originalOpen>>;
});
try {
const observation = await observeStableFile(path, {
retainFailedClose: (owner) => retained.push(owner),
});
expect(observation).toMatchObject({
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
prefix: Buffer.from("#!"),
});
} finally {
open.mockRestore();
for (const owner of retained) if (owner.handle.fd >= 0) await owner.close();
}
});
it("adopts and retries the descriptor when hashing succeeded but close failed", async () => {
const root = await createTestTempDirectory("rea-stable-hash-close-");
const path = join(root, "input.bin");
@@ -18,23 +18,40 @@ import type { RetainedResourceScope } from "../../../../src/application/Analysis
const injected = vi.hoisted(() => ({
failNextClose: false,
failReadClose: true,
holdCloseFailures: false,
releaseCloseFailures: false,
closeCalls: 0,
failCloseReadStage: undefined as "fingerprint" | "shebang" | undefined,
failReadStage: undefined as "fingerprint" | "shebang" | undefined,
fingerprintReads: 0,
installationPath: undefined as string | undefined,
failCloseReadStage: undefined as
| "preProbeFingerprint"
| "fingerprint"
| "identity"
| undefined,
failReadStage: undefined as
| "preProbeFingerprint"
| "fingerprint"
| "identity"
| undefined,
abortOnReadFailure: undefined as (() => void) | undefined,
abortOnClose: undefined as
| { stage: "fingerprint" | "shebang"; abort: () => void }
| {
stage: "preProbeFingerprint" | "fingerprint" | "identity";
abort: () => void;
}
| undefined,
handles: [] as import("node:fs/promises").FileHandle[],
}));
afterEach(async () => {
injected.failNextClose = false;
injected.failReadClose = true;
injected.holdCloseFailures = false;
injected.releaseCloseFailures = false;
injected.closeCalls = 0;
injected.fingerprintReads = 0;
injected.installationPath = undefined;
injected.failCloseReadStage = undefined;
injected.failReadStage = undefined;
injected.abortOnReadFailure = undefined;
@@ -53,6 +70,7 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
openRegularFile: vi.fn(
async (...args: Parameters<typeof original.openRegularFile>) => {
const handle = await original.openRegularFile(...args);
const isInstallation = String(args[0]) === injected.installationPath;
injected.handles.push(handle);
const failNextClose = injected.failNextClose;
const failCloseReadStage = injected.failCloseReadStage;
@@ -70,6 +88,21 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
let readStageMatched = false;
let failedClose = false;
let abortCloseMatched = false;
let fingerprintStage:
| "preProbeFingerprint"
| "fingerprint"
| "identity"
| undefined;
const tracksFingerprint = [
failCloseReadStage,
failReadStage,
abortOnClose?.stage,
].some(
(stage) =>
stage === "preProbeFingerprint" ||
stage === "fingerprint" ||
stage === "identity",
);
return {
get fd() {
return handle.fd;
@@ -83,21 +116,33 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
position: number,
) => {
if (
(abortOnClose?.stage === "fingerprint" && length > 2) ||
(abortOnClose?.stage === "shebang" && length === 2)
)
abortCloseMatched = true;
if (
(failReadStage === "fingerprint" && length > 2) ||
(failReadStage === "shebang" && length === 2)
length > 2 &&
tracksFingerprint &&
fingerprintStage === undefined
) {
fingerprintStage = isInstallation
? injected.fingerprintReads === 0
? "preProbeFingerprint"
: "identity"
: "fingerprint";
if (isInstallation) injected.fingerprintReads += 1;
}
const readStage =
length === 2
? undefined
: length > 2
? fingerprintStage
: undefined;
if (abortOnClose?.stage === readStage) abortCloseMatched = true;
if (failReadStage !== undefined && failReadStage === readStage) {
readStageMatched = true;
injected.abortOnReadFailure?.();
throw new Error("injected read fault");
}
if (failCloseReadStage === "fingerprint" && length > 2)
readStageMatched = true;
if (failCloseReadStage === "shebang" && length === 2)
if (
failCloseReadStage !== undefined &&
failCloseReadStage === readStage
)
readStageMatched = true;
return handle.read(buffer, offset, length, position);
},
@@ -106,7 +151,7 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
if (abortCloseMatched) abortOnClose?.abort();
if (
(failNextClose && !failedClose) ||
(readStageMatched && !failedClose) ||
(readStageMatched && injected.failReadClose && !failedClose) ||
(injected.holdCloseFailures && !injected.releaseCloseFailures)
) {
failedClose = true;
@@ -125,12 +170,13 @@ import { composeBinarySession } from "../../../../src/application/binary/BinaryS
import { KunaTargetCommands } from "../../../../src/kuna/KunaTargetCommands.js";
import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js";
import { inspectKunaProvider } from "../../../../src/kuna/KunaDoctor.js";
import { kunaInstallationCancellation } from "../../../../src/kuna/KunaInstallation.js";
import { inspectKunaInstallationOwned } from "./kunaResourceScope.js";
import { fakeKunaCalls } from "./fakeKuna.js";
import { run, session } from "./kunaSessionFixture.js";
it.skipIf(process.platform === "win32").each(
(["fingerprint", "shebang"] as const).flatMap((stage) =>
(["identity"] as const).flatMap((stage) =>
(["health", "search_procedures"] as const).map((operation) => ({
stage,
operation,
@@ -142,6 +188,8 @@ it.skipIf(process.platform === "win32").each(
const fixture = await session("rea-kuna-installation-owner-resolution-");
const client = fixture.open();
injected.closeCalls = 0;
injected.fingerprintReads = 0;
injected.installationPath = fixture.kuna;
injected.failCloseReadStage = stage;
try {
const inspected = await client.execute(
@@ -174,7 +222,7 @@ it.skipIf(process.platform === "win32").each(
expect(producer.cause).toMatchObject({
name: "RegularFileCleanupFailure",
});
const failedCloseCall = stage === "fingerprint" ? 1 : 2;
const failedCloseCall = 2;
expect(injected.closeCalls).toBe(failedCloseCall);
expect(await client.close()).toEqual({ ok: true, value: null });
@@ -203,6 +251,7 @@ it.skipIf(process.platform === "win32")(
const commands = new KunaTargetCommands(fixture.target, {
signal: controller.signal,
});
injected.fingerprintReads = 0;
injected.failReadStage = "fingerprint";
injected.abortOnReadFailure = () => controller.abort();
let originatingError:
@@ -252,6 +301,7 @@ it.skipIf(process.platform === "win32")(
const cache = new KunaProjectCache(fixture.cacheRoot, {
maxBytes: 64 * 1024 ** 2,
});
injected.fingerprintReads = 0;
injected.failReadStage = "fingerprint";
try {
const result = await cache.build({
@@ -289,6 +339,7 @@ it.skipIf(process.platform === "win32")(
const commands = new KunaTargetCommands(fixture.target, {
signal: controller.signal,
});
injected.fingerprintReads = 0;
injected.failReadStage = "fingerprint";
try {
const result = await commands.run({
@@ -493,9 +544,7 @@ it.skipIf(process.platform === "win32")(
},
);
it
.skipIf(process.platform === "win32")
.each([{ stage: "fingerprint" }, { stage: "shebang" }] as const)(
it.skipIf(process.platform === "win32").each([{ stage: "identity" }] as const)(
"resolves the Kuna profile cleanup error after its $stage descriptor closes",
async ({ stage }) => {
const fixture = await session("rea-kuna-profile-owner-resolution-");
@@ -503,6 +552,8 @@ it
new AnalysisProviderRegistry([fixture.provider]),
);
injected.closeCalls = 0;
injected.fingerprintReads = 0;
injected.installationPath = fixture.kuna;
injected.failCloseReadStage = stage;
try {
const resolved = await binary.previewTarget(fixture.target);
@@ -518,7 +569,7 @@ it
expect(await binary.close()).toEqual({ ok: true, value: null });
expect(originatingError.cleanupIncomplete).toBe(false);
expect(injected.closeCalls).toBe(stage === "fingerprint" ? 2 : 3);
expect(injected.closeCalls).toBe(3);
expect(await binary.status()).toMatchObject({ cleanup_pending: false });
} finally {
injected.failCloseReadStage = undefined;
@@ -563,7 +614,7 @@ it.skipIf(process.platform === "win32")(
expect(injected.closeCalls).toBe(3);
expect(await binary.status()).toMatchObject({ cleanup_pending: false });
expect((await fakeKunaCalls(fixture.kuna)).map(({ argv }) => argv)).toEqual(
[["--version"], ["--version"]],
[["--version"]],
);
},
);
@@ -604,11 +655,115 @@ it.skipIf(process.platform === "win32")(
await resources.close();
}
expect((await fakeKunaCalls(fixture.kuna)).map(({ argv }) => argv)).toEqual(
[["--version"], ["--version"]],
[["--version"]],
);
},
);
it.skipIf(process.platform === "win32")(
"preserves the completed pre-probe fingerprint when cancellation arrives during its close",
async () => {
const fixture = await session("rea-kuna-pre-probe-fingerprint-cancel-");
const bytes = await readFile(fixture.kuna);
const fingerprint = {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
};
const beforeCalls = await fakeKunaCalls(fixture.kuna);
const controller = new AbortController();
const reason = Object.freeze({ request: "cancel before version probe" });
injected.fingerprintReads = 0;
injected.closeCalls = 0;
injected.installationPath = fixture.kuna;
injected.abortOnClose = {
stage: "preProbeFingerprint",
abort: () => controller.abort(reason),
};
try {
const cause: unknown = await inspectKunaInstallationOwned(
{
REA_KUNA_COMMAND: fixture.kuna,
REA_KUNA_CACHE_DIR: fixture.cacheRoot,
},
{ signal: controller.signal },
).then(
() => undefined,
(failure: unknown) => failure,
);
const cancellation = kunaInstallationCancellation(cause);
expect(cancellation?.reason).toBe(reason);
expect(cancellation?.diagnostics).toMatchObject({
kuna_installation: {
status: "partial",
command: fixture.kuna,
executable: fixture.kuna,
completed_hash: fingerprint,
},
});
expect(
cancellation?.diagnostics?.["kuna_installation"],
).not.toHaveProperty("version");
expect(injected.closeCalls).toBe(1);
expect(await fakeKunaCalls(fixture.kuna)).toEqual(beforeCalls);
} finally {
injected.abortOnClose = undefined;
controller.abort(reason);
}
},
);
it.skipIf(process.platform === "win32")(
"keeps a physical post-probe read failure primary with the version and prior fingerprint",
async () => {
const fixture = await session("rea-kuna-post-probe-read-failure-");
const bytes = await readFile(fixture.kuna);
const beforeProbeFingerprint = {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
};
const controller = new AbortController();
injected.fingerprintReads = 0;
injected.installationPath = fixture.kuna;
injected.failReadStage = "identity";
injected.failReadClose = false;
injected.abortOnReadFailure = () =>
controller.abort({ request: "abort after physical read failure" });
try {
const result = await inspectKunaInstallationOwned(
{
REA_KUNA_COMMAND: fixture.kuna,
REA_KUNA_CACHE_DIR: fixture.cacheRoot,
},
{ signal: controller.signal },
);
expect(controller.signal.aborted).toBe(true);
expect(result).toMatchObject({
ok: false,
error: {
code: "executable_missing",
reason: expect.stringContaining("injected read fault"),
diagnostics: {
version: "1.833",
verified: true,
identity_observation_phase: "post_probe",
identity_observation_failure: "injected read fault",
before_probe_sha256: beforeProbeFingerprint.sha256,
before_probe_bytes: beforeProbeFingerprint.bytes,
deadline_ms: 10_000,
stdout: "kuna 1.833\n",
stdout_bytes: 11,
output_truncated: false,
},
},
});
} finally {
injected.failReadStage = undefined;
injected.abortOnReadFailure = undefined;
injected.failReadClose = true;
}
},
);
it.skipIf(process.platform === "win32")(
"drains shared cache cleanup outside a retired client's target key",
async () => {
@@ -652,7 +807,7 @@ it.skipIf(process.platform === "win32")(
it.skipIf(process.platform === "win32").each(
(["doctor", "profile", "health"] as const).flatMap((consumer) =>
(["fingerprint", "shebang"] as const).flatMap((stage) =>
(["identity"] as const).flatMap((stage) =>
[false, true].map((closeRefusal) => ({
consumer,
stage,
@@ -688,6 +843,8 @@ it.skipIf(process.platform === "win32").each(
run: (operation) => resources.run(operation),
};
const client = consumer === "health" ? fixture.open() : undefined;
injected.fingerprintReads = 0;
injected.installationPath = fixture.kuna;
injected.abortOnClose = { stage, abort: () => controller.abort(reason) };
injected.failCloseReadStage = closeRefusal ? stage : undefined;
let failure: AnalysisError | undefined;
@@ -756,11 +913,9 @@ it.skipIf(process.platform === "win32").each(
executable,
version: "1.833",
completed_hash: fingerprint,
...(stage === "shebang" ? { script: true } : {}),
...(stage === "identity" ? { script: true } : {}),
},
});
if (stage === "fingerprint")
expect(diagnostics?.["kuna_installation"]).not.toHaveProperty("script");
expect(injected.handles.some((handle) => handle.fd >= 0)).toBe(
closeRefusal,
);
@@ -57,6 +57,48 @@ it("identifies a caller-selected kuna by version and digest", async () => {
]);
});
it("refuses to pair version output with executable bytes replaced during the probe", async () => {
const root = await createTestTempDirectory(
"rea-kuna-replaced-during-version-",
);
const kuna = join(root, "kuna");
const replacement = `#!${process.execPath}\nconsole.log("kuna 1.900");\n`;
const source = `#!${process.execPath}
const fs = require("node:fs");
const replacement = ${JSON.stringify(replacement)};
console.log("kuna 1.833");
fs.writeFileSync(process.argv[1] + ".new", replacement, { mode: 0o700 });
fs.renameSync(process.argv[1] + ".new", process.argv[1]);
`;
await writeFile(kuna, source, { mode: 0o700 });
await chmod(kuna, 0o700);
const before = await readFile(kuna);
const result = await inspectKunaInstallation({
REA_KUNA_COMMAND: kuna,
REA_KUNA_CACHE_DIR: join(root, "cache"),
});
const after = await readFile(kuna);
expect(createHash("sha256").update(after).digest("hex")).not.toBe(
createHash("sha256").update(before).digest("hex"),
);
expect(result).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
diagnostics: {
installation_identity_changed: true,
version: "1.833",
before_sha256: createHash("sha256").update(before).digest("hex"),
after_sha256: createHash("sha256").update(after).digest("hex"),
before_bytes: before.length,
after_bytes: after.length,
},
},
});
});
it("accepts another release but reports it as unverified", async () => {
const root = await createTestTempDirectory("rea-kuna-unverified-");
const kuna = await writeFakeKuna(root, {
@@ -99,7 +141,7 @@ it("reports a failed version probe with its exit status", async () => {
error: {
code: "version_unresolved",
reason: expect.stringContaining("version probe failed"),
diagnostics: { exit_code: 3, stop_requested: true },
diagnostics: { exit_code: 3, probe_failure_reason: "process" },
},
});
});
@@ -219,7 +261,10 @@ it("stops a version probe that ignores SIGTERM instead of waiting on it", async
ok: false,
error: {
code: "version_unresolved",
diagnostics: { stop_requested: true, deadline_ms: 4_000 },
diagnostics: {
probe_failure_reason: "timeout",
deadline_ms: 4_000,
},
},
});
// The owned process-group stop is bounded and must not wait on inherited output forever.
@@ -300,7 +345,10 @@ setTimeout(() => process.exit(9), 10000).unref();
ok: false,
error: {
code: "version_unresolved",
diagnostics: { deadline_ms: 1000, stop_requested: true },
diagnostics: {
deadline_ms: 1000,
probe_failure_reason: "timeout",
},
},
},
});
@@ -15,9 +15,11 @@ const gates = vi.hoisted(() => {
return {
createGate,
hashClose: createGate(),
shebangRead: createGate(),
identityRead: createGate(),
opens: 0,
failClose: false,
holdHashClose: false,
holdIdentityRead: false,
};
});
@@ -39,14 +41,14 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
stat: (...statArgs: Parameters<typeof handle.stat>) =>
handle.stat(...statArgs),
read: async (...readArgs: Parameters<typeof handle.read>) => {
if (index === 1) {
gates.shebangRead.enter();
await gates.shebangRead.hold;
if (index === 1 && gates.holdIdentityRead) {
gates.identityRead.enter();
await gates.identityRead.hold;
}
return handle.read(...readArgs);
},
close: async () => {
if (index === 0) {
if (index === 1 && gates.holdHashClose) {
gates.hashClose.enter();
await gates.hashClose.hold;
}
@@ -73,19 +75,21 @@ import { session } from "./kunaSessionFixture.js";
beforeEach(() => {
gates.opens = 0;
gates.failClose = false;
gates.holdHashClose = false;
gates.holdIdentityRead = false;
gates.hashClose = gates.createGate();
gates.shebangRead = gates.createGate();
gates.identityRead = gates.createGate();
});
afterEach(() => {
gates.failClose = false;
gates.hashClose.release();
gates.shebangRead.release();
gates.identityRead.release();
});
it
.skipIf(process.platform === "win32")
.each(["hash-close", "shebang-read"] as const)(
.each(["hash-close", "identity-read"] as const)(
"keeps installation cleanup waiting for its active phase when cancelled during %s",
async (phase) => {
const root = await createTestTempDirectory("rea-kuna-inspection-lifetime-");
@@ -101,6 +105,8 @@ it
run: (operation) => scope.run(operation),
};
const controller = new AbortController();
gates.holdHashClose = phase === "hash-close";
gates.holdIdentityRead = phase === "identity-read";
const inspection = inspectKunaInstallation(
{ REA_KUNA_COMMAND: executable },
{ resourceScope, signal: controller.signal },
@@ -112,7 +118,9 @@ it
);
let cleanup: Promise<void> | undefined;
try {
await gates.hashClose.started;
await (phase === "hash-close"
? gates.hashClose.started
: gates.identityRead.started);
cleanup = scope.retryCleanup();
let cleanupFinished = false;
void cleanup.then(() => {
@@ -120,23 +128,11 @@ it
});
await Promise.resolve();
expect(cleanupFinished).toBe(false);
if (phase === "hash-close") controller.abort();
gates.hashClose.release();
const reachedShebang = await Promise.race([
gates.shebangRead.started.then(() => true),
inspection.then(
() => false,
() => false,
),
]);
expect(reachedShebang).toBe(phase === "shebang-read");
if (phase === "shebang-read") {
controller.abort();
await Promise.resolve();
expect(cleanupFinished).toBe(false);
}
gates.shebangRead.release();
controller.abort();
if (phase === "hash-close") gates.hashClose.release();
else gates.identityRead.release();
await Promise.resolve();
expect(cleanupFinished).toBe(false);
const failure: unknown = await inspection.catch(
(cause: unknown) => cause,
);
@@ -144,11 +140,11 @@ it
expect(cancellation).toBeDefined();
expect(cancellation?.reason).toBe(controller.signal.reason);
await expect(cleanup).resolves.toBeUndefined();
expect(gates.opens).toBe(phase === "hash-close" ? 1 : 2);
expect(gates.opens).toBe(2);
expect(scope.hasPending).toBe(false);
} finally {
gates.hashClose.release();
gates.shebangRead.release();
gates.identityRead.release();
await inspectionOutcome;
await cleanup;
await scope.close();
@@ -158,15 +154,17 @@ it
it
.skipIf(process.platform === "win32")
.each(["hash-close", "shebang-read", "close-refusal"] as const)(
.each(["hash-close", "identity-read", "close-refusal"] as const)(
"joins session acquisition finalization and retains cleanup retry during %s cancellation",
async (phase) => {
gates.hashClose.release();
gates.shebangRead.release();
gates.identityRead.release();
const fixture = await session("rea-kuna-session-finalization-");
gates.opens = 0;
gates.hashClose = gates.createGate();
gates.shebangRead = gates.createGate();
gates.identityRead = gates.createGate();
gates.holdHashClose = phase !== "identity-read";
gates.holdIdentityRead = phase === "identity-read";
const client = fixture.open();
const controller = new AbortController();
const reason = { selected: "caller reason" };
@@ -177,18 +175,16 @@ it
finished = true;
});
try {
await gates.hashClose.started;
if (phase === "shebang-read") {
gates.hashClose.release();
await gates.shebangRead.started;
}
await (phase === "identity-read"
? gates.identityRead.started
: gates.hashClose.started);
controller.abort(reason);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(finished).toBe(false);
if (phase === "hash-close") replacement = client.execute("health", {});
gates.failClose = phase === "close-refusal";
gates.hashClose.release();
gates.shebangRead.release();
gates.identityRead.release();
const result = await pending;
if (result.ok) throw new Error("Expected interrupted installation");
if (phase === "close-refusal") {
@@ -211,7 +207,7 @@ it
} finally {
gates.failClose = false;
gates.hashClose.release();
gates.shebangRead.release();
gates.identityRead.release();
await pending;
await replacement;
expect((await client.close()).ok).toBe(true);
@@ -120,7 +120,7 @@ it.skipIf(process.platform === "win32")(
diagnostics: {
exit_code: 0,
signal: null,
stop_requested: true,
probe_failure_reason: "timeout",
stdout: "kuna 1.833\n",
stderr: "x".repeat(1024),
stdout_bytes: 11,
@@ -188,7 +188,6 @@ it.skipIf(process.platform === "win32")(
exit_code: 0,
signal: null,
probe_failure_reason: "process",
stop_requested: true,
cleanup_incomplete: true,
stdout: "kuna 1.833\n",
stderr: "x".repeat(1024),