pack, publish: include entries on filesystems whose readdir reports DT_UNKNOWN - #38716
pack, publish: include entries on filesystems whose readdir reports DT_UNKNOWN#38716robobun wants to merge 8 commits into
Conversation
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
Comment |
|
Updated 12:05 PM PT - Aug 15th, 2026
❌ @robobun, your commit 2c6552f has some failures in 🧪 To try this PR locally: bunx bun-pr 38716That installs a local version of the PR into your bun-38716 --bun |
|
Status: ready for review at 2c6552f (CI for it pending; the previous revision's build had no failures). Reproduced on the current release build with the shim from the test (zeroes Final shape: an opt-in |
There was a problem hiding this comment.
LGTM — targeted fix following the same lstatat fallback pattern already used in install/prune.rs, walker_skippable.rs, node_fs.rs and GlobWalker.rs.
What was reviewed:
- Confirmed all five pack walkers plus the two publish readdir loops are covered; the scoped-package inner loop in
iterate_bundled_depsdoesn't readsub_entry.kind, so it needed no change. lstat(notstat) preserves the existing symlinks-are-not-packed contract on DT_UNKNOWN filesystems; the test asserts this.- Fast path unchanged when
d_typeis populated (single enum compare);lstatfailure keeps the entryUnknownand skipped as before. - Test shim asserts its own marker on stderr so it can't pass vacuously if bun's readdir stops going through libc
syscall().
Extended reasoning...
Overview
Fixes bun pm pack / bun publish silently dropping every file except package.json on filesystems whose readdir returns DT_UNKNOWN for every entry (FUSE/sshfs, some NFS, XFS with ftype=0). Adds a small entry_kind() helper to pack_command.rs that falls back to lstatat when the iterator kind is Unknown, and applies it at the six readdir consumers in the pack walkers plus the two in publish_command.rs (readme lookup and directories.bin recursion). Ships a Linux-only test that simulates the condition via an LD_PRELOAD shim interposing getdents64.
Security risks
None. This is a CLI-local directory walk; the only new syscall is an lstatat on entries the iterator already yielded, relative to the already-open directory fd. No user-controlled path parsing, no network, no auth surface changed.
Level of scrutiny
Low-to-medium. The Rust change is ~15 net lines of logic, entirely additive on the Unknown branch (normal filesystems hit only one extra enum comparison per entry). The pattern is copied verbatim from four existing in-tree call sites (install/prune.rs:1334, sys/walker_skippable.rs:69, runtime/node/node_fs.rs:6372, glob/GlobWalker.rs:1119), so there is no novel design here. The choice of lstat over stat is correctly reasoned (matches d_type semantics, keeps symlink handling identical across filesystem types) and the test explicitly covers it.
Other factors
- I checked that every readdir loop in the two touched files that inspects
entry.kindwas updated. The one loop that wasn't (the@scope/sub-iteration initerate_bundled_deps) never readssub_entry.kind, so it's correctly left alone. - The test is well-constructed per the repo's review rules: hermetic (local mock registry via
Bun.serve({port: 0})), asserts exact sorted tarball paths, covers all four walker variants plus a directory-only ignore pattern and a symlink, usestest.concurrent,skipIfs whenccis unavailable, and guards against becoming vacuous by asserting the shim's stderr marker. - Cross-platform: the PR notes
cargo checkpasses on Windows and macOS targets; the new branch is dead on Windows (its iterator never yieldsUnknown) but still compiles sincelstatat/kind_from_modeare cross-platform. - Existing
bun-pack.test.ts(76 tests) passes per the description; no behavior change on the normal-filesystem path. - No prior human or bot review comments to address; bug hunting system found nothing.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it adds a new public method to bun_sys and introduces an LD_PRELOAD-based C shim as a test technique, a human sign-off on those two choices would still be worthwhile.
What was reviewed:
resolve_kindmatches the existing lstat-fallback pattern inwalker_skippable.rs:69-70;as_zstr()/lstatat/kind_from_modeare all in scope and correctly typed on POSIX, and the Windows arm is a no-op returningself.kind.- Checked every
DirIterator::iterateloop in both files: the inner@scopeloop initerate_bundled_deps(line ~911) doesn't filter onkind, so it correctly needs noresolve_kindcall. - The test asserts on the shim's stderr marker before anything else, so it can't pass vacuously if bun stops routing getdents64 through libc
syscall();skipIf(!isLinux || !cc)gates it and the shim dir is disposed inafterAll.
Extended reasoning...
Overview
The PR fixes silent data loss in bun pm pack and bun publish on filesystems whose readdir returns DT_UNKNOWN for every entry (FUSE/sshfs, some NFS, XFS with ftype=0). It adds IteratorResult::resolve_kind(dir) to src/sys/lib.rs — an lstat fallback when kind == Unknown, no-op otherwise — and calls it at seven readdir loops across pack_command.rs (5 sites) and publish_command.rs (2 sites plus the readme lookup). A new Linux-only test compiles a small LD_PRELOAD shim in C that interposes libc syscall() and zeroes d_type in every getdents64 record, then asserts exact tarball contents for the plain tree walk, the files walk, bundledDependencies, and a bun publish against a local mock registry.
Security risks
None identified. The change only widens which entries the pack walkers consider from "File or Directory" to "File or Directory after lstat resolution"; symlinks remain excluded because lstat (not stat) is used, so packing behaves identically to a d_type-reporting filesystem. No new user-controlled input reaches a syscall path that wasn't already there.
Level of scrutiny
Medium. The call-site changes are mechanical and obviously correct — each is a one-line entry.resolve_kind(dir.fd) before the existing kind filter, and I confirmed the fallback matches the pattern already in src/sys/walker_skippable.rs. But two aspects warrant a maintainer's eyes: (1) resolve_kind is a new public method on bun_sys::dir_iterator::IteratorResult, a foundational crate — the placement and signature look right (the kind_from_dt comment already told callers to lstat, and this centralizes it), but adding shared API surface is a design decision; (2) the LD_PRELOAD C shim test pattern is novel for this repo and a maintainer should confirm it's acceptable long-term (it depends on bun continuing to route getdents64 through libc syscall(), which the test guards against with a stderr marker assertion, but the coupling is worth acknowledging).
Other factors
- The comment-cop bot left flags on the
resolve_kinddoc comment; the author trimmed it to three lines and responded that it's API documentation, not a workaround justification. I agree — the current three-line rustdoc is appropriate for a public method and the flag looks like a false positive, but two of those inline threads are still marked unresolved. - The PR description notes overlap with #38707 on the readme comparison line in
publish_command.rs; whichever lands second needs to keepresolve_kindin the merged line. - CI for the final revision (486b072) was still building at last timeline update; the first revision's CI passed on glibc and musl with the new tests running (not skipped).
- I verified the one readdir loop in the touched files that was not changed (the inner
@scopeloop initerate_bundled_deps) doesn't filter onkind, so leaving it alone is correct.
|
One correction to the automated review above, for whoever picks this up: the LD_PRELOAD shim is not a new technique in this test suite. Eleven existing test files compile a shim the same way (for example |
On filesystems that do not fill in d_type (FUSE, NFS, XFS with ftype=0) every readdir entry has kind Unknown. The pack tree walkers only accept File and Directory, so `bun pm pack` and `bun publish` produced a tarball containing package.json and the explicitly listed bins and silently dropped everything else, including bundledDependencies. The publish "directories.bin" walk also stopped recursing into subdirectories there. Resolve Unknown with lstat (not stat, so symlinks still resolve to SymLink and stay out of the tarball) before the kind is looked at, in every pack and publish readdir consumer.
The lstat fallback belongs next to the iterator that hands out Unknown in the first place: it can use the entry's NUL-terminated name directly instead of copying it, and publish no longer imports a readdir helper from pack. Windows never yields Unknown, so the method is the identity there.
Replace the per-entry resolve_kind() with an opt-in flag on WrappedIterator, applied in next(). walker_skippable and prune drop their own copies of the lstat fallback and set the flag instead; pack and publish set it on each iterator they create. The getdents64 d_type-zeroing shim moves out of the test into test/fixtures, with a harness helper so other tests can use it; the test gains an install-from-folder case covering the walker.
486b072 to
a02e49a
Compare
Compiling it synchronously from the first test body blocked the runner for as long as the compiler took (several seconds on a loaded machine), which counted against every concurrently started test and timed them out.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it adds new public API surface to bun_sys::dir_iterator (the shared readdir iterator) and refactors walker_skippable's control flow, a human look at the API shape would still be worthwhile — this is the pattern the follow-up sweep is meant to copy.
Checked: the walker_skippable behavioral change (unstat'able entries now yield as Unknown instead of being skipped) — every consumer (PackageInstall, FileCopier, Hardlinker, build_command) matches only File/Directory and continues otherwise, so it's equivalent.
Checked: resolve_unknown_kind borrows self.name.as_zstr() from the getdents buffer while the iterator state is untouched, and lstatat matches the semantics of the lstatat call being replaced in both walker_skippable and prune.rs.
Checked: the flag defaults to false in both iterate() constructors, so existing iterator users are unaffected; the seven pack/publish loops set it immediately after construction.
The four unresolved comment-cop flags are firing on the 3-line field doc and on pre-existing comments that were re-indented by the let ... else rewrite — not new prose.
Extended reasoning...
Overview
This PR fixes bun pm pack and bun publish on filesystems whose getdents64 returns DT_UNKNOWN for every entry (FUSE/sshfs, some NFS, XFS with ftype=0). It adds an opt-in resolve_unknown_entry_types flag to bun_sys::dir_iterator::WrappedIterator that lstats entries whose kind came back Unknown, sets it at the seven pack/publish readdir loops, and migrates the two existing private copies of the same fallback (walker_skippable, install/prune.rs) onto the shared flag. It ships an LD_PRELOAD C shim that zeroes d_type in every getdents64 record, exposed as dtUnknownReaddir in the harness, plus a test file covering pack (tree walk, files field, bundledDependencies), publish (directories.bin recursion, readme, tarball contents against a mock registry), and install of a file: dependency through the walker.
Security risks
None identified. The change adds an lstatat on entries that were previously discarded; input is directory entry names from getdents64. The LD_PRELOAD shim is test-only, compiled in a temp dir, and gated to Linux with a C compiler.
Level of scrutiny
This warrants human review. src/sys/lib.rs is core shared infrastructure — the directory iterator is used by the resolver, glob walker, installer, and every CLI command that reads a directory. The PR introduces new public API surface (resolve_unknown_entry_types) that is explicitly intended as the pattern for a follow-up sweep across bun create, fs.watch, fs.cp, shell ls -R, and other consumers, so the shape here is a design decision worth maintainer sign-off. The walker_skippable refactor also changes error-path behavior (an entry whose lstat fails now yields as Unknown rather than being silently skipped) — I verified every current consumer handles that identically, but it's the kind of subtle invariant a maintainer should confirm.
Other factors
- The author iterated the design three times (private helper → per-entry
resolve_kind→ iterator flag), and the final shape letswalker_skippableandprune.rsdrop their own copies — that's the right layering per REVIEW.md, but confirms this is a design choice rather than a mechanical fix. - The
walker_skippable.rsdiff is large but is mostly reindentation from clippy'slet ... elserequirement after thematchwas flattened; the pre-existingname_bufferNUL-terminator comment and skip-hash logic are unchanged. - Test coverage is thorough and asserts on a stderr marker so it fails loudly if bun stops going through the interposed
syscall()wrapper; the shim technique matches eleven existing tests. - CI on the latest revision (a02e49a → build #97656) was still building at the time of review.
- The four unresolved comment-cop inline flags are on the 3-line public-field doc comment and on pre-existing comments that moved due to reindentation; the author's earlier responses (that these are API docs, not workaround justifications) are accurate.
The sweep of the remaining readdir consumers (#38961) is where the walker's own resolution policy is being decided, so moving those two onto the iterator flag belongs there; this PR is the flag plus pack and publish. The install test case went with it.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it adds new public API surface to bun_sys::dir_iterator::WrappedIterator (a foundational iterator used across the codebase) and introduces a reusable harness helper plus C fixture, a human look at the API shape would still be worthwhile.
What was reviewed:
resolve_unknown_kindmirrors the existinglstatat+kind_from_modepattern already inwalker_skippable.rs:92-93; the new field defaultsfalsein bothiterate()constructors so existing callers are unaffected.- Checked that
name.as_zstr()iscfg(not(windows))-gated to match the call site, and that the borrowed name (into the getdents64 buffer) stays valid across thelstatatcall. - The seven flag-setting sites each precede a
kind != File && kind != Directoryfilter or a== Directoryrecursion check that would otherwise dropUnknown. - Note: the PR description still references the
walker_skippable/prune.rsconsolidation and a fifthbun installtest case, but those were backed out in 2c6552f — the diff now touches only the six listed files.
Extended reasoning...
Overview
The PR fixes bun pm pack and bun publish silently producing near-empty tarballs on filesystems whose readdir returns DT_UNKNOWN for every entry (FUSE, some NFS, XFS with ftype=0). It adds an opt-in resolve_unknown_entry_types: bool field to bun_sys::dir_iterator::WrappedIterator; when set, next() calls lstatat on any entry whose kind came back Unknown and stores the resolved kind. Seven readdir loops in pack_command.rs and publish_command.rs set the flag. A new LD_PRELOAD shim (test/fixtures/dt-unknown-readdir-shim.c) plus a harness helper (dtUnknownReaddir) simulate such a filesystem, and four test cases cover the tree walk, files field, bundledDependencies, and bun publish with directories.bin.
The final commit (2c6552f) backed out the walker_skippable.rs and install/prune.rs consolidation described in the PR body, so the diff is narrower than the description — only src/sys/lib.rs, the two CLI command files, the test, the fixture, and test/harness.ts.
Security risks
None identified. The change adds an lstatat on entries the process just read from a directory it already has open; no new trust boundary is crossed. The C shim is test-only, compiled from a checked-in fixture, and loaded only into spawned test subprocesses via LD_PRELOAD.
Level of scrutiny
Medium-high. The behavioural fix itself is small and defaults off, but src/sys/lib.rs is the foundational syscall wrapper layer and WrappedIterator is used by the resolver, glob walker, installer, and every CLI directory walk. Adding a public mutable field is a minor API-shape decision that the PR itself iterated on three times (per-site helper → per-entry method → iterator flag), which suggests it merits a maintainer's sign-off on the final shape. The new dtUnknownReaddir harness helper is also intended for reuse by a follow-up sweep, so its placement and interface are worth a human glance.
Other factors
- The
resolve_unknown_kindimplementation is byte-for-byte the same pattern already proven inwalker_skippable.rs(lstatat + kind_from_mode with thest_mode as Modecast), so correctness risk is low. - Both
iterate()constructors (POSIX and Windows) initialize the new field tofalse, and the Windows arm ofresolve_unknown_kindis a no-op, so no cross-platform behaviour change. - Tests assert on a stderr marker written by the shim, so they cannot pass vacuously if bun stops routing
getdents64through libc'ssyscall()wrapper; they usetest.concurrentwith isolated tempdirs and a localBun.serve({port: 0})mock registry. - The comment-cop bot fired repeatedly on doc-comment length; all threads are resolved and the current diff's comments are short.
- The PR description lists other readdir consumers with the same bug class (
bun create,fs.watch,fs.cp, shellls -R, etc.) as an intentional follow-up; whether that scope split is acceptable is a maintainer call.
Problem
ftype=0),bun pm packandbun publishproduce a tarball containing onlypackage.jsonplus the bins named inpackage.json; every other file and everybundledDependenciesentry is silently dropped, and the command exits 0.bun publishwithdirectories.binadditionally stops recursing into subdirectories of the bin directory on such a filesystem, so nested bins are missing from the published manifest.bun_sys's directory iterator mapsDT_UNKNOWNtoFileKind::Unknownand leaves resolving it to the caller (src/sys/lib.rs,kind_from_dt). Every readdir loop insrc/runtime/cli/pack_command.rs(iterate_project_tree,iterate_included_project_tree,add_entire_tree,add_bundled_dep, and thenode_modulesscan initerate_bundled_deps) discards anything that is notFileorDirectorybefore doing anything else, andpublish_command.rs'sdirectories.binwalk only recurses intoDirectory.Fix
bun_sys::dir_iterator::WrappedIteratorgetsresolve_unknown_entry_types(same name and shape as the existing flag onwalker_skippable::Walker). When set,next()lstats an entry whose kind came backUnknownand stores the result; an entry that cannot be stat'ed staysUnknown. Off by default: the resolver and the glob walker deliberately defer the stat to the entries they end up using. The Windows iterator always knows the kind, so the flag is a no-op there (it still compiles; checked forx86_64-pc-windows-msvcandaarch64-apple-darwin).match entry.kindarms andis_excluded()(which checks the kind for directory-only ignore patterns such asbuild/) all see the resolved kind. pack: skip bins reached through symlinks; publish: do not read the readme through a symlink #38707 edits one of the same lines infind_workspace_readme; whichever lands second keeps both.lstatrather thanstat:d_typedescribes the entry itself, so a symlink isDT_LNKon a normal filesystem and is never packed.lstatreports the same thing, so a project packs identically on both kinds of filesystem;statwould have started packing symlink targets only onDT_UNKNOWNfilesystems.d_typenever take the new path: the only added work is one boolean and one enum comparison per entry.install/bin.rs(linking and unlinkingdirectories.binbins, the install-side twin of the publish walk fixed here),PackageManager/updatePackageJSONAndInstall.rs(dangling.bincleanup onbun remove),PackageManager/PackageManagerResolution.rs(versions installed in the cache),cli/create_command.rs(template copy and listing),cli/bunx_command.rs(directories.binlookup),cli/init_command.rs,cli/pm_licenses_command.rs,node/path_watcher.rs(recursivefs.watch),shell/builtin/ls.rs(ls -R),node_fs.rs(fs.cp, via the node-tier iterator), and the two private copies of the fallback inwalker_skippable.rsandinstall/prune.rs. Resolve readdir entries of unknown type in the remaining consumers (create, bin links, bunx, fs.watch, cp, ls, init) #38961 currently targets an earlier per-entry shape of this PR and needs rebasing onto this one; once every consumer is either opted in or deliberately lazy, flipping the flag's default and keeping explicit opt-outs for the lazy ones is a small further step that PR can take, which would make the bug unreachable for new consumers. Not done here so that this PR stays reviewable on its own.test/cli/install/dt-unknown-readdir.test.ts. Neither this container nor CI has/dev/fuse, sotest/fixtures/dt-unknown-readdir-shim.c(exposed asdtUnknownReaddirin the harness so Resolve readdir entries of unknown type in the remaining consumers (create, bin links, bunx, fs.watch, cp, ls, init) #38961 can reuse it) is anLD_PRELOADshim that interposes libcsyscall(), which bun issuesgetdents64through, and zeroesd_typein every record. It writes a marker to stderr when it does, and every test asserts on the marker, so the tests cannot pass vacuously if bun stops going through the wrapper. Eleven existing tests use the sameLD_PRELOADtechnique;shell-pipe-read-fault.test.tsinterposessyscall()the same way. The shim is compiled asynchronously frombeforeAllwith an explicit hook timeout: compiling it from the first test body blocked the runner for as long as the compiler took, which on a loaded machine timed out the other concurrently started tests, and the default 5 s hook timeout is too short for a C compiler on a loaded machine. Cases: the plain tree walk with a directory-only.npmignorepattern and a symlink, thefilesfield walk with a directory-only exclude,bundledDependencies(scoped and unscoped), andbun publishagainst a local mock registry (attached tarball contents, nesteddirectories.binentries, readme). All four fail on the current release build (tarball has onlypackage.json; manifestbinlacks the nested entry) and pass with this change; earlier revisions of the same cases ran (not skipped) and passed in CI on the glibc and musl lanes. Expected contents were checked against what the same projects produce on a normal filesystem.bun-pack.test.ts(76) and the bin, readme and tarball tests ofbun-publish.test.ts;cargo clippyonbun_sysandbun_runtimeis clean.Background
d_type: the entry type a Linux/BSD readdir (getdents64) returns alongside each name. Filling it in is optional for the filesystem; those that do not returnDT_UNKNOWNfor every entry, and callers are expected tostatthe entry themselves when they need the type.lstatvsstat:statfollows a symlink and reports its target's type;lstatreports the symlink itself, which is whatd_typereports on filesystems that fill it in.bun_sys::dir_iterator::WrappedIterator: the low-level readdir loop shared by the installer, the glob walker and the CLI commands; it owns the directory fd and an 8 KiBgetdents64buffer, andnext()yields oneIteratorResult(a name borrowed from that buffer, NUL-terminated, plus a kind) at a time.iterate_project_treewalks the whole project whenpackage.jsonhas nofilesfield;iterate_included_project_treeplusadd_entire_treehandle thefilesfield;iterate_bundled_depsplusadd_bundled_depcopybundledDependenciesout ofnode_modules.bun publishbuilds its tarball with the same walkers and separately walksdirectories.binto fill in the manifest'sbinmap.Reproduction on the current release build
Project with
index.js,lib/a.js,lib/nested/b.js, abinentry, and two bundled dependencies undernode_modules, packed with the shim preloaded:Same project without the shim packs all 8 files.
bun publishunder the shim sendsbin: {"more": "bins/more", "a.js": "bins/a.js"}(missingbins/more/b.js) and a tarball containing onlypackage/package.json.Earlier shapes of this PR
pub(crate) fn entry_kindinpack_command.rsthat copied the entry name into aZBoxandlstated it, imported bypublish_command.rs.IteratorResult::resolve_kind(dir)inbun_sys, called from each loop. Resolve readdir entries of unknown type in the remaining consumers (create, bin links, bunx, fs.watch, cp, ls, init) #38961 was branched from this shape.walker_skippableandprune.rsmoved onto it and an install test case for the walker. Those parts were dropped again in 2c6552f: Resolve readdir entries of unknown type in the remaining consumers (create, bin links, bunx, fs.watch, cp, ls, init) #38961 is where the walker's own policy is being decided (it resolves unconditionally, since five of its six callers already opt in), so having both PRs rewrite the same function only created conflicts.