Skip to content

install: record the version of tarball and git packages in the lockfile, and audit them - #46143

Draft
robobun wants to merge 2 commits into
mainfrom
robobun/ca180673/audit-tarball-git-deps
Draft

robobun wants to merge 2 commits into
mainfrom
robobun/ca180673/audit-tarball-git-deps

Conversation

@robobun

@robobun robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun audit skips every package from a tarball, git or GitHub: No vulnerabilities found (checked 0 packages), exit 0.
  • Cause: the lockfile has no version for these rows, so collect_packages_for_audit skips them (src/runtime/cli/audit_command.rs:456 on main).

Fix

  • bun install records the package.json version in the row: a "version" key in bun.lock, an unused slot in bun.lockb.
  • bun audit and bun audit fix send the row under that name and version, or under the version in a registry tarball URL.
  • A row with no version is a warning, not an exit code. Bun fills it in from the cache when it next saves the lockfile.
  • Verified: test/cli/install/bun-audit.test.ts (45 new tests), 39 other install test files (Notes).

Background

  • A registry row has its version in the resolution (pkg@1.2.3). These rows have a URL or a commit there.
  • npm and pnpm send these packages too. A registry matches them by name only, so the report names the source of the copy, and a malware advisory for such a name is not counted.
  • Considered reading node_modules at audit time: a frozen CI install can have no copy.

Downsides

  • Private tarball and git package names now go to the advisory endpoint, as with npm.
  • bun audit warns about an older lockfile until Bun next saves it (bun install --lockfile-only does). An older Bun drops the key again.
  • Registry-only projects: same lockfile bytes, syscalls and allocations. +0.3% instructions in bun audit and in a lockfile save. +29 KB .text (Notes).
Notes

Cost for a project with registry packages only. Release builds of main 491aaad and of this head (6016ffc), built on the same machine on 2026-10-22. 2,000 packages, loopback registry. Instructions are user-mode counts over interleaved pairs, with the standard deviation of the pair differences.

main this PR
bun.lock 448,163 B the same bytes
bun.lockb 692,904 B the same bytes
bun audit, file syscalls 72 72
bun audit, allocation calls 8,431 8,431
bun audit, instructions (60 pairs) 32,617,517 32,713,193 (+95,676, +0.29%, sd 67,217)
bun pm hash (loads bun.lock), instructions (40 pairs) 29,566,753 29,587,256 (+20,504, sd 43,826)
bun install --lockfile-only (load and save), allocation calls 6,554 6,554
bun install --lockfile-only, instructions (40 pairs) 64,258,659 64,436,149 (+177,490, +0.28%, sd 6,011)
cold install of 50 packages, hoisted, instructions (20 pairs) 10,134,143 10,177,889 (+43,745, sd 163,217)
warm install of 50 packages, hoisted, instructions (20 pairs) 2,332,012 2,334,911 (+2,899, sd 81,860)
cold install of 50 packages, isolated, instructions (20 pairs) 11,818,091 11,750,026 (-68,066, sd 172,184)
warm install of 50 packages, isolated, instructions (20 pairs) 3,448,423 3,482,857 (+34,433, sd 275,339)
.text 81,973,349 B 82,002,789 B (+29,440 B)
  • The difference in bun audit grows with the number of packages: +27,000 instructions for 500 packages, +186,000 for 4,000, and none for one. That is about 47 instructions for each package.
  • This head changes no linker file, so the install rows show only the noise of the measurement.
  • In repeated runs, bun audit made 72 or 73 file syscalls on either build.
  • With 5 recorded tarball rows, bun audit makes the same 72 file syscalls.
  • Tools: perf_event_open for the instruction counts, ptrace for the syscall counts, gdb breakpoints on the mimalloc entry points for the allocation counts. perf, strace, valgrind and bloaty are not on the machine.

What is sent. Against a loopback registry, vul@1.0.0 with an advisory for <2.0.0:

dependency main this PR npm 11.16.0
vul@1.0.0 from the registry exit 1 exit 1 exit 1
tarball URL exit 0, sends {} exit 1, sends {"vul":["1.0.0"]} exit 1
file: tarball exit 0 exit 1 exit 1
git+file: repository exit 0 exit 1 exit 1
alias of a tarball URL exit 0 exit 1 exit 1
tarball URL, node_modules and cache deleted exit 0 exit 1 exit 1
  • A github: dependency has the same result in the new test (sends a package that came from GitHub, stub API).
  • A package is sent under the name and version of its package.json, as written there. A package with no version is not sent, as with npm (shouldAudit in @npmcli/arborist/lib/audit-report.js).
  • A tarball row whose URL is <registry>/<name>/-/<name>-<version>.tgz, for the registry of that name or for the public registry, is sent with the version in the URL. A released Bun wrote such rows when it migrated a yarn.lock, and bun add <that URL> writes one. The URL has to be exactly what Bun builds for that name and version.
  • bun audit fix lists a vulnerable tarball or git copy under pinned to a tarball or a commit, not changed, with its resolution. It counts as remaining. The key in --json is pinned.

