fix(ghidra): preserve catalog and metadata admission failures

This commit is contained in:
morluto committed 2026-10-24 03:00:07 +08:00
1 parent 324ca9453e
commit 3e112595da
5 files changed
+105 -12

No files matched your search

+5 -1
View File
@@ -15,7 +15,11 @@ snapshot parser and a separate optional annotation extension. Check the
[release boundary](installation.md#released-package-and-main) when using npm.
See [import language and analysis seeds](installation.md#ghidra-import-language-and-analysis-seeds)
to override the header-detected language or seed analysis.
to override the header-detected language or seed analysis. An inaccessible language
catalog remains a read failure; it does not prove that a configured language is
undefined. Installation `application.properties` has a fixed 1 MiB read allowance
with no runtime override. The failure retains that allowance and directs recovery
at the selected installation metadata.
Provider selection and profile identity follow
[MCP contracts](mcp-contracts.md#identity-and-discovery). One deep
+26 -1
View File
@@ -2,11 +2,15 @@ import { createHash } from "node:crypto";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import {
artifactFileCompletion,
mapArtifactFileFailure,
ArtifactFileCleanupFailure,
artifactFilePrimaryFailure,
withArtifactRegularFile,
} from "../artifacts/ArtifactFileOperation.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { RegularFileCapacityError } from "../filesystem/readStableFileBytes.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import { fixedProducerInputAllowance } from "../domain/fixedAllowanceRemediation.js";
import { NonRegularFileReadError } from "../filesystem/RegularFile.js";
import { readStableFileBytes } from "../filesystem/readStableFileBytes.js";
import { ghidraApplicationProperties } from "./GhidraInstallationPolicy.js";
@@ -85,6 +89,27 @@ export const readGhidraInstallationProperties = async (
},
}),
};
throw ghidraFileFailure(cause, "ghidra:installation", diagnostics);
const qualified = mapArtifactFileFailure(cause, (primary) =>
primary instanceof RegularFileCapacityError
? new AnalysisResourceConstraintError(
"ghidra:installation",
"file-size",
primary.message,
{
maximum_bytes: primary.maximumBytes,
observed_bytes: primary.observedBytes.toString(),
},
{
cause: primary,
diagnostics,
...fixedProducerInputAllowance(
MAX_GHIDRA_INSTALLATION_PROPERTIES_BYTES,
"The Ghidra installation-properties read allowance",
),
},
)
: primary,
);
throw ghidraFileFailure(qualified, "ghidra:installation", diagnostics);
}
};
+13 -4
View File
@@ -58,8 +58,10 @@ const languageDirectories = async (installDir: string): Promise<string[]> => {
let modules: string[];
try {
modules = await readdir(join(installDir, root));
} catch {
continue;
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
continue;
throw cause;
}
for (const module of modules.sort())
directories.push(join(installDir, root, module, "data", "languages"));
@@ -111,8 +113,15 @@ export const readGhidraLanguageCatalog = (
let entries: string[];
try {
entries = await readdir(directory);
} catch {
continue;
} catch (cause: unknown) {
// A module may contain no language directory; inability to read one is not absence.
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
)
continue;
throw cause;
}
for (const entry of entries.sort()) {
if (!entry.endsWith(".ldefs")) continue;
+40 -1
View File
@@ -1,4 +1,11 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import {
chmod,
mkdir,
mkdtemp,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, parse, relative } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -402,3 +409,35 @@ describe("Ghidra language override profile", () => {
).toBe(false);
});
});
it
.skipIf(process.platform === "win32" || process.getuid?.() === 0)
.each(["Ghidra/Processors", "Ghidra/Processors/x86/data/languages"])(
"preserves a physical refusal reading the language catalog directory %s",
async (relativePath) => {
const root = await createTestTempDirectory(
"rea-ghidra-catalog-permission-",
);
const languages = join(root, "Ghidra/Processors/x86/data/languages");
await mkdir(languages, { recursive: true });
await writeFile(join(languages, "x86.ldefs"), X86_LDEFS);
const resources = new ArtifactResourceScope();
const blocked = join(root, relativePath);
try {
const original = await readGhidraLanguageCatalog(root, resources);
expect(original.length).toBeGreaterThan(0);
await chmod(blocked, 0);
await expect(readdir(blocked)).rejects.toMatchObject({ code: "EACCES" });
await expect(
readGhidraLanguageCatalog(root, resources),
).rejects.toMatchObject({ code: "EACCES", path: blocked });
await chmod(blocked, 0o755);
expect(await readGhidraLanguageCatalog(root, resources)).toEqual(
original,
);
} finally {
await chmod(blocked, 0o755);
await resources.close();
}
},
);
@@ -153,11 +153,15 @@ it("refuses an oversized descriptor before reading file bytes", async () => {
};
const resources = createInspectionResources();
try {
await expect(
readGhidraInstallationProperties(path, {
resourceOwner: resources.owner,
}),
).rejects.toMatchObject({
const failure = await readGhidraInstallationProperties(path, {
resourceOwner: resources.owner,
}).then(
() => {
throw new Error("Oversized properties accepted");
},
(cause) => cause as AnalysisError,
);
expect(failure).toMatchObject({
_tag: "AnalysisResourceConstraintError",
resource: "file-size",
retryable: false,
@@ -167,6 +171,18 @@ it("refuses an oversized descriptor before reading file bytes", async () => {
},
diagnostics: { path, stage: "installation_properties" },
});
expect(projectAnalysisError(failure)).toMatchObject({
code: "resource_constraint",
retryable: false,
remediation: {
action: expect.stringContaining(
"Ghidra installation-properties read allowance of 1048576 bytes (this release has no runtime override)",
),
},
});
expect(projectAnalysisError(failure).remediation.action).toContain(
"Increasing an evidence-reply file-size allowance does not admit this input",
);
expect(reads).toBe(0);
} finally {
await resources.close();