From ab05bee4c8224ed197a18965ed64a5b569f0e1ab Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 12 Jun 2026 09:24:16 +0000 Subject: [PATCH 1/2] fix(fs): preserve recursive delete child whiteouts --- crates/bashkit/src/fs/overlay.rs | 48 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/crates/bashkit/src/fs/overlay.rs b/crates/bashkit/src/fs/overlay.rs index 4d6935b0..bdc10e2f 100644 --- a/crates/bashkit/src/fs/overlay.rs +++ b/crates/bashkit/src/fs/overlay.rs @@ -323,11 +323,14 @@ impl OverlayFs { match meta.file_type { FileType::File if !self.upper.exists(&child).await.unwrap_or(false) => { self.hide_lower_file(meta.size); + self.add_whiteout(&child); } FileType::Directory => { self.hide_lower_dir(); - // Recurse into subdirectories + // THREAT[TM-DOS-038]: Materialize child whiteouts so + // recreating the parent does not reveal accounted children. Box::pin(self.hide_lower_children_recursive(&child)).await; + self.add_whiteout(&child); } _ => {} } @@ -618,16 +621,17 @@ impl FileSystem for OverlayFs { } // If was in lower, add whiteout and track hiding. - // If in_upper was also true, the lower was already hidden (by the upper - // override). The whiteout replaces the override as the hiding mechanism, - // so no additional deduction needed. + // File upper overrides already hide the lower file. Recursive directory + // deletes must still materialize child whiteouts because upper directory + // overlays merge lower children instead of replacing the subtree. if in_lower { - // Newly hiding the lower entry only if there was no upper override - if !in_upper && let Ok(meta) = self.lower.stat(&path).await { + if let Ok(meta) = self.lower.stat(&path).await { match meta.file_type { - FileType::File => self.hide_lower_file(meta.size), + FileType::File if !in_upper => self.hide_lower_file(meta.size), FileType::Directory => { - self.hide_lower_dir(); + if !in_upper { + self.hide_lower_dir(); + } // THREAT[TM-DOS-038]: Recursive delete must track all // lower children for accurate usage deduction. if recursive { @@ -1625,6 +1629,34 @@ mod tests { ); } + #[tokio::test] + async fn test_recursive_delete_recreate_keeps_lower_children_hidden() { + let lower = Arc::new(InMemoryFs::new()); + lower.mkdir(Path::new("/dir"), true).await.unwrap(); + lower + .write_file(Path::new("/dir/a"), &[b'a'; 80]) + .await + .unwrap(); + + let overlay = OverlayFs::new(lower); + + overlay.remove(Path::new("/dir"), true).await.unwrap(); + overlay.mkdir(Path::new("/dir"), false).await.unwrap(); + + assert!( + overlay.exists(Path::new("/dir")).await.unwrap(), + "recreated directory should exist" + ); + assert!( + !overlay.exists(Path::new("/dir/a")).await.unwrap(), + "recursive delete child whiteout must survive parent recreation" + ); + assert!( + overlay.read_file(Path::new("/dir/a")).await.is_err(), + "lower child must not reappear after mkdir removes parent whiteout" + ); + } + #[tokio::test] async fn test_recursive_delete_skips_already_hidden_children() { let lower = Arc::new(InMemoryFs::new()); From f70db5b3cb5e48fee6ae95aa14b452a2d2b0ce6e Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 12 Jun 2026 10:11:15 +0000 Subject: [PATCH 2/2] fix(fs): extend whiteout materialization to Symlink/Fifo entries Symlink and Fifo lower-layer entries were not getting per-child whiteouts during recursive delete (hide_lower_children_recursive) and were not being deducted from lower_hidden in remove(), allowing them to reappear and causing inaccurate usage accounting after parent directory recreation. --- crates/bashkit/src/fs/overlay.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bashkit/src/fs/overlay.rs b/crates/bashkit/src/fs/overlay.rs index bdc10e2f..cb3007c1 100644 --- a/crates/bashkit/src/fs/overlay.rs +++ b/crates/bashkit/src/fs/overlay.rs @@ -321,7 +321,9 @@ impl OverlayFs { } if let Ok(meta) = self.lower.stat(&child).await { match meta.file_type { - FileType::File if !self.upper.exists(&child).await.unwrap_or(false) => { + FileType::File | FileType::Symlink | FileType::Fifo + if !self.upper.exists(&child).await.unwrap_or(false) => + { self.hide_lower_file(meta.size); self.add_whiteout(&child); } @@ -627,7 +629,9 @@ impl FileSystem for OverlayFs { if in_lower { if let Ok(meta) = self.lower.stat(&path).await { match meta.file_type { - FileType::File if !in_upper => self.hide_lower_file(meta.size), + FileType::File | FileType::Symlink | FileType::Fifo if !in_upper => { + self.hide_lower_file(meta.size) + } FileType::Directory => { if !in_upper { self.hide_lower_dir();