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:
morluto 2026-10-07 12:19:28 +08:00 committed by GitHub
parent 3f1a77f68e
commit bef4321afb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 249 additions and 32 deletions

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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.

View File

@ -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": [

View File

@ -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

View File

@ -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")

View File

@ -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: [

View File

@ -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",

View File

@ -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",

View File

@ -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);
});
});

View File

@ -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

View File

@ -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({}),

View File

@ -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);

View File

@ -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({