Skip to content

Delete PathString#31579

Merged
Jarred-Sumner merged 10 commits into
mainfrom
farm/83392263/delete-pathstring
Jun 2, 2026
Merged

Delete PathString#31579
Jarred-Sumner merged 10 commits into
mainfrom
farm/83392263/delete-pathstring

Conversation

@robobun

@robobun robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator

What

Deletes the packed-pointer type bun_core::PathString (a &[u8] crammed into a u64/u128) and replaces every use with the byte-slice idioms the codebase already maintains for these roles.

Why it's clean to delete

PathString conflated two distinct roles behind one Copy type whose ownership was a container-level contract rather than type-enforced:

  1. a cheap borrow of arena/process-lifetime &'static [u8], and
  2. an owned heap buffer (via init_owned/deinit_owned), freed by a containing struct's Drop.

The codebase already has purpose-built types for both, and src/resolver/lib.rs even carried a TODO(port) asking for exactly this once slice() could return &'static [u8] directly.

How each site was replaced

  • Arena / process-lifetime pathsbun_ptr::Interned (a Copy, #[repr(transparent)] &'static [u8] proof type): resolver Entry.abs_path & EntryCache.symlink, router Route.{match_name,public_path,abs_path} (stored in SoA columns), and the test-runner file lists (Scanner, Coordinator, runner, ChangedFilesFilter, test_command). This deletes the path_string_static and arena_slice lifetime-widening shims — they only existed because PathString::slice() tied the borrow to &self.
  • Borrowed-but-not-'staticbun_core::RawSlice<u8>: bundler EntryPoint.output_path (AST-lifetime, SoA column), dir_iterator's entry name, and node::PathOrBuffer::Path.
  • Ephemeral borrows&[u8]/&ZStr: bun_sys/bun_io PathOrFileDescriptor::Path (compiler-checked lifetimes), RuntimeTranspilerCache::save/from_file_with_cache_file_path (&ZStr, keeping the NUL-termination invariant explicit), patch.
  • Owned sites → owning types, removing the manual init_owned/deinit_owned footgun:
    • Store.Bytes.stored_nameBox<[u8]> (freed by Bytes's Drop).
    • PathLike::Stringbun_ptr::cow_slice::CowSlice<u8> (owned-or-borrowed; frees its buffer in PathLike's Drop, so the blob Store no longer frees it explicitly). PathLike::clone dupes an owned path and shares a borrowed one.
    • async readdir-recursive task's root_path/basename → owned Box<[u8]>, dropping the previous Box::leak + manual Box::from_raw reconstruct.

Net behavior-preserving; −208 lines overall (deletes PathString.rs, two shims, and the leak/reconstruct dance).

Verification

  • Full debug (ASAN) bun bd and build:release both compile and link.
  • Targeted suites pass (release): test/js/node/fs/fs.test.ts (readdir recursive incl. the owned task buffers), blob.test.ts + workers (named File/Blob serialize round-trip — the stored_name + owned PathLike::String paths), bundler, filesystem_router, resolver symlink/realpath, transpiler-cache.
  • Added an ASAN-gated test (test/js/web/fetch/blob.test.ts) that round-trips a named File through structuredClone in a loop under GC pressure, exercising the stored_name (Box<[u8]>) and owned PathLike::String (CowSlice) alloc/free paths — a double-free or leak there is a hard crash under bun bd (ASAN). Passes on the ASAN build.

Note: this is a behavior-preserving refactor, so the test is coverage for the new ownership model (it passes both before and after), not a fail-before-fix regression.

Footprint tradeoff

PathString's packing only saved space on macOS/BSD (where it was a u64); on Linux/Windows it was already a u128 == fat-pointer width. Interned/RawSlice<u8> are fat pointers (16 bytes), so on macOS/BSD the resolver Entry/EntryCache and router path columns grow 8 bytes per field. On Linux/Windows there's no footprint change. This is the known price of removing the packing; flagging it in case the per-Entry footprint on macOS matters.

Related PRs (overlap)

Replace the packed ptr+len `bun_core::PathString` with the existing
byte-slice idioms and remove the type.

- Arena / process-lifetime paths (resolver `Entry.abs_path` &
  `EntryCache.symlink`, router `Route` path columns, test-runner file
  lists) -> `bun_ptr::Interned`. This deletes the `path_string_static`
  and `arena_slice` lifetime-widening shims, which only existed because
  `PathString::slice()` tied the borrow to `&self`.
- Bundler `EntryPoint.output_path`, dir_iterator entry name, and the
  `node::PathOrBuffer` path variant -> `bun_core::RawSlice<u8>`
  (borrowed, outlives-holder).
- `bun_sys`/`bun_io` `PathOrFileDescriptor::Path` -> `&[u8]` (ephemeral
  borrow, compiler-checked).
- Owned sites -> owning types, removing the manual `init_owned`/
  `deinit_owned` contract: `Store.Bytes.stored_name` -> `Box<[u8]>`
  (freed by `Bytes`'s Drop), `PathLike::String` -> owned-or-borrowed
  `bun_ptr::cow_slice::CowSlice<u8>` (freed by `PathLike`'s Drop, so the
  blob `Store` no longer frees it explicitly), and the async
  readdir-recursive task's `root_path`/`basename` -> owned `Box<[u8]>`
  (drops the leak/reconstruct dance).

Net change is behavior-preserving.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Status: opened. Deletes bun_core::PathString entirely (37 files, −208 lines net).

  • Borrow sites → bun_ptr::Interned (arena/'static) or bun_core::RawSlice<u8> (holder-lifetime) or &[u8]/&ZStr (ephemeral). Deleted the path_string_static + arena_slice shims.
  • Owned sites → owning types: Bytes.stored_nameBox<[u8]>, PathLike::Stringbun_ptr::cow_slice::CowSlice<u8>, readdir-recursive task paths → Box<[u8]>. Removed the manual init_owned/deinit_owned contract (the owned payloads now self-free via Drop).

Verified: full bun bd (ASAN) + build:release build & link; fs/blob/worker/bundler/router/resolver/transpiler-cache suites pass on release; added an ASAN-gated structuredClone round-trip test covering both owned-buffer paths — Bytes.stored_name (Box<[u8]>) and PathLike::String (CowSlice). Behavior-preserving refactor. macOS/BSD footprint note + related-PR overlap (#30728, #30109) in the PR description.

Post-merge-with-main verification (781779f): re-verified the branch after the main merge — it compiles clean (bun bd), the ownership test passes, cargo clippy --workspace is green under the new clippy.toml, and zero functional PathString references remain (only comments + the unrelated EventPathString alias).

CI: the diff is green; each red has been a different known-flaky/infra lane. clippy + miri + Format + Lint all pass on every run. The rotating Buildkite reds across runs: macOS PTY timeout (terminal.test.ts, CI-tagged flaky), streams RSS-threshold with a negative delta, an install-registry flake, and on the latest run a single Windows-2019 Cannot find package 'duckdb' (the native-binary package failed to install on that agent — only that one test errored; every other package-resolving test on the lane passed, so it's not the resolver changes). Ready to merge on a green re-run.

The structuredClone ownership test shares blob.test.ts with a pre-existing
RSS-threshold leak test that flakes on the debug build (its `isASAN`
threshold check keys on the binary being named `bun-asan`, but `bun bd`
produces `bun-debug`, so the strict non-ASAN threshold is applied under
ASAN's inflated RSS). Isolating the ownership test keeps it runnable
without that unrelated flake.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f236d3a-9342-4c79-be80-da866cdef638

📥 Commits

Reviewing files that changed from the base of the PR and between 1c484f2 and 781779f.

📒 Files selected for processing (12)
  • src/bun_core/lib.rs
  • src/io/lib.rs
  • src/resolver/lib.rs
  • src/resolver/resolver.rs
  • src/runtime/cli/build_command.rs
  • src/runtime/cli/test/ChangedFilesFilter.rs
  • src/runtime/cli/test/parallel/Coordinator.rs
  • src/runtime/cli/test/parallel/runner.rs
  • src/runtime/cli/test_command.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/blob/Store.rs
  • src/sys/lib.rs

Walkthrough

Removes the PathString packed type and replaces usages across the repo with RawSlice, Box<[u8]>, CowSlice, Interned, or ZStr depending on ownership/lifetime; updates IO, resolver, router, node fs, blob/store, JSC, bundler, and test infra.

Changes

PathString removal and type replacement refactoring

Layer / File(s) Summary
Core PathString type removal and module reorganization
src/bun_core/lib.rs, src/bun_core/string/mod.rs, src/bun_core/string/PathString.rs
Removes PathString and its crate-root re-export; updates string module exports to expose other string utilities.
System I/O layer: PathOrFileDescriptor lifetime parameterization
src/sys/lib.rs, src/io/lib.rs, src/io/openForWriting.rs
Make PathOrFileDescriptor<'a>::Path(&'a [u8]); update write_file_with_path_buffer and open-for-writing call sites to accept borrowed byte slices.
Bundler and linker I/O path changes
src/bundler/LinkerGraph.rs, src/bundler/OutputFile.rs, src/bundler/linker_context/writeOutputFilesToDisk.rs
Entry-point output_path uses RawSlice<u8>; file write destinations pass borrowed slices to updated PathOrFileDescriptor.
Resolver filesystem caching: switch to Interned
src/resolver/fs.rs, src/resolver/lib.rs, src/resolver/resolver.rs
Switch symlink/abs-path caching to bun_ptr::Interned and construct cached values with Interned::from_static(...); remove path_string_static.
Router path storage: switch to Interned
src/router/Cargo.toml, src/router/lib.rs
AbsPath, Route.match_name, and Route.public_path change to Interned; parsing/indexing/dedupe use as_bytes(); add bun_ptr workspace dependency.
Test infrastructure: switch to Interned for test file tracking
src/runtime/cli/test/Scanner.rs, src/runtime/cli/test/ChangedFilesFilter.rs, src/runtime/cli/test_command.rs, src/runtime/cli/test/parallel/Coordinator.rs, src/runtime/cli/test/parallel/runner.rs
Test file collections and APIs move from PathString to Interned; use as_bytes() for comparisons, sorting, and worker dispatch.
Directory iteration: switch to RawSlice for borrowed entry names
src/runtime/node/dir_iterator.rs
IteratorResult.name becomes RawSlice<u8> and exposes name_assume_z() for NUL-terminated views; platform iterators updated.
PathOrBuffer enum: switch to RawSlice
src/runtime/node/types.rs
PathOrBuffer::Path payload changes to RawSlice<u8>.
Async filesystem operations: heap-owned Box<[u8]> for path storage
src/runtime/node/node_fs.rs
Owned recursive readdir/root/basename buffers use Box<[u8]>; ZStr views are created from boxed buffers; cleanup simplified.
PathLike::String: switch to CowSlice for owned/borrowed representation
src/jsc/node_path.rs, src/runtime/shell/builtin/cp.rs, src/runtime/shell/builtin/mkdir.rs
PathLike::String uses CowSlice<u8>; Default/Clone/Drop and sizing updated; call sites construct CowSlice wrappers.
Web Blob and store serialization: Box<[u8]> and CowSlice for stored names
src/jsc/webcore_types.rs, src/runtime/webcore/Blob.rs, src/runtime/webcore/blob/Store.rs, src/runtime/api/output_file_jsc.rs
store::Bytes.stored_name becomes Box<[u8]>; constructors, Drop, and structured-clone serialize/deserialize now use boxed slices and CowSlice for file-path payloads.
JSC caching and hot reload: ZStr and Interned
src/jsc/RuntimeTranspilerCache.rs, src/jsc/hot_reloader.rs
Transpiler cache functions accept &ZStr; hot reloader uses Interned and as_bytes() for path bytes.
Output file and JS bundle handling: CowSlice and boxed slices
src/runtime/api/js_bundle_completion_task.rs, src/runtime/api/standalone_graph_jsc.rs, src/runtime/cli/build_command.rs
Saved-file and sourcemap paths use CowSlice/boxed slices and pass slices directly to PathOrFileDescriptor.
Miscellaneous updates and utilities
src/runtime/webcore/FileSink.rs, src/patch/lib.rs, src/jsc/webcore_types.rs
Various import/usage updates to use byte slices, as_bytes(), and boxed-slice ownership for names.
New integration test
test/js/web/fetch/blob-file-name-ownership.test.ts
Adds a test verifying structuredClone preserves File name/size through GC and round-trip cloning.

Possibly related PRs

  • oven-sh/bun#31210: Modifies src/runtime/webcore/Blob.rs structured-clone handling; related to stored-name ownership/serialization.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Delete PathString' directly and clearly describes the main objective of the changeset—removing the PathString type and its usages throughout the codebase.
Description check ✅ Passed The description comprehensively explains what PathString is, why it's being deleted, how each usage is replaced, verification steps, and footprint tradeoffs—all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/sys/lib.rs`:
- Around line 9225-9231: The function write_file_with_path_buffer is moving
fields out of a shared &WriteFileArgs by destructuring args.data and matching
args.file; make WriteFileData<'a> and PathOrFileDescriptor<'a> implement Copy
(e.g. add #[derive(Clone, Copy)] to their definitions) so they can be moved from
a & reference, or alternatively change the code in write_file_with_path_buffer
to pattern-match by reference (match &args.file and destructure args.data by
reference) to avoid moving out of a borrowed value; update the types or the
match/destructure in write_file_with_path_buffer accordingly, referencing
PathOrFileDescriptor, WriteFileData, WriteFileArgs, args.data and args.file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1a0972b2-6d88-4302-aeb8-fa34691afa86

📥 Commits

Reviewing files that changed from the base of the PR and between 6162fb2 and edd4589.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (38)
  • src/bun_core/lib.rs
  • src/bun_core/string/PathString.rs
  • src/bun_core/string/mod.rs
  • src/bundler/LinkerGraph.rs
  • src/bundler/OutputFile.rs
  • src/bundler/linker_context/writeOutputFilesToDisk.rs
  • src/io/lib.rs
  • src/io/openForWriting.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • src/jsc/hot_reloader.rs
  • src/jsc/node_path.rs
  • src/jsc/webcore_types.rs
  • src/patch/lib.rs
  • src/resolver/fs.rs
  • src/resolver/lib.rs
  • src/resolver/resolver.rs
  • src/router/Cargo.toml
  • src/router/lib.rs
  • src/runtime/api/filesystem_router.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • src/runtime/api/output_file_jsc.rs
  • src/runtime/api/standalone_graph_jsc.rs
  • src/runtime/cli/build_command.rs
  • src/runtime/cli/test/ChangedFilesFilter.rs
  • src/runtime/cli/test/Scanner.rs
  • src/runtime/cli/test/parallel/Coordinator.rs
  • src/runtime/cli/test/parallel/runner.rs
  • src/runtime/cli/test_command.rs
  • src/runtime/node/dir_iterator.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/types.rs
  • src/runtime/shell/builtin/cp.rs
  • src/runtime/shell/builtin/mkdir.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/FileSink.rs
  • src/runtime/webcore/blob/Store.rs
  • src/sys/lib.rs
  • test/js/web/fetch/blob.test.ts
💤 Files with no reviewable changes (2)
  • src/bun_core/string/PathString.rs
  • src/bun_core/string/mod.rs

Comment thread src/sys/lib.rs
Comment thread src/runtime/api/output_file_jsc.rs Outdated
The SavedFile::to_js comment still described the old PathString
ownership (Store::drop freeing via deinit_owned); update it to match
the Saved arm: the owned PathLike::String (a CowSlice) frees itself in
PathLike::drop.
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun fix conflicts

…pathstring

# Conflicts:
#	src/jsc/node_path.rs
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Merged latest main (commit 84bd8d8) to resolve conflicts. The only conflict was PathLike's Drop impl — main added Buffer unpin logic (b.pinned / buffer.unpin()) in the arm I'd collapsed; resolved by keeping both as separate arms (String no-op since CowSlice self-frees on field drop, plus main's new Buffer unpin). node_fs.rs/types.rs auto-merged cleanly; PathString remains fully deleted. Rebuilt (bun bd, ASAN) — compiles & links; ownership test + Buffer-path fs smoke pass. Now MERGEABLE.

@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up for whoever merges: this is a behavior-preserving refactor — deleting PathString and swapping in the existing idioms (Interned, RawSlice<u8>, CowSlice<u8>, Box<[u8]>, &[u8]/&ZStr). I audited every owned site (PathLike::String, Bytes::stored_name): the old PathString ownership (a Copy packed ptr+len with the owning contract on the container) was used correctly at every call site — all pathlike.clone() uses feed transient, synchronously-consumed fs-op args whose Drop was a no-op, so no reachable double-free or leak existed on the old code. The new owning types are equally correct, just type-enforced instead of contract-enforced.

Because there's no behavioral change, there's no regression it "fixes" — the added test (test/js/web/fetch/blob-file-name-ownership.test.ts) is ASAN coverage for the owned-buffer alloc/free paths (structuredClone of a named File), a guard against future regressions, not a fail-before-fix reproduction.

State: branch is up to date with main (merged), bun bd (ASAN debug) + build:release both compile and link, and the targeted suites (fs/blob/worker/bundler/router/resolver/transpiler-cache) pass. Ready for a human merge.

Comment thread src/runtime/node/node_fs.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/node/node_fs.rs (1)

3972-3979: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate buffer before coercing length.

This reorders fs.read() argument validation. A non-TypedArray buffer can now trigger length coercion first, which changes the thrown error and allows user-defined valueOf/toString side effects on the later arg before rejecting the invalid buffer.

Proposed fix
             let length_float: f64 = if let Some(arg) = arguments.next_eat() {
                 arg.to_number(ctx)?
             } else {
                 0.0
             };
-            let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| {
+            let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| {
                 ctx.throw_invalid_argument_type_value(b"buffer", b"TypedArray", buffer_value)
             })?;
-            let length_float: f64 = if let Some(arg) = arguments.next_eat() {
-                arg.to_number(ctx)?
-            } else {
-                0.0
-            };
             let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| {
                 ctx.throw_invalid_argument_type_value(b"buffer", b"TypedArray", buffer_value)
             })?;
+            let length_float: f64 = if let Some(arg) = arguments.next_eat() {
+                arg.to_number(ctx)?
+            } else {
+                0.0
+            };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node/node_fs.rs` around lines 3972 - 3979, In the fs.read
argument handling, validate and convert the buffer before coercing the optional
length: call Buffer::from_js(ctx, buffer_value) and handle
ctx.throw_invalid_argument_type_value(b"buffer", b"TypedArray", buffer_value)
first (the Buffer::from_js call currently assigned to `buffer`), and only then
read/coerce the length via arguments.next_eat() and arg.to_number(ctx) into
`length_float`; this ensures `buffer_value` is checked prior to any potential
side-effecting coercions (avoid calling to_number on a later arg before
`Buffer::from_js` succeeds).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/web/fetch/blob-file-name-ownership.test.ts`:
- Line 35: The test currently asserts expect(stderr).toBe("") which can fail on
ASAN builds due to a known startup warning; before asserting, filter out any
stderr lines that start with "WARNING: ASAN interferes" (e.g. using the existing
stderr variable: split into lines, filter with line =>
!line.startsWith("WARNING: ASAN interferes"), then rejoin or check the filtered
array) and then assert the filtered stderr is empty using the existing
expect(...).toBe(""); update the assertion that references stderr to use this
filtered result.

---

Outside diff comments:
In `@src/runtime/node/node_fs.rs`:
- Around line 3972-3979: In the fs.read argument handling, validate and convert
the buffer before coercing the optional length: call Buffer::from_js(ctx,
buffer_value) and handle ctx.throw_invalid_argument_type_value(b"buffer",
b"TypedArray", buffer_value) first (the Buffer::from_js call currently assigned
to `buffer`), and only then read/coerce the length via arguments.next_eat() and
arg.to_number(ctx) into `length_float`; this ensures `buffer_value` is checked
prior to any potential side-effecting coercions (avoid calling to_number on a
later arg before `Buffer::from_js` succeeds).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c0def94e-3232-40b5-aba5-4af7fb152614

📥 Commits

Reviewing files that changed from the base of the PR and between edd4589 and 60b49d4.

📒 Files selected for processing (5)
  • src/jsc/node_path.rs
  • src/runtime/api/output_file_jsc.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/node/types.rs
  • test/js/web/fetch/blob-file-name-ownership.test.ts

Comment thread test/js/web/fetch/blob-file-name-ownership.test.ts Outdated
The owned-buffer ownership test runs a subprocess and asserts empty
stderr. On ASAN debug builds the subprocess emits a startup
'WARNING: ASAN interferes ...' line; filter it out so the assertion
still catches a real ASAN double-free report without flaking.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Re the outside-diff fs.read argument-order note (node_fs.rs ~3972): that reorder is from #31559 (the hardening round on main, commit bb94ed4) and arrived here via the merge — it's not part of this PR's PathString work, so leaving it as-is. The ASAN-stderr test nit is applied in 226b623.

clippy::undocumented-unsafe-blocks requires the // SAFETY: comment on
the line immediately preceding the unsafe block. Two detach_lifetime
blocks had their comment separated by an intervening let binding.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up for review — the precise ownership invariant, since this is the crux of whether the refactor is safe:

On main, PathLike::String wrapped a Copy PathString and its PathLike::Drop was a no-op. The owned buffer was freed in exactly one place — Store::drop for Data::File called stored.pathlike's deinit_owned(). So the invariant was:

  • Owned PathLike::String (from PathString::init_owned) lives only inside a File store → freed once by Store::drop.
  • file.pathlike.clone() (Blob.rs rf_args.path, the copy/stat arg sites) bitwise-copies the packed ptr, but those clones are transient fs-op args whose PathLike::Drop is a no-op → no double free.
  • PathString::init(...) sites (node_fs mkdir_recursive args, shell cp/mkdir) are borrowed and synchronously consumed.

The refactor moves that same single-free to the type: PathLike::String(CowSlice<u8>) self-frees in Drop when owned, so Store::drop's Data::File arm becomes empty. Every owned construction site (output_file_jsc, standalone_graph_jsc, the Blob name/write paths) now uses CowSlice::init_owned; every transient/borrowed site uses CowSlice::init_unchecked(bytes, /*is_owned=*/false) — a no-op Drop, matching the old borrowed PathString::init. Same free count (one) at every site, now enforced by the type instead of a container-level contract.

Net: no behavioral change, hence no regression to reproduce. test/js/web/fetch/blob-file-name-ownership.test.ts is ASAN coverage over the owned alloc/free path (structuredClone of a named File, looped under Bun.gc), a forward regression guard. bun bd (ASAN) + build:release both build/link clean and the fs/blob/bundler/router/resolver/transpiler-cache suites pass. Ready for a human merge.

Comment thread test/js/web/fetch/blob-file-name-ownership.test.ts Outdated
Comment thread src/runtime/node/dir_iterator.rs Outdated
robobun added 2 commits May 29, 2026 21:21
- dir_iterator.rs: IteratorResultWName's doc still called it a 'Fake
  PathString'; it now mirrors IteratorResult.name (RawSlice<u16> +
  slice_assume_z), matching the file header.
- blob-file-name-ownership test: the File([bytes], name) round-trip
  only exercises the Bytes.stored_name (Box<[u8]>) path. Added a
  structuredClone(Bun.file(path)) case so the owned PathLike::String
  (CowSlice) deserialize arm is covered too, and corrected the comment.
@Jarred-Sumner Jarred-Sumner merged commit a0d1472 into main Jun 2, 2026
74 of 76 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/83392263/delete-pathstring branch June 2, 2026 04:55
robobun added a commit that referenced this pull request Jun 2, 2026
Main deleted PathString (#31579); PathLike::String now carries a
CowSlice<u8>, matching the surrounding mkdir_if_not_exists call.
robobun added a commit that referenced this pull request Jun 5, 2026
Main deleted PathString (#31579); PathLike::String now carries a
CowSlice<u8>, matching the surrounding mkdir_if_not_exists call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants