From 36c91428fc6ad9fc30336617c24e7031c8d574a2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:46:36 +0800 Subject: [PATCH] fix(rust): retain compatible cargo-auditable revisions Admit documented compatible format revisions, preserve the original uint32 revision and disclose unverified fallback for unknown revisions. Keep malformed package and compressed-input rejection. Co-authored-by: Dragolone <810170966qq@gmail.com> --- docs/rust-binaries.md | 5 +++ src/domain/rust/rustBinary.ts | 6 ++- src/rust/RustAuditableReader.test.ts | 53 +++++++++++++++++++++++--- src/rust/RustAuditableReader.ts | 12 +----- src/rust/RustBinaryProvider.ts | 16 ++++++++ tests/acceptance/rustBinaryCli.test.ts | 36 ++++++++++++++++- 6 files changed, 108 insertions(+), 20 deletions(-) diff --git a/docs/rust-binaries.md b/docs/rust-binaries.md index d7cb7a750..e0f7a28f1 100644 --- a/docs/rust-binaries.md +++ b/docs/rust-binaries.md @@ -55,6 +55,11 @@ dependency sections are typed failures. A cycle is reported and is not walked. A missing section returns success with `auditable_metadata: null` and a limitation. Absence of `.dep-v0` does not mean the binary has no dependencies. +Unknown embedded format revisions are retained in `format_version` and +interpreted using the highest known preceding revision, following the +cargo-auditable schema. The output includes a limitation that identifies this +fallback and notes that the particular revision has not been independently +verified. An omitted revision remains `null` and follows revision 0 semantics. Embedded names and versions are file content. They do not authenticate the packages or the compiler. diff --git a/src/domain/rust/rustBinary.ts b/src/domain/rust/rustBinary.ts index f09cde222..95cc60a80 100644 --- a/src/domain/rust/rustBinary.ts +++ b/src/domain/rust/rustBinary.ts @@ -2,7 +2,8 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; import { localPathStringSchema } from "../localPath.js"; -export const RUST_AUDITABLE_FORMAT_VERSION = 1; +/** Known revisions used to interpret compatible cargo-auditable documents. */ +export const RUST_AUDITABLE_FORMAT_VERSIONS = [0, 1, 8] as const; const index = z.number().int().nonnegative(); const rustcHash = z.string().regex(/^[0-9a-f]{40}$/u); @@ -29,7 +30,8 @@ export const rustAuditableMetadataSchema = z.strictObject({ section_bytes: z.number().int().positive(), compressed_bytes: z.number().int().positive(), uncompressed_bytes: z.number().int().positive(), - format_version: z.literal(RUST_AUDITABLE_FORMAT_VERSION).nullable(), + /** Original embedded revision; null means the source omitted the field. */ + format_version: z.number().int().nonnegative().max(0xffff_ffff).nullable(), packages: z.array(rustPackageSchema), root_package_index: index.nullable(), dependency_edges: z.array(dependencyEdgeSchema), diff --git a/src/rust/RustAuditableReader.test.ts b/src/rust/RustAuditableReader.test.ts index cb281185a..3edfdf289 100644 --- a/src/rust/RustAuditableReader.test.ts +++ b/src/rust/RustAuditableReader.test.ts @@ -1,5 +1,6 @@ import { deflateSync } from "node:zlib"; import { expect, it } from "vitest"; +import { rustAuditableMetadataSchema } from "../domain/rust/rustBinary.js"; import { RUST_SECTION_BYTES, readAuditableSection, @@ -153,14 +154,54 @@ it("stops an inflate that exceeds the section ratio", () => { ).toThrow(/1 MiB/); }); -it.each([{ packages: [] }, { packages: [{ future_package_field: true }] }])( - "rejects an unknown format before interpreting its packages: %j", - ({ packages }) => { - expect(() => section({ format: 99, packages })).toThrow( +it.each([0, 8])("reads documented cargo-auditable revision %i", (format) => { + const parsed = section({ + format, + packages: [ + { name: "sample", version: "0.1.0", source: "local", root: true }, + ], + }); + expect(parsed).toMatchObject({ + format_version: format, + root_package_index: 0, + }); + expect(rustAuditableMetadataSchema.parse(parsed)).toEqual(parsed); +}); + +it.each([{ source: 4 }, { source: 9 }, { source: 99 }])( + "preserves unknown revision $source for the documented preceding-revision interpretation", + ({ source }) => { + const parsed = section({ + format: source, + packages: [ + { name: "sample", version: "0.1.0", source: "local", root: true }, + ], + }); + expect(parsed).toMatchObject({ + format_version: source, + }); + expect(rustAuditableMetadataSchema.parse(parsed)).toEqual(parsed); + }, +); + +it("rejects unknown package fields even for a forward-compatible revision", () => { + expect(() => + section({ + format: 99, + packages: [ + { name: "sample", version: "0.1.0", source: "local", future: true }, + ], + }), + ).toThrow(/package schema/); +}); + +it.each([-1, 1.5, 0x1_0000_0000])( + "rejects invalid cargo-auditable format value %s", + (format) => { + expect(() => section({ format, packages: [] })).toThrow( expect.objectContaining({ name: "RustAuditableFormatFailure", - kind: "unsupported", - message: "Unsupported cargo-auditable format version 99", + kind: "malformed", }), ); }, diff --git a/src/rust/RustAuditableReader.ts b/src/rust/RustAuditableReader.ts index d28b95704..c8805864a 100644 --- a/src/rust/RustAuditableReader.ts +++ b/src/rust/RustAuditableReader.ts @@ -1,7 +1,6 @@ import { deflateSync, inflateSync } from "node:zlib"; import { z } from "zod"; import { - RUST_AUDITABLE_FORMAT_VERSION, cyclicDependencyIndices, type RustAuditableMetadata, type RustPackage, @@ -28,11 +27,10 @@ const packageSchema = z.strictObject({ root: z.boolean().optional(), }); const documentVersionSchema = z.object({ - format: z.number().int().nonnegative().optional(), + format: z.number().int().nonnegative().max(0xffff_ffff).optional(), }); const documentSchema = documentVersionSchema .extend({ - format: z.literal(RUST_AUDITABLE_FORMAT_VERSION).optional(), packages: z.array(packageSchema), }) .strict(); @@ -239,14 +237,6 @@ const parseDocument = (bytes: Buffer): z.infer => { "Dependency JSON does not match the format schema", { cause: version.error }, ); - if ( - version.data.format !== undefined && - version.data.format !== RUST_AUDITABLE_FORMAT_VERSION - ) - throw new RustAuditableFormatFailure( - "unsupported", - `Unsupported cargo-auditable format version ${version.data.format}`, - ); admitGraphExpansion(value); // One malformed package must fail before Zod retains an issue for every field. rejectMalformedPackages(value); diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index d0ce690f0..9808b615c 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -13,6 +13,7 @@ import { AnalysisUnsupportedTargetError, } from "../domain/analysisErrorCore.js"; import { + RUST_AUDITABLE_FORMAT_VERSIONS, rustBinarySchema, type InspectRustBinaryInput, type RustBinary, @@ -97,6 +98,7 @@ const limitationsFor = ( "No .dep-v0 section was found. Absence of .dep-v0 does not mean the binary has no dependencies.", ] : []), + ...unknownFormatLimitation(image.auditable_metadata?.format_version), ...(image.auditable_metadata?.packages.some((pkg) => pkg.kind === null) ? [ "An omitted package kind is left null. The embedded format treats an omitted kind as runtime, and that default was not stored in the section.", @@ -111,6 +113,20 @@ const limitationsFor = ( ...rustcLimitation(image.rustc_commit), ]; +const unknownFormatLimitation = ( + format: number | null | undefined, +): string[] => { + if (format === null || format === undefined) return []; + let preceding = 0; + for (const known of RUST_AUDITABLE_FORMAT_VERSIONS) { + if (known === format) return []; + if (known < format) preceding = known; + } + return [ + `cargo-auditable format revision ${format} is unknown. Its schema directs readers to interpret it using the highest known preceding revision (${preceding}); this specific revision has not been independently verified.`, + ]; +}; + const rustcLimitation = ( commit: ReturnType["rustc_commit"], ): string[] => { diff --git a/tests/acceptance/rustBinaryCli.test.ts b/tests/acceptance/rustBinaryCli.test.ts index 2f984c618..d1d86c311 100644 --- a/tests/acceptance/rustBinaryCli.test.ts +++ b/tests/acceptance/rustBinaryCli.test.ts @@ -3,7 +3,10 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../src/contracts/toolContracts.js"; -import { createRustImage } from "../fixtures/rust/image.js"; +import { + createRustImage, + rustDependencyPayload, +} from "../fixtures/rust/image.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; @@ -49,6 +52,37 @@ cliTest("returns null metadata when .dep-v0 is absent", async ({ cli }) => { ); }); +cliTest( + "preserves an unknown cargo-auditable revision and reports its fallback", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-rust-cli-unknown-format-"); + const path = join(root, "sample"); + const payload = rustDependencyPayload({ + format: 4, + packages: [ + { name: "sample", version: "0.1.0", source: "local", root: true }, + ], + }); + await writeFile(path, createRustImage({ format: "elf", payload })); + const result = await cli.run({ + arguments: ["inspect-rust-binary", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const evidence = toolContract("inspect_rust_binary").outputSchema.parse( + result.json, + ); + expect(evidence.normalized_result.auditable_metadata?.format_version).toBe( + 4, + ); + expect(evidence.normalized_result.limitations.join("\n")).toContain( + "highest known preceding revision (1)", + ); + expect(evidence.normalized_result.limitations.join("\n")).toContain( + "this specific revision has not been independently verified", + ); + }, +); + cliTest("reports malformed zlib as invalid input", async ({ cli }) => { const root = await createTestTempDirectory("rea-rust-cli-zlib-"); const path = join(root, "broken.elf");