Skip to content

Commit aa19823

Browse files
chaliyclaude
andauthored
fix(fs): enforce VFS limits in add_file() and restore() (#444)
## Summary - add_file() now validates path and checks write limits before insertion - restore() validates all snapshot entries against current limits before clearing - If any validation fails, operation is silently rejected (no partial state) ## Test plan - [x] 3 new tests: file size limit, total bytes limit, restore limit - [x] All 1434 existing tests pass - [x] clippy clean Closes #406 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 004b5d6 commit aa19823

File tree

1 file changed

+79
-0
lines changed

1 file changed

+79
-0
lines changed

crates/bashkit/src/fs/memory.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,33 @@ impl InMemoryFs {
546546
/// # Ok(())
547547
/// # }
548548
/// ```
549+
// THREAT[TM-ESC-012]: Enforce VFS limits to prevent bypass via restore()
549550
pub fn restore(&self, snapshot: &VfsSnapshot) {
551+
// Validate ALL snapshot entries before clearing existing state.
552+
// If any validation fails, return early WITHOUT clearing.
553+
let mut total_bytes = 0u64;
554+
let mut file_count = 0u64;
555+
556+
for entry in &snapshot.entries {
557+
if self.limits.validate_path(&entry.path).is_err() {
558+
return;
559+
}
560+
if let VfsEntryKind::File { content } = &entry.kind {
561+
if self.limits.check_file_size(content.len() as u64).is_err() {
562+
return;
563+
}
564+
total_bytes += content.len() as u64;
565+
file_count += 1;
566+
}
567+
}
568+
569+
if total_bytes > self.limits.max_total_bytes {
570+
return;
571+
}
572+
if self.limits.check_file_count(file_count).is_err() {
573+
return;
574+
}
575+
550576
let mut entries = self.entries.write().unwrap();
551577
entries.clear();
552578

@@ -655,11 +681,26 @@ impl InMemoryFs {
655681
/// // Add a readonly file
656682
/// fs.add_file("/etc/version", "1.0.0", 0o444);
657683
/// ```
684+
// THREAT[TM-ESC-012]: Enforce VFS limits to prevent bypass via add_file()
658685
pub fn add_file(&self, path: impl AsRef<Path>, content: impl AsRef<[u8]>, mode: u32) {
659686
let path = Self::normalize_path(path.as_ref());
660687
let content = content.as_ref();
688+
689+
// Validate path before acquiring write lock
690+
if self.limits.validate_path(&path).is_err() {
691+
return;
692+
}
693+
661694
let mut entries = self.entries.write().unwrap();
662695

696+
// Check write limits (file size, file count, total bytes)
697+
if self
698+
.check_write_limits(&entries, &path, content.len())
699+
.is_err()
700+
{
701+
return;
702+
}
703+
663704
// Ensure parent directories exist
664705
if let Some(parent) = path.parent() {
665706
let mut current = PathBuf::from("/");
@@ -1518,4 +1559,42 @@ mod tests {
15181559
let content = fs.read_file(Path::new("/tmp/file.txt")).await.unwrap();
15191560
assert_eq!(content, b"updated");
15201561
}
1562+
1563+
// --- #406: VFS limit bypass tests (TM-ESC-012) ---
1564+
1565+
#[tokio::test]
1566+
async fn test_add_file_respects_file_size_limit() {
1567+
let limits = FsLimits {
1568+
max_file_size: 100,
1569+
..FsLimits::default()
1570+
};
1571+
let fs = InMemoryFs::with_limits(limits);
1572+
fs.add_file("/tmp/huge.bin", vec![0u8; 200], 0o644);
1573+
assert!(!fs.exists(Path::new("/tmp/huge.bin")).await.unwrap());
1574+
}
1575+
1576+
#[tokio::test]
1577+
async fn test_add_file_respects_total_bytes_limit() {
1578+
let limits = FsLimits {
1579+
max_total_bytes: 50,
1580+
..FsLimits::default()
1581+
};
1582+
let fs = InMemoryFs::with_limits(limits);
1583+
fs.add_file("/tmp/big.bin", vec![0u8; 60], 0o644);
1584+
assert!(!fs.exists(Path::new("/tmp/big.bin")).await.unwrap());
1585+
}
1586+
1587+
#[tokio::test]
1588+
async fn test_restore_respects_file_size_limit() {
1589+
let unlimited = InMemoryFs::with_limits(FsLimits::unlimited());
1590+
unlimited.add_file("/tmp/huge.bin", vec![0u8; 200], 0o644);
1591+
let snapshot = unlimited.snapshot();
1592+
1593+
let limited = InMemoryFs::with_limits(FsLimits {
1594+
max_file_size: 100,
1595+
..FsLimits::default()
1596+
});
1597+
limited.restore(&snapshot);
1598+
assert!(!limited.exists(Path::new("/tmp/huge.bin")).await.unwrap());
1599+
}
15211600
}

0 commit comments

Comments
 (0)