Skip to content

fix(plugin): serialize main-thread round trips — fixes host heap corruption on Linux/WebKitGTK - #68

Merged
runyourempire merged 3 commits into
mainfrom
worktree-fix-main-thread-serialization
Aug 12, 2026
Merged

fix(plugin): serialize main-thread round trips — fixes host heap corruption on Linux/WebKitGTK#68
runyourempire merged 3 commits into
mainfrom
worktree-fix-main-thread-serialization

Conversation

@runyourempire

Copy link
Copy Markdown
Collaborator

What this is

The E2E host-crash test webview_reload_during_introspection_is_safe had been failing
intermittently 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:

malloc(): unaligned tcache chunk detected     (SIGABRT, exit 134)
corrupted double-linked list

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:

Hypothesis Measured
Caused by webview reloads No — reload-hammering alone 0/4; introspection with no reload at all 4/4
Caused by request volume No — one unthrottled loop, same total calls 0/4; three concurrent loops 5/8
Caused by the HTTP/tokio layer Noget_memory_stats (touches no webview) at same concurrency 0/5
Caused by rmcp 3.1.2 No — pre-rmcp tree 6/8 vs current tree 5/8
Fixed by locking the dispatch alone No — still 4/8
Fixed by removing block_in_place No — 8/8, worse
Fixed by serializing the whole round trip Yes — 0/28

Concurrency 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.rs serializes the round trip through one dispatch lock. The deadline covers the wait for
the 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_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 — 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

  • Local Linux/WebKitGTK: 0 crashes in 28 runs post-fix vs 5/8 pre-fix, across list_windows,
    get_window_state, all-ops, and at concurrency 3, 6 and 8.
  • Real E2E reload test: 5/5 pass (was 1/3 failing).
  • New regression test: 6/6 pass fixed, 2/4 host deaths unfixed.
  • Gate: 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

…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
audit and others added 2 commits August 12, 2026 22:00
…" 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>
@runyourempire

Copy link
Copy Markdown
Collaborator Author

Soak results — the fix holds

CI (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 list_windows, get_window_state and all-ops at concurrency 3, 6 and 8. The real E2E reload test went 5/5 (was 1/3 failing), and the new regression test is 6/6 fixed vs 2/4 host deaths unfixed.

Two further commits since the review

  • docs: records the fix in CLAUDE.md with the full measured bisect, and corrects the 0.8.2 "VERIFIED FIXED" host-crash claim — that evidence was Windows/WebView2 only and never covered Linux/WebKitGTK, where this different crash stayed live.
  • chore(release): closes two version-drift gaps of the same class — server.json is now covered by both bump scripts (it was hand-caught for 0.8.7 and 0.8.8; verified by dry-running both), and the VS Code lockfile is synced to 0.8.8 (verified with npm ci + lint + build).

@runyourempire
runyourempire merged commit 4be2369 into main Aug 12, 2026
21 checks passed
@runyourempire
runyourempire deleted the worktree-fix-main-thread-serialization branch August 12, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant