fix(carrier): C-03 — decouple manifest re-encryption with dirty tracking and unmount flush

This commit is contained in:
2026-09-19 23:13:34 +02:00
parent 8455dc02db
commit c606fa8132
5 changed files with 298 additions and 21 deletions
+75 -10
View File
@@ -277,6 +277,10 @@ pub struct CarrierFsInner {
pub custom_leak_rules: Arc<Vec<String>>,
pub leak_counter: Arc<AtomicU64>,
pub manifest: CarrierManifest,
pub manifest_dirty: bool,
pub manifest_save_count: u64,
pub dirty_ops_count: u32,
pub last_manifest_save: u64,
pub last_activity: Arc<AtomicU64>,
}
@@ -284,7 +288,38 @@ impl CarrierFsInner {
pub fn is_leak(&self, name: &str) -> bool {
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
}
/// Markiert das Manifest als modifiziert (dirty) ohne teure synchrone Neuverschlüsselung (C-03).
/// Erst bei flush(), close(), unmount/drop oder nach Überschreiten von 50 Operationen wird
/// das vollständige 1-MB-Manifest tatsächlich neu verschlüsselt und geschrieben.
///
/// Konsistenzhinweis:
/// Bei einem abrupten Prozessabbruch gilt der zuletzt gespeicherte Zustand. Dank C-02
/// (rollierendes Manifest auf Block 0 und Block 1) ist die Integrität stets gewährleistet.
pub fn mark_dirty(&mut self) {
self.manifest_dirty = true;
self.dirty_ops_count += 1;
if self.dirty_ops_count >= 50 {
let _ = self.flush_manifest_if_dirty();
}
}
/// Schreibt das Manifest nur, wenn ungespeicherte Änderungen anstehen (C-03).
pub fn flush_manifest_if_dirty(&mut self) -> Result<()> {
if self.manifest_dirty {
self.save_manifest()?;
}
Ok(())
}
pub fn save_manifest(&mut self) -> Result<()> {
self.manifest_save_count += 1;
self.manifest_dirty = false;
self.dirty_ops_count = 0;
self.last_manifest_save = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.manifest.manifest_generation += 1;
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
@@ -304,7 +339,7 @@ impl CarrierFsInner {
// C-02: Rollierendes Dual-Block-Manifest.
// Gerade Generation -> Block 0, ungerade Generation -> Block 1.
let target_block = if self.manifest.manifest_generation % 2 == 0 {
let target_block = if self.manifest.manifest_generation.is_multiple_of(2) {
0
} else {
1
@@ -375,6 +410,14 @@ impl CarrierFsInner {
impl Drop for CarrierFsInner {
fn drop(&mut self) {
if self.manifest_dirty {
if let Err(e) = self.save_manifest() {
error!(
"Fehler beim finalen Speichern des Carrier-Manifests im Drop: {:?}",
e
);
}
}
crate::windows::unlock_memory(self.dek_outer.as_ptr(), 32);
crate::windows::unlock_memory(self.dek_inner.as_ptr(), 32);
}
@@ -491,6 +534,10 @@ impl CarrierFs {
custom_leak_rules: Arc::new(custom_leak_rules),
leak_counter: Arc::new(AtomicU64::new(0)),
manifest,
manifest_dirty: false,
manifest_save_count: 0,
dirty_ops_count: 0,
last_manifest_save: now,
last_activity: Arc::new(AtomicU64::new(now)),
};
@@ -519,6 +566,21 @@ impl CarrierFs {
)
}
pub fn sync_manifest(&self) -> Result<()> {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.flush_manifest_if_dirty()
}
pub fn manifest_save_count(&self) -> u64 {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.manifest_save_count
}
pub fn is_manifest_dirty(&self) -> bool {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.manifest_dirty
}
pub fn leak_counter(&self) -> Arc<AtomicU64> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.leak_counter.clone()
@@ -597,7 +659,7 @@ impl DavFileSystem for CarrierFs {
n.size = 0;
n.modified_at = now;
inner.manifest.inodes.insert(n.id, n.clone());
let _ = inner.save_manifest();
inner.mark_dirty();
}
n
}
@@ -628,7 +690,7 @@ impl DavFileSystem for CarrierFs {
};
inner.manifest.inodes.insert(new_id, new_inode.clone());
let _ = inner.save_manifest();
inner.mark_dirty();
new_inode
} else {
return Err(FsError::NotFound);
@@ -750,7 +812,7 @@ impl DavFileSystem for CarrierFs {
};
inner.manifest.inodes.insert(new_id, new_dir);
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
Ok(())
})
@@ -783,7 +845,7 @@ impl DavFileSystem for CarrierFs {
}
inner.manifest.inodes.remove(&node.id);
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
Ok(())
})
@@ -812,7 +874,7 @@ impl DavFileSystem for CarrierFs {
}
inner.manifest.inodes.remove(&node.id);
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
Ok(())
})
@@ -869,7 +931,7 @@ impl DavFileSystem for CarrierFs {
inode.modified_at = now;
}
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
Ok(())
})
@@ -998,7 +1060,7 @@ impl DavFileSystem for CarrierFs {
};
inner.manifest.inodes.insert(new_id, new_inode);
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
Ok(())
})
@@ -1305,7 +1367,10 @@ impl DavFile for CarrierFile {
inode.modified_at = now;
}
inner.save_manifest().map_err(|_| FsError::GeneralFailure)?;
inner.mark_dirty();
inner
.flush_manifest_if_dirty()
.map_err(|_| FsError::GeneralFailure)?;
self.meta.size = self.file_size;
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
@@ -1335,7 +1400,7 @@ impl Drop for CarrierFile {
inode.blocks = self.blocks.clone();
inode.modified_at = now;
}
let _ = inner.save_manifest();
inner.mark_dirty();
}
}
}
+4
View File
@@ -225,6 +225,7 @@ pub async fn mount_container(
);
let fs_leak_counter = fs.leak_counter();
let last_activity = fs.last_activity();
let fs_clone = fs.clone();
let dav_server = DavHandler::builder()
.filesystem(Box::new(fs))
.locksystem(FakeLs::new())
@@ -282,6 +283,7 @@ pub async fn mount_container(
// Netzlaufwerk bzw. Verzeichnis einbinden
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
let _ = fs_clone.sync_carrier_manifest();
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
return Err(e);
@@ -541,6 +543,7 @@ pub async fn mount_container(
}
if stealth {
let _ = fs_clone.sync_carrier_manifest();
let _ = unmount_drive(drive_letter);
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
@@ -555,6 +558,7 @@ pub async fn mount_container(
#[cfg(not(windows))]
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
} else {
let _ = fs_clone.sync_carrier_manifest();
#[cfg(windows)]
{
print!(
+28 -6
View File
@@ -752,13 +752,24 @@ impl Database {
}
outer_plaintext_0[32..ct_end_0].copy_from_slice(&inner_ct_0);
let (outer_ct_0, outer_nonce_0, outer_tag_0) =
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext_0, FORMAT_VERSION, 0)?;
let (outer_ct_0, outer_nonce_0, outer_tag_0) = crate::crypto::encrypt_chunk(
dek_0,
c_id,
0,
&outer_plaintext_0,
FORMAT_VERSION,
0,
)?;
conn.execute(
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
VALUES (?1, 0, 0, ?2, ?3, ?4)",
params![c_id, outer_nonce_0.as_slice(), outer_tag_0.as_slice(), outer_ct_0],
params![
c_id,
outer_nonce_0.as_slice(),
outer_tag_0.as_slice(),
outer_ct_0
],
)?;
// 2. Block 1 schreiben (Redundante Zweitkopie, Gen 0, C-02)
@@ -778,13 +789,24 @@ impl Database {
}
outer_plaintext_1[32..ct_end_1].copy_from_slice(&inner_ct_1);
let (outer_ct_1, outer_nonce_1, outer_tag_1) =
crate::crypto::encrypt_chunk(dek_0, c_id, 1, &outer_plaintext_1, FORMAT_VERSION, 0)?;
let (outer_ct_1, outer_nonce_1, outer_tag_1) = crate::crypto::encrypt_chunk(
dek_0,
c_id,
1,
&outer_plaintext_1,
FORMAT_VERSION,
0,
)?;
conn.execute(
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
VALUES (?1, 1, 0, ?2, ?3, ?4)",
params![c_id, outer_nonce_1.as_slice(), outer_tag_1.as_slice(), outer_ct_1],
params![
c_id,
outer_nonce_1.as_slice(),
outer_tag_1.as_slice(),
outer_ct_1
],
)?;
// Blöcke 2..total_blocks-1 mit DEK_0 vorallokieren (C-02)
+7
View File
@@ -788,6 +788,13 @@ impl SanctumFs {
}
}
pub fn sync_carrier_manifest(&self) -> Result<()> {
if let Some(ref cfs) = self.carrier_fs {
cfs.sync_manifest()?;
}
Ok(())
}
pub fn leak_count(&self) -> u64 {
self.leak_counter().load(Ordering::Relaxed)
}