From 68fe35867c2e1938beb002d77f76773f92ddb5dc Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:14:31 -0800 Subject: [PATCH] fix(audit): name a symlinked sibling debug companion Selecting an executable skipped every symlink beside it before the name check, so an app.pdb link left no companion path and no skip count, and native.debug_companions read not_observed. Selecting the directory reports the same link. Check the name first. The map branch already needs a regular file, so no link is followed. --- docs/release-exposure.md | 2 +- src/artifacts/ReleaseExposureReader.test.ts | 18 ++++++++++++++++++ src/artifacts/ReleaseExposureReader.ts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/release-exposure.md b/docs/release-exposure.md index 46ffc78d5d..5fa9e983dc 100644 --- a/docs/release-exposure.md +++ b/docs/release-exposure.md @@ -56,7 +56,7 @@ A member of a package that inventory did not expand is reported as the host file A package member skipped by a read allowance still counts as readable for `packaging.extractable_payload`, because the format stores it unencrypted. An encrypted member or one that failed to read counts as unreadable. -A map read counts against the source-map allowance when it starts, so an oversized or unreadable `.map` uses a read as a parsed one does. A sibling `.map` beside a selected file spends the same allowance, after the selected artifact has been read, and past it only the bounded header is read. A sibling whose header can begin a JSON object is then counted as skipped. A `.map` too large to parse is a linker map when its header cannot begin a JSON object. A larger `.map` that begins as JSON, or that has a JavaScript name such as `app.js.map`, is counted as skipped. `native.debug_companions` identifies a companion from its name or from a `.map` body that is not a source map, so its `exposed` result has `inferred` confidence. +A map read counts against the source-map allowance when it starts, so an oversized or unreadable `.map` uses a read as a parsed one does. A sibling `.map` beside a selected file spends the same allowance, after the selected artifact has been read, and past it only the bounded header is read. A sibling whose header can begin a JSON object is then counted as skipped. A `.map` too large to parse is a linker map when its header cannot begin a JSON object. A larger `.map` that begins as JSON, or that has a JavaScript name such as `app.js.map`, is counted as skipped. `native.debug_companions` identifies a companion from its name or from a `.map` body that is not a source map, so its `exposed` result has `inferred` confidence. A symlink with a companion name counts by that name, and its target is not read. ## Status diff --git a/src/artifacts/ReleaseExposureReader.test.ts b/src/artifacts/ReleaseExposureReader.test.ts index 0c7f3f1ed6..f87a835e72 100644 --- a/src/artifacts/ReleaseExposureReader.test.ts +++ b/src/artifacts/ReleaseExposureReader.test.ts @@ -1037,6 +1037,24 @@ describe("ReleaseExposureReader mixed maps and sibling paths", () => { expect(scanned.debug_companions.paths).toHaveLength(2); }, ); + + it.skipIf(process.platform === "win32")( + "names a symlinked sibling companion without following it", + async () => { + const root = await createTestTempDirectory("rea-release-sibling-link-"); + await writeFile(join(root, "app"), "plain file"); + await writeFile(join(root, "symbols"), "debug companion"); + await symlink(join(root, "symbols"), join(root, "app.pdb")); + await symlink(join(root, "missing"), join(root, "app.debug")); + await symlink(join(root, "app"), join(root, "plain-link")); + const expected = [join(root, "app.debug"), join(root, "app.pdb")]; + const selected = await inspect(join(root, "app")); + expect([...selected.debug_companions.paths].sort()).toEqual(expected); + expect(selected.debug_companions.skipped).toBe(0); + const directory = await inspect(root); + expect([...directory.debug_companions.paths].sort()).toEqual(expected); + }, + ); }); describe("ReleaseExposureReader acquisition and sibling review regressions", () => { diff --git a/src/artifacts/ReleaseExposureReader.ts b/src/artifacts/ReleaseExposureReader.ts index b0cc32fe6e..a607b1914a 100644 --- a/src/artifacts/ReleaseExposureReader.ts +++ b/src/artifacts/ReleaseExposureReader.ts @@ -2461,7 +2461,7 @@ const scanSiblings = async ( throw new AnalysisCancelledError(OPERATION, { cause: input.signal.reason, }); - if (entry.name === own || entry.isSymbolicLink()) continue; + if (entry.name === own) continue; const sibling = appendCallerPath(directory, entry.name); if (hasCompanionName(entry.name)) { state.companionPaths.add(sibling);