fix(plugin): serialize main-thread round trips — fixes host heap corruption on Linux/WebKitGTK - #68
Merged
Conversation
…uption on Linux/WebKitGTK Several of Victauri's main-thread round trips in flight at once corrupt the process heap and glibc aborts the host app (`malloc(): unaligned tcache chunk detected` / `corrupted double-linked list`). bridge.rs now serializes the round trip through a single dispatch lock, with the deadline covering the wait for the lock so serializing cannot stack N callers into N * the timeout when the UI wedges — each caller still gives up after the same 10s total as before. This is a LONG-STANDING bug, not new in 0.8.8. It surfaced as an intermittent E2E failure that looked like an rmcp 3.1.2 regression; it is not. Measured, not assumed — every part of the original theory was wrong: reload-hammering alone ............ 0/4 deaths (NOT about reloads) introspection, no reload at all ... 4/4 deaths one loop, same total calls ........ 0/4 deaths (NOT about volume) three concurrent loops ............ 5/8 deaths (concurrency is the variable) no-webview tool, same concurrency . 0/5 deaths (NOT the HTTP/tokio layer) pre-rmcp-3.1.2 tree ............... 6/8 deaths (NOT caused by rmcp) lock around the dispatch only ..... 4/8 deaths (dispatch is not what matters) serialize the whole round trip .... 0/28 deaths (the fix) Independently corroborated in CI: a control branch running the pre-rmcp commit on today's runner fails the same two E2E tests, so the rmcp upgrade is exonerated and every earlier 0.8.x is affected. Serializing costs effectively nothing: the closures already execute one at a time on the single main thread, so this only stops several being in flight around it. Also adds the regression test that actually reproduces it. Both existing host-crash tests call eval_js in their hot loop; an eval is a full JS round trip (~10-30ms), which paced them to ~100 ops/sec and never built up enough concurrent round trips to trip the corruption. The new test calls only the fast window ops. Tuned by measurement: at 4 tasks the unfixed build survived 4/4, at 12 it died 2/4 with SIGABRT while the fixed build passed 6/6. And closes the CI gap that hid this for several releases: the demo app's stderr was discarded, so a dead host surfaced only as "connection refused" with the glibc abort message lost. CI now captures it and prints it when E2E fails. Gate: fmt, clippy (default/no-default/release, -D warnings), cargo test --workspace, cargo doc -D warnings, semver-checks (no update required). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3UneTjEcPtULZuER1BSvY
…" scope Adds a CLAUDE.md Current State entry for the Linux/WebKitGTK heap corruption fix, including the full measured bisect so the wrong theory is not re-derived. Also annotates the 0.8.2 entry: its "VERIFIED FIXED" evidence was Windows/ WebView2 only (a 4DA soak). It never covered Linux/WebKitGTK, where a different host-crash bug stayed live until now. The 0.8.2 fix is real for what it addressed; it just was not the last host crash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e lockfile
Two release-prep gaps found while preparing 0.8.8, both of the same class: a
versioned file that no script owns, so it silently rots until someone notices.
* server.json (the MCP Registry manifest) was missed entirely by both bump
scripts. It was caught by hand for 0.8.7 and again for 0.8.8; now both
bump-version.{ps1,sh} update it. Each helper substitutes every occurrence, so
one call covers both the manifest `version` and the cargo package `version`.
Verified by dry-running both scripts.
* editors/vscode/package-lock.json still declared 0.8.7 while its package.json
said 0.8.8. Harmless (`npm ci` succeeds either way, and the vsix takes its
version from package.json) but it is exactly the drift the surface-audit
workflow exists to catch. Both version fields synced; verified with
npm ci + lint + build.
The VS Code extension stays deliberately decoupled from the workspace version,
so its bump remains manual — only the lockfile is brought back in sync here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
Soak results — the fix holdsCI (ubuntu, xvfb): 8 consecutive green E2E runs, 0 failures. Against the ~44% pre-fix failure rate, eight greens by luck is under 1% (0.56^8). Per this project's own rule, a single green run never proves an intermittent race fixed — so the job was rerun deliberately. Local (Ubuntu 24.04 + WebKitGTK 2.52): 0 crashes in 28 runs, vs 5/8 pre-fix, across Two further commits since the review
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The E2E host-crash test
webview_reload_during_introspection_is_safehad been failingintermittently on ubuntu (~44-57%) since the rmcp 3.1.2 train, which looked like a 0.8.8
regression and blocked the release. It is not an rmcp regression — it is a long-standing bug
present in released 0.8.7 and every earlier 0.8.x. This fixes it.
The bug
Several of Victauri's main-thread round trips in flight at once corrupt the process heap, and
glibc aborts the host app:
CI never showed this because the demo app's stderr was discarded — a dead host surfaced only as
"connection refused". Reproduced locally on Ubuntu 24.04 + WebKitGTK 2.52 with the app's output
captured, which is how the actual cause finally became visible.
Every part of the original theory was wrong
Measured, not assumed. Each row is a real run:
get_memory_stats(touches no webview) at same concurrency 0/5block_in_placeConcurrency is the variable, not load, not reloads, not rmcp.
Independently corroborated in CI: control branch #67 runs the pre-rmcp commit on today's runner
and fails the same two E2E tests, while macOS and Windows pass.
The fix
bridge.rsserializes the round trip through one dispatch lock. The deadline covers the wait forthe lock, so serializing cannot stack N callers into N × timeout when the UI wedges — each
caller still gives up after the same 10s total as before.
Serializing costs effectively nothing: the closures already execute one at a time on the single
main thread, so this only stops several being in flight around it.
The regression test that actually reproduces it
Both existing host-crash tests call
eval_jsin their hot loop. An eval is a full JS round trip(~10-30ms), which paced them to ~100 ops/sec and never built up enough concurrent round trips to
trip the corruption — that is exactly why this survived several releases. The new test calls only
the fast window ops, with nothing to throttle it.
Tuned by measurement rather than guessed: at 4 tasks the unfixed build survived 4/4 (useless as a
gate), at 12 it died 2/4 with SIGABRT while the fixed build passed 6/6.
Also
CI now captures the demo app's stdout/stderr and prints it when the E2E job fails, so the next
host death is diagnosable from the log instead of requiring a local repro.
Verification
list_windows,get_window_state, all-ops, and at concurrency 3, 6 and 8.cargo fmt --check; clippy default /--no-default-features/--release, all--all-targets -D warnings;cargo test --workspace;RUSTDOCFLAGS="-D warnings" cargo doc;cargo semver-checks— no semver update required (the change is inside a private fn).🤖 Generated with Claude Code
https://claude.ai/code/session_01B3UneTjEcPtULZuER1BSvY