diff --git a/docs/rust-binaries.md b/docs/rust-binaries.md index e52ba22a47..7e16a60bfe 100644 --- a/docs/rust-binaries.md +++ b/docs/rust-binaries.md @@ -109,9 +109,11 @@ removed the field entirely, as recorded in its [changelog](https://github.com/rust-secure-code/cargo-auditable/blob/v0.5.0/CHANGELOG.md). `rustc_commit` is present only when a `/rustc/<40-hex>/` path is found in -read-only data. Its `evidence_kind` is `inferred`. The hash is not a claim that -that compiler produced the binary. Several distinct hashes leave a single -commit unknown. +read-only data. For ELF, read-only non-executable `PT_LOAD` file ranges are +searched alongside read-only sections, so an optional missing section-header +table does not remove this search source. Its `evidence_kind` is `inferred`. +The hash is not a claim that that compiler produced the binary. Several +distinct hashes leave a single commit unknown. ## Limits and failures diff --git a/src/rust/RustBinaryImage.ts b/src/rust/RustBinaryImage.ts index 01d0d9a66d..7a1dd53e9c 100644 --- a/src/rust/RustBinaryImage.ts +++ b/src/rust/RustBinaryImage.ts @@ -1,7 +1,7 @@ import { BinaryFormatFailure, BinaryResourceFailure, - type ImageSection, + type BinaryContainer, } from "../native/metadata/BinaryContainer.js"; import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js"; import type { @@ -42,7 +42,7 @@ export const readRustBinaryImage = (bytes: Buffer): RustBinaryImage => { dependency === null ? null : readDependency(bytes, container.format, dependency), - rustc_commit: rustcCommit(bytes, container.sections), + rustc_commit: rustcCommit(bytes, container), }; }; @@ -108,29 +108,35 @@ const readDependency = ( const rustcCommit = ( bytes: Buffer, - sections: readonly ImageSection[], + container: BinaryContainer, ): RustBinaryImage["rustc_commit"] => { const hashes: { hash: string; file_offset: number }[] = []; const seen = new Set(); - // Overlapping declared sections are scanned once, so total work stays - // bounded by the file size however many sections reference a range. - for (const range of mergedReadOnlyRanges(sections)) + const sections = container.sections + .filter((section) => section.read_only) + .map(({ offset, size }) => ({ offset, size })); + const loadRegions = + container.format === "elf" + ? (container.load_regions ?? []) + .filter((region) => region.read && !region.write && !region.execute) + .map(({ mapping }) => mapping) + : []; + // Overlapping section and ELF load ranges are scanned once, so total work + // stays bounded by the file size however many mappings reference a range. + for (const range of mergedReadOnlyFileRanges([...sections, ...loadRegions])) collectHashes(bytes, range, seen, hashes); return hashes.length === 0 ? null : { evidence_kind: "inferred", hashes }; }; -const mergedReadOnlyRanges = ( - sections: readonly ImageSection[], +const mergedReadOnlyFileRanges = ( + ranges: readonly { readonly offset: number; readonly size: number }[], ): { readonly offset: number; readonly end: number }[] => { - const ranges = sections - .filter((section) => section.read_only && section.size > 0) - .map((section) => ({ - offset: section.offset, - end: section.offset + section.size, - })) + const sorted = ranges + .filter(({ size }) => size > 0) + .map(({ offset, size }) => ({ offset, end: offset + size })) .sort((left, right) => left.offset - right.offset); const merged: { offset: number; end: number }[] = []; - for (const range of ranges) { + for (const range of sorted) { const last = merged.at(-1); if (last !== undefined && range.offset <= last.end) last.end = Math.max(last.end, range.end); diff --git a/tests/boundary/rust/rustBinaryImage.test.ts b/tests/boundary/rust/rustBinaryImage.test.ts index de26cacace..56a752bf73 100644 --- a/tests/boundary/rust/rustBinaryImage.test.ts +++ b/tests/boundary/rust/rustBinaryImage.test.ts @@ -127,6 +127,29 @@ it("returns null metadata when the dependency section is absent", () => { ]); }); +it("finds rustc paths in read-only ELF load data without section headers", () => { + const bytes = createRustImage({ + format: "elf", + omitSection: true, + includeRustc: true, + }); + bytes.writeBigUInt64LE(0n, 40); + bytes.writeUInt16LE(0, 60); + bytes.writeUInt16LE(0, 62); + bytes.writeUInt32LE(4, 132); // PF_R, no write or execute permission. + + expect(readRustBinaryImage(bytes)).toMatchObject({ + auditable_metadata: null, + rustc_commit: { + evidence_kind: "inferred", + hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }], + }, + }); + + bytes.writeUInt32LE(6, 132); // PF_R | PF_W is not read-only data. + expect(readRustBinaryImage(bytes).rustc_commit).toBeNull(); +}); + it("refuses a universal Mach-O container", () => { expect(() => readRustBinaryImage(fat([0x0100000c]))).toThrow( RustAuditableFormatFailure,