tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 9c002b5a70618ce822428e4b2d2e81a338f607f6
parent c4888cef7e32ab0438960020070050592d4bbaeb
Author: Alex Franchuk <afranchuk@mozilla.com>
Date:   Mon, 15 Dec 2025 18:57:37 +0000

Bug 1989439 p2 - Exclusively acquire the Glean store in the crashping crate r=gsvelto

A Glean store cannot be accessed by more than one application at a time.

Differential Revision: https://phabricator.services.mozilla.com/D274951

Diffstat:
Mtoolkit/crashreporter/crashping/src/lib.rs | 44+++++++++++++++++++++++++++++++++++++++-----
Atoolkit/crashreporter/crashping/src/single_instance.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoolkit/crashreporter/crashping/tests/single_instance.rs | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 344 insertions(+), 5 deletions(-)

diff --git a/toolkit/crashreporter/crashping/src/lib.rs b/toolkit/crashreporter/crashping/src/lib.rs @@ -15,6 +15,8 @@ mod glean_metrics { include!(env!("GLEAN_METRICS_FILE")); } mod annotations; +mod single_instance; + use annotations::ANNOTATIONS; const TELEMETRY_SERVER: &str = "https://incoming.telemetry.mozilla.org"; @@ -42,6 +44,18 @@ pub struct InitGlean { pub clear_uploader_for_tests: bool, } +/// A handle taking ownership of the Glean store. +pub struct GleanHandle { + single_instance: single_instance::SingleInstance, +} + +impl GleanHandle { + /// Own the Glean store for the lifetime of the application. + pub fn application_lifetime(self) { + self.single_instance.retain_until_application_exit(); + } +} + impl InitGlean { /// The data_dir should be a dedicated directory for use by Glean. pub fn new(data_dir: PathBuf, app_id: &str, client_info_metrics: ClientInfoMetrics) -> Self { @@ -56,15 +70,36 @@ impl InitGlean { } } - pub fn initialize(self) { + pub fn initialize(self) -> std::io::Result<GleanHandle> { self.init_with(glean::initialize) } + /// Initialize using glean::test_reset_glean. + /// + /// This will not do any process locking of the Glean store. pub fn test_reset_glean(self, clear_stores: bool) { - self.init_with(move |c, m| glean::test_reset_glean(c, m, clear_stores)) + self.init_with_no_lock(move |c, m| glean::test_reset_glean(c, m, clear_stores)) + } + + /// Initialize with the given function. + /// + /// This will take exclusive ownership of the Glean store for the current process (potentially + /// blocking). The returned GleanHandle tracks ownership. + pub fn init_with<F: FnOnce(Configuration, ClientInfoMetrics)>( + self, + f: F, + ) -> std::io::Result<GleanHandle> { + std::fs::create_dir_all(&self.configuration.data_path)?; + let handle = GleanHandle { + single_instance: single_instance::SingleInstance::acquire( + &self.configuration.data_path.join("crashping.pid"), + )?, + }; + self.init_with_no_lock(f); + Ok(handle) } - pub fn init_with<F: FnOnce(Configuration, ClientInfoMetrics)>(self, f: F) { + pub fn init_with_no_lock<F: FnOnce(Configuration, ClientInfoMetrics)>(mut self, f: F) { // Clear the uploader for tests, if configured. // No need to check `cfg!(test)`, since we don't set an uploader in unit tests (and if we // did, it would be test-specific). @@ -74,9 +109,8 @@ impl InitGlean { self.configuration.uploader = None; self.configuration.server_endpoint = None; } - init(); - f(self.configuration, self.client_info_metrics) + f(self.configuration, self.client_info_metrics); } } diff --git a/toolkit/crashreporter/crashping/src/single_instance.rs b/toolkit/crashreporter/crashping/src/single_instance.rs @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! The Glean DB can only be accessed by one process at a time, so this module offers a simple way +//! to negotiate this (allowing other processes to wait). +//! +//! TODO: Use std file locking when the MSRV is >=1.89. + +use std::{ + fs::{File, OpenOptions}, + io::{Result, Write}, + path::Path, + process, +}; + +pub struct SingleInstance { + lockfile: File, +} + +impl SingleInstance { + pub fn acquire(path: &Path) -> Result<Self> { + let mut lockfile = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path)?; + + lock(&lockfile)?; + write!(&mut lockfile, "{}", process::id())?; + Ok(SingleInstance { lockfile }) + } + + pub fn retain_until_application_exit(self) { + std::mem::forget(self); + } +} + +impl Drop for SingleInstance { + fn drop(&mut self) { + // Don't attempt to delete the file, as there are races that can't be avoided if we do. + let _ = self.lockfile.set_len(0); + if let Err(e) = unlock(&self.lockfile) { + log::error!("failed to unlock lockfile: {e}"); + } + } +} + +#[cfg(unix)] +mod unix { + use std::ffi::c_int; + use std::fs::File; + use std::io::{Error, Result}; + use std::os::fd::AsRawFd; + + const LOCK_EX: c_int = 2; + const LOCK_UN: c_int = 8; + + extern "C" { + fn flock(fd: c_int, operation: c_int) -> c_int; + } + + pub fn lock(file: &File) -> Result<()> { + let result = unsafe { flock(file.as_raw_fd(), LOCK_EX) }; + (result == 0).then_some(()).ok_or_else(Error::last_os_error) + } + + pub fn unlock(file: &File) -> Result<()> { + let result = unsafe { flock(file.as_raw_fd(), LOCK_UN) }; + (result == 0).then_some(()).ok_or_else(Error::last_os_error) + } +} + +#[cfg(unix)] +use unix::{lock, unlock}; + +#[cfg(windows)] +mod windows { + use std::ffi::{c_int, c_void}; + use std::fs::File; + use std::io::{Error, Result}; + use std::os::windows::io::AsRawHandle; + + type HANDLE = *mut c_void; + type DWORD = u32; + type BOOL = c_int; + + const LOCKFILE_EXCLUSIVE_LOCK: DWORD = 2; + + #[repr(C)] + struct OVERLAPPED { + internal: usize, + internalhigh: usize, + // Omit the union of the offset fields with the `PVOID Pointer` since we don't need it. + offset: DWORD, + offsethigh: DWORD, + hevent: HANDLE, + } + + impl Default for OVERLAPPED { + fn default() -> Self { + // # Safety + // A zeroed OVERLAPPED is defined as valid. + unsafe { std::mem::zeroed() } + } + } + + extern "system" { + fn LockFileEx( + hfile: HANDLE, + dwflags: DWORD, + dwreserved: DWORD, + nnumberofbytestolocklow: DWORD, + nnumberofbytestolockhigh: DWORD, + lpoverlapped: *mut OVERLAPPED, + ) -> BOOL; + + fn UnlockFileEx( + hfile: HANDLE, + dwreserved: DWORD, + nnumberofbytestounlocklow: DWORD, + nnumberofbytestounlockhigh: DWORD, + lpoverlapped: *mut OVERLAPPED, + ) -> BOOL; + } + + pub fn lock(file: &File) -> Result<()> { + let mut overlapped = OVERLAPPED::default(); + let result = unsafe { + LockFileEx( + file.as_raw_handle(), + LOCKFILE_EXCLUSIVE_LOCK, + 0, + u32::MAX, + u32::MAX, + &mut overlapped, + ) + }; + (result != 0).then_some(()).ok_or_else(Error::last_os_error) + } + + pub fn unlock(file: &File) -> Result<()> { + let mut overlapped = OVERLAPPED::default(); + let result = + unsafe { UnlockFileEx(file.as_raw_handle(), 0, u32::MAX, u32::MAX, &mut overlapped) }; + (result != 0).then_some(()).ok_or_else(Error::last_os_error) + } +} + +#[cfg(windows)] +use windows::{lock, unlock}; diff --git a/toolkit/crashreporter/crashping/tests/single_instance.rs b/toolkit/crashreporter/crashping/tests/single_instance.rs @@ -0,0 +1,154 @@ +use crashping::{ClientInfoMetrics, InitGlean}; +use std::process::{Child, Command}; +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Arc, Condvar, Mutex, +}; +use std::time::Duration; + +#[test] +#[ignore] +fn do_glean_init() { + let data_dir = std::env::temp_dir() + .join("crashping_tests") + .join("do_glean_init"); + let _glean_handle = InitGlean::new( + data_dir, + "my_app", + ClientInfoMetrics { + app_build: "build".into(), + app_display_version: "version".into(), + channel: None, + locale: None, + }, + ) + .initialize() + .unwrap(); + + eprintln!("got handle"); + + // Wait and hold the glean handle indefinitely. The caller will kill us. + std::thread::park(); +} + +struct GleanInitChild { + child: Child, + has_handle: Arc<AtomicBool>, +} + +#[derive(Default)] +struct Signal { + condvar: Condvar, + gate: Mutex<()>, +} + +impl Signal { + fn wait(&self, guard: std::sync::MutexGuard<'_, ()>) { + let (_guard, result) = self + .condvar + .wait_timeout(guard, Duration::from_millis(250)) + .unwrap(); + if result.timed_out() { + panic!("failed to see signal change before timeout"); + } + } + + fn notify(&self) { + self.condvar.notify_all(); + } + + fn lock(&self) -> std::sync::MutexGuard<'_, ()> { + self.gate.lock().unwrap() + } +} + +impl GleanInitChild { + fn new(has_handle_changed: Arc<Signal>) -> Self { + let me = std::env::current_exe().unwrap(); + let mut child = Command::new(me) + .args(["--ignored", "--exact", "--nocapture", "do_glean_init"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("failed to execute test child"); + let mut stderr = std::io::BufReader::new(child.stderr.take().unwrap()); + let has_handle = Arc::new(AtomicBool::new(false)); + let has_handle_t = has_handle.clone(); + std::thread::spawn(move || { + use std::io::BufRead; + let mut line = String::new(); + // Catch this sort of error with the top-level timeout. + let Ok(_) = stderr.read_line(&mut line) else { + return; + }; + if line == "got handle\n" { + let _guard = has_handle_changed.lock(); + has_handle_t.store(true, Relaxed); + has_handle_changed.notify(); + } + }); + + GleanInitChild { child, has_handle } + } + + fn has_handle(&mut self) -> bool { + self.has_handle.load(Relaxed) + } + + fn kill(&mut self) { + self.child.kill().unwrap(); + self.has_handle.store(false, Relaxed); + } +} + +impl Drop for GleanInitChild { + fn drop(&mut self) { + self.kill(); + } +} + +#[test] +fn exclusive_access() { + let has_handle_changed = Arc::new(Signal::default()); + + let guard = has_handle_changed.lock(); + + // Spawn two children + let mut child_a = GleanInitChild::new(has_handle_changed.clone()); + let mut child_b = GleanInitChild::new(has_handle_changed.clone()); + + has_handle_changed.wait(guard); + + // Give a little time for the other child to potentially get a handle too (if there's a bug). + std::thread::sleep(Duration::from_millis(100)); + + assert_ne!( + child_a.has_handle(), + child_b.has_handle(), + "exactly one of the children should have the handle" + ); + let was_child_a = child_a.has_handle(); + + let guard = has_handle_changed.lock(); + // Kill the child that has the glean store. + if was_child_a { + child_a.kill(); + } else { + child_b.kill(); + } + assert_eq!(child_a.has_handle() || child_b.has_handle(), false); + has_handle_changed.wait(guard); + + // Ensure the other child acquired the handle. + if was_child_a { + assert!( + child_b.has_handle() && !child_a.has_handle(), + "child b should have the handle" + ); + } else { + assert!( + child_a.has_handle() && !child_b.has_handle(), + "child a should ahve the handle" + ); + } +}