install: reject bun.lockb packages whose resolution tag no writer produces
Problem
- A
bun.lockbpackage whose resolution tag byte is 0 (Uninitialized) or 100 (SingleFileModule) loads.bun installprintsSaved lockfile, exits 0, installs nothing for it, and writes it back out. A debug build panics instead:unreachable!()inResolutionType::eql(src/install/resolution.rs:525). Install fuzzer ledger entry 16544. - Cause: the tag check in
load_fields(src/install/lockfile/Package.rs) accepted every tag constant. Dependencies on such a package keep a valid package id, soverify_resolutionscounts them as resolved. No installer has a path for either tag.
Fix
Tag::belongs_in_lockfilelists the nine tags a writer produces. The bun.lockb loader rejects every other byte withinvalid resolution tag, as it did for unknown bytes:bun installprintsIgnoring lockfileand resolves again,bun bun.lockbexits 1. The bun.lock writer, which skipped the same two tags with its own list, uses the helper too.- Correct because a package is appended only once it is resolved, and
SingleFileModuleis a placeholder with no producer. The one way to get such a file today is the yarn.lock migration bug that the open #39001 fixes, withsaveTextLockfile = false. That file misses a dependency on every install. Now the next install resolves it again from package.json. - Verified:
test/cli/install/bun-lockb.test.ts, four new cases (both tags, format v3 through the registry and format v2 through the checked in fixture). All four fail without the src change.
Background
bun.lockbstores packages as columns. Eachresolutionentry is a tag byte plus a union that says where the package comes from (npm, folder, git, workspace, ...). Tag 0 is the state of a package that is not resolved yet.load_fieldscopies each column from the file and checks the enum bytes first, because the rest of the loader matches on them.- On a load error
bun installwarnsIgnoring lockfileand resolves from package.json.--frozen-lockfilefails withlockfile had changes.
Notes
Debug build panic path for tag 0: ResolutionType::eql <- Lockfile::get_package_id <- the debug_assert! in Lockfile::append_package_with_id <- Package::clone <- Lockfile::clean_with_logger. The release build skips the assertion and carries the package through. Tag 100 has an eql arm and reaches the _ arm of PackageInstaller::install_package_with_name_and_resolution instead, which panics in debug builds and counts the package as installed in release builds. isolated_install treats the two tags as one class as well.
The format v2 migration arms for the two tags return the same error as the loader. History of this PR: the first version rejected tag 0 only. The self-review found that tag 100 reproduces the report one byte away. The comment that explained the loader's list was then flagged as too long, so the list moved next to the tag constants, the writer shares it, and the loader reads the tag offset with offset_of! like the Meta, Bin and Scripts checks next to it.
Before this change bun install --frozen-lockfile already refused the mutated file (lockfile had changes). It accepted the file that the plain install saved afterwards, so the broken state was sticky. The new tests check that the saved file has no such tag left.
Suites run with the debug build: bun-lockb, bun-lock, bun-install, migrate-bun-lockb-v2, bun-workspaces, bun-link, bun-add, bun-update, bun-remove, catalogs, migration/migrate, and the port-era-markers, pre-port-identifiers and byte-search source lints. bun-install.test.ts: 224 pass, 14 fail. The failures are the bitbucket, gitlab and external tarball URL tests plus --registry CLI flag. All of them fail the same way on the released build in this environment (no network). bun-link.test.ts has one failure, should link dependency without crashing. It expects exact stdout and gets the debug only debug_trace of an install failure. Both are unrelated to this change.
The fuzz report had a second part: a bun.lock whose "packages" key names one package (xbin) while the tuple id names another (p13@1.1.0) installs p13 at node_modules/xbin with exit 0. This PR does not change that, on purpose:
- The key is the install path and the tuple is the package that lives there. They differ in every lockfile with an
npm:alias ("my-alias": ["no-deps@1.0.0", ...], checked with the released build) and with overrides. A check on the key name rejects those lockfiles. - A check of the dependency's real name against the package name is not sound either.
enqueue_dependency_with_main_and_success_fnredirects a plain dependency to a same namednpm:alias declared elsewhere in the tree (known_npm_aliases). When the alias is removed later, the redirected edge survivesclean_with_logger. So bun itself writes a lockfile in which a plainfooedge points at a package namedbar, with no alias or override left to explain it.--frozen-lockfilewould then fail on a lockfile bun wrote. - The version is not checked today either: a
bun.lockthat pinsno-deps@2.0.0under a package.json that says1.0.0installs 2.0.0 under--frozen-lockfile(checked with the released build). Locked edges are trusted as a whole. Validating them against package.json is a feature in the area of #33632, not a loader bug.
The report's third point, never exit 0 with a root dependency bound to a package that cannot be installed, has two producers: this loader, fixed here, and the yarn.lock migration, fixed by the open #39001. A release mode check at the append_package* entry points would also cover a future producer. It is left out here because both known producers are covered.
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-lockb.test.ts