diff --git a/docs/uf2-dfuse.md b/docs/uf2-dfuse.md index 6eadfd3c43..209b221fff 100644 --- a/docs/uf2-dfuse.md +++ b/docs/uf2-dfuse.md @@ -127,13 +127,13 @@ its declaration, so the same bytes moved to another device memory are Root `format_metadata` (`dfuse`) records: -| Field | Meaning | -| ---------------------------------------------------- | ------------------------------------------------------------------------------- | -| `image_size`, `target_count` | `DFUImageSize` and `bTargets` from the prefix | -| `element_count`, `empty_element_count`, `data_bytes` | All elements, elements of 0 bytes (which have no entry), and element data bytes | -| `overlapping_element_count` | Elements that overlap an earlier element of the same alternate setting | -| `device_version`, `product_id`, `vendor_id` | `bcdDevice`, `idProduct` and `idVendor` from the suffix | -| `suffix_crc`, `computed_crc` | The stored `dwCRC`, and the same CRC computed over every byte before it | +| Field | Meaning | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `image_size`, `target_count` | `DFUImageSize` and `bTargets` from the prefix | +| `element_count`, `empty_element_count`, `data_bytes` | All elements, elements of 0 bytes (which have no entry), and element data bytes | +| `overlapping_element_count` | Elements that overlap an earlier element in address order within the same alternate setting | +| `device_version`, `product_id`, `vendor_id` | `bcdDevice`, `idProduct` and `idVendor` from the suffix | +| `suffix_crc`, `computed_crc` | The stored `dwCRC`, and the same CRC computed over every byte before it | Recognition already requires `bVersion` 1 and `bcdDFU` `0x011A`, so they are not repeated. The DFU suffix CRC is CRC-32 without the final inversion, as diff --git a/src/domain/firmware/dfuse.ts b/src/domain/firmware/dfuse.ts index 3a0f01dfb3..df9e8d3817 100644 --- a/src/domain/firmware/dfuse.ts +++ b/src/domain/firmware/dfuse.ts @@ -212,7 +212,7 @@ const parseDfuSe = ( ...(overlaps.length === 0 ? [] : [ - `${String(overlaps.length)} DfuSe element(s) overlap an earlier element of the same alternate setting; each stays a separate entry and no bytes were merged: ${named(overlaps)}.`, + `${String(overlaps.length)} DfuSe element(s) overlap an element earlier in address order within the same alternate setting; each stays a separate entry and no bytes were merged: ${named(overlaps)}.`, ]), ...(walk.unlistedCount === 0 ? [] diff --git a/src/domain/firmware/uf2.ts b/src/domain/firmware/uf2.ts index c906123ab6..d5ea5a4b76 100644 --- a/src/domain/firmware/uf2.ts +++ b/src/domain/firmware/uf2.ts @@ -8,6 +8,7 @@ import { UF2_FAMILY_REGISTRY, uf2FamilyName } from "./uf2Families.js"; export const UF2_BLOCK_BYTES = 512; const UF2_DATA_OFFSET = 32; const UF2_DATA_BYTES = 476; +const UF2_MD5_BYTES = 24; const UF2_MAGIC_START0 = 0x0a32_4655; const UF2_MAGIC_START1 = 0x9e5d_5157; const UF2_MAGIC_END = 0x0ab1_6f30; @@ -236,9 +237,12 @@ class StreamingUf2Decoder implements RegionDecoder { ? "end magic number does not match" : size > UF2_DATA_BYTES ? `payload size ${String(size)} exceeds the ${String(UF2_DATA_BYTES)}-byte data area` - : address + size > ADDRESS_SPACE - ? `payload at ${hex32(address)} with ${String(size)} bytes wraps past 0xffffffff` - : undefined; + : (flags & UF2_FLAG_MD5) !== 0 && + size > UF2_DATA_BYTES - UF2_MD5_BYTES + ? "payload overlaps the reserved MD5 checksum metadata" + : address + size > ADDRESS_SPACE + ? `payload at ${hex32(address)} with ${String(size)} bytes wraps past 0xffffffff` + : undefined; if (fault !== undefined) { this.#invalid += 1; this.#firstInvalid ??= { block: index, reason: fault }; diff --git a/tests/boundary/mcp/firmwareContainersMcp.test.ts b/tests/boundary/mcp/firmwareContainersMcp.test.ts index dcbd447de5..577dac3213 100644 --- a/tests/boundary/mcp/firmwareContainersMcp.test.ts +++ b/tests/boundary/mcp/firmwareContainersMcp.test.ts @@ -14,6 +14,7 @@ import { artifactInspectionResultSchema } from "../../../src/domain/artifactInsp import { dfuseFile } from "../../../src/domain/firmware/dfuse.fixture.js"; import { RP2040_FAMILY, + UF2_FLAGS, RP2XXX_ABSOLUTE_FAMILY, patternBytes, uf2Block, @@ -113,6 +114,74 @@ it.each(["full", "compact"] as const)( }, ); +it("preserves valid regions while excluding a UF2 payload overlapping MD5 metadata", async () => { + const root = await createTestTempDirectory("rea-uf2-md5-mcp-"); + const path = join(root, "firmware.uf2"); + const valid = patternBytes(452, 1); + const plain = patternBytes(476, 2); + await writeFile( + path, + uf2File([ + uf2Block({ + address: 0x1000, + payload: valid, + blockNo: 0, + numBlocks: 3, + flags: UF2_FLAGS.md5, + }), + uf2Block({ + address: 0x2000, + payload: patternBytes(453, 3), + blockNo: 1, + numBlocks: 3, + flags: UF2_FLAGS.md5, + }), + uf2Block({ address: 0x3000, payload: plain, blockNo: 2, numBlocks: 3 }), + ]), + ); + const { call } = await connectLocalToolsMcp({}); + let outputRoot: string | undefined; + try { + const opened = await call("open_binary", { path }); + expect(opened.isError, JSON.stringify(opened)).not.toBe(true); + const inspected = await call("inspect_artifact", {}); + expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true); + const inspection = artifactInspectionResultSchema.parse( + parseEvidence(inspected.structuredContent).normalized_result, + ); + expect(inspection.coverage.status).toBe("partial"); + expect(inspection.limitations).toContainEqual( + expect.stringMatching(/invalid.*block 1.*MD5/iu), + ); + const extracted = await call("extract_artifact", {}); + expect(extracted.isError, JSON.stringify(extracted)).not.toBe(true); + const result = artifactExtractionResultSchema.parse( + parseEvidence(extracted.structuredContent).normalized_result, + ); + outputRoot = result.output_root; + expect(result.coverage.status).toBe("unknown"); + expect( + Buffer.compare( + await readFile(join(outputRoot, "no-family/0x00001000-0x000011c3.bin")), + valid, + ), + ).toBe(0); + expect( + Buffer.compare( + await readFile(join(outputRoot, "no-family/0x00003000-0x000031db.bin")), + plain, + ), + ).toBe(0); + await expect( + readFile(join(outputRoot, "no-family/0x00002000-0x000021c4.bin")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await call("close_binary", {}); + } finally { + if (outputRoot !== undefined) + await rm(outputRoot, { recursive: true, force: true }); + } +}); + it("delivers a DfuSe CRC contradiction under both integrity policies through MCP", async () => { const root = await createTestTempDirectory("rea-dfuse-mcp-"); const path = join(root, "firmware.dfu");