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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user