Matches by name. A registry knows these copies by name and version only.

  • The report has one more line for such a copy: 1.0.0 from github:owner/vul#abc1234, matched by name.
  • A malware advisory (CWE-506) is about the bytes that the registry serves. When every copy of the name in the project is from a tarball, git or GitHub, Bun names it in a warning on stderr and does not count it. With a registry copy of the name in the project, it counts.
  • npm sends these packages and counts every match. [BUG] npm audits git dependencies npm/cli#5110 (open, Priority 2) reports the false matches. Yarn 4 sends only npm: packages (plugin-npm-cli/sources/npmAuditUtils.ts).

Rows with no recorded version. A bun.lock or bun.lockb from an older Bun:

  • bun audit: a warning that names each row that a dependency leads to, N not audited in the summary, and the exit code of the other packages. bun audit fix --dry-run --json lists the rows under unrecorded. --silent prints no warning.
  • When Bun saves the lockfile, a row with no version takes it from the package.json of its cache entry. The entry is named after the commit or the integrity of the row, so it has the bytes that the lockfile pins.
  • No install saves the lockfile only for this. overrides.test.ts (a bun.lock written by Bun 1.4 ... bun install and --frozen-lockfile say nothing) and bun-install-git-deps.test.ts (Nothing changed, so nothing is saved) test that. The second head of this PR saved once on a plain install and failed the first of them in CI.
  • bun install --lockfile-only saves the lockfile and changes nothing else. The warning names that command.
  • A lockfile that this PR wrote, then rewritten by an older Bun: the rows have no version again. Dependabot runs Bun 1.4.2 for every repository (bun/Dockerfile in dependabot-core), so each of its PRs drops the key, and the next save with a newer Bun writes it back.

Limits.

  • The version comes from the cache, so the package has to be there. After bun install it is.
  • A tarball row with no integrity has no version: the next install can get other bytes for it. The install that pins it saves the lockfile, with the version.
  • A row under a name that the package does not declare gets no version. The yarn.lock migrators name a row after the dependency or the repository. bun update <dependency> resolves it again under the declared name.
  • The cache entry of a GitHub row is named after the commit, not after the integrity.
  • A version longer than 256 bytes (node-semver MAX_LENGTH) is recorded as no version.
  • Only bun audit and bun audit fix read the record. Lockfile::package_version, the install-time security scanner and bun install --yarn still see the URL or the commit.

Migrated lockfiles. The package-lock.json and pnpm-lock.yaml migrators record the version that the other lockfile states, for a row that has a commit or an integrity. A package-lock node with no version stays unrecorded: the arborist fixture external-link-dep in this repository has such a node for abbrev@1.1.1. The yarn migrators record nothing, because yarn.lock does not state the name that the package declares. The save at the end of the migrating install does.

Questions for a maintainer.

  1. A row with no recorded version is a warning. The docs say that a package Bun cannot audit does not affect the exit code, and npm exits 0 for a lock entry with no version. REVIEW.md says that a security check fails closed. With exit 1, each Dependabot PR would fail the audit until Dependabot's Bun writes the key. bun audit --json prints {} and exits 0 for such a project, as on main.
  2. The version key in bun.lock, with no lockfileVersion bump. install: fix regressions and older bugs found after the package manager consolidation #45307 added hasInstallScript to the same object in the same way. An older Bun reads a lockfile with the key and ignores it. Should a plain bun install save an older lockfile once to add the key? This PR does not, because of the two tests above.
  3. A match by name on a tarball, git or GitHub copy fails the exit code, except for a malware advisory. Is that the wanted default?

Self-review. A self-review of the first head raised 37 concerns. This head addresses 34. Not done: (1) An install does not copy bin and hasInstallScript from the package.json into a migrated row. That is a separate bug with its own fix. (2) A package-lock node with no version is not read as a package with no version, for the reason under Migrated lockfiles. (3) Rows with no version keep their own unrecorded list. unaudited is a list of registries that did not answer. A self-review of this head is in progress.

