Conversation
…not detach them scryptSync/pbkdf2Sync/Bun.password.verifySync capture a raw slice into a Buffer argument, then convert a later argument whose toString() or property getter can run user JS. That JS can transfer() the captured buffer's backing and recycle it, so the KDF runs over freed memory and verifySync compares the wrong bytes. The async Buffer arm already pins the backing (transfer() copies while a pin is held). Pin on the sync arm too and release in Drop, matching PathLike.
|
Reproduced all four faces on stock bun ( Follow-up cleanup: the now-redundant async re-pin in CI: builds 76956 and 76972 are green on every lane that ran except the |
|
Updated 10:45 AM PT - Jul 21st, 2026
❌ @robobun, your commit 8cea940 has some failures in 🧪 To try this PR locally: bunx bun-pr 34966That installs a local version of the PR into your bun-34966 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
WalkthroughChangesStringOrBuffer now pins JS-backed buffers during conversion and unpins them on drop. Related readFile handling is clarified, async write parsing is simplified, and synchronous crypto regression tests cover buffer retention and release. Buffer pinning lifecycle
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
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 `@test/js/node/crypto/scrypt.test.ts`:
- Around line 5-9: Remove the regression-narrative comments at
test/js/node/crypto/scrypt.test.ts lines 5-9, test/js/node/crypto/pbkdf2.test.ts
lines 167-170, and test/js/bun/util/password.test.ts lines 395-398; leave the
test names and assertions unchanged, and retain only an issue URL if one is
required by the regression-test guideline.
🪄 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: eda4b9fd-2141-4126-9212-d12ab12b1a01
📒 Files selected for processing (4)
src/runtime/node/types.rstest/js/bun/util/password.test.tstest/js/node/crypto/pbkdf2.test.tstest/js/node/crypto/scrypt.test.ts
StringOrBuffer::from_js now pins on both paths, so the will_be_async re-pin in Write::from_js is a no-op round-trip. The two comments in fetch.rs/Blob.rs that asserted Drop is a no-op for Buffer now reference the owns_buffer allocation instead.
StringOrBuffer::from_js returns a pinned Buffer and releases it on Drop, so PinnedView was a no-op pin/unpin round-trip on every Bun.markdown call with a Buffer input.
|
Superseded by #35757, which consolidates all four sites into one change: |
|
Closing: superseded by #36165, which landed while this was open. #36165 fixes the same four entry points ( The approach here (pin at the The two cleanups bundled here ( |
Repro
The same shape reproduces on:
crypto.pbkdf2Sync(new StringSubclass(pw), saltBuf, ...):PBKDF2::from_jscaptures the salt slice first, then converts the password; a String-object password'stoString()detaches the salt.crypto.scryptSync(pwBuf, new StringSubclass(salt), ...):Scrypt::from_jscaptures the password slice first, then converts the salt.Bun.password.verifySync(pwBuf, new StringSubclass(hash)): the hash'stoString()detaches the password, so the comparison runs over freed bytes and returnsfalsefor the correct password.Cause
StringOrBuffer::from_js_maybe_asynconly pins the backingJSC::ArrayBufferon the async path. On the sync path it stores a raw(ptr, len)into the backing with no pin, and the caller then converts a later argument whosetoString()or property getter can run user JS. That JS cantransfer()the captured buffer's backing and recycle the pages, so the KDF / verify reads freed memory. The backing is bmalloc/libpas-owned, so ASAN does not catch the read.Fix
Pin on the sync Buffer arm too, and release the pin in
Drop for StringOrBuffer. While a pin is held,ArrayBuffer::transferTo()copies the bytes and leaves the source attached (seeJSC__JSValue__pinArrayBufferinbindings.cpp), so the captured slice stays valid across any subsequent argument conversion.unprotect()already clearspinnedbefore unpinning, so the async path's accounting is unchanged. This is the same pin-then-Drop patternPathLikealready uses for buffer path arguments.The
will_be_asyncre-pin block inargs::Write::from_js(src/runtime/node/node_fs.rs) and thePinnedViewhelper insrc/runtime/api/MarkdownObject.rsare removed:StringOrBuffer::from_jsalready returns a pinned Buffer, so both were no-op pin/unpin round-trips. The two comments infetch.rs/Blob.rsthat assertedStringOrBuffer::Dropis a no-op for Buffer are reworded to reference the readFile-owned allocation instead.Verification
New tests in
scrypt.test.ts,pbkdf2.test.ts, andpassword.test.tscover all four faces plus a pin-balance check (inputs are detachable again afterscryptSyncreturns). All fail on the released binary and pass with this change; the fulltest/js/node/cryptoandtest/js/bun/util/password.test.tssuites pass.[review] gate passed · iteration 1 · 8 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file