Repository navigation
Conversation
… clone-only memo `store_reads_repeatably` called `resolve_file_stat`, which writes `max_size`, `mode`, `seekable` and `last_modified` on the `Store` that the body shares with the program's `Bun.file()`. It now takes the mode from `mode_for_clone`, the lookup that `store_yields_bytes_once` had inline. That lookup stats a path once per `Store` into `mode_seen_by_clone`. The rule of the arm is the same: an fd and every path that is not a regular file are teed.
|
Updated 12:03 PM PT - Oct 24th, 2026
✅ @robobun, your commit 0286dee380169c1bf5ebcd0a30a79cdafd9eb508 passed in 🧪 To try this PR locally: bunx bun-pr 46666That installs a local version of the PR into your bun-46666 --bun |
|
Status Reproduced on the 1.4.3 and 1.4.4 release binaries. bun 1.4.2 is right. import fs from "node:fs"; import os from "node:os"; import path from "node:path";
const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "clone-")), "log.txt");
fs.writeFileSync(p, "12345");
const f = Bun.file(p);
const original = new Response(f.stream());
const clone = original.clone(); // nothing reads a body
fs.writeFileSync(p, "123456789");
console.log(f.size, await clone.text(), await Bun.readableStreamToText(original.body));
// 1.4.4: 5 123456789 12345
// 1.4.2 and this PR: 9 123456789 123456789The regression tests are in Not fixed here: a cloned procfs body is still empty through |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Determine file repeatability src/runtime/webcore/Blob.rs |
store_reads_repeatably uses mode_for_clone to classify file stores. The helper caches path mode without updating shared file-stat fields. |
Validate cloned file bodies test/js/web/fetch/body-clone.test.ts |
Tests exercise Bun.file() and .stream() bodies across Response and Request clone paths, file growth, existence and inspection-size checks, and Linux procfs reads. The tests add whole-file reads through arrayBuffer(), bytes(), and blob().text(). Related .body, blob-size, and Bun.serve tests remain TODOs. |
Possibly related PRs
- oven-sh/bun#46688: Both PRs cover procfs files with zero
st_size; this PR tests cloned bodies, while the related PR changes synchronous Blob reads. - oven-sh/bun#43871: Both PRs address procfs reads where file metadata reports zero size; this PR adds clone coverage.
Suggested reviewers: dylan-conway, jarred-sumner
Priority:
Merge Risk: 
Minimal · up to 0286d
-
The clone regression has targeted coverage, and no demonstrated new merge-blocking issue remains. The reported FIFO and procfs limitations should remain visible to owners.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description explains the problem, fix, verification, tradeoffs, and remaining limitations. It does not use the template headings exactly, but it provides the required information in equivalent sections.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I diffed the old and new store_reads_repeatably predicate on a path store: a failed stat returned true before as well (seekable stayed None, so != Some(false) held), so the only behavior change there is dropping the resolve_file_stat side effect and sharing the memo with store_yields_bytes_once. I also checked store_yields_bytes_once after the extraction — the fd resolve_file_stat pre-step, the None for an fd whose fstat failed, and the final predicate are unchanged from the old inline code.
Extended reasoning...
The change touches src/runtime/webcore/Blob.rs (extracting mode_for_clone and rewriting store_reads_repeatably to use it instead of resolve_file_stat) and parameterizes the clone test block in test/js/web/fetch/body-clone.test.ts over Bun.file() and Bun.file().stream() bodies. No security-sensitive surface is involved. A confirmed inline finding (the shared mode_seen_by_clone memo causing a hang when a path becomes a FIFO between two clones, which the author also disclosed as a known downside) means a human should weigh the tradeoff, so this is not an approve.
A cloned procfs body reads whole through arrayBuffer(), bytes() and text() of its blob(). Add those next to the text() test. Three readers take the length of a file body from its stat, and procfs reports st_size 0. They see the cloned body as empty, as they see a Bun.file() body that nothing cloned: .body, the size of the blob(), and Bun.serve. Add them as todo with the observed result.
| /// opens it again). A file descriptor never does: its offset, and for a | ||
| /// pipe its bytes, are shared. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// `st_mode` for a body's `clone()`. A path is stat'd once into `mode_seen_by_clone`, not through `resolve_file_stat`: | ||
| /// the `Bun.file()` sharing the store answers from what that caches. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
Problem
clone()of a body over an unreadBun.file(path).stream()freezes theBun.file(). The file grows from 5 to 9 bytes:f.sizestays 5, both bodies stop at 5. Regressed in 1.4.3.store_reads_repeatably(src/runtime/webcore/Blob.rs:5754) callsresolve_file_stat, which caches a stat on theStorethe body shares with theBun.file(). clone() of a Bun.file() body no longer freezes what the Bun.file() answers or throws for a body that cannot be read #44504 fixed theBun.file()body only.Fix
mode_for_clone: onestatperStore, kept inmode_seen_by_clone, which onlyclone()reads.clone()makes the samenewfstatatcalls as 1.4.4.test/js/web/fetch/body-clone.test.ts. The describe of clone() of a Bun.file() body no longer freezes what the Bun.file() answers or throws for a body that cannot be read #44504 runs for.stream()bodies too.Background
Bun.file()is aBlobwhoseStoreholds the path and a never-invalidated stat cache.clone()turns an unread native stream back into a Blob over thatStore(webcore: clone() keeps the Blob behind an unread native body stream instead of teeing it #42053), or tees it.EISDIR. Considered a tee as in 1.4.2: the body loses its type.Downsides
clone()plus the first.bodyorconsole.logcosts 2newfstatatper freshBun.file()(1.4.4: 1). The second still freezes theBun.file().Bun.file(), aBun.file()body is cloned, the path becomes a FIFO, then a.stream()body is cloned. Neither body settles. 1.4.4 reads both..bodyandBun.serve, and itsblob()hassize0 (1.4.2: whole). Leave a body stream unsized when the file's st_size is 0 #41593 reads.bodywhole (measured with this PR), Bun.serve: read a regular file whose st_size is 0 to its end #43974 ownsBun.serve, I found no owner forsize.Notes
Repro (the file grows from 5 to 9 bytes after
clone(), and 3 of 4 lines differ on 1.4.4)clone()(control)9 1234567899 123456789clone():f.size, the clone, the original9 123456789 1234567895 12345 12345clone(): the program's ownf.size,f.text()9 1234567895 12345/proc/versionwhole: clone, original,ftrue true truefalse false falseA longer script with ten rows (Request bodies,
clone.blob(), a fetch upload ofclone.body, a file that is empty atclone(), a deleted file,utimes): 1.4.2 has 10 right, 1.4.4 has 2 right, this PR has 10 right.The change.
store_reads_repeatablykeeps its rule: tee an fd and every path that is not a regular file, lift a regular file or a path whose stat fails. It takes the mode frommode_for_clone: what a stat of theBun.file()cached if there is one, else onestatperStorekept inmode_seen_by_clone.store_yields_bytes_oncecalls the same helper, so the code it had inline is now in one place.Tests. The describe
clone() of a Bun.file() body does not change what the Bun.file() answersfrom #44504 is now adescribe.eachover the body formsBun.file()andBun.file().stream(): 28 tests and 6test.todo. New in both forms: a file that is empty atclone()and written afterwards, and a cloned procfs body read througharrayBuffer(),bytes()andtext()of itsblob(). On the 1.4.4 release the 14 with aBun.file()body pass, and 13 of the 14 with a.stream()body fail. The fourteenth,exists()after a file was created, passes there only becauseclone()writes the cache. The 6test.todoare the three procfs readers of the next section, in both forms.The procfs readers (a cloned body over
Bun.file("/proc/sys/kernel/ostype").stream(), the clone and the original give the same result in every cell, Linux x64)text(),arrayBuffer(),bytes()""text()of itsblob()""for awaitof.body""""sizeof itsblob()Bun.serve"",Content-Length: 0"",Content-Length: 0"",Content-Length: 0The last three readers take the length of a file body from its stat, and procfs reports
st_size0. They give the same empty result for aBun.file()body that nothing cloned, on 1.4.2 too. In 1.4.2 the clone of a.stream()body was a tee, which copied the bytes, so those readers did not see a file. The three aretest.todoin the procfs block ofbody-clone.test.ts, each with the observed result..body: Leave a body stream unsized when the file's st_size is 0 #41593. Its branch (it carries Bun.file: read a regular file whose st_size is 0 instead of returning empty #43871) merges onto this branch with no conflict. On a debug build of that merge the.bodytodo passes under--todo, andbody-clone,bodyandwasm-streamingpass (118, 782, 34). The PR that merges second must remove that.todo.Bun.serve: Bun.serve: read a regular file whose st_size is 0 to its end #43974, by its description. It conflicts with main inFileResponseStream.rs,FileRoute.rsandRequestContext.rs, so it is not built with this PR.sizeof theblob(): I found no open PR that changes it.blob()returns the file-backed Blob, and itssizeis thest_size, asBun.file(path).sizeis. After a read ofsize,text()of that Blob is"", as it is forf.sizeand thenf.text()on 1.4.2. Bun.file: do not use a cached stat size as the byte budget of a read or a copy #43910 (a draft stacked on Blob: resolve_size applies resolved_size #43897) owns that order by its description. It is not built with this PR, and the merge with Leave a body stream unsized when the file's st_size is 0 #41593 does not change it.Syscalls (
newfstatat/fstat/openat, N iterations minus the N = 0 run, counted with a seccomp and ptrace counter becausestraceis not installed here, on the 1.4.4 release binary and this PR's debug build)clone()of a.stream()body, a freshBun.file()eachclone(), one sharedBun.file()clone()of aslice().stream()bodyclone()of aBun.file()body, fresh / sharedclone()+ drain ofclone.body, freshclone()+ drain ofclone.body, sharedclone()+clone.text(), freshclone()+console.log(clone), fresh / sharedThe FIFO order (one shared
Bun.file(p):pis a regular file at the first act, then becomes a FIFO that gets 19 bytes, then a.stream()body is cloned and both bodies are read)Bun.file()body is cloned.stream()body is clonedf.sizeThe memo lives as long as the
Store, as #44504 chose. On 1.4.4 the two arms keep separate answers, so only the second row hangs there. Here both arms sharemode_seen_by_clone.Differential against 1.4.2 and main, measured on 2026-10-08 with this same change on 620b50f (10,200 rows: 5 ways to clone x 5 body forms x 12 file states x a fresh or already sized
Bun.file()x 17 observations, and 0 rows differed run to run). 245 rows change from main. 185 take the value of 1.4.2. 60 match neither build, and each equals what main answers for aBun.file()body: 40 printed sizes (the size on disk at print time, not the size atclone()) and 20clone.blob()sizes (the size on disk, with the type main keeps). No row changes for aBun.file()body, for a body whose.bodywas read before the clone, for a directory, or for/dev/null.Not fixed here
clone(), the first.body,textStream(),console.log, HEAD response orWebAssembly.compileStreamingof either body caches the stat on theStore, as it does for aBun.file()body. Afterclone()andclone.body, a growth of the file leavesf.sizestale. A second commit that marked the cloned Blob and sized it like a stream fixed these and the.bodyrow of the procfs table. The self-review asked to drop it, and I did: it costs one moreread()per drained body (2.09, main 1.08, measured on 10-08) and it is stream sizing, which Bun.file: do not use a cached stat size as the byte budget of a read or a copy #43910 and Leave a body stream unsized when the file's st_size is 0 #41593 own.text(),json(),bytes()of a cloned body still movef.lastModified.Bun.file(fd)still caches thefstaton the fd'sStore(left on purpose by clone() of a Bun.file() body no longer freezes what the Bun.file() answers or throws for a body that cannot be read #44504).Suites on the debug build, all pass: body-clone (118), body, response, blob, blob-write, wasm-streaming, FormData, bun-file, bun-file-exists, bun-file-read, inspect, console-log, bun-serve-file, bun-serve-static.
Credit: the once-per-
Storememomode_seen_by_cloneis from #44504.