fix(providers): preserve bounded artifact reads and cleanup owners

Refs #4373, #4455, #4457, #4458.

Use canonical artifact ownership in Go, Rust, SQLite and PE readers and their failure consumers. Admit PE read budgets before allocation and ignore zero-extent sections in interval ownership. Close Rust resources through CLI and MCP lifecycles.
This commit is contained in:
morluto committed 2026-10-17 00:44:02 +08:00
1 parent 542fa84957
commit effb71f59c
23 files changed
+1068 -231

No files matched your search

+26 -12
View File
@@ -1,4 +1,9 @@
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
ArtifactFileCleanupFailure,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
AnalysisError,
@@ -49,25 +54,32 @@ export const sqliteDatabaseFailure = (
const operation = "inspect_sqlite_database";
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure) {
if (cause.cleanup !== undefined)
return new ProviderCleanupError(
if (cause.cleanup !== undefined) {
const physicalRead =
cause instanceof ArtifactFileCleanupFailure
? cause.fileCleanupFailure.outcome
: undefined;
const previous =
physicalRead?.kind === "failed"
? sqliteDatabaseFailure(physicalRead.cause, path, phase)
: signal?.aborted
? new AnalysisCancelledError(operation, { cause: signal.reason })
: physicalRead?.kind === "completed" || cause.cause === undefined
? undefined
: sqliteDatabaseFailure(cause.cause, path, phase);
const error = new ProviderCleanupError(
SQLITE_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{
reason: cause.cleanup.reason,
previous_error:
cause.cause === undefined
? null
: projectAnalysisError(
signal?.aborted
? new AnalysisCancelledError(operation, {
cause: cause.cause,
})
: sqliteDatabaseFailure(cause.cause, path, phase),
),
previous === undefined ? null : projectAnalysisError(previous),
},
{ operation, cause },
);
bindArtifactFileCleanup(cause, error);
return error;
}
if (cause.reason === "io" && cause.cause !== undefined)
return sqliteDatabaseFailure(cause.cause, path, phase, signal);
if (cause.reason === "integrity")
@@ -75,7 +87,9 @@ export const sqliteDatabaseFailure = (
cause,
});
if (cause.reason === "cancelled")
return new AnalysisCancelledError(operation, { cause });
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
return new AnalysisInputError(operation, { cause }, [
{
path: ["path"],
+47 -54
View File
@@ -9,7 +9,10 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import {
withArtifactFileHandle,
assertArtifactReadActive,
} from "../artifacts/ArtifactFileOperation.js";
import {
readStableArtifact,
type StableArtifactFileSystem,
@@ -180,61 +183,51 @@ const verifyInactiveJournal = async (
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
),
);
const owner = new OwnedFileHandle(file);
let failure: unknown;
try {
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed before open: ${path}`,
);
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
// 28-byte fixed header. A new active journal may zero only its magic bytes.
const header = Buffer.alloc(28);
let bytesRead = 0;
if (before.size > 0n) {
while (bytesRead < header.length) {
signal?.throwIfAborted();
const read = await file.read(
header,
bytesRead,
header.length - bytesRead,
bytesRead,
await withArtifactFileHandle(
path,
file,
resourceScope,
signal,
async (file) => {
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed before open: ${path}`,
);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
// TRUNCATE commits leave an empty file; PERSIST commits zero the complete
// 28-byte fixed header. A new active journal may zero only its magic bytes.
const header = Buffer.alloc(28);
let bytesRead = 0;
if (before.size > 0n) {
while (bytesRead < header.length) {
assertArtifactReadActive(signal);
const read = await file.read(
header,
bytesRead,
header.length - bytesRead,
bytesRead,
);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
}
}
}
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed during inspection: ${path}`,
);
if (
before.size !== 0n &&
(bytesRead !== header.length || header.some((byte) => byte !== 0))
)
throw new ArtifactReaderFailure(
phase === "initial" ? "format" : "integrity",
phase === "initial"
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
: `Rollback journal became active or malformed during database acquisition: ${path}`,
);
} catch (cause: unknown) {
failure = cause;
throw cause;
} finally {
const cleanup = await resourceScope.release({
kind: "file-handle",
resource: path,
handle: owner,
});
if (cleanup.kind === "failed")
throw ArtifactReaderFailure.withCleanup(
failure ?? cleanup.cause,
ArtifactReaderFailure.cleanupObservation(cleanup.cause, path),
);
}
if (!sameState(before, await file.stat({ bigint: true })))
throw new ArtifactReaderFailure(
"integrity",
`SQLite journal changed during inspection: ${path}`,
);
if (
before.size !== 0n &&
(bytesRead !== header.length || header.some((byte) => byte !== 0))
)
throw new ArtifactReaderFailure(
phase === "initial" ? "format" : "integrity",
phase === "initial"
? `Rollback journal may require recovery: ${path}. Supply a clean offline snapshot; only empty journals or a fully zeroed header are supported.`
: `Rollback journal became active or malformed during database acquisition: ${path}`,
);
},
);
};
// Absence is a caller value to correct, not malformed path or content.
+25 -3
View File
@@ -1,3 +1,8 @@
import {
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -176,13 +181,30 @@ export const executeSqliteDatabaseWorker = async (request: {
? { cleanup: cause.cleanup }
: {}),
};
if (request.signal?.aborted)
throw new AnalysisCancelledError(operation, options);
throw new AnalysisOutputError(
if (
cause instanceof ArtifactReaderFailure &&
(cause.reason === "cancelled" ||
(cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "completed" &&
request.signal?.aborted))
) {
const error = new AnalysisCancelledError(operation, {
...options,
cause:
cause.reason === "cancelled"
? artifactFileCancellationCause(cause)
: request.signal?.reason,
});
bindArtifactFileCleanup(cause, error);
throw error;
}
const error = new AnalysisOutputError(
operation,
`Owned SQLite worker reply is missing or malformed: ${cause instanceof Error ? cause.message : String(cause)}`,
options,
);
bindArtifactFileCleanup(cause, error);
throw error;
}
if (!reply.ok) throw rejectedReply(reply, capturedOutput);
return reply.inspection;