Tests. On a debug build of this head, with a raised timeout for each test: bun-audit.test.ts (302 tests) and 39 other files in test/cli/install pass. They are overrides, bun-install, bun-install-registry, bun-install-git-deps, bun-install-tarball-integrity, bun-install-lifecycle-scripts, bun-lock, bun-lockb, bun-workspaces, bun-add, bun-add-catalog, bun-add-filter, bun-update, bun-update-transitive, bun-update-security-provider, bun-dedupe, bun-pm-scan, bun-pm-why, bun-pm-fetch, hoist, catalogs, lockfile-version-2, minimum-release-age, redacted-config-logs, GHSA-pfwx-36v6-832x, migrate-bun-lockb-v2, the three frozen-lockfile files, and the 10 files in migration/. On a release build of main 491aaad, the 45 tests of the new block fail. On a release build of this head they pass. bun-types.test.ts passes (24 tests). The three next-pages snapshots were not run.

Related. #45668 changes how bun audit ranks an unknown severity. It touches other regions of audit_command.rs. #46116 adds an early return to collect_packages_for_audit for a lockfile with no package. With both, that return has to keep the list of rows with no version. A later PR will match each advisory range against each installed copy. It builds on audit_rows. A migrated tarball or git row also has no bin: that is a separate bug with its own fix.

Debug dump. The lockfile debug dump prints package_json_version where it printed man_dir, a slot that nothing assigned. That renames one line per package in five snapshot files. The three next-pages snapshots were rewritten by a script.

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:01 PM PT - Oct 21st, 2026

✅ @robobun, your commit 6016ffc332b27df62b1634deaaa9a371f82df67a passed in Build #130524! 🎉


🧪   To try this PR locally:

bunx bun-pr 46143

That installs a local version of the PR into your bun-46143 executable, so you can run:

bun-46143 --bun

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft. The third head (6016ffc) is pushed. A self-review of it is in progress.

What changed from the first head.

  1. A row with no recorded version is a warning. It does not change the exit code.
  2. When Bun saves the lockfile, a row with no version takes it from the package.json of its cache entry. No install saves the lockfile only for that. bun install --lockfile-only saves it and changes nothing else.
  3. A tarball row at the registry's own URL (<registry>/<name>/-/<name>-<version>.tgz) is audited from the URL.
  4. The report names the source of a copy that a registry matched by name. A malware advisory for such a name is a warning, and it is not counted.
  5. The yarn migrators record no version, and Bun records none under a name that the package does not declare.
  6. bun audit fix lists these copies under pinned to a tarball or a commit, not changed.

The second head (0573dd0) saved the lockfile once on a plain bun install to add the version. CI showed that this breaks test/cli/install/overrides.test.ts (a bun.lock written by Bun 1.4 > the rule is a tarball URL: bun install and --frozen-lockfile say nothing). Item 2 is the fix.

How it was reproduced. A project depends on vul@1.0.0 through a tarball URL. A loopback registry has an advisory for vul <2.0.0. On main, bun audit sends {} to the advisory endpoint, prints No vulnerabilities found (checked 0 packages) and exits 0. With this PR it sends {"vul":["1.0.0"]}, prints the advisory and exits 1. The same holds for a file: tarball, a git repository and a GitHub dependency.

The tests are in the tarball and git dependencies block of test/cli/install/bun-audit.test.ts. All 45 of them fail on a release build of main 491aaad and pass on debug and release builds of this branch. overrides.test.ts and 38 other install test files also pass on that debug build.

…in the lockfile

A tarball, git or GitHub row has a URL or a commit where a registry row
has a version. `bun install` now records the `version` of the package's
package.json in the row: a `"version"` key in bun.lock, and a slot of
`Meta` that nothing assigned in bun.lockb. There is no lockfileVersion
bump: an older bun ignores the key.

A row that has no version gets it when the lockfile is saved, from the
package.json of the row's cache entry. That entry is named after the
commit or the integrity of the row, so it has the bytes that the
lockfile pins. The version is recorded only when the package declares
the name that the row has. No install saves the lockfile only to add a
version.

The package-lock.json and pnpm-lock.yaml migrators record the version
that the other lockfile states, for a row that has a commit or an
integrity.
`bun audit` and `bun audit fix` skipped every row that is not a registry
package. They now send a tarball, git or GitHub row under its name and
its recorded version. A tarball row at the URL that the registry serves
its name from is sent with the version in the URL.

A row with no recorded version is named in a warning and counted as
`not audited`. It does not change the exit code.

A registry matches these copies by name only. The report says where
such a copy came from. A malware advisory (CWE-506) for a name that the
project has from a tarball, git or GitHub only is a warning, and it is
not counted.

`bun audit fix` lists a vulnerable tarball or git copy under `pinned to
a tarball or a commit, not changed`.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants