diff --git a/src/firmware/FirmwareProvider.ts b/src/firmware/FirmwareProvider.ts index a7a43c2332..fb43ac6c69 100644 --- a/src/firmware/FirmwareProvider.ts +++ b/src/firmware/FirmwareProvider.ts @@ -131,6 +131,18 @@ const limitations = [ "The actual launcher executable is hashed. Interpreter, shared libraries and installed Python package dependencies are not independently attested; source revision remains unknown for caller-supplied builds.", ] as const; +/** Unblob's safe-link handling rewrites absolute targets inside its output. */ +const UNBLOB_LINK_TARGET_LIMITATION = + "Symlink link_target values are read from Unblob's extracted tree, not from the firmware's filesystem metadata. Unblob rewrites absolute targets relative to the extraction root, so a relative link_target may have been stored as an absolute path in the image."; + +const publishedSymlinks = (result: unknown): boolean => { + const parsed = firmwareResultSchemas.extract_firmware.safeParse(result); + return ( + parsed.success && + parsed.data.unpublished_entries.some((entry) => entry.kind === "symlink") + ); +}; + /** Queued bring-your-own firmware adapters with private snapshots and owned cleanup. */ export class FirmwareProvider implements FirmwareAnalysisPort { #tail: Promise = Promise.resolve(); @@ -541,6 +553,9 @@ export class FirmwareProvider implements FirmwareAnalysisPort { rawResult: raw, limitations: [ ...limitations, + ...(engineName === "unblob" && publishedSymlinks(result) + ? [UNBLOB_LINK_TARGET_LIMITATION] + : []), ...(admitted.status === "compatible" ? [admitted.limitation] : []), diff --git a/tests/boundary/firmware/firmwareIntegration.test.ts b/tests/boundary/firmware/firmwareIntegration.test.ts index 0aeb8d0492..3e7953da5e 100644 --- a/tests/boundary/firmware/firmwareIntegration.test.ts +++ b/tests/boundary/firmware/firmwareIntegration.test.ts @@ -619,6 +619,12 @@ it.each(["depth", "dependency", "link"])( continuation: expect.stringContaining("installs nothing"), }, ]); + const linkLimitation = expect.stringContaining( + "Unblob rewrites absolute targets relative to the extraction root", + ); + if (mode === "link") + expect(result.value.limitations).toContainEqual(linkLimitation); + else expect(result.value.limitations).not.toContainEqual(linkLimitation); if (mode === "link") { expect(normalized.unpublished_entries).toEqual([ {