fix(storage): S-06 — container advisory lock prevents parallel mount and concurrent writes

This commit is contained in:
2026-09-19 09:05:12 +02:00
parent 9634beb744
commit f9733dfc73
7 changed files with 280 additions and 3 deletions
+8
View File
@@ -253,6 +253,10 @@ enum Commands {
#[arg(short, long, default_value_t = false)]
quiet: bool,
/// Erzwingt die Synchronisation auch bei erkanntem Advisory Lock eines anderen Prozesses (S-06)
#[arg(short = 'f', long, default_value_t = false)]
force: bool,
/// Optionaler 24-Wort Notfallschlüssel (umgeht Passwortabfrage)
#[arg(long, num_args = 0..=1, default_missing_value = "")]
recovery_key: Option<String>,
@@ -1362,6 +1366,7 @@ fn handle_sync(
checksum: bool,
exclude: Vec<String>,
quiet: bool,
force: bool,
recovery_key: Option<&str>,
) -> Result<()> {
if !container_path.exists() {
@@ -1418,6 +1423,7 @@ fn handle_sync(
checksum,
exclude_patterns: exclude,
quiet,
force,
};
if !quiet {
@@ -1686,6 +1692,7 @@ async fn run() -> Result<()> {
checksum,
exclude,
quiet,
force,
recovery_key,
} => {
handle_sync(
@@ -1699,6 +1706,7 @@ async fn run() -> Result<()> {
checksum,
exclude,
quiet,
force,
recovery_key.as_deref(),
)?;
}
+3
View File
@@ -182,6 +182,9 @@ pub async fn mount_container(
}
};
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
// 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren
let mut token_bytes = [0u8; 16];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes);
+104 -2
View File
@@ -61,6 +61,17 @@ pub struct UnlockedKeys(
pub Option<i64>, // 4: Carrier Node ID (Inode der Alibi-Datei in nodes)
);
/// RAII-Guard für den Advisory-Lock eines gemounteten Containers (S-06).
pub struct AdvisoryLockGuard {
db: Database,
}
impl Drop for AdvisoryLockGuard {
fn drop(&mut self) {
let _ = self.db.release_advisory_lock();
}
}
impl UnlockedKeys {
pub fn dek(&self) -> &Zeroizing<[u8; 32]> {
&self.0
@@ -411,6 +422,11 @@ impl Database {
[],
);
// Spalten für Advisory-Lock (S-06)
let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_pid INTEGER", []);
let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_host TEXT", []);
let _ = conn.execute("ALTER TABLE meta ADD COLUMN lock_time INTEGER", []);
Ok(())
}
@@ -576,7 +592,10 @@ impl Database {
header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL,
metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0
metadata_gen INTEGER NOT NULL DEFAULT 0,
lock_pid INTEGER,
lock_host TEXT,
lock_time INTEGER
);
CREATE TABLE IF NOT EXISTS nodes (
@@ -783,7 +802,10 @@ impl Database {
header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL,
metadata_mac BLOB,
metadata_gen INTEGER NOT NULL DEFAULT 0
metadata_gen INTEGER NOT NULL DEFAULT 0,
lock_pid INTEGER,
lock_host TEXT,
lock_time INTEGER
);
CREATE TABLE IF NOT EXISTS nodes (
@@ -1820,6 +1842,86 @@ impl Database {
Ok(())
}
/// Prüft den aktuellen Advisory-Lock-Status (S-06).
/// Gibt `Some((pid, host, timestamp))` zurück, falls ein Lock aktiv und der Prozess noch am Leben ist.
pub fn check_advisory_lock(&self) -> Result<Option<(u32, String, u64)>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT lock_pid, lock_host, lock_time FROM meta WHERE slot_id = 0 LIMIT 1")?;
let lock_info = stmt
.query_row([], |row| {
let pid: Option<i64> = row.get(0)?;
let host: Option<String> = row.get(1)?;
let time: Option<i64> = row.get(2)?;
Ok((pid, host, time))
})
.optional()?;
if let Some((Some(pid), Some(host), Some(time))) = lock_info {
let current_host = std::env::var("COMPUTERNAME")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or_else(|_| "localhost".to_string());
// Falls gleicher Host, prüfe ob PID noch läuft
if host == current_host {
if crate::platform::is_process_alive(pid as u32) {
return Ok(Some((pid as u32, host, time as u64)));
} else {
// Verwaister Lock von abgestürztem Prozess -> ignorieren/löschen
drop(stmt);
drop(conn);
let _ = self.release_advisory_lock();
return Ok(None);
}
} else {
return Ok(Some((pid as u32, host, time as u64)));
}
}
Ok(None)
}
/// Setzt einen Advisory Lock auf den Container (S-06).
pub fn acquire_advisory_lock(&self, force: bool) -> Result<()> {
if !force {
if let Some((pid, host, time)) = self.check_advisory_lock()? {
bail!(
"Container ist gesperrt: Wird aktuell von Prozess {} auf Host '{}' verwendet (seit UNIX-Zeit {}). Verwenden Sie --force zum Überschreiben.",
pid, host, time
);
}
}
let pid = std::process::id();
let host = std::env::var("COMPUTERNAME")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or_else(|_| "localhost".to_string());
let now = current_timestamp();
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE meta SET lock_pid = ?1, lock_host = ?2, lock_time = ?3 WHERE slot_id = 0",
params![pid as i64, host, now],
)?;
Ok(())
}
/// Entfernt den Advisory Lock (S-06).
pub fn release_advisory_lock(&self) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE meta SET lock_pid = NULL, lock_host = NULL, lock_time = NULL WHERE slot_id = 0",
[],
)?;
Ok(())
}
/// Setzt einen Advisory Lock und gibt einen RAII Guard zurück (S-06).
pub fn acquire_advisory_lock_guard(&self, force: bool) -> Result<AdvisoryLockGuard> {
self.acquire_advisory_lock(force)?;
Ok(AdvisoryLockGuard { db: self.clone() })
}
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
self.canonical_nodes_bytes_for_vault(0)
+19
View File
@@ -83,6 +83,7 @@ pub struct SyncOptions {
pub checksum: bool,
pub exclude_patterns: Vec<String>,
pub quiet: bool,
pub force: bool,
}
impl Default for SyncOptions {
@@ -95,6 +96,7 @@ impl Default for SyncOptions {
checksum: false,
exclude_patterns: Vec::new(),
quiet: false,
force: false,
}
}
}
@@ -453,6 +455,23 @@ pub fn run_sync(
let start_time = Instant::now();
let mut stats = SyncStats::default();
// S-06: Advisory-Lock gegen parallelen Mount / gleichzeitigen Zugriff
if let Some((pid, host, time)) = db.check_advisory_lock()? {
if !options.force {
bail!(
"Container ist gesperrt: Wird aktuell von Prozess {} auf Host '{}' verwendet (seit UNIX-Zeit {}). Verwenden Sie --force zum Überschreiben.",
pid, host, time
);
} else if !options.quiet {
println!(
" {} Warnung: Aktiver Advisory-Lock (Prozess {} auf '{}') wird durch --force überschrieben.",
ui::yellow("[!]"),
pid,
host
);
}
}
db.set_active_dek(zeroize::Zeroizing::new(*dek));
match options.direction {
+28 -1
View File
@@ -819,6 +819,33 @@ pub fn unmount_drive_wnet(_drive_letter: char) -> Result<()> {
Ok(())
}
/// Prüft, ob ein Prozess mit der angegebenen PID auf dem lokalen System aktiv ist (S-06).
pub fn is_process_alive(pid: u32) -> bool {
#[cfg(windows)]
{
extern "system" {
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> isize;
fn CloseHandle(handle: isize) -> i32;
fn GetExitCodeProcess(handle: isize, exit_code: *mut u32) -> i32;
}
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const STILL_ACTIVE: u32 = 259;
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle == 0 {
return false;
}
let mut exit_code: u32 = 0;
let success = unsafe { GetExitCodeProcess(handle, &mut exit_code) };
unsafe { CloseHandle(handle) };
success != 0 && exit_code == STILL_ACTIVE
}
#[cfg(not(windows))]
{
std::path::Path::new(&format!("/proc/{}", pid)).exists()
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -835,7 +862,7 @@ mod tests {
fn test_find_next_available_drive() {
let drive = find_next_available_drive().expect("Find next drive");
assert!(drive.is_ascii_alphabetic());
assert!(drive >= 'D' && drive <= 'Z');
assert!(('D'..='Z').contains(&drive));
}
#[test]