fix(firmware): accept the verified Binwalk and Unblob release lines (#844)
* fix(firmware): accept the verified Binwalk and Unblob release lines Exact --version equality rejected every build except Binwalk 3.1.0 and Unblob 26.6.4, then stored that constant as the observed engine version. Admit the 3.1 and 26.6 lines, keep the audited build as verified, and report the banner that actually ran. * chore(docs): regenerate skill metadata after firmware contract text docs:check failed because SKILL.md lagged the Binwalk/Unblob release-line tool descriptions. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
3f1a77f68e
commit
bef4321afb
|
|
@ -1,7 +1,10 @@
|
|||
# Firmware analysis
|
||||
|
||||
REA integrates **Binwalk 3.1.0** for region inspection and **Unblob 26.6.4** for
|
||||
explicit extraction. This first firmware milestone is verified on Linux x64.
|
||||
REA accepts **Binwalk 3.1.x** for region inspection and **Unblob 26.6.x** for
|
||||
explicit extraction. The report parsers are verified with Binwalk 3.1.0 and
|
||||
Unblob 26.6.4. Another build on those lines is accepted, the observed version
|
||||
is reported, and a limitation records that the parser was not verified against
|
||||
that build. This first firmware milestone is verified on Linux x64.
|
||||
No device, root privilege, mount, emulation or execution of extracted programs
|
||||
is needed. Arbitrary firmware bytes do not pass native executable admission;
|
||||
select a returned ELF separately for supported Ghidra analysis.
|
||||
|
|
@ -16,7 +19,8 @@ export REA_UNBLOB_COMMAND=/absolute/path/to/unblob
|
|||
```
|
||||
|
||||
REA does not install these tools, their dependencies or system packages. It
|
||||
checks the actual tool version when executing a request. Inspection and
|
||||
checks the tool's `--version` banner when executing a request. Binwalk must
|
||||
print `binwalk <version>` and Unblob must print the version alone. Inspection and
|
||||
extraction have independent prerequisites; one does not require the other.
|
||||
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
|
||||
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed.
|
||||
|
|
|
|||
|
|
@ -637,9 +637,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "569db1182f19869a3bdb22cf217bc75526f317b59635c8866d0e62741077cd52",
|
||||
"tools_sha256": "3d17936cd393a494987f7f52a133f23f9b582718d57719b2e06c678a7c5ca20a",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55",
|
||||
"combined_sha256": "de30f3145fb936a97fcb7bcc58ed9f20a974bae59b239dbf24664c145eff4275",
|
||||
"providers_sha256": "7949ddcf2eb5ba41b022bb3dfa1951ab96dbf5640285fa7fd5d35b0e4a5bdab3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,7 +399,9 @@ for the exact ABI, authority, graph and observation boundaries.
|
|||
`npm run fixtures:firmware` uses existing Python 3 and a host C compiler to make
|
||||
an ignored gzip/USTAR firmware fixture and independent offset/hash oracle.
|
||||
`npm run verify:firmware` requires caller-supplied Binwalk 3.1.0, Unblob 26.6.4
|
||||
and util-linux prlimit on Linux. It verifies CLI/MCP parity, selected ranges,
|
||||
and util-linux prlimit on Linux. The provider also accepts other 3.1.x and
|
||||
26.6.x builds and reports them as unverified; this lane proves the audited
|
||||
releases. It verifies CLI/MCP parity, selected ranges,
|
||||
unknown chunks, depth limits and extracted child digests. The optional
|
||||
`REA_FIRMWARE_VERIFY_EXT4=1` lane requires existing mke2fs/debugfs; the separate
|
||||
`REA_FIRMWARE_VERIFY_GHIDRA=1` lane checks a selected host ELF through real Ghidra.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_d68a1575bdaa1a044ccce8e27c5dd335612f909bc81c52ce3089b576fd772940",
|
||||
"manifest_id": "ecm_0c8012eb19ae0f5a8e41f0676680b500668b9966cf95a320878e9554d4669e5a",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "d2d971cc67bb0f7b80b4e39382a6bd1146445ed1dbf52043f89e2da2985c97ba"
|
||||
"sha256": "50b3311f68cf2b73686c3b96ceb5f1cbe9054df7886ddf88b5c701f999c6c9cc"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "26"
|
||||
tool_count: 126
|
||||
catalog_digest: "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55"
|
||||
catalog_digest: "de30f3145fb936a97fcb7bcc58ed9f20a974bae59b239dbf24664c145eff4275"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ const workflowAvailabilityFor = ({
|
|||
: {
|
||||
reason: "provider_missing",
|
||||
remediation:
|
||||
"On Linux, provide an absolute REA_BINWALK_COMMAND (Binwalk 3.1.0) or REA_UNBLOB_COMMAND (Unblob 26.6.4) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.",
|
||||
"On Linux, provide an absolute REA_BINWALK_COMMAND (Binwalk 3.1.x, verified with 3.1.0) or REA_UNBLOB_COMMAND (Unblob 26.6.x, verified with 26.6.4) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.",
|
||||
};
|
||||
}
|
||||
if (kind === "android-provider")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const FIRMWARE_TOOL_CONTRACTS = [
|
|||
...toolContractMetadata("inspect_firmware_regions"),
|
||||
kind: "firmware-provider",
|
||||
description:
|
||||
"Inspect signature candidates in a local firmware file with Binwalk 3.1.0. Returns offsets, reported lengths, descriptions and raw confidence values. Length validation and runtime addresses remain unknown. No extraction or target execution. Linux, caller-supplied tool and util-linux prlimit required; 128 MiB input limit, one worker and 120-second deadline.",
|
||||
"Inspect signature candidates in a local firmware file with Binwalk 3.1.x (verified with 3.1.0). Returns offsets, reported lengths, descriptions and raw confidence values. Length validation and runtime addresses remain unknown. No extraction or target execution. Linux, caller-supplied tool and util-linux prlimit required; 128 MiB input limit, one worker and 120-second deadline.",
|
||||
inputSchema: firmwareInputSchemas.inspect_firmware_regions,
|
||||
outputSchema: evidenceResultOf(
|
||||
firmwareResultSchemas.inspect_firmware_regions,
|
||||
|
|
@ -30,7 +30,7 @@ export const FIRMWARE_TOOL_CONTRACTS = [
|
|||
...toolContractMetadata("extract_firmware"),
|
||||
kind: "firmware-provider",
|
||||
description:
|
||||
"Extract caller-selected firmware bytes with Unblob 26.6.4 into an absent output directory. Returns verified child paths/digests, task derivations, unknown chunks and extraction diagnostics inline. Preserves the default sandbox; publishes regular files only. Linux, caller-supplied tool, format-specific extractors and util-linux prlimit required. No mounts or extracted-code execution. Select a returned native file with open_binary for Ghidra analysis; decompressed offsets are not original-file addresses.",
|
||||
"Extract caller-selected firmware bytes with Unblob 26.6.x (verified with 26.6.4) into an absent output directory. Returns verified child paths/digests, task derivations, unknown chunks and extraction diagnostics inline. Preserves the default sandbox; publishes regular files only. Linux, caller-supplied tool, format-specific extractors and util-linux prlimit required. No mounts or extracted-code execution. Select a returned native file with open_binary for Ghidra analysis; decompressed offsets are not original-file addresses.",
|
||||
inputSchema: firmwareInputSchemas.extract_firmware,
|
||||
outputSchema: evidenceResultOf(firmwareResultSchemas.extract_firmware),
|
||||
examples: [
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
|||
import { publishFirmwareExtraction } from "./FirmwarePublication.js";
|
||||
import {
|
||||
FIRMWARE_LIMITS,
|
||||
FIRMWARE_RELEASES,
|
||||
BINWALK_PROVIDER_IDENTITY,
|
||||
UNBLOB_PROVIDER_IDENTITY,
|
||||
} from "./FirmwareRelease.js";
|
||||
import { admitFirmwareVersion } from "./FirmwareVersion.js";
|
||||
import {
|
||||
resolveFirmwareCommand,
|
||||
runFirmwareCommand,
|
||||
|
|
@ -143,21 +143,22 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
|
|||
});
|
||||
const versionRun = await run(["--version"]);
|
||||
const version = versionRun.stdout.text.trim();
|
||||
const expected = FIRMWARE_RELEASES[engineName].version;
|
||||
if (
|
||||
versionRun.stdout.bytes > 1024 ||
|
||||
(engineName === "binwalk"
|
||||
? version !== `binwalk ${expected}`
|
||||
: version !== expected)
|
||||
)
|
||||
if (versionRun.stdout.bytes > 1024)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
engineName,
|
||||
request.operation,
|
||||
`Unsupported tool version ${version}; this adapter is verified against ${expected}`,
|
||||
`Tool version banner is ${String(versionRun.stdout.bytes)} bytes; expected a short ${engineName} version line.`,
|
||||
);
|
||||
const admitted = admitFirmwareVersion(engineName, version);
|
||||
if (admitted.status === "unsupported" || admitted.status === "unresolved")
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
engineName,
|
||||
request.operation,
|
||||
admitted.message,
|
||||
);
|
||||
const engine = {
|
||||
name: engineName,
|
||||
version: expected,
|
||||
version: admitted.version,
|
||||
executable_path: command.command,
|
||||
executable_sha256: command.sha256,
|
||||
source_revision: null,
|
||||
|
|
@ -259,16 +260,23 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
|
|||
});
|
||||
raw = { report, execution: processRun, version_execution: versionRun };
|
||||
}
|
||||
const catalog =
|
||||
engineName === "binwalk"
|
||||
? BINWALK_PROVIDER_IDENTITY
|
||||
: UNBLOB_PROVIDER_IDENTITY;
|
||||
outcome = ok(
|
||||
createAnalysisExecution(
|
||||
result,
|
||||
engineName === "binwalk"
|
||||
? BINWALK_PROVIDER_IDENTITY
|
||||
: UNBLOB_PROVIDER_IDENTITY,
|
||||
{ ...catalog, version: admitted.version },
|
||||
{
|
||||
subject: target.subject,
|
||||
rawResult: raw,
|
||||
limitations,
|
||||
limitations: [
|
||||
...limitations,
|
||||
...(admitted.status === "compatible"
|
||||
? [admitted.limitation]
|
||||
: []),
|
||||
],
|
||||
locations: [
|
||||
{
|
||||
kind: "file-offset-range",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const FIRMWARE_RELEASES = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
/** Catalog identities; Evidence uses the version observed from the executed tool. */
|
||||
/** Catalog identity for the verified Binwalk release. Evidence replaces version with the observed banner. */
|
||||
export const BINWALK_PROVIDER_IDENTITY = {
|
||||
id: "binwalk",
|
||||
name: "Binwalk",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { admitFirmwareVersion } from "./FirmwareVersion.js";
|
||||
|
||||
describe("firmware release lines", () => {
|
||||
it("accepts the verified banners without an extra limitation", () => {
|
||||
expect(admitFirmwareVersion("binwalk", "binwalk 3.1.0")).toEqual({
|
||||
status: "verified",
|
||||
version: "3.1.0",
|
||||
});
|
||||
expect(admitFirmwareVersion("unblob", "26.6.4")).toEqual({
|
||||
status: "verified",
|
||||
version: "26.6.4",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["binwalk", "binwalk 3.1.2", "3.1.2"],
|
||||
["binwalk", "binwalk 3.1.0-1", "3.1.0-1"],
|
||||
["unblob", "26.6.5", "26.6.5"],
|
||||
] as const)(
|
||||
"accepts %s %s on the verified line",
|
||||
(engine, banner, version) => {
|
||||
const admitted = admitFirmwareVersion(engine, banner);
|
||||
expect(admitted).toMatchObject({ status: "compatible", version });
|
||||
if (admitted.status !== "compatible")
|
||||
throw new Error("expected a compatible release");
|
||||
expect(admitted.limitation).toContain(version);
|
||||
expect(admitted.limitation).toContain(
|
||||
engine === "binwalk" ? "3.1.0" : "26.6.4",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["binwalk", "binwalk 3.2.0", "Binwalk 3.1.x"],
|
||||
["binwalk", "unsupported", "Binwalk 3.1.x"],
|
||||
["binwalk", "3.1.2", "Binwalk 3.1.x"],
|
||||
["unblob", "26.7.0", "Unblob 26.6.x"],
|
||||
["unblob", "unblob 26.6.4", "Unblob 26.6.x"],
|
||||
["unblob", "", "missing"],
|
||||
] as const)("rejects %s banner %j", (engine, banner, detail) => {
|
||||
const admitted = admitFirmwareVersion(engine, banner);
|
||||
expect(
|
||||
admitted.status === "verified" || admitted.status === "compatible",
|
||||
).toBe(false);
|
||||
if (admitted.status === "verified" || admitted.status === "compatible")
|
||||
throw new Error("expected a rejected banner");
|
||||
expect(admitted.message).toContain(detail);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { FIRMWARE_RELEASES } from "./FirmwareRelease.js";
|
||||
|
||||
/** Caller-supplied firmware engine selected by the operation. */
|
||||
export type FirmwareEngineName = keyof typeof FIRMWARE_RELEASES;
|
||||
|
||||
interface ReleaseNumber {
|
||||
readonly major: number;
|
||||
readonly minor: number;
|
||||
readonly patch: number;
|
||||
readonly suffix: string;
|
||||
readonly token: string;
|
||||
}
|
||||
|
||||
const TOKEN = /^(\d+)\.(\d+)\.(\d+)([-+][0-9A-Za-z.-]+)?$/u;
|
||||
|
||||
const parseToken = (token: string): ReleaseNumber | undefined => {
|
||||
const match = TOKEN.exec(token);
|
||||
if (match === null) return undefined;
|
||||
const major = Number(match[1]);
|
||||
const minor = Number(match[2]);
|
||||
const patch = Number(match[3]);
|
||||
if (![major, minor, patch].every((part) => Number.isSafeInteger(part)))
|
||||
return undefined;
|
||||
const suffix = match[4] ?? "";
|
||||
return { major, minor, patch, suffix, token };
|
||||
};
|
||||
|
||||
const verifiedRelease = (engine: FirmwareEngineName): ReleaseNumber => {
|
||||
const parsed = parseToken(FIRMWARE_RELEASES[engine].version);
|
||||
if (parsed === undefined || parsed.suffix !== "")
|
||||
throw new Error(
|
||||
`FIRMWARE_RELEASES.${engine}.version is not a release number`,
|
||||
);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
/** Caller-facing engine name. */
|
||||
export const firmwareEngineLabel = (engine: FirmwareEngineName): string => {
|
||||
switch (engine) {
|
||||
case "binwalk":
|
||||
return "Binwalk";
|
||||
case "unblob":
|
||||
return "Unblob";
|
||||
default: {
|
||||
const unreachable: never = engine;
|
||||
return unreachable;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Release line accepted for one engine, such as `Binwalk 3.1.x`. */
|
||||
export const firmwareReleaseLine = (engine: FirmwareEngineName): string => {
|
||||
const verified = verifiedRelease(engine);
|
||||
return `${firmwareEngineLabel(engine)} ${String(verified.major)}.${String(verified.minor)}.x`;
|
||||
};
|
||||
|
||||
/** Admission of one observed `--version` banner. */
|
||||
export type FirmwareVersionAdmission =
|
||||
| { readonly status: "verified"; readonly version: string }
|
||||
| {
|
||||
readonly status: "compatible";
|
||||
readonly version: string;
|
||||
readonly limitation: string;
|
||||
}
|
||||
| { readonly status: "unsupported"; readonly message: string }
|
||||
| { readonly status: "unresolved"; readonly message: string };
|
||||
|
||||
/**
|
||||
* Classify a `--version` banner against the audited release line.
|
||||
* Binwalk prints `binwalk <version>`; Unblob prints the version alone.
|
||||
*/
|
||||
export const admitFirmwareVersion = (
|
||||
engine: FirmwareEngineName,
|
||||
banner: string,
|
||||
): FirmwareVersionAdmission => {
|
||||
const token = versionToken(engine, banner);
|
||||
const parsed = token === undefined ? undefined : parseToken(token);
|
||||
const verified = verifiedRelease(engine);
|
||||
const accepted = `accepted releases are ${firmwareReleaseLine(engine)} (verified with ${verified.token})`;
|
||||
if (parsed === undefined)
|
||||
return {
|
||||
status: "unresolved",
|
||||
message: `Unsupported tool version ${banner.length === 0 ? "missing" : banner}; ${accepted}.`,
|
||||
};
|
||||
if (parsed.major !== verified.major || parsed.minor !== verified.minor)
|
||||
return {
|
||||
status: "unsupported",
|
||||
message: `Unsupported tool version ${banner}; ${accepted}.`,
|
||||
};
|
||||
if (parsed.token === verified.token)
|
||||
return { status: "verified", version: parsed.token };
|
||||
const line = `${String(verified.major)}.${String(verified.minor)}.x`;
|
||||
return {
|
||||
status: "compatible",
|
||||
version: parsed.token,
|
||||
limitation: `This session uses ${firmwareEngineLabel(engine)} ${parsed.token}. The report parser is verified against ${verified.token}; other ${line} builds are accepted and the observed version is reported.`,
|
||||
};
|
||||
};
|
||||
|
||||
const versionToken = (
|
||||
engine: FirmwareEngineName,
|
||||
banner: string,
|
||||
): string | undefined => {
|
||||
switch (engine) {
|
||||
case "binwalk":
|
||||
return banner.startsWith("binwalk ")
|
||||
? banner.slice("binwalk ".length)
|
||||
: undefined;
|
||||
case "unblob":
|
||||
return banner;
|
||||
default: {
|
||||
const unreachable: never = engine;
|
||||
return unreachable;
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -340,6 +340,37 @@ it("retains an uncertain workspace and blocks new launches after cleanup failure
|
|||
expect(fixture.launches).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("accepts another build on the verified release line and reports it", async () => {
|
||||
const fixture = await firmwareFixture("normal", "binwalk 3.1.2");
|
||||
const result = await fixture.service.execute("inspect_firmware_regions", {
|
||||
path: fixture.path,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.provider.version).toBe("3.1.2");
|
||||
expect(result.value.limitations.join("\n")).toContain(
|
||||
"verified against 3.1.0",
|
||||
);
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
engine: { version: "3.1.2" },
|
||||
});
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
});
|
||||
|
||||
it("names the accepted release line when the banner is another line", async () => {
|
||||
const fixture = await firmwareFixture("normal", "binwalk 3.2.0");
|
||||
const result = await fixture.service.execute("inspect_firmware_regions", {
|
||||
path: fixture.path,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: expect.stringContaining("Binwalk 3.1.x"),
|
||||
},
|
||||
});
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
});
|
||||
|
||||
it("rejects absent or unsupported engines without installing anything", async () => {
|
||||
const missing = await new FirmwareAnalysisService(
|
||||
new FirmwareProvider({}),
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ import { join } from "node:path";
|
|||
const args = process.argv.slice(2);
|
||||
const mode = process.env.REA_FIRMWARE_FIXTURE_MODE;
|
||||
const engine = process.env.REA_FIRMWARE_FIXTURE_ENGINE;
|
||||
const banner = process.env.REA_FIRMWARE_FIXTURE_VERSION;
|
||||
if (args.includes("--version")) {
|
||||
console.log(
|
||||
mode === "version"
|
||||
? "unsupported"
|
||||
: engine === "binwalk"
|
||||
? "binwalk 3.1.0"
|
||||
: "26.6.4",
|
||||
: banner !== undefined && banner.length > 0
|
||||
? banner
|
||||
: engine === "binwalk"
|
||||
? "binwalk 3.1.0"
|
||||
: "26.6.4",
|
||||
);
|
||||
} else if (mode === "stall") {
|
||||
setInterval(() => {}, 1000);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.
|
|||
import { createTestTempDirectory } from "../temporaryDirectory.js";
|
||||
|
||||
/** Exercise real subprocess ownership with synthetic pinned producer representations. */
|
||||
export const firmwareFixture = async (mode = "normal") => {
|
||||
export const firmwareFixture = async (mode = "normal", banner?: string) => {
|
||||
const root = await createTestTempDirectory("rea-firmware-test-");
|
||||
const path = join(root, "input.bin");
|
||||
await writeFile(path, Buffer.from("fixture-input-data"));
|
||||
|
|
@ -43,6 +43,9 @@ export const firmwareFixture = async (mode = "normal") => {
|
|||
...options.env,
|
||||
REA_FIRMWARE_FIXTURE_MODE: mode,
|
||||
REA_FIRMWARE_FIXTURE_ENGINE: engine,
|
||||
...(banner === undefined
|
||||
? {}
|
||||
: { REA_FIRMWARE_FIXTURE_VERSION: banner }),
|
||||
},
|
||||
});
|
||||
launches.push({
|
||||
|
|
|
|||
Loading…
Reference in New Issue