fix: Add mutex to nix_util::settings to prevent concurrent access segfault

Fixes #106

The Nix settings system uses global mutable state without internal
synchronization. When multiple threads call settings::set concurrently
(as happens in parallel test execution), this causes a segfault in the
C++ std::set implementation.

Changes:
- Add mutex to serialize access through the Rust API
- Add documentation explaining thread safety limitations
- Add Once guard in nix-flake tests to minimize concurrent access

The mutex provides protection between Rust callers, though it cannot
completely prevent C++ Nix code from modifying settings concurrently.

(cherry picked from commit 203917657b60c4e1dcbaf442bec64c37c634abc4)
This commit is contained in:
Robert Hensing 2025-08-26 16:45:25 +02:00
parent 200dcf0891
commit 3bef494271
2 changed files with 41 additions and 1 deletions

View file

@ -259,9 +259,16 @@ mod tests {
use nix_store::store::Store;
use super::*;
use std::sync::Once;
static INIT: Once = Once::new();
fn init() {
nix_util::settings::set("experimental-features", "flakes").unwrap();
// Only set experimental-features once to minimize the window where
// concurrent Nix operations might read the setting while it's being modified
INIT.call_once(|| {
nix_util::settings::set("experimental-features", "flakes").unwrap();
});
}
#[test]

View file

@ -1,22 +1,54 @@
use anyhow::Result;
use nix_c_raw as raw;
use std::sync::Mutex;
use crate::{
check_call, context, result_string_init,
string_return::{callback_get_result_string, callback_get_result_string_data},
};
// Global mutex to protect concurrent access to Nix settings
// See the documentation on `set()` for important thread safety information.
static SETTINGS_MUTEX: Mutex<()> = Mutex::new(());
/// Set a Nix setting.
///
/// # Thread Safety
///
/// This function uses a mutex to serialize access through the Rust API.
/// However, the underlying Nix settings system uses global mutable state
/// without internal synchronization.
///
/// The mutex provides protection between Rust callers but cannot prevent:
/// - C++ Nix code from modifying settings concurrently
/// - Other Nix operations from reading settings during modification
///
/// For multi-threaded applications, ensure that no other Nix operations
/// are running while changing settings. Settings are best modified during
/// single-threaded initialization.
pub fn set(key: &str, value: &str) -> Result<()> {
// Lock the mutex to ensure thread-safe access to global settings
let guard = SETTINGS_MUTEX.lock().unwrap();
let mut ctx = context::Context::new();
let key = std::ffi::CString::new(key)?;
let value = std::ffi::CString::new(value)?;
unsafe {
check_call!(raw::setting_set(&mut ctx, key.as_ptr(), value.as_ptr()))?;
}
drop(guard);
Ok(())
}
/// Get a Nix setting.
///
/// # Thread Safety
///
/// See the documentation on [`set()`] for important thread safety information.
pub fn get(key: &str) -> Result<String> {
// Lock the mutex to ensure thread-safe access to global settings
let guard = SETTINGS_MUTEX.lock().unwrap();
let mut ctx = context::Context::new();
let key = std::ffi::CString::new(key)?;
let mut r: Result<String> = result_string_init!();
@ -28,6 +60,7 @@ pub fn get(key: &str) -> Result<String> {
callback_get_result_string_data(&mut r)
))?;
}
drop(guard);
r
}