Bug 2067218 - Add a mutex to avoid duplicate helpers for a profile. r=aborondo,nrishel

temp: guard test

Differential Revision: https://phabricator.services.mozilla.com/D322655
This commit is contained in:
Harshit
2026-09-11 19:39:40 +00:00
committed by hsohaney@mozilla.com
parent 476a507146
commit 95a29fb0af
2 changed files with 116 additions and 15 deletions
@@ -2,14 +2,18 @@
* 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 named Windows event that shuts a helper down.
//! The two named Windows objects that govern a helper's lifetime.
//!
//! [`StopEvent`] is a manual-reset event named after a profile and an
//! installation. A helper serves exactly one profile, so signalling the event
//! asks that profile's helper, and only it, to exit.
//! asks that profile's helper, and only it, to exit. Manual reset means a
//! signal landing before the helper reaches its wait is still seen.
//!
//! Manual reset means a signal landing before the helper reaches its wait is
//! still seen rather than missed.
//! [`ProfileGuard`] is a mutex named after the same profile. Only the first
//! helper to serve a profile takes it.
//!
//! Note: currently, the helpers are not revived on system restarts. Firefox needs to
//! readd these helpers on first launch for the system login session.
use std::ffi::OsStr;
use std::hash::Hasher;
@@ -17,9 +21,12 @@ use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use fnv::FnvHasher;
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, WAIT_OBJECT_0};
use windows_sys::Win32::Foundation::{
CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HANDLE, WAIT_OBJECT_0,
};
use windows_sys::Win32::System::Threading::{
CreateEventW, OpenEventW, SetEvent, WaitForSingleObject, EVENT_MODIFY_STATE, INFINITE,
CreateEventW, CreateMutexW, OpenEventW, SetEvent, WaitForSingleObject, EVENT_MODIFY_STATE,
INFINITE,
};
/// `Local\` scopes this to the caller's logon session. The prefix is case sensitive.
@@ -28,9 +35,12 @@ const PREFIX: &str = "Local\\MozillaNotificationHelper";
struct OwnedHandle(HANDLE);
// SAFETY: Win32 handles belong to the process rather than to a thread, so an
// event handle can be waited on and signalled from any of them. OwnedHandle
// owns its handle, so nothing else closes it out from under that thread.
// SAFETY: Win32 handles belong to the process rather than to a thread, so any
// of them can use and close one. OwnedHandle owns its handle, so nothing else
// closes it out from under that thread.
//
// This moves the handle, not the kernel object's ownership: a mutex stays owned
// by the thread that acquired it, wherever its handle ends up.
unsafe impl Send for OwnedHandle {}
impl OwnedHandle {
@@ -47,12 +57,42 @@ impl OwnedHandle {
impl Drop for OwnedHandle {
fn drop(&mut self) {
// SAFETY: OwnedHandle::new rejects a null handle, so self.0 is one that
// CreateEventW or OpenEventW returned and that nothing has closed yet.
// drop runs once, so it is closed exactly once.
// CreateEventW, OpenEventW or CreateMutexW returned and that nothing
// has closed yet. drop runs once, so it is closed exactly once.
unsafe { CloseHandle(self.0) };
}
}
/// Ensures that there is only one helper for each profile.
///
/// The mutex is never released explicitly: a helper holds it until the process
/// ends, and Windows drops an abandoned mutex when its last handle closes.
pub struct ProfileGuard {
_handle: OwnedHandle,
}
impl ProfileGuard {
/// Returns `None` when a helper is already serving `profile`, including one
/// left running by an earlier Firefox session.
/// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw
pub fn acquire(profile: &Path) -> Result<Option<Self>, String> {
let name = wide(&object_name("profile", profile)?);
// SAFETY: A null security descriptor asks for the default one, and
// `name` is NUL terminated by `wide` and outlives the call.
let handle = unsafe { CreateMutexW(std::ptr::null(), 1, name.as_ptr()) };
let handle = OwnedHandle::new(handle)
.ok_or_else(|| format!("CreateMutexW failed: {}", last_error()))?;
if last_error() == ERROR_ALREADY_EXISTS {
// Dropping `handle` closes it, leaving the mutex to its owner.
return Ok(None);
}
Ok(Some(ProfileGuard { _handle: handle }))
}
}
/// The shutdown channel for one profile's helper.
pub struct StopEvent {
handle: OwnedHandle,
@@ -98,7 +138,8 @@ pub fn signal(profile: &Path) -> Result<(), String> {
};
// SAFETY: `handle` is still open, nothing having closed it since
// OpenEventW, and that call requested the EVENT_MODIFY_STATE
// OpenEventW, and that call requested the EVENT_MODIFY_STATE right that
// SetEvent requires.
if unsafe { SetEvent(handle.get()) } == 0 {
return Err(format!("SetEvent failed: {}", last_error()));
}
@@ -168,6 +209,18 @@ mod tests {
);
}
/// A mutex and an event sharing a name would collide in the one namespace,
/// so the kind has to reach the hash.
#[test]
fn a_guard_never_collides_with_a_stop_event() {
let profile = Path::new(r"c:\profiles\kinds");
assert_ne!(
object_name("profile", profile).unwrap(),
object_name("stop", profile).unwrap()
);
}
#[test]
fn signal_wakes_a_waiting_helper() {
let profile = Path::new(r"c:\profiles\signal-wakes");
@@ -203,4 +256,31 @@ mod tests {
signal(mine).unwrap();
waiter.join().unwrap().unwrap();
}
#[test]
fn a_second_helper_is_refused_then_allowed_once_the_first_exits() {
let profile = Path::new(r"c:\profiles\guard-test");
let first = ProfileGuard::acquire(profile).unwrap();
assert!(first.is_some(), "the first helper takes the guard");
assert!(
ProfileGuard::acquire(profile).unwrap().is_none(),
"a second helper is refused while the first holds it"
);
drop(first);
assert!(
ProfileGuard::acquire(profile).unwrap().is_some(),
"the guard is released when its owner exits"
);
}
#[test]
fn separate_profiles_do_not_block_each_other() {
let one = ProfileGuard::acquire(Path::new(r"c:\profiles\one")).unwrap();
let two = ProfileGuard::acquire(Path::new(r"c:\profiles\two")).unwrap();
assert!(one.is_some() && two.is_some());
}
}
@@ -55,9 +55,19 @@ fn check_profile(path: &Path) -> Result<(), String> {
/// Starts the notification work for `profile`, parks until another process asks
/// it to stop, then tears the work down.
fn run(profile: &Path) -> ExitCode {
// TODO: nothing stops a second helper starting for a profile that already
// has one, so every Firefox restart while enabled stacks another. The
// per-profile guard that fixes this lands separately.
// Ensure that only one helper exists per profile. Held until the process exits.
let _guard = match lifecycle::ProfileGuard::acquire(profile) {
Ok(Some(guard)) => guard,
Ok(None) => {
// A helper is already serving this profile. Leave it alone.
return ExitCode::SUCCESS;
}
Err(message) => {
eprintln!("{PROGRAM}: {message}");
return ExitCode::FAILURE;
}
};
let stop = match lifecycle::StopEvent::open(profile) {
Ok(stop) => stop,
Err(message) => {
@@ -172,4 +182,15 @@ mod tests {
fn stop_takes_no_value() {
assert!(Args::try_parse_from([PROGRAM, "--stop=yes", "--profile", r"c:\p"]).is_err());
}
/// Firefox starts a helper on every launch, so finding one already running
/// is the ordinary case rather than a failure.
#[test]
fn run_declines_quietly_when_a_helper_already_has_the_profile() {
let profile = Path::new(r"c:\profiles\run-declines");
let _held = lifecycle::ProfileGuard::acquire(profile).unwrap().unwrap();
assert_eq!(run(profile), ExitCode::SUCCESS);
}
}