feat(resilience): implement CHAOS-01 to CHAOS-03 crash consistency and input guards

- CHAOS-01: write chunk and update node size atomically via write_chunk_and_update_size in a single SQLite transaction
- CHAOS-02: reject null-bytes and ASCII control characters in VFS paths and node names via validate_path_safety
- CHAOS-03: invalidate in-memory chunk cache on I/O and disk-full errors to prevent drop failure cascades
This commit is contained in:
2026-09-10 13:58:39 +02:00
parent 541190cff4
commit dae0b3b7c3
2 changed files with 179 additions and 29 deletions
+81
View File
@@ -1200,6 +1200,43 @@ impl Database {
Ok(())
}
/// Schreibt einen verschlüsselten Chunk und aktualisiert Dateigröße und Modifikationszeitstempel atomar
/// in einer einzigen SQLite-Transaktion (Crash-Konsistenz / Power-Loss Schutz / CHAOS-01).
pub fn write_chunk_and_update_size(
&self,
node_id: i64,
chunk_index: u32,
nonce: &[u8; 12],
tag: &[u8; 16],
ciphertext: &[u8],
new_size: u64,
modified_at: u64,
) -> Result<()> {
let mut conn = self.conn.lock().unwrap();
let tx = conn.transaction()?;
tx.execute(
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(node_id, chunk_index) DO UPDATE SET
nonce = excluded.nonce,
tag = excluded.tag,
ciphertext = excluded.ciphertext",
params![
node_id,
chunk_index,
nonce.as_slice(),
tag.as_slice(),
ciphertext,
],
)?;
tx.execute(
"UPDATE nodes SET size = ?1, modified_at = ?2 WHERE id = ?3",
params![new_size as i64, modified_at as i64, node_id],
)?;
tx.commit()?;
Ok(())
}
/// Schneidet überzählige Chunks ab (z. B. beim Truncate oder Überschreiben mit kleinerer Datei)
/// und shreddert die abzuschneidenden Chunks vorher mit kryptografischem Zufallsrauschen.
pub fn truncate_chunks_after(&self, node_id: i64, max_chunk_index: u32) -> Result<()> {
@@ -1709,4 +1746,48 @@ mod tests {
).unwrap();
assert_eq!(has_vault_id_chunks, 0, "vault_id darf nicht in chunks existieren");
}
#[test]
fn test_atomic_chunk_write_and_size_update() {
let db = Database::open_in_memory().unwrap();
let (salt, kdf, wrapped_dek, nonce, tag) = (
[1u8; 16],
KdfParams::default(),
vec![2u8; 40],
[3u8; 12],
[4u8; 16],
);
db.init_schema(&salt, &kdf, &wrapped_dek, &nonce, &tag).unwrap();
let file = db.create_node(1, "crash_test.bin", false).unwrap();
assert_eq!(file.size, 0);
let chunk_nonce = [5u8; 12];
let chunk_tag = [6u8; 16];
let ciphertext = vec![7u8; 1024];
let new_size = 1024u64;
let modified_at = 2000000u64;
db.write_chunk_and_update_size(
file.id,
0,
&chunk_nonce,
&chunk_tag,
&ciphertext,
new_size,
modified_at,
).expect("Atomic write");
// Chunk verifizieren
let chunk = db.read_chunk(file.id, 0).unwrap().expect("Chunk must exist");
assert_eq!(chunk.ciphertext, ciphertext);
assert_eq!(chunk.nonce, chunk_nonce);
assert_eq!(chunk.tag, chunk_tag);
// Inode verifizieren
let nodes = db.list_children(1).unwrap();
let updated_file = nodes.iter().find(|n| n.id == file.id).unwrap();
assert_eq!(updated_file.size, new_size);
assert_eq!(updated_file.modified_at, modified_at);
}
}