Repository navigation
Conversation
…every method click(), press(), scroll(), scrollTo() and resize() checked m_closed only in unwrapThis(), before toNumber()/toUInt32(), option getters and modifier toString() ran. User code there could call view.close(); the method then sent its command for a view that close() had already removed from the routing table, so the reply was dropped and the returned promise never settled (and m_pendingActivityCount never returned to zero). screenshot() and cdp() already re-checked m_closed after parsing. Fold that re-check into the shared pre-dispatch guard (checkSlot -> checkReady) so every method validates open + slot-free as the last step before sending.
|
Warning Review limit reached
On-demand reviews are free for the next 11 days. After that, they cost $0.25 per reviewed file. Or wait 11 seconds for your next included review. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (4)
Comment |
|
Status: ready for review. CI build #113651: 180 of 181 jobs passed. The one red job is the Reproduced on 1.4.3-canary Test: |
There was a problem hiding this comment.
LGTM — the shared guard is the right place for the m_closed re-check, and it now runs after the last user-code entry point in every dispatching method.
What was reviewed:
- Traced each of the 15
checkReadycall sites and confirmed the guard sits after the finaltoNumber/toWTFString/get/JSONStringifywithRETURN_IF_EXCEPTIONin between; no coercion runs afterward. screenshot()/cdp()behavior is unchanged:close()clears the pending slots, so checkingm_closedbefore the slot in the merged helper cannot change which error fires.- Test is hermetic (fake-Chrome fixture via
runScenario), covers bothclickoverloads and the two previously-guarded methods, and asserts the exactcode + message— no sleeps or timing dependence.
Extended reasoning...
Overview
This PR touches one C++ file (src/runtime/webview/JSWebViewPrototype.cpp) and one test file. It renames the static pre-dispatch helper checkSlot to checkReady, adds an m_closed check to it, and updates all 15 callers. Two now-redundant inline m_closed re-checks in screenshot() and cdp() are removed. A new test.concurrent case in the existing pipe-transport test file exercises 11 method/argument shapes that close the view mid-coercion and asserts each throws ERR_INVALID_STATE: WebView is closed synchronously.
Security risks
None. This is a liveness re-check on an internal WebView object; no auth, crypto, path handling, or untrusted-input parsing is touched. The error goes through the centralized Bun::throwError(..., ErrorCode::ERR_INVALID_STATE, ...) machinery like the code it replaces.
Level of scrutiny
Low-to-moderate. The change is mechanical and directly implements the REVIEW.md rule "re-validate liveness guards after every callback" by moving the guard into the shared helper. I verified every call site places checkReady after all argument conversion (each toNumber/get/toWTFString is followed by RETURN_IF_EXCEPTION before the guard), and grepped for stragglers — no remaining checkSlot call sites exist (only a stale comment reference in JSWebViewConstructor.cpp, which is harmless). The ordering swap in screenshot() (slot-then-closed → closed-then-slot) is not observable because close() rejects and clears the slots.
Other factors
The test follows repo conventions exactly: appended to the existing feature test file, test.concurrent, single .toEqual on a results object, hermetic via the fake-Chrome fixture, no timing dependence. It covers the variant matrix (both click overloads, options getters, valueOf, toString, toJSON) and includes screenshot/cdp as regression coverage for the folded-in checks. The bug hunt exited on dry_streak with no findings and no outstanding reviewer objections exist on the PR.
|
Updated 8:01 PM PT - Sep 9th, 2026
❌ @robobun, your commit 199e9b5 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 42160That installs a local version of the PR into your bun-42160 --bun |
Problem
Bun.WebView: if user code that runs whileclick(),press(),scroll(),scrollTo()orresize()convert their arguments (avalueOf(), amodifiersentry'stoString(), an options getter) callsview.close(), the returned promise never settles.screenshot()andcdp()in the same position throwWebView is closed.src/runtime/webview/JSWebViewPrototype.cpp. These five methods checkedm_closedonly inunwrapThis(), beforetoNumber()/toUInt32()/JSObject::get()ran.close()removes the view from the transport's routing table (CDP::Ops::close,WK::Ops::close). The command sent after that gets a reply thathandleResponsecannot route (viewFor()is null), so nothing settles the slot andm_pendingActivityCountnever returns to zero.Fix
checkSlottocheckReadyand make it checkm_closedbefore the slot. Every method already calls it as the last step before sending, after all argument conversion.m_closedre-checks inscreenshot()andcdp(). The helper now does the same thing with the same error (ERR_INVALID_STATE,WebView is closed), so their behavior is unchanged.unwrapThis()stays: it avoids running any conversion on a view that is already closed.test/js/bun/webview/webview-chrome-pipe.test.ts(new test, 11 shapes, runs against the fake-Chrome fixture so it needs no browser; stock bun fails 9 of 11). Also ranwebview-chrome.test.ts(58 pass),webview-chrome-disconnect.test.tsandwebview-chrome-ws.test.tsagainst Chrome 143 with the debug build.Background
WebViewmethod validates its arguments inJSWebViewPrototype.cpp, then calls an instance method that builds the wire command and stores the returned promise in a per-operationWriteBarrier<JSPromise>slot (m_pendingMiscfor the input methods).m_views(Chrome) orviewsById(WebKit) weak map.close()rejects the view's slots and erases it from that map, so a reply for a closed view is dropped by design.m_pendingActivityCountis what keeps a view with an in-flight operation alive for the GC. A slot that is set but never settled pins the view.Notes
5f554969band main with real Chrome and withtest/js/bun/webview/fake-chrome-fixture.ts. With the fake, the unfixed build returns a pending promise for all nine input shapes; with the fix each call throws synchronously, like every other argument error in these methods.navigate(),evaluate()andtype()only calltoWTFString()on a value already checked withisString(), andgoBack()/goForward()/reload()take no arguments, so they could not hit this today. They go through the same helper now, which also covers webview: navigate/reload/goBack/goForward accept { waitUntil, timeout } #30645 (it adds option parsing to the navigation methods and carried its own copies of the re-check).close()whileTarget.createTargetis in flight).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/webview/webview.test.ts, test/js/bun/webview/webview-chrome-pipe.test.ts