Repository navigation
Conversation
The bindgen v2 dictionary converter read each member with a plain [[Get]], so a polluted Object.prototype supplied any option the caller did not pass. Read members with getIfPropertyExistsPrototypePollutionMitigation, the same lookup the v1 generator and JSValue::get use. This covers TLSOptions (Bun.serve, Bun.listen, Bun.connect, fetch tls, Bun.SQL, RedisClient), SocketOptions and SocketHandler.
node:https and node:net take about 2 s to load in a debug build, which put the Bun.serve case at 4.3 s against the 5 s default timeout. The cases now use only native APIs in the child and run in 0.8 to 1.5 s.
|
Status Reproduced with a script that sets one data value on
The four new cases in PR: #44824. It stays a draft until the self-review of the diff is done. |
|
Updated 6:52 PM PT - Oct 15th, 2026
❌ @robobun, your commit 9cdfdaf has 2 failures in
🧪 To try this PR locally: bunx bun-pr 44824That installs a local version of the PR into your bun-44824 --bun |
…genv2-dictionary-proto-pollution # Conflicts: # src/codegen/bindgenv2/internal/dictionary.ts # test/js/bun/http/bun-serve-ssl.test.ts # test/js/bun/net/socket.test.ts
|
Closing: superseded by #45104. #45104 replaced bindgen v1 and v2 with one generator and removed I ran the four test cases of this PR on a debug build of main (8ba82c3). All four pass. They fail on main before #45104 (116dd64). Two of them check things that the new test in |
Problem
Bun.servetls,Bun.listen,Bun.connectandfetchtlstake options fromObject.prototype.Object.prototype.keyFile = "/x"makesBun.serve({ tls: { cert, key } })throwUnable to access keyFile path.Object.prototype.timeout = 5000makes eachBun.connectthrowExpected "onTimeout" callback to be a function.src/codegen/bindgenv2/internal/dictionary.ts:317. The generated converter reads each member withobject->get(), which also looks inObject.prototype.SSLConfig#23169 (1.3.0) and PortSocketConfigto bindings generator #23755 (1.3.1). Before them, ZiggetTruthyread these options and stopped beforeObject.prototype. Bun 1.2.23 ignores both values.Fix
Bun::getIfPropertyExistsPrototypePollutionMitigation, which stops beforeObject.prototype.src/codegen/bindgen.ts:665) and RustJSValue::getuse the same getter.test/js/bun/net/socket.test.tsandtest/js/bun/http/bun-serve-ssl.test.tsfail on 1.4.3-canary and pass here. Also rantest/js/node/tlsandtest/js/node/net.Background
Object.prototype, so each plain object appears to have it.TLSOptions,SocketOptions,SocketHandler,FakeTimersOptions.Downsides
Object.prototypeon purpose loses that.node:tlsreads its options in JavaScript, like Node.Object.prototype.servernamestill sets the SNI oftls.connect(), as in Node v26.3.0.textgrows by 512 bytes. A loop that converts three dictionaries per call is 53 ns slower per call, below the 800 ns noise floor.Notes
Status: draft until the self-review of the diff is done.
A/B, same script, a data value on
Object.prototypein each caseBun.serve({ tls: { cert, key } })+keyFile = "/nonexistent/attacker.key"Unable to access keyFile pathBun.connect({ socket: { data } })+timeout = 5000Expected "onTimeout" callback to be a functionBun.connectwith notlsmember +tls = truePLAINTEXTPLAINTEXTThe tests add three more: a
datahandler onObject.prototypereceives the bytes of a client that has nodatahandler,rejectUnauthorized = falseletsBun.connect({ tls: { serverName } })accept a self-signed certificate, and the same value lets aBun.serveserver withrequestCert: trueanswer a client that has no certificate.History.
git show f14f3b03bb^:src/bun.js/api/server/SSLConfig.zigreads each member withobj.getTruthy(...),getBooleanStrictorget.git show 4a06991d3b^:src/bun.js/api/bun/socket/Handlers.zigusesgetTruthyComptime. Those callJSC__JSValue__getIfPropertyExistsImpl, which callsBun::getIfPropertyExistsPrototypePollutionMitigationUnsafe.git tag --containsgivesbun-v1.3.0for the first port andbun-v1.3.1for the second.What does not change (same output on 1.2.23, 1.4.3-canary and this branch, the last one with
BUN_JSC_validateExceptionChecks=1):Object.prototype.Proxywork as an options object. Thegettrap sees the same keys.Cost, measured on Linux x64.
size build/release/bunon main (116dd64) with and without the emitter change.text106,027,183 to 106,027,695 (+512).data115,224 andbss1,823,240 do not change.Bun.listen({ socket: {...}, tls: {} })with nohostname. Each call convertsSocketOptions,SocketHandlerandTLSOptions(51 member reads) and then throwsExpected either "hostname" or "unix". 11 interleaved runs of 300,000 calls for each release binary: median 2974 ns per call before and 3027 ns after (+53 ns, +1.8%). Runs of the same binary differ by up to 800 ns, so the difference is below the noise.perfandvalgrindare not installed in my environment, so there is no instruction count. The getter (src/jsc/bindings/ObjectBindings.cpp) fills aPropertySloton the stack and allocates nothing.Local test runs (debug + ASAN build, Linux x64). On the first version of this branch (based on 620b50f) I ran
test/js/node/tls,test/js/node/net,socket.test.ts,tcp-server.test.tsandbun-serve-ssl.test.tson a build of main and on the branch: 56 failures on main, 23 on the branch. All 23 are timeouts at the 5 s default or tests that need the public internet, and 22 of them also fail on main. The other one (kqueue should not dispatch spurious drain events on readable) passes 3 of 3 times when it runs alone. After the merge with main (116dd64) I ran the four new cases again: they pass on the branch and fail on 1.4.3-canary. Not run: macOS, Windows.Bun.SQLis not in this PR. Its option normaliser (src/js/internal/sql/shared.ts) also reads fromObject.prototype:Object.prototype.adapter = "sqlite"makesnew Bun.SQL("postgres://...")use SQLite. That is not a regression (1.2.23 does the same), and the cause is in JavaScript, not in the generator. A null-prototype copy in the normaliser does not close it:createPooledConnectionHandlelater readspath,idleTimeout,connectionTimeoutandmaxLifetimefrom the normalised object, which is an ordinary object that has those keys only when they are set. WithObject.prototype.pathset afternew Bun.SQL("postgres://...")returns, the client sends its StartupMessage to that unix socket. A complete fix changes the shape ofsql.optionsor separates it from the state of the pool, so it is a separate change.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/net/socket.test.ts