fix(ghidra): preserve catalog and metadata admission failures
This commit is contained in:
1 parent
324ca9453e
commit
3e112595da
5 files changed
+105
-12
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user