feat(desktop): add WebKit renderer recovery ladder for Linux - #3271
Open
wpfleger96 wants to merge 2 commits into
Open
feat(desktop): add WebKit renderer recovery ladder for Linux#3271wpfleger96 wants to merge 2 commits into
wpfleger96 wants to merge 2 commits into
Conversation
Some Linux GPU and compositor combinations crash the WebKit web process during startup, leaving Buzz with an invisible window and no way out (#2338, #2643). WebKit memoizes each renderer environment variable once per process, so a safer configuration can only ever be applied by launching a fresh process — the recovery is therefore built out of explicit relaunches rather than runtime toggles. On a crashed web process inside the startup window, the app prepares a durable episode record, releases the single-instance D-Bus name, proves the name is free, spawns a child with the next tier's exact environment, and exits. The child claims the record exclusively and confirms once it has started cleanly; a confirmed tier is reused on later launches. Recovery is crash-driven only. A window that never paints but also never crashes is not detected, because no available signal distinguishes it from a healthy window. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…lease boundary Two safety invariants in the recovery ladder were not enforced. A refusal raised after the single-instance name was released left the parent running without the name and unable to take it back, because the launcher's Result<(), Refusal> could not express which side of that irreversible boundary the refusal came from — and binary resolution, the one preflight most likely to fail, happened inside the launcher. Both refusal sides are now distinct outcomes, every preflight runs before the release, and a post-release refusal exits instead of lingering. The exclusive claim serialized claim-file creation, not record transitions: read-validate-write ran as three unsynchronized steps, so a delayed generation could stamp a stale receipt over a newer episode, a duplicated crash callback could spawn a second child, and a claim loser could reach the single-instance name ahead of the true owner. Every transition now runs under a crash-releasing advisory lock on a file outside the cleared directory, receipts must name the current (token, generation) and move the phase forward, the eligible-crash path is one-shot, and claim losers exit before Tauri is built. --safe-rendering against a user-set owned variable was silently dropped; it is now a fatal pre-Tauri error naming the conflicting assignments. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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.
Some Linux GPU and compositor combinations crash the WebKit web process during startup, leaving Buzz with an invisible window and no way out. WebKit memoizes each of its renderer environment variables exactly once per process (
AcceleratedBackingStore.cpp,std::call_once), so a safer configuration can only ever be applied by launching a fresh process — this recovery is built out of explicit relaunches, not runtime toggles.Closes #2338Scope
In: crash-driven recovery,
--safe-rendering,--reset-rendering-mode.Out: automatic no-paint detection. A window that never paints but also never crashes is deliberately not detected — no available signal distinguishes it from a healthy window, so that track stays gated on validation against affected hardware.
How it works
On a
Crashedweb-process termination inside the startup window, the app runs one explicit handoff:tauri::App::request_restartcannot serve here — it has no child-environment parameter and skips the single-instance name release, so every child born from it dies as a duplicate.std::env::set_varis equally out: unsound in a process with live GTK and Tokio threads, and WebKit would have memoized the variable already.Spawn success and a zero exit code are both worthless as survival signals, since a child that loses the name forwards its argv and exits 0. Only the child's claim receipt, correlated against the name's owner, proves a handoff happened.
Releasing the name is irreversible — the single-instance plugin cannot be re-registered — so the handoff outcome distinguishes a refusal raised before the release from one raised after. Every preflight that can fail, including resolving the binary to re-execute, runs before the release; a refusal there leaves the app running with its name. A refusal after it (the name turned out not to be free, or
spawnfailed) exits rather than lingering as a Buzz that no longer owns the name. The eligible-crash path is one-shot, and a recovery child that loses its claim exits before Tauri is built rather than competing for the name against the episode's true owner.Episode state
A durable record moves
prepared → started → owned → confirmed, with exclusive(token, generation)claims taken viacreate_new(O_EXCL). Claim files are never deleted — a stale claim is inert evidence that a generation already ran.An exclusive claim only serializes the creation of one claim file, which is not enough on its own: reading the record, validating it, and writing the next phase are three operations. So every transition runs under an advisory
flockon a lock file that is a sibling of the state directory (a lock inode inside it would be unlinked by--reset-rendering-modefrom under a concurrent holder).flockrather than anO_EXCLlockfile because the kernel releases it on process death — a lockfile left behind by a process that crashed mid-transition is exactly the failure this feature exists to handle. Receipts additionally have to prove the record is still the one they belong to: a write requires the matching(token, generation)and a forward phase move, so aconfirmedreceipt racing a crash handoff cannot resurrect the episode the crash just disproved.Exhaustedoutranks every phase, which is what makes the terminal state terminal against a late receipt. Ownership is correlated against the bus (GetConnectionUnixProcessID, unique name,GetId) asabsent | correlated | mismatched | uncorrelatable; an owner that exists but cannot be tied to the record is a handoff failure, never a free name.ownedreceipts are bound to the bus id, because a unique name like:1.4only identifies a connection on the bus that issued it.Tier table
full-gpushm-transportWEBKIT_DMABUF_RENDERER_FORCE_SHM=1cpu-raster+ WEBKIT_SKIA_ENABLE_CPU_RENDERING=1no-accelWEBKIT_DISABLE_DMABUF_RENDERER=1,WEBKIT_SKIA_ENABLE_CPU_RENDERING=1x11-fallback+ GDK_BACKEND=x11WEBKIT_DISABLE_COMPOSITING_MODEis owned (a user value opts out) but no tier sets it, pending the 3-var vs 4-var isolation on #2643.One crash advances exactly one rung and no rung is ever repeated, so the tier count is the per-package attempt cap — no separate counter can drift from it.
Environment ownership
Package-scoped. AppImage owns the 4
WEBKIT_*variables; native additionally ownsGDK_BACKEND, which on AppImage is a package invariant re-exported by linuxdeploy's GTK AppRun hook before the binary starts and therefore never a user signal.Any genuine user assignment of an owned variable — any value, including
0and empty — disables recovery wholesale with a log line.--safe-renderingcombined with such an assignment is refused as a fatal pre-Tauri error naming the conflicting variables, rather than guessed either way: honouring the flag would overwrite configuration the user typed, and honouring the environment would silently ignore a rescue flag from a user whose app will not start. Recovery children are taggedBUZZ_RENDER_EPISODE/BUZZ_RENDER_GENERATION/BUZZ_RENDER_PROFILEso they never reinterpret an injected profile as user configuration.AppImage verification
Built
Buzz_0.4.26_aarch64.AppImageand ran it underxvfb-run dbus-run-session.Crash-driven walk from a clean state — all four AppImage rungs, then terminal:
Exactly one survivor, and it is the D-Bus name owner:
Its environment is the tier and nothing inherited:
The record ends
exhaustedat tier 3 naming that pid, with one claim file per generation and the lock file a sibling of the state directory:Flags on the same build:
--safe-renderingforcesno-acceland leaves no state directory behind;WEBKIT_DISABLE_DMABUF_RENDERER=0in the user's environment disables recovery wholesale and persists nothing;--safe-renderingunder that same assignment exits 1 before Tauri with the conflict named and nothing persisted;--reset-rendering-modeclears the record and exits without launching, and reports accurately on a second run.Layout
desktop/src-tauri/src/render_recovery/—profiles(package scoping + tier table),state(durable record + exclusive claims),episode(attempt identity + pure ladder rules),dbus(name observation),classify(record + observation → decision),launcher(the explicit relaunch),cli(flags),session(live process),wiring(Tauri plugin). 58 unit tests cover the three generation races, injected deaths at all four points, the per-package cap, and the version-scoped crash-free-startup fact. The races that need real contention — a stale generation claiming while a newer one is prepared, a confirmation against a crash handoff, two termination callbacks, a rollback against a later episode — are driven from two threads through a barrier with the state lock held across the window each one needs, and each asserts the second writer actually blocks.lib.rsgains 37 lines: the boot call before Tauri is built, the four boot outcomes it must act on, and plugin registration after the single-instance plugin.Related:
desktop/scripts/fix-appimage.shcarries no rendering environment exports and is unchanged here — the ladder owns those variables exclusively.