Repository navigation
Conversation
…udes last Node coerces byteOffset, then looks up the encoding and checks the type of the value. It hands end to the native search after that. Bun coerced end first, so a throwing end won over a throwing byteOffset, an unknown encoding and an invalid value type. Each search path now coerces end right before it re-fetches the buffer state, so a valueOf that detaches or resizes the buffer is still safe. Three tests in buffer-indexOf-detach.test.ts passed their encoding object in the end position, which aborts Node since it has end. They pass it as the encoding again, and the file runs under Node as its header says.
|
Warning Review paused — included plan limit reachedKeep your review moving with free on-demand reviews.
On-demand reviews are free for one more day.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Promotion and pricing detailsOn-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file. Review limit detailsOr wait 10 minutes for your next included review. Limit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (3)
Comment |
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 findings, I also checked the reentrancy shape of the three new toIndexOfEnd call sites in src/jsc/bindings/JSBuffer.cpp: in each path (number, string, Uint8Array) the end coercion is the last call that can run user JS before refetchBufferState, and toInt32 on an already-number value in the number path cannot enter JS. The helper's {} return on exception is never consumed since every caller checks the scope on the next line, and the ERR_UNKNOWN_ENCODING return at :1864 still fires before end is touched, matching Node.
Extended reasoning...
The diff is a small reordering inside the shared indexOf helper that backs indexOf, lastIndexOf and includes, so all three siblings are covered by construction. I traced each of the three value paths for any user-JS-reachable call between the new toIndexOfEnd and refetchBufferState and found none; the encoding toWTFString and the string toString both happen before end is coerced. The two inline findings concern divergences from Node that predate this PR (the entry-time byteLength default for lastIndexOf and ToString vs ToPrimitive for a non-string encoding), which the PR description also lists as intentionally unchanged, so I did not treat them as reasons to block on their own. I am not approving outright because this is JSC binding code where user callbacks can detach or resize the haystack, and a human should confirm the ordering choice against Node is the one they want to commit to.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟣
src/jsc/bindings/JSBuffer.cpp— Callers passing an object as the encoding whose valueOf and toString differ get a different encoding (or ERR_UNKNOWN_ENCODING) than Node. JSBuffer.cpp:1855 uses toWTFString, which is ToString and calls toString first; Node's getEncodingOps doesencoding += '', which is ToPrimitive default and calls valueOf first. The PR's new order test uses logging objects whose valueOf and toString are the same function, so it cannot see the difference. Fix: coerce the encoding with toPrimitive(default) then ToString (or JSValue::toStringOrNull after toPrimitive) at this shared site so string and Uint8Array values match Node.Extended reasoning...
Trace: buf = Buffer.from("6162", "hex") = [0x61, 0x62]. enc = { valueOf() { return "hex"; }, toString() { return "utf8"; } }. buf.indexOf("6162", 0, undefined, enc). Bun: line 1855 toWTFString -> toString -> "utf8" -> searches for the UTF-8 bytes of "6162" -> -1. Node:
encoding += ''-> valueOf -> "hex" -> needle [0x61,0x62] -> 0. With toString returning "bogus", Bun throws ERR_UNKNOWN_ENCODING at 1864 while Node succeeds. The finder let it go as unchanged by the diff, which is true of the line, but the diff moves theendcoercion to run after this call and adds a test that claims to pin the coercion order of the encoding argument; the test cannot fail on this divergence because the logging object aliases both methods. Population: any object with distinct valueOf/toString in the encoding slot (e.g. a wrapper class exposing valueOf). Remedy: use ToPrimitive(default)+ToString here; add a test with distinct valueOf/toString.Verification: pre-existing — triggers only when a caller passes a non-string object as
encodingwhosevalueOfandtoStringreturn different values. Mechanism verified: /home/claude/bun/src/jsc/bindings/JSBuffer.cpp:1855encodingString = encodingValue.toWTFString(lexicalGlobalObject);is ES ToString, i.e. ToPrimitive(hint String) → OrdinaryToPrimitive triestoStringbeforevalueOf. Node's…
|
Status: CI is green (Buildkite build 118433). The diff is ready for a maintainer. How I reproduced it (Node v26.3.0 against bun 1.4.3): const T = n => ({ valueOf() { throw new Error(n); }, toString() { throw new Error(n); } });
const b = () => Buffer.from("abcd");
b().indexOf("a", T("off"), T("end")); // node: Error off bun: Error end
b().indexOf("a", 0, T("end"), "bogus"); // node: ERR_UNKNOWN_ENCODING bun: Error end
b().indexOf(null, 0, T("end")); // node: ERR_INVALID_ARG_TYPE bun: Error end
|
|
On the encoding finding: I left it out of this PR because it is not specific to A fix belongs in the shared encoding path, with tests for each API. The PR notes list it as a known difference. The order test uses one function for I replied to the |
|
Updated 1:28 PM PT - Sep 19th, 2026
✅ @robobun, your commit 307a79e3e30fcfc4ab6ffa17353c0dfe6a56509b passed in 🧪 To try this PR locally: bunx bun-pr 43522That installs a local version of the PR into your bun-43522 --bun |
Problem
Buffer.prototype.indexOf,lastIndexOfandincludescoerceendbefore every other argument. With two invalid arguments, Bun throws a different error than Node v26.3.0. Example:Buffer.from("abcd").indexOf("a", 0, end, "bogus")whereend.valueOfthrows. Node throwsERR_UNKNOWN_ENCODING. Bun throws the error fromend.valueOf.indexOfhelper (src/jsc/bindings/JSBuffer.cpp:1792). It callsendValue.toNumber()before thebyteOffsetcoercion, the encoding lookup and the value type check.Fix
toIndexOfEndhelper coercesendin each search path (number, string,Uint8Array). The order isbyteOffset, encoding, value type check,end, as inbidirectionalIndexOfin Node'slib/buffer.js.end.valueOfthat detaches or resizes a buffer stays safe.test/js/node/buffer.test.js(12 of 20 new test runs fail on bun 1.4.3, all pass here). Alsobuffer-indexOf-detach.test.ts, Node'stest-buffer-indexof.jsandtest-buffer-includes.js.NaNendchange (not an order bug, see Notes), and a move of the tests out of theBuffer.writeloop (they stay with the otherindexOftests).Background
indexOf(...).toNumber()on an object runs user JavaScript (valueOf). That code can throw, or detach or resize anArrayBuffer, so the position of each coercion is observable.refetchBufferStatereads the buffer pointer and length again after the last coercion.end. Its native functions runCHECK(args[5]->IsNumber()), so an objectendaborts the Node process. Bun coercesendwithToNumberwhere Node reads it.Notes
Node reference (v26.3.0, unchanged on
main):Buffer.prototype.indexOfandbidirectionalIndexOfinlib/buffer.js, andIndexOfString,IndexOfBuffer,SlowIndexOfNumberinsrc/node_buffer.cc.Probe on node v26.3.0 against bun 1.4.3.
T(n)is an object whosevalueOfandtoStringthrowError(n). The result is the same forindexOf,lastIndexOfandincludes. All rows match Node with this change:Order of the first coercion call per argument, with a logging object in each position. Node ran in a child process because it aborts at the
endstep:The new test
re-reads the buffer after end is coercedguards the position of the coercion against the buffer re-fetch. With the coercion moved afterrefetchBufferState, that test fails (indexOfreturns 0 from the freed haystack).Three tests in
test/js/node/buffer-indexOf-detach.test.tspassed their encoding object as the third argument. That position isendsince Node added it, sonode --testaborted on that file. They now pass the object as the fourth argument. The file passes under Node v26.3.0 again (13 of 13), as its header says. The new tests inbuffer.test.jscover a detach fromend.Differences that this PR does not change:
NaNendmeans the whole buffer in Bun. In Node it reachesstatic_cast<int64_t>(NaN), which is undefined behavior, and the search range is empty. AnInfinityendtakes the same cast: the range is empty in Node on x64.end(this.length) before it coerces anything. If thebyteOffsetcoercion grows a length-tracking view over a resizableArrayBuffer, Node stops the search at the old length. Bun searches the new length.encoding += ''(valueOffirst). Bun usesToString(toStringfirst).Other
Buffermethods also differ from Node on the order of their checks. This PR does not change them. Two examples:buf.toString("bogus", 5, 2)returns""in Node and throwsERR_UNKNOWN_ENCODINGin Bun.buf.fill("6162", 0, 4, { toString: () => "hex" })throwsERR_INVALID_ARG_TYPEin Node and fills in Bun.PR #41498 edits
refetchBufferStatein the same helper. The two diffs do not touch the same lines.