From e1a7ad00bdfd279e3ec03c04775dc24d5788a188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kay=20Y=C4=B1lmaz?= <60583610+gokay-ai@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:52:19 +0300 Subject: [PATCH] fix(desktop): disable JSC JIT on AVX-less Linux CPUs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gökay Yılmaz <60583610+gokay-ai@users.noreply.github.com> --- desktop/src-tauri/src/webkit_rendering.rs | 124 ++++++++++------ .../src-tauri/src/webkit_rendering/tests.rs | 132 ++++++++++++++---- docs/linux-rendering-troubleshooting.md | 11 ++ 3 files changed, 204 insertions(+), 63 deletions(-) diff --git a/desktop/src-tauri/src/webkit_rendering.rs b/desktop/src-tauri/src/webkit_rendering.rs index 905da5eeed..05bdff6bce 100644 --- a/desktop/src-tauri/src/webkit_rendering.rs +++ b/desktop/src-tauri/src/webkit_rendering.rs @@ -13,7 +13,9 @@ //! //! * an NVIDIA GPU, the driver family behind most upstream reports; and //! * AppImage packaging, where linuxdeploy's AppRun hook pins `GDK_BACKEND=x11` -//! and the dmabuf renderer buys nothing on that XWayland path (#2338). +//! and the dmabuf renderer buys nothing on that XWayland path (#2338); and +//! * an x86_64 CPU without AVX, where affected WebKitGTK 2.52 JavaScriptCore +//! builds can execute an AVX instruction and kill WebKitWebProcess (#3747). //! //! `--safe-rendering` is the manual escape hatch for a machine neither signal //! recognises; it also disables accelerated compositing, for that launch only. @@ -33,22 +35,26 @@ const NVIDIA_PCI_VENDOR: &str = "0x10de"; /// Where DRM devices advertise their PCI vendor. const DRM_ROOT: &str = "/sys/class/drm"; +/// Linux's processor feature inventory. +const CPU_INFO: &str = "/proc/cpuinfo"; /// Drops the zero-copy dmabuf buffer path. The workaround for #2338. const DISABLE_DMABUF: &str = "WEBKIT_DISABLE_DMABUF_RENDERER"; /// Drops accelerated compositing as well. `--safe-rendering` only. const DISABLE_COMPOSITING: &str = "WEBKIT_DISABLE_COMPOSITING_MODE"; +/// Disables JavaScriptCore's JIT on AVX-less x86_64 CPUs. The workaround for +/// #3747. +const DISABLE_JSC_JIT: &str = "JSC_useJIT"; /// What the heuristic applies: the #2338 workaround alone, matching the /// ecosystem precedents. `DISABLE_COMPOSITING` is deliberately not here — no /// report has isolated it as necessary, and it costs more rendering than this. -const HEURISTIC: [&str; 1] = [DISABLE_DMABUF]; +const HEURISTIC: [(&str, &str); 1] = [(DISABLE_DMABUF, "1")]; -/// What `--safe-rendering` applies, which is also every variable this module may -/// set and therefore every variable a user assignment takes away from it. Being -/// the same list is the invariant: nothing outside it is ever written, so a user -/// value for any other WebKit variable is not a conflict. -const OWNED: [&str; 2] = [DISABLE_DMABUF, DISABLE_COMPOSITING]; +/// What `--safe-rendering` applies, which is also every rendering variable a +/// user assignment takes away from this module. The JSC workaround is an +/// independent decision with its own user override. +const OWNED: [(&str, &str); 2] = [(DISABLE_DMABUF, "1"), (DISABLE_COMPOSITING, "1")]; /// Reads one environment variable. Injected so the decision is testable without /// mutating the process environment. `OsString` rather than `String` because @@ -58,9 +64,9 @@ type EnvLookup<'a> = &'a dyn Fn(&str) -> Option; /// What this launch should do about its rendering environment. #[derive(Debug, PartialEq, Eq)] enum Plan { - /// Set each of these to `1`, then report `why`. + /// Apply each `(variable, value)` assignment, then report `why`. Apply { - vars: &'static [&'static str], + assignments: Vec<(&'static str, &'static str)>, why: String, }, /// Change nothing, and report `why`. @@ -82,13 +88,18 @@ pub fn apply() -> Result<(), String> { std::env::args_os(), &|key| std::env::var_os(key), Path::new(DRM_ROOT), + std::env::consts::ARCH, + Path::new(CPU_INFO), ) { - Plan::Apply { vars, why } => { - for var in vars { + Plan::Apply { assignments, why } => { + for (var, value) in &assignments { // Safe here and only here — see the doc comment above. - std::env::set_var(var, "1"); + std::env::set_var(var, value); } - let applied: Vec = vars.iter().map(|var| format!("{var}=1")).collect(); + let applied: Vec = assignments + .iter() + .map(|(var, value)| format!("{var}={value}")) + .collect(); eprintln!("buzz-desktop: {} — {why}", applied.join(" ")); Ok(()) } @@ -100,12 +111,14 @@ pub fn apply() -> Result<(), String> { } } -/// The whole decision, as a pure function of argv, the environment, and the DRM -/// device tree. +/// The whole decision, as a function of injected argv, environment, platform, +/// and Linux hardware inventory paths. fn plan( args: impl IntoIterator>, env: EnvLookup<'_>, drm_root: &Path, + arch: &str, + cpu_info: &Path, ) -> Plan { let safe_rendering = args .into_iter() @@ -116,47 +129,80 @@ fn plan( // A user who has assigned one of these has taken over the decision, so // the heuristic stands down wholesale — writing the *other* variable // behind their back would be exactly the surprise they opted out of. - return match safe_rendering { + if safe_rendering { // Two incompatible answers to one question, and no basis for // picking: honouring the flag would overwrite configuration the // user typed, honouring the environment would silently ignore a // rescue flag from a user whose app does not start. - true => Plan::Fatal { + return Plan::Fatal { diagnostic: conflict(&user_set), - }, - false => Plan::Leave { - why: format!("{} set in the environment", describe(&user_set)), - }, - }; + }; + } } + let mut assignments = Vec::new(); + let mut reasons = Vec::new(); + if safe_rendering { - return Plan::Apply { - vars: &OWNED, - why: format!("{SAFE_RENDERING} requested, this launch only"), - }; + assignments.extend(OWNED); + reasons.push(format!("{SAFE_RENDERING} requested, this launch only")); + } else if user_set.is_empty() { + let signals = [ + (nvidia_gpu(drm_root), "NVIDIA GPU"), + (env("APPIMAGE").is_some(), "AppImage"), + ]; + let hits: Vec<&str> = signals + .iter() + .filter_map(|(hit, label)| hit.then_some(*label)) + .collect(); + if !hits.is_empty() { + assignments.extend(HEURISTIC); + reasons.push(hits.join(", ")); + } + } else { + reasons.push(format!("{} set in the environment", describe(&user_set))); } - let signals = [ - (nvidia_gpu(drm_root), "NVIDIA GPU"), - (env("APPIMAGE").is_some(), "AppImage"), - ]; - let hits: Vec<&str> = signals - .iter() - .filter_map(|(hit, label)| hit.then_some(*label)) - .collect(); + if env(DISABLE_JSC_JIT).is_some() { + reasons.push(format!("{DISABLE_JSC_JIT} set in the environment")); + } else if avx_available(arch, cpu_info) == Some(false) { + assignments.push((DISABLE_JSC_JIT, "0")); + reasons.push("x86_64 CPU does not advertise AVX".to_string()); + } - match hits.is_empty() { + match assignments.is_empty() { true => Plan::Leave { - why: "no NVIDIA GPU and not an AppImage".to_string(), + why: match reasons.is_empty() { + true => "no NVIDIA GPU, not an AppImage, and no AVX-less x86_64 CPU detected" + .to_string(), + false => reasons.join("; "), + }, }, false => Plan::Apply { - vars: &HEURISTIC, - why: hits.join(", "), + assignments, + why: reasons.join("; "), }, } } +/// Whether an x86_64 CPU advertises AVX. Unknown architecture or unreadable / +/// malformed CPU data produces no decision rather than disabling the JIT. +fn avx_available(arch: &str, cpu_info: &Path) -> Option { + if arch != "x86_64" { + return None; + } + + let cpu_info = std::fs::read_to_string(cpu_info).ok()?; + cpu_info.lines().find_map(|line| { + let (key, features) = line.split_once(':')?; + key.trim().eq_ignore_ascii_case("flags").then(|| { + features + .split_ascii_whitespace() + .any(|feature| feature.eq_ignore_ascii_case("avx")) + }) + }) +} + /// Owned variables the environment already carries, keyed by name. /// /// Presence is the test, not truthiness: `VAR=0` and `VAR=` are both genuine @@ -164,7 +210,7 @@ fn plan( fn user_set(env: EnvLookup<'_>) -> Vec<(&'static str, OsString)> { OWNED .iter() - .filter_map(|key| env(key).map(|value| (*key, value))) + .filter_map(|&(key, _)| env(key).map(|value| (key, value))) .collect() } diff --git a/desktop/src-tauri/src/webkit_rendering/tests.rs b/desktop/src-tauri/src/webkit_rendering/tests.rs index 5be1612b21..632e386b92 100644 --- a/desktop/src-tauri/src/webkit_rendering/tests.rs +++ b/desktop/src-tauri/src/webkit_rendering/tests.rs @@ -33,10 +33,26 @@ fn env_from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option { } } -/// The variables a plan would set, or `None` for a plan that sets nothing. -fn applied(plan: &Plan) -> Option<&[&str]> { +fn cpu_info(features: &str) -> tempfile::NamedTempFile { + let file = tempfile::NamedTempFile::new().expect("cpu info"); + std::fs::write(file.path(), format!("processor : 0\nflags : {features}\n")) + .expect("cpu features"); + file +} + +fn test_plan( + args: impl IntoIterator>, + env: EnvLookup<'_>, + drm_root: &Path, +) -> Plan { + let cpu_info = cpu_info("sse sse2 avx avx2"); + plan(args, env, drm_root, "x86_64", cpu_info.path()) +} + +/// The assignments a plan would apply, or `None` for a plan that changes nothing. +fn applied(plan: &Plan) -> Option<&[(&str, &str)]> { match plan { - Plan::Apply { vars, .. } => Some(vars), + Plan::Apply { assignments, .. } => Some(assignments), _ => None, } } @@ -46,11 +62,11 @@ fn applied(plan: &Plan) -> Option<&[&str]> { #[test] fn test_nvidia_gpu_disables_the_dmabuf_renderer() { let drm = drm(&["0x10de"]); - let plan = plan(NO_ARGS, &env_from(&[]), drm.path()); + let plan = test_plan(NO_ARGS, &env_from(&[]), drm.path()); assert_eq!( applied(&plan), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")][..]) ); let Plan::Apply { why, .. } = &plan else { unreachable!() @@ -65,8 +81,8 @@ fn test_an_nvidia_gpu_alongside_another_vendor_still_counts() { let drm = drm(&["0x8086", "0x10de"]); assert_eq!( - applied(&plan(NO_ARGS, &env_from(&[]), drm.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + applied(&test_plan(NO_ARGS, &env_from(&[]), drm.path())), + Some(&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")][..]) ); } @@ -75,8 +91,8 @@ fn test_the_vendor_id_match_ignores_case() { let drm = drm(&["0x10DE"]); assert_eq!( - applied(&plan(NO_ARGS, &env_from(&[]), drm.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + applied(&test_plan(NO_ARGS, &env_from(&[]), drm.path())), + Some(&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")][..]) ); } @@ -86,11 +102,11 @@ fn test_an_appimage_launch_disables_the_dmabuf_renderer() { // #2338's reporter (Intel Mesa under the AppRun's pinned XWayland backend). let drm = drm(&["0x8086"]); let env = env_from(&[("APPIMAGE", "/home/u/Buzz.AppImage")]); - let plan = plan(NO_ARGS, &env, drm.path()); + let plan = test_plan(NO_ARGS, &env, drm.path()); assert_eq!( applied(&plan), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + Some(&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")][..]) ); let Plan::Apply { why, .. } = &plan else { unreachable!() @@ -103,7 +119,7 @@ fn test_a_plain_non_nvidia_launch_changes_nothing() { let drm = drm(&["0x8086", "0x1002"]); assert!(matches!( - plan(NO_ARGS, &env_from(&[]), drm.path()), + test_plan(NO_ARGS, &env_from(&[]), drm.path()), Plan::Leave { .. } )); } @@ -116,7 +132,7 @@ fn test_an_unreadable_drm_tree_is_not_treated_as_a_hit() { let missing = std::path::Path::new("/nonexistent/class/drm"); assert!(matches!( - plan(NO_ARGS, &env_from(&[]), missing), + test_plan(NO_ARGS, &env_from(&[]), missing), Plan::Leave { .. } )); } @@ -132,8 +148,8 @@ fn test_a_device_without_a_vendor_file_is_skipped_not_fatal() { std::fs::write(device.join("vendor"), "0x10de\n").expect("vendor"); assert_eq!( - applied(&plan(NO_ARGS, &env_from(&[]), root.path())), - Some(&["WEBKIT_DISABLE_DMABUF_RENDERER"][..]) + applied(&test_plan(NO_ARGS, &env_from(&[]), root.path())), + Some(&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")][..]) ); } @@ -145,7 +161,7 @@ fn test_a_user_set_variable_disables_the_heuristic_wholesale() { // dmabuf renderer *on*, on a machine the heuristic would have opted out. let drm = drm(&["0x10de"]); let env = env_from(&[(DISABLE_DMABUF, "0")]); - let plan = plan(NO_ARGS, &env, drm.path()); + let plan = test_plan(NO_ARGS, &env, drm.path()); let Plan::Leave { why } = &plan else { panic!("a user assignment must not be overwritten: {plan:?}"); @@ -159,7 +175,7 @@ fn test_an_empty_assignment_is_still_a_user_assignment() { let env = env_from(&[(DISABLE_DMABUF, "")]); assert!(matches!( - plan(NO_ARGS, &env, drm.path()), + test_plan(NO_ARGS, &env, drm.path()), Plan::Leave { .. } )); } @@ -173,7 +189,7 @@ fn test_a_user_set_compositing_variable_also_stands_the_heuristic_down() { let env = env_from(&[(DISABLE_COMPOSITING, "1")]); assert!(matches!( - plan(NO_ARGS, &env, drm.path()), + test_plan(NO_ARGS, &env, drm.path()), Plan::Leave { .. } )); } @@ -186,14 +202,14 @@ fn test_safe_rendering_applies_the_safest_set_without_any_hardware_signal() { // must not depend on either one. let drm = drm(&["0x8086"]); let args = ["buzz://channel/1", SAFE_RENDERING]; - let plan = plan(args, &env_from(&[]), drm.path()); + let plan = test_plan(args, &env_from(&[]), drm.path()); assert_eq!( applied(&plan), Some( &[ - "WEBKIT_DISABLE_DMABUF_RENDERER", - "WEBKIT_DISABLE_COMPOSITING_MODE" + ("WEBKIT_DISABLE_DMABUF_RENDERER", "1"), + ("WEBKIT_DISABLE_COMPOSITING_MODE", "1") ][..] ) ); @@ -204,7 +220,7 @@ fn test_an_unrelated_flag_is_not_mistaken_for_safe_rendering() { let drm = drm(&["0x8086"]); assert!(matches!( - plan(["--safe-renderingX"], &env_from(&[]), drm.path()), + test_plan(["--safe-renderingX"], &env_from(&[]), drm.path()), Plan::Leave { .. } )); } @@ -213,7 +229,7 @@ fn test_an_unrelated_flag_is_not_mistaken_for_safe_rendering() { fn test_safe_rendering_against_a_user_set_variable_is_fatal_not_guessed() { let drm = drm(&["0x8086"]); let env = env_from(&[(DISABLE_DMABUF, "0")]); - let plan = plan([SAFE_RENDERING], &env, drm.path()); + let plan = test_plan([SAFE_RENDERING], &env, drm.path()); let Plan::Fatal { diagnostic } = &plan else { panic!("the flag and the environment disagree; neither may be guessed: {plan:?}"); @@ -242,9 +258,77 @@ fn test_a_non_utf8_user_assignment_is_reported_not_ignored() { false => None, }; - let Plan::Fatal { diagnostic } = plan([SAFE_RENDERING], &env, drm.path()) else { + let Plan::Fatal { diagnostic } = test_plan([SAFE_RENDERING], &env, drm.path()) else { panic!("a non-UTF-8 assignment is still a user assignment"); }; assert!(diagnostic.contains(DISABLE_DMABUF), "{diagnostic}"); } } + +// ── JavaScriptCore AVX fallback ───────────────────────────────────────────── + +#[test] +fn test_avx_absent_disables_the_jsc_jit() { + let drm = drm(&["0x8086"]); + let cpu_info = cpu_info("sse sse2 ssse3"); + let plan = plan( + NO_ARGS, + &env_from(&[]), + drm.path(), + "x86_64", + cpu_info.path(), + ); + + assert_eq!(applied(&plan), Some(&[(DISABLE_JSC_JIT, "0")][..])); +} + +#[test] +fn test_avx_present_leaves_the_jsc_jit_unchanged() { + let drm = drm(&["0x8086"]); + let cpu_info = cpu_info("sse sse2 avx avx2"); + + assert!(matches!( + plan( + NO_ARGS, + &env_from(&[]), + drm.path(), + "x86_64", + cpu_info.path(), + ), + Plan::Leave { .. } + )); +} + +#[test] +fn test_a_user_set_jsc_jit_value_is_never_overwritten() { + let drm = drm(&["0x8086"]); + let cpu_info = cpu_info("sse sse2"); + let env = env_from(&[(DISABLE_JSC_JIT, "1")]); + + assert!(matches!( + plan(NO_ARGS, &env, drm.path(), "x86_64", cpu_info.path()), + Plan::Leave { .. } + )); +} + +#[test] +fn test_non_x86_and_unreadable_cpu_data_do_not_disable_the_jit() { + let drm = drm(&["0x8086"]); + let cpu_info = cpu_info("sse sse2"); + let missing = Path::new("/nonexistent/proc/cpuinfo"); + + assert!(matches!( + plan( + NO_ARGS, + &env_from(&[]), + drm.path(), + "aarch64", + cpu_info.path(), + ), + Plan::Leave { .. } + )); + assert!(matches!( + plan(NO_ARGS, &env_from(&[]), drm.path(), "x86_64", missing), + Plan::Leave { .. } + )); +} diff --git a/docs/linux-rendering-troubleshooting.md b/docs/linux-rendering-troubleshooting.md index 1e09ef1aca..aef28a814f 100644 --- a/docs/linux-rendering-troubleshooting.md +++ b/docs/linux-rendering-troubleshooting.md @@ -7,11 +7,22 @@ This guide covers the most common rendering failures on Linux and how to resolve | Symptom | Likely cause | Fix | |---------|-------------|-----| | Blank or transparent window, then `SIGABRT` with `colrv1_configure_skpaint` in the output | COLRv1 color emoji font (AppImage only) | Upgrade to the latest AppImage (v0.5.2+) | +| Window disappears and `WebKitWebProcess` reports `SIGILL` on an older x86_64 CPU | WebKitGTK 2.52 JavaScriptCore emits AVX instructions on a CPU without AVX | Buzz automatically sets `JSC_useJIT=0`; set it manually when running an older build | | Blank window on startup, no crash output | dmabuf renderer incompatibility (NVIDIA or AppImage) | `WEBKIT_DISABLE_DMABUF_RENDERER=1 ./Buzz.AppImage` or `--safe-rendering` | | Blank window on any hardware, no crash output | Unknown GPU/driver combination | `--safe-rendering` flag (see below) | --- +## Crash: `WebKitWebProcess` `SIGILL` on an AVX-less CPU + +**Affected hardware:** Older x86_64 CPUs that do not advertise the AVX feature, when used with affected WebKitGTK 2.52 builds. Issue [#3747](https://github.com/block/buzz/issues/3747). + +**Symptom:** Buzz starts and its WebKit child process exits with `SIGILL` (illegal instruction), often at an AVX `vmovaps` instruction in JavaScriptCore. + +**Fix:** Buzz detects AVX-less x86_64 CPUs before WebKit starts and sets `JSC_useJIT=0` automatically. An explicitly provided `JSC_useJIT` value is always preserved. For an older Buzz build, launch with `JSC_useJIT=0 buzz-desktop` (or prefix the AppImage command the same way). + +--- + ## Crash: `colrv1_configure_skpaint` assertion abort (AppImage) **Affected distributions:** Fedora 40+ and any distro shipping Google's Noto Color Emoji in COLRv1 format (`Noto-COLRv1.ttf`). Issues [#2548](https://github.com/block/buzz/issues/2548), [#2982](https://github.com/block/buzz/issues/2982).