fix(browser): auto-remove macOS Gatekeeper quarantine — prevent SIGKILL

Root cause: On Apple Silicon macOS, the Camoufox browser binary
inherits com.apple.quarantine from npm download. When camoufox-browser
launches it headlessly, Gatekeeper silently kills the process with
SIGKILL (exitCode=null, signal=SIGKILL) before it can run.

Fix:
1. remove_macos_quarantine() — runs 'xattr -dr com.apple.quarantine'
   on the Camoufox cache directory after every fetch and on every
   subsequent startup (in case cache was from a prior npx run)
2. check_known_errors() — detects 'sigkill' in daemon stderr and
   shows the exact fix command to the user
3. Runs quarantine removal in TWO places:
   - Immediately after camoufox-js fetch downloads the binary
   - Always, even when binary already exists in cache
This commit is contained in:
2026-06-13 07:58:05 +05:30
parent 0334cad50e
commit 68f7629f6f

View File

@@ -10,6 +10,7 @@
//! - Both packages share the same cache directory (~/Library/Caches/camoufox/). //! - Both packages share the same cache directory (~/Library/Caches/camoufox/).
//! We use the pinned `camoufox-js` for fetch, `camoufox-browser` for daemon. //! We use the pinned `camoufox-js` for fetch, `camoufox-browser` for daemon.
use std::process::Stdio;
use std::time::Duration; use std::time::Duration;
use tokio::process::Command; use tokio::process::Command;
@@ -95,9 +96,38 @@ fn check_known_errors(log: &str) -> Option<&'static str> {
if lower.contains("error while loading shared libraries") { if lower.contains("error while loading shared libraries") {
return Some("Missing Linux system libraries."); return Some("Missing Linux system libraries.");
} }
if lower.contains("sigkill") {
return Some("macOS Gatekeeper killed the browser (SIGKILL). Run: xattr -dr com.apple.quarantine ~/Library/Caches/camoufox");
}
None None
} }
/// Remove macOS quarantine attribute from the Camoufox cache directory.
/// Gatekeeper kills unsigned headless browsers with SIGKILL on Apple Silicon.
async fn remove_macos_quarantine() {
if !cfg!(target_os = "macos") {
return;
}
let cache = browser_cache_dir();
if !cache.exists() {
return;
}
let output = Command::new("xattr")
.args(&["-dr", "com.apple.quarantine", &cache.to_string_lossy()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.output()
.await;
match output {
Ok(o) if o.status.success() => {
info!("Removed macOS quarantine from {}", cache.display());
}
_ => {
warn!("Could not remove quarantine (xattr not available or failed)");
}
}
}
/// Initialize the browser daemon and return a ready client. /// Initialize the browser daemon and return a ready client.
/// ///
/// Steps: /// Steps:
@@ -122,7 +152,11 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
if !is_browser_installed() { if !is_browser_installed() {
info!("Downloading Camoufox browser using camoufox-js@{}...", CAMOUFOX_JS_VERSION); info!("Downloading Camoufox browser using camoufox-js@{}...", CAMOUFOX_JS_VERSION);
match run_fetch(npx, CAMOUFOX_JS_VERSION).await { match run_fetch(npx, CAMOUFOX_JS_VERSION).await {
Ok(msg) => info!("Camoufox browser fetch: {}", msg), Ok(msg) => {
info!("Camoufox browser fetch: {}", msg);
// On macOS, remove Gatekeeper quarantine immediately after download
remove_macos_quarantine().await;
}
Err(e) => { Err(e) => {
return BrowserInitResult::Unavailable(format!( return BrowserInitResult::Unavailable(format!(
"Failed to download Camoufox browser: {}\n\n\ "Failed to download Camoufox browser: {}\n\n\
@@ -133,6 +167,10 @@ pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
} }
} }
// On macOS, always try to remove Gatekeeper quarantine from cached binary
// (even if it was already installed — prior npx downloads may have set it)
remove_macos_quarantine().await;
// 3. Ensure the `camoufox-browser` npm package is globally installed for daemon // 3. Ensure the `camoufox-browser` npm package is globally installed for daemon
if !is_npm_package_installed(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await { if !is_npm_package_installed(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await {
info!("Installing camoufox-browser@{} (daemon)...", CAMOUFOX_BROWSER_VERSION); info!("Installing camoufox-browser@{} (daemon)...", CAMOUFOX_BROWSER_VERSION);