Conversation
|
Warning Review limit reached
Next review available in: 18 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (13)
Comment |
|
Updated 10:58 PM PT - Jul 6th, 2026
❌ @robobun, your commit 0b58efe has some failures in 🧪 To try this PR locally: bunx bun-pr 33513That installs a local version of the PR into your bun-33513 --bun |
There was a problem hiding this comment.
I didn't find any bugs, but this threads a new field through the TLS session-lifetime path across 12 files (JS → bindgen → Rust FFI mirrors → C ABI struct → BoringSSL), so it's worth a human look at the security-sensitive and cross-language-ABI parts.
Extended reasoning...
Overview
This PR wires sessionTimeout from tls.createServer() / https.createServer() / Bun.serve({tls}) all the way down to BoringSSL's SSL_CTX_set_timeout() and SSL_CTX_set_session_psk_dhe_timeout(). It adds a session_timeout: i32 field to the SSLConfig bindgen dictionary, the hand-written ExternSSLConfig mirror in src/jsc/generated.rs, the Rust SSLConfig and BunSocketContextOptions structs, the C us_bun_socket_context_options_t struct, and the C++ uWS::SocketContextOptions mirror. The value is also folded into SSLConfig::content_hash()/is_same()/Clone and BunSocketContextOptions::digest() so servers differing only in session lifetime don't share a cached SSL_CTX. Tests cover TLSv1.2 and TLSv1.3 resumption on tls.createServer and https.createServer, plus the sessionTimeout: 0 / omitted default cases.
Security risks
TLS session lifetime is a security parameter — it bounds how long a stolen session ticket remains useful. The change only shortens lifetimes relative to BoringSSL's default when a positive value is supplied (the > 0 guard in us_ssl_ctx_build_raw leaves defaults untouched for 0/omitted, and node:tls validation already rejects negatives). I don't see a way for this to weaken security, but session-ticket handling is squarely in the "never remove a flag you don't understand in a TLS/crypto path" category and deserves a maintainer's eyes.
Level of scrutiny
High. Beyond the crypto-path aspect, the new field is appended to a #[repr(C)] struct that is mirrored in three languages (libusockets.h, App.h with a static_assert on size, uws_sys/SocketContext.rs, and the hand-written ExternSSLConfig in generated.rs which must match the C++ bindgen output field-for-field). The field is consistently appended last in every mirror, and generated.rs explicitly documents itself as hand-ported until the codegen grows a Rust backend, so this looks correct — but ABI drift here would be a silent memory-layout bug rather than a compile error.
Other factors
The implementation is thorough and follows the exact pattern of the neighboring client_renegotiation_limit/client_renegotiation_window fields at every layer. Test coverage is solid (both protocol versions, both the tls and https entry points, and the SSL_CTX cache-keying case). No bugs were found by the bug-hunting pass. Given the breadth (12 files), the TLS/crypto surface, and the cross-language ABI plumbing, this is above my auto-approve threshold.
|
No changes needed from the review, but the cross-language ABI point deserves a concrete answer rather than "it compiles", so here is what backs each of the three boundaries the new field crosses. 1. Rust Drift here is not actually silent. 2. C 3. Rust On the security direction: the |
tls.createServer({ sessionTimeout }) was validated and then discarded: the
value never reached SSLConfig, so the SSL_CTX kept BoringSSL's default session
lifetime. Tickets a server minted stayed resumable for hours no matter what the
application configured, on both TLSv1.2 and TLSv1.3.
Plumb it through SSLConfig and us_bun_socket_context_options_t to
SSL_CTX_set_timeout(). BoringSSL keeps the TLS 1.3 session lifetime in a
separate SSL_CTX_set_session_psk_dhe_timeout() knob, while OpenSSL (and so
Node) derives both versions from the single timeout, so set both. The value is
part of the SSL_CTX cache digest, so two servers that differ only in
sessionTimeout no longer share a context.
30f13a5 to
3e74413
Compare
|
Force-pushed ( They moved out of The verification section of the description is updated to match, and now quotes the actual diff a build of the base commit produces, which is worth spelling out: the |
validateSecureContextOptions exempts null from the range check, the way Node's own configSecureContext does, but the native option parser only reads undefined as absent: a null reaching it is rejected as "not a number". Collapse it on the three paths that forward the option, so tls.createSecureContext, tls.connect, tls.createServer and https.createServer all keep accepting it.
|
Good catch, this was a real regression and it was wider than the two call sites in the report. Fixed in
All three now collapse I kept the fix in JS rather than making the bindgen field nullable: Node checks null in JS, the dictionary's other numeric members are already normalized in JS before they cross ( Worth noting for scope: Coverage: a
|
Node's https.Server extends tls.Server, so both entry points reject a negative, non-integer or non-number sessionTimeout synchronously at construction. Bun's https.Server is http.Server, which builds its TLS options bag by hand and ran no check: a negative value silently fell back to the library default, and 1.5 or a string surfaced as an async error at listen instead. Move the check out of tls.ts into internal/tls, which both modules already import, and call it from the TLS branch of the http.Server constructor. A plain http.createServer has no TLS context, so it keeps ignoring the option.
|
Fixed in Two of the three cases are regressions this PR introduced, not just cosmetic mismatches: before it,
The check moved out of Two deliberate boundaries:
Covered by a |
|
Fixed in `0b58efe` — you're right, this is a genuine fourth forwarding site and distinct from the three already handled. `http.createServer().listen({ tls })` (Bun-only; Node's `listen` has no `tls`) rebuilds the TLS bag from `arg0.tls` via `normalizeServerTls({ ...otherTLS })` and assigns it to the local `tls`, discarding the constructor's normalized `this[tlsSymbol]` entirely. So it bypassed both the validation and the null coercion. It now mirrors the constructor branch: ```js `validateSessionTimeout` is the shared helper from `internal/tls` (the same one the constructor and `tls.ts` call), so all four sites reject identically. The validation runs synchronously inside `listen()`, before `Bun.serve`, so the throw reaches the caller rather than surfacing as an async `'error'`. Coverage: a `test.each` over `-1` / `1.5` / `"300"` asserts `server.listen({ port: 0, tls: { ...cert, sessionTimeout } })` throws the matching code, plus a case that `sessionTimeout: null` listens fine. That's the fourth distinct path now pinned; grepping the two files confirms these are all the sites that forward the option into a native TLS bag. |
|
Status on `0b58efe4`: the diff is green locally and on every lane that actually ran; Buildkite is infra-blocked.
I will not push another `ci: retrigger` commit. This is ready for a maintainer to re-run Buildkite (rebuild or "retry failed") once agent capacity is back; everything that touched a compiler or a test has passed. |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-06, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
tls.createServer({ sessionTimeout })is validated and then discarded. The value never reachesSSLConfig, so theSSL_CTXkeeps BoringSSL's default session lifetime: tickets a server mints stay resumable for hours regardless of what the application configured, and an expired one coming back is still accepted. Same result on TLSv1.2 and TLSv1.3. Reported via a cross-runtime comparison against Node v26.3.0.Repro
Cause
validateSecureContextOptionsrange-checksoptions.sessionTimeoutand nothing else reads it. It is not a member of theSSLConfigbindgen dictionary, so it never crosses into native, never lands inus_bun_socket_context_options_t, andus_ssl_ctx_build_rawnever callsSSL_CTX_set_timeout. Session lifetime is decided entirely by BoringSSL's defaults.tls.Serveradditionally hand-builds its listen-time options bag, so the option has to be named there explicitly.Fix
Carry
sessionTimeoutfrom JS to theSSL_CTX:SSLConfig.bindv2.tsgains asessionTimeoutmember, which coverstls.createSecureContext,tls.connect,Bun.serve({ tls })andBun.listen({ tls })at once.tls.Serverstores it insetSecureContextand passes it in its[buntls]bag;http.Server(whathttps.createServerreturns) passes it in its own TLS bag.tls.tsintointernal/tls, which both modules already import, so every path rejects the same valuestls.createServerdoes. In Node the parity is automatic (https.Serverextendstls.Server); in Bunhttps.Serverishttp.Server, which hand-builds its TLS bag, and the Bun-onlyhttp.Server.listen({ tls })override builds yet another, so both call the shared check. Plainhttp.createServer(no TLS) still ignores the option, as in Node.us_ssl_ctx_build_rawcallsSSL_CTX_set_timeout()when the value is positive. BoringSSL keeps the TLS 1.3 session lifetime in a separateSSL_CTX_set_session_psk_dhe_timeout()knob, while OpenSSL (and so Node) derives both versions from the single timeout value, so both are set. 0 and "omitted" both keep the library default, matching Node.BunSocketContextOptions::digest()andSSLConfig::content_hash(), so two servers differing only insessionTimeoutno longer share one cachedSSL_CTX.Negative values are rejected by that validation and, on the
Bun.serve({ tls })path which has no such layer, skipped by the> 0guard in the C. AnullsessionTimeout is exempt from that validation (Node'sconfigSecureContextmakes the same exemption and simply skips the native setter), but the generated option parser only readsundefinedas absent, so the three JS paths that forward the option collapsenullbefore it crosses.Verification
New test
test/js/node/tls/node-tls-session-timeout.test.ts, alongside the other per-feature files in that directory (renegotiation.test.ts,ssl-ctx-cache.test.ts). Servers withsessionTimeout: 1,sessionTimeout: 0,sessionTimeout: nulland nosessionTimeoutare started on both TLSv1.2 and TLSv1.3, then each is handed its own ticket back after the window has passed. Only the one-second servers refuse it, on both the client's and the server'sisSessionReused(). They all share cert, key and version, so this also pins theSSL_CTXcache keying. A second test covershttps.createServer, a third pins thatnullreachestls.createSecureContextandhttps.createServeras "not provided", and atest.eachover-1,2**31,1.5and"300"pins that both entry points throw the same error code so they cannot drift apart again. All of it matches Node v26.3.0 byte-for-byte on message and code.Against a build of the base commit (48ff9eb), every
sessionTimeout: 1server still resumes, while the0/ omitted controls correctly keep resuming, so neither assertion is vacuous:With the fix applied, both tests pass and the repro above matches Node exactly on both protocol versions.