From bc500956ffe6472b10acc009c1d9b3d999a5c993 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 24 Aug 2026 17:57:43 -0700 Subject: [PATCH 1/8] Switch to separated file system `Context` --- litebox/src/fs/in_mem.rs | 14 +- litebox/src/fs/nine_p/tests.rs | 197 +++++-- litebox/src/fs/resolver.rs | 67 +-- litebox/src/fs/tests.rs | 513 ++++++++++++------ .../tests/common/mod.rs | 5 +- litebox_runner_linux_userland/tests/loader.rs | 6 +- litebox_shim_linux/src/lib.rs | 22 +- litebox_shim_linux/src/syscalls/file.rs | 53 +- litebox_shim_linux/src/syscalls/unix.rs | 28 +- litebox_shim_linux/src/transport.rs | 14 +- 10 files changed, 606 insertions(+), 313 deletions(-) diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index f7969fc54..ac8ef14d2 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -702,24 +702,26 @@ struct Permissions { #[cfg(test)] pub(super) fn with_root_privileges( fs: &mut super::resolver::Resolver>, - f: impl FnOnce(&mut super::resolver::Resolver>), + context: &mut super::resolver::Context, + f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), ) { - with_user(fs, UserInfo::ROOT.user, UserInfo::ROOT.group, f); + with_user(fs, context, UserInfo::ROOT.user, UserInfo::ROOT.group, f); } /// Run `f` with the acting user set to `user`/`group`. See [`with_root_privileges`]. #[cfg(test)] pub(super) fn with_user( fs: &mut super::resolver::Resolver>, + context: &mut super::resolver::Context, user: u16, group: u16, - f: impl FnOnce(&mut super::resolver::Resolver>), + f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), ) { let user = UserInfo { user, group }; - let original_user = fs.swap_acting_user(user); + let original_user = context.swap_acting_user(user); fs.backend_mut().current_user = user; - f(fs); - let user_again = fs.swap_acting_user(original_user); + f(fs, context); + let user_again = context.swap_acting_user(original_user); fs.backend_mut().current_user = original_user; assert!(user_again.user == user.user && user_again.group == user.group); } diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox/src/fs/nine_p/tests.rs index 58456aed6..8a61d1e39 100644 --- a/litebox/src/fs/nine_p/tests.rs +++ b/litebox/src/fs/nine_p/tests.rs @@ -208,13 +208,19 @@ fn connect_9p( #[test] fn test_nine_p_create_and_read_file() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); // Create a file and write to it let fd = fs - .open("/hello.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/hello.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("failed to create file via 9P"); let data = b"Hello from litebox 9P!"; @@ -231,7 +237,7 @@ fn test_nine_p_create_and_read_file() { // Read the file back through 9P let fd = fs - .open("/hello.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/hello.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading via 9P"); let mut buf = alloc::vec![0u8; 256]; @@ -243,19 +249,21 @@ fn test_nine_p_create_and_read_file() { #[test] fn test_nine_p_mkdir_and_readdir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); // Create directories - fs.mkdir("/subdir", Mode::RWXU) + fs.mkdir(&ctx, "/subdir", Mode::RWXU) .expect("failed to mkdir via 9P"); - fs.mkdir("/subdir/nested", Mode::RWXU) + fs.mkdir(&ctx, "/subdir/nested", Mode::RWXU) .expect("failed to mkdir nested via 9P"); // Create a file inside the subdirectory let fd = fs .open( + &ctx, "/subdir/file.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -266,7 +274,7 @@ fn test_nine_p_mkdir_and_readdir() { // Read the root directory let fd = fs - .open("/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("failed to open root dir"); let entries = fs.read_dir(&fd).expect("failed to readdir root"); fs.close(&fd).unwrap(); @@ -279,7 +287,12 @@ fn test_nine_p_mkdir_and_readdir() { // Read the subdirectory let fd = fs - .open("/subdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + &ctx, + "/subdir", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) .expect("failed to open subdir"); let entries = fs.read_dir(&fd).expect("failed to readdir subdir"); fs.close(&fd).unwrap(); @@ -297,29 +310,37 @@ fn test_nine_p_mkdir_and_readdir() { #[test] fn test_nine_p_unlink_and_rmdir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); // Create a file, then delete it let fd = fs - .open("/to_delete.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/to_delete.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("failed to create file"); fs.close(&fd).unwrap(); - fs.unlink("/to_delete.txt") + fs.unlink(&ctx, "/to_delete.txt") .expect("failed to unlink file via 9P"); // Verify the file is gone assert!( - fs.open("/to_delete.txt", OFlags::RDONLY, Mode::empty()) + fs.open(&ctx, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) .is_err(), "file should no longer exist" ); // Create a directory, then remove it - fs.mkdir("/to_remove", Mode::RWXU).expect("failed to mkdir"); - fs.rmdir("/to_remove").expect("failed to rmdir via 9P"); + fs.mkdir(&ctx, "/to_remove", Mode::RWXU) + .expect("failed to mkdir"); + fs.rmdir(&ctx, "/to_remove") + .expect("failed to rmdir via 9P"); // Verify the directory is gone on the host assert!( @@ -330,6 +351,7 @@ fn test_nine_p_unlink_and_rmdir() { #[test] fn test_nine_p_file_status() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -337,6 +359,7 @@ fn test_nine_p_file_status() { // Create a file with known content let fd = fs .open( + &ctx, "/status_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -348,7 +371,7 @@ fn test_nine_p_file_status() { // Check file_status via path let status = fs - .file_status("/status_test.txt") + .file_status(&ctx, "/status_test.txt") .expect("failed to stat file"); assert_eq!( status.file_type, @@ -358,8 +381,10 @@ fn test_nine_p_file_status() { assert_eq!(status.size, 10, "file size should be 10 bytes"); // Check directory status - fs.mkdir("/stat_dir", Mode::RWXU).unwrap(); - let status = fs.file_status("/stat_dir").expect("failed to stat dir"); + fs.mkdir(&ctx, "/stat_dir", Mode::RWXU).unwrap(); + let status = fs + .file_status(&ctx, "/stat_dir") + .expect("failed to stat dir"); assert_eq!( status.file_type, crate::fs::FileType::Directory, @@ -369,20 +394,26 @@ fn test_nine_p_file_status() { #[test] fn test_nine_p_seek_and_partial_read() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); // Write a file with known content let fd = fs - .open("/seek_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/seek_test.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("failed to create file"); fs.write(&fd, b"ABCDEFGHIJ", None).unwrap(); fs.close(&fd).unwrap(); // Open for reading and seek let fd = fs - .open("/seek_test.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); // Seek to offset 5 @@ -401,13 +432,19 @@ fn test_nine_p_seek_and_partial_read() { #[test] fn test_nine_p_truncate() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); // Write a file let fd = fs - .open("/trunc_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .open( + &ctx, + "/trunc_test.txt", + OFlags::CREAT | OFlags::RDWR, + Mode::RWXU, + ) .expect("failed to create file"); fs.write(&fd, b"Hello, World!", None).unwrap(); @@ -423,6 +460,7 @@ fn test_nine_p_truncate() { #[test] fn test_nine_p_host_files_visible() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -439,7 +477,7 @@ fn test_nine_p_host_files_visible() { // Read file created on the host through 9P let fd = fs - .open("/host_file.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/host_file.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open host file via 9P"); let mut buf = alloc::vec![0u8; 256]; let n = fs.read(&fd, &mut buf, None).unwrap(); @@ -449,6 +487,7 @@ fn test_nine_p_host_files_visible() { // List host directory through 9P let fd = fs .open( + &ctx, "/host_dir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -541,29 +580,32 @@ fn connect_9p_broken( /// breaks after the filesystem has been attached. #[test] fn test_nine_p_broken_open() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // 2 writes: version + attach. The next write (open's walk) will fail. let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.open("/anything.txt", OFlags::RDONLY, Mode::empty()); + let result = fs.open(&ctx, "/anything.txt", OFlags::RDONLY, Mode::empty()); assert!(matches!(result, Err(OpenError::Io))); } /// Creating a file should fail when the connection is broken. #[test] fn test_nine_p_broken_create() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.open("/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); + let result = fs.open(&ctx, "/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); assert!(matches!(result, Err(OpenError::Io))); } /// Reading from an fd obtained before the break should fail. #[test] fn test_nine_p_broken_read() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -571,7 +613,12 @@ fn test_nine_p_broken_read() { { let fs = connect_9p(&litebox, &server); let fd = fs - .open("/read_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/read_me.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .unwrap(); fs.write(&fd, b"data", None).unwrap(); fs.close(&fd).unwrap(); @@ -580,7 +627,7 @@ fn test_nine_p_broken_read() { // 4 writes: version + attach + walk + lopen. Then read will fail. let fs = connect_9p_broken(&litebox, &server, 4); let fd = fs - .open("/read_me.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/read_me.txt", OFlags::RDONLY, Mode::empty()) .expect("open should succeed before break"); let mut buf = alloc::vec![0u8; 64]; @@ -591,6 +638,7 @@ fn test_nine_p_broken_read() { /// Writing to an fd obtained before the break should fail. #[test] fn test_nine_p_broken_write() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -598,7 +646,12 @@ fn test_nine_p_broken_write() { // parent directory's fid + create. Then write will fail. let fs = connect_9p_broken(&litebox, &server, 5); let fd = fs - .open("/write_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/write_me.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("create should succeed before break"); let result = fs.write(&fd, b"data", None); @@ -608,24 +661,26 @@ fn test_nine_p_broken_write() { /// mkdir should fail when the connection is broken. #[test] fn test_nine_p_broken_mkdir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.mkdir("/broken_dir", Mode::RWXU); + let result = fs.mkdir(&ctx, "/broken_dir", Mode::RWXU); assert!(matches!(result, Err(MkdirError::Io))); } /// readdir should fail when the connection breaks during the directory read. #[test] fn test_nine_p_broken_readdir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // 4 writes: version + attach + walk + lopen for the directory. let fs = connect_9p_broken(&litebox, &server, 4); let fd = fs - .open("/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("open dir should succeed before break"); let result = fs.read_dir(&fd); @@ -635,6 +690,7 @@ fn test_nine_p_broken_readdir() { /// unlink should fail when the connection is broken. #[test] fn test_nine_p_broken_unlink() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -642,47 +698,55 @@ fn test_nine_p_broken_unlink() { { let fs = connect_9p(&litebox, &server); let fd = fs - .open("/to_unlink.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/to_unlink.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .unwrap(); fs.close(&fd).unwrap(); } let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.unlink("/to_unlink.txt"); + let result = fs.unlink(&ctx, "/to_unlink.txt"); assert!(matches!(result, Err(UnlinkError::Io))); } /// rmdir should fail when the connection is broken. #[test] fn test_nine_p_broken_rmdir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // Pre-create a directory { let fs = connect_9p(&litebox, &server); - fs.mkdir("/to_rmdir", Mode::RWXU).unwrap(); + fs.mkdir(&ctx, "/to_rmdir", Mode::RWXU).unwrap(); } let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.rmdir("/to_rmdir"); + let result = fs.rmdir(&ctx, "/to_rmdir"); assert!(matches!(result, Err(RmdirError::Io))); } /// file_status should fail when the connection is broken. #[test] fn test_nine_p_broken_file_status() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.file_status("/"); + let result = fs.file_status(&ctx, "/"); assert!(matches!(result, Err(FileStatusError::Io))); } /// truncate should fail when the connection breaks after open. #[test] fn test_nine_p_broken_truncate() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -690,7 +754,12 @@ fn test_nine_p_broken_truncate() { { let fs = connect_9p(&litebox, &server); let fd = fs - .open("/to_trunc.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/to_trunc.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .unwrap(); fs.write(&fd, b"some data", None).unwrap(); fs.close(&fd).unwrap(); @@ -699,7 +768,7 @@ fn test_nine_p_broken_truncate() { // 4 writes: version + attach + walk + lopen. Then truncate will fail. let fs = connect_9p_broken(&litebox, &server, 4); let fd = fs - .open("/to_trunc.txt", OFlags::RDWR, Mode::empty()) + .open(&ctx, "/to_trunc.txt", OFlags::RDWR, Mode::empty()) .expect("open should succeed before break"); let result = fs.truncate(&fd, 0, true); @@ -709,6 +778,7 @@ fn test_nine_p_broken_truncate() { /// seek (RelativeToEnd, which requires a getattr) should fail when broken. #[test] fn test_nine_p_broken_seek() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); @@ -716,7 +786,12 @@ fn test_nine_p_broken_seek() { { let fs = connect_9p(&litebox, &server); let fd = fs - .open("/to_seek.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/to_seek.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .unwrap(); fs.write(&fd, b"data", None).unwrap(); fs.close(&fd).unwrap(); @@ -725,7 +800,7 @@ fn test_nine_p_broken_seek() { // 4 writes: version + attach + walk + lopen. Then the getattr for seek will fail. let fs = connect_9p_broken(&litebox, &server, 4); let fd = fs - .open("/to_seek.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/to_seek.txt", OFlags::RDONLY, Mode::empty()) .expect("open should succeed before break"); let result = fs.seek(&fd, -1, crate::fs::SeekWhence::RelativeToEnd); @@ -736,6 +811,8 @@ fn test_nine_p_broken_seek() { fn test_nine_p_deep_path_walk() { use core::fmt::Write as _; + let ctx = crate::fs::resolver::Context::new(); + let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -745,21 +822,26 @@ fn test_nine_p_deep_path_walk() { for i in 0..20 { path.push('/'); write!(path, "d{i}").unwrap(); - fs.mkdir(&*path, Mode::RWXU) + fs.mkdir(&ctx, &*path, Mode::RWXU) .expect("failed to mkdir deep path component"); } // Create a file at the bottom let file_path = path.clone() + "/deep_file.txt"; let fd = fs - .open(&*file_path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + &*file_path, + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("failed to create file in deep path"); fs.write(&fd, b"deep content", None).unwrap(); fs.close(&fd).unwrap(); // Read it back let fd = fs - .open(&*file_path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, &*file_path, OFlags::RDONLY, Mode::empty()) .expect("failed to open file in deep path"); let mut buf = alloc::vec![0u8; 64]; let n = fs.read(&fd, &mut buf, None).unwrap(); @@ -768,7 +850,7 @@ fn test_nine_p_deep_path_walk() { // Verify file_status works through the deep path let status = fs - .file_status(&*file_path) + .file_status(&ctx, &*file_path) .expect("failed to stat deep file"); assert_eq!(status.file_type, crate::fs::FileType::RegularFile); assert_eq!(status.size, 12); @@ -776,6 +858,7 @@ fn test_nine_p_deep_path_walk() { #[test] fn test_nine_p_chmod() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -783,6 +866,7 @@ fn test_nine_p_chmod() { // Create a file let fd = fs .open( + &ctx, "/chmod_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -791,7 +875,7 @@ fn test_nine_p_chmod() { fs.close(&fd).unwrap(); // Change permissions to read-only for user - fs.chmod("/chmod_test.txt", Mode::RUSR) + fs.chmod(&ctx, "/chmod_test.txt", Mode::RUSR) .expect("chmod failed"); // Verify via host filesystem @@ -806,7 +890,7 @@ fn test_nine_p_chmod() { // Also verify via 9P file_status let status = fs - .file_status("/chmod_test.txt") + .file_status(&ctx, "/chmod_test.txt") .expect("file_status failed"); assert!(status.mode.contains(Mode::RUSR), "mode should contain RUSR"); assert!( @@ -817,6 +901,7 @@ fn test_nine_p_chmod() { #[test] fn test_nine_p_chown() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -824,6 +909,7 @@ fn test_nine_p_chown() { // Create a file let fd = fs .open( + &ctx, "/chown_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -833,11 +919,12 @@ fn test_nine_p_chown() { // Get current ownership let status_before = fs - .file_status("/chown_test.txt") + .file_status(&ctx, "/chown_test.txt") .expect("file_status failed"); // Change group to the same value (chown to a different uid/gid requires root) fs.chown( + &ctx, "/chown_test.txt", Some(status_before.owner.user), Some(status_before.owner.group), @@ -846,7 +933,7 @@ fn test_nine_p_chown() { // Verify ownership hasn't changed let status_after = fs - .file_status("/chown_test.txt") + .file_status(&ctx, "/chown_test.txt") .expect("file_status failed after chown"); assert_eq!(status_after.owner.user, status_before.owner.user); assert_eq!(status_after.owner.group, status_before.owner.group); @@ -854,6 +941,7 @@ fn test_nine_p_chown() { #[test] fn test_nine_p_fd_file_status() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -861,6 +949,7 @@ fn test_nine_p_fd_file_status() { // Create a file with known content let fd = fs .open( + &ctx, "/fd_stat_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -871,7 +960,7 @@ fn test_nine_p_fd_file_status() { // Open the file and check fd_file_status let fd = fs - .open("/fd_stat_test.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/fd_stat_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file"); let status = fs.fd_file_status(&fd).expect("fd_file_status failed"); @@ -882,7 +971,7 @@ fn test_nine_p_fd_file_status() { fs.close(&fd).unwrap(); let fd = fs - .open("/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("failed to open root dir"); let status = fs .fd_file_status(&fd) @@ -893,6 +982,7 @@ fn test_nine_p_fd_file_status() { #[test] fn test_nine_p_large_read_write() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); @@ -906,7 +996,12 @@ fn test_nine_p_large_read_write() { .collect(); let fd = fs - .open("/large_test.bin", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .open( + &ctx, + "/large_test.bin", + OFlags::CREAT | OFlags::RDWR, + Mode::RWXU, + ) .expect("failed to create file"); // Write in a loop (the client caps each write to msize - IOHDRSZ) @@ -922,7 +1017,7 @@ fn test_nine_p_large_read_write() { // Read it all back let fd = fs - .open("/large_test.bin", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/large_test.bin", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); let mut read_buf = alloc::vec![0u8; data_size]; @@ -944,12 +1039,18 @@ fn test_nine_p_large_read_write() { #[test] fn test_nine_p_explicit_offset_read_write() { + let ctx = crate::fs::resolver::Context::new(); let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); let fs = connect_9p(&litebox, &server); let fd = fs - .open("/offset_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .open( + &ctx, + "/offset_test.txt", + OFlags::CREAT | OFlags::RDWR, + Mode::RWXU, + ) .expect("failed to create file"); // Write "AAAAAAAAAA" at offset 0 using implicit offset @@ -974,7 +1075,7 @@ fn test_nine_p_explicit_offset_read_write() { // Now test explicit offset reads let fd = fs - .open("/offset_test.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/offset_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open for reading"); // Read 5 bytes at explicit offset 5 → "BBBBB" diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index 90bd37a8e..297c4a541 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -25,18 +25,12 @@ use super::{ }; /// The north-facing filesystem entry point, generic over a [`Backend`](super::backend::Backend). -// NOTE(jayb): the `Context` separation is in preparation for multi-process support; specifically, -// each guest process would have their own `Context` but would share the resolver. Currently, the -// interfaces do not show the full actual separated context support (yet!); instead, callers share -// the single `migration_context` below. Nonetheless, future changes will separate this out. pub struct Resolver< Platform: sync::RawSyncPrimitivesProvider, Backend: super::backend::Backend + 'static, > { litebox: LiteBox, backend: Backend, - /// Stand-in for the per-caller context, until callers own their own. See the note above. - migration_context: Context, } impl @@ -48,22 +42,9 @@ impl UserInfo { - core::mem::replace(&mut self.migration_context.user_info, user) - } - /// Direct access to the backend, so that the tests can reach backend-owned state (namely its /// own copy of the acting user). /// @@ -87,6 +68,20 @@ pub struct Context { } impl Context { + /// Set the acting user for all subsequent operations on this context, returning the previous + /// one. + /// + /// Non-test callers set up whatever needs a different user while constructing the backend; + /// this exists so that the tests can exercise operations that depend on the acting user. + /// + /// TODO(DO NOT COMMIT): Temporarily still kept as test-only, but within the same PR, we should + /// expose getters/setters for the context, or maybe make the fields public, while also making + /// it non-exhaustive or something? + #[cfg(test)] + pub(super) fn swap_acting_user(&mut self, user: UserInfo) -> UserInfo { + core::mem::replace(&mut self.user_info, user) + } + /// A new default context, anchored at `/` for a non-root user. pub fn new() -> Context { Self { @@ -419,16 +414,6 @@ impl - Resolver -{ - fn context_pre_context_management_changes(&self) -> &Context { - &self.migration_context - } -} - impl Resolver { @@ -437,6 +422,7 @@ impl Result<(), ChmodError> { - let context = self.context_pre_context_management_changes(); + pub fn chmod(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), ChmodError> { let path = context.resolve(path)?; let handle = self .path_handle(context, &path) @@ -799,11 +783,11 @@ impl, group: Option, ) -> Result<(), ChownError> { - let context = self.context_pre_context_management_changes(); let path = context.resolve(path)?; let handle = self .path_handle(context, &path) @@ -815,8 +799,7 @@ impl Result<(), UnlinkError> { - let context = self.context_pre_context_management_changes(); + pub fn unlink(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { let path = context.resolve(path)?; let Some((parent, name)) = self.parent_dir_and_name(context, &path) @@ -840,8 +823,7 @@ impl Result<(), MkdirError> { - let context = self.context_pre_context_management_changes(); + pub fn mkdir(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), MkdirError> { let path = context.resolve(path)?; let Some((parent, name)) = self.parent_dir_and_name(context, &path) @@ -865,8 +847,7 @@ impl Result<(), RmdirError> { - let context = self.context_pre_context_management_changes(); + pub fn rmdir(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { let path = context.resolve(path)?; let Some((parent, name)) = self.parent_dir_and_name(context, &path) @@ -929,9 +910,13 @@ impl Result { + pub fn file_status( + &self, + context: &Context, + path: impl Arg, + ) -> Result { let fd = self - .open(path, OFlags::PATH, Mode::empty()) + .open(context, path, OFlags::PATH, Mode::empty()) .map_err(|error| match error { OpenError::PathError(error) => error.into(), OpenError::Io diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 6a5cbd707..5debe49ec 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -62,21 +62,22 @@ mod in_mem { #[test] fn root_file_creation_and_deletion() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { // Test file creation let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // Test file deletion - fs.unlink(path).expect("Failed to unlink file"); + fs.unlink(ctx, path).expect("Failed to unlink file"); assert!( - fs.open(path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); }); @@ -84,13 +85,14 @@ mod in_mem { #[test] fn root_file_read_write() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { // Create and write to a file let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&fd, data, None).expect("Failed to write to file"); @@ -98,7 +100,7 @@ mod in_mem { // Read from the file let fd = fs - .open(path, OFlags::RDONLY, Mode::RWXU) + .open(ctx, path, OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -112,16 +114,17 @@ mod in_mem { #[test] fn write_only_open_does_not_require_read_permission() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); let path = "/tmp/write_only"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) .expect("Failed to create write-only file"); fs.write(&fd, b"x", None).expect("Failed to write file"); @@ -133,49 +136,51 @@ mod in_mem { fs.close(&fd).expect("Failed to close file"); assert!(matches!( - fs.open(path, OFlags::RDONLY, Mode::empty()), + fs.open(&ctx, path, OFlags::RDONLY, Mode::empty()), Err(crate::fs::errors::OpenError::AccessNotAllowed) )); } #[test] fn newly_created_file_does_not_require_its_own_permissions() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); let path = "/tmp/zero_mode"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) .expect("Failed to create zero-mode file"); fs.write(&fd, b"x", None).expect("Failed to write file"); fs.close(&fd).expect("Failed to close file"); - let status = fs.file_status(path).expect("Failed to stat file"); + let status = fs.file_status(&ctx, path).expect("Failed to stat file"); assert_eq!(status.mode, Mode::empty()); assert!(matches!( - fs.open(path, OFlags::WRONLY, Mode::empty()), + fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()), Err(crate::fs::errors::OpenError::AccessNotAllowed) )); } #[test] fn root_directory_creation_and_removal() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { // Test directory creation let path = "/testdir"; - fs.mkdir(path, Mode::RWXU) + fs.mkdir(ctx, path, Mode::RWXU) .expect("Failed to create directory"); // Test directory removal - fs.rmdir(path).expect("Failed to remove directory"); + fs.rmdir(ctx, path).expect("Failed to remove directory"); assert!( - fs.open(path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), "Directory should not exist" ); }); @@ -183,44 +188,46 @@ mod in_mem { #[test] fn file_creation_and_deletion() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); // Test file creation let path = "/tmp/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // Test file deletion - fs.unlink(path).expect("Failed to unlink file"); + fs.unlink(&ctx, path).expect("Failed to unlink file"); assert!( - fs.open(path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } #[test] fn file_read_write() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); // Create and write to a file let path = "/tmp/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&fd, data, None).expect("Failed to write to file"); @@ -230,7 +237,7 @@ mod in_mem { // Read from the file let fd = fs - .open(path, OFlags::RDONLY, Mode::RWXU) + .open(&ctx, path, OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -247,34 +254,36 @@ mod in_mem { #[test] fn directory_creation_and_removal() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir("/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); // Test directory creation let path = "/tmp/testdir"; - fs.mkdir(path, Mode::RWXU) + fs.mkdir(&ctx, path, Mode::RWXU) .expect("Failed to create directory"); // Test directory removal - fs.rmdir(path).expect("Failed to remove directory"); + fs.rmdir(&ctx, path).expect("Failed to remove directory"); assert!( - fs.open(path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), "Directory should not exist" ); } #[test] fn read_dir_empty() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { let fd = fs - .open("/", OFlags::RDONLY, Mode::empty()) + .open(ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs .read_dir(&fd) @@ -293,24 +302,35 @@ mod in_mem { #[test] fn read_dir_with_files_and_dirs() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { // Create a directory structure - fs.mkdir("/testdir", Mode::RWXU) + fs.mkdir(ctx, "/testdir", Mode::RWXU) .expect("Failed to create directory"); let fd1 = fs - .open("/testfile1", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ctx, + "/testfile1", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("Failed to create file1"); fs.close(&fd1).expect("Failed to close file1"); let fd2 = fs - .open("/testfile2", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ctx, + "/testfile2", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("Failed to create file2"); fs.close(&fd2).expect("Failed to close file2"); // Read root directory let fd = fs - .open("/", OFlags::RDONLY, Mode::empty()) + .open(ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read directory"); fs.close(&fd).expect("Failed to close directory"); @@ -343,7 +363,7 @@ mod in_mem { // Read the subdirectory (should be empty) let fd = fs - .open("/testdir", OFlags::RDONLY, Mode::empty()) + .open(ctx, "/testdir", OFlags::RDONLY, Mode::empty()) .expect("Failed to open subdirectory"); let entries = fs .read_dir(&fd) @@ -358,18 +378,19 @@ mod in_mem { #[test] fn read_dir_file_not_directory() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), |fs| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { // Create a file let fd = fs - .open("/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(ctx, "/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // Try to read_dir on the file (should fail) let fd = fs - .open("/testfile", OFlags::RDONLY, Mode::empty()) + .open(ctx, "/testfile", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let result = fs.read_dir(&fd); fs.close(&fd).expect("Failed to close file"); @@ -383,87 +404,106 @@ mod in_mem { #[test] fn parent_dir_write_permissions_are_enforced() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { // A root-owned 0755 directory, holding a file and a directory to try to remove. fs.mkdir( + ctx, "/rootdir", Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, ) .expect("Failed to create directory"); let fd = fs - .open("/rootdir/file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ctx, + "/rootdir/file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); - fs.mkdir("/rootdir/sub", Mode::RWXU) + fs.mkdir(ctx, "/rootdir/sub", Mode::RWXU) .expect("Failed to create subdirectory"); // A world-writable directory, for the positive case. - fs.mkdir("/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(ctx, "/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create directory"); }); - in_mem::with_user(&mut fs, 1000, 1000, |fs| { + in_mem::with_user(&mut fs, &mut ctx, 1000, 1000, |fs, ctx| { assert!(matches!( - fs.open("/rootdir/new", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU), + fs.open( + ctx, + "/rootdir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU + ), Err(crate::fs::errors::OpenError::NoWritePerms) )); assert!(matches!( - fs.mkdir("/rootdir/newdir", Mode::RWXU), + fs.mkdir(ctx, "/rootdir/newdir", Mode::RWXU), Err(crate::fs::errors::MkdirError::NoWritePerms) )); assert!(matches!( - fs.unlink("/rootdir/file"), + fs.unlink(ctx, "/rootdir/file"), Err(crate::fs::errors::UnlinkError::NoWritePerms) )); assert!(matches!( - fs.rmdir("/rootdir/sub"), + fs.rmdir(ctx, "/rootdir/sub"), Err(crate::fs::errors::RmdirError::NoWritePerms) )); // The same operations succeed in a directory the user may write. let fd = fs - .open("/opendir/new", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ctx, + "/opendir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); - fs.mkdir("/opendir/newdir", Mode::RWXU) + fs.mkdir(ctx, "/opendir/newdir", Mode::RWXU) .expect("Failed to create directory"); - fs.unlink("/opendir/new").expect("Failed to unlink file"); - fs.rmdir("/opendir/newdir") + fs.unlink(ctx, "/opendir/new") + .expect("Failed to unlink file"); + fs.rmdir(ctx, "/opendir/newdir") .expect("Failed to remove directory"); }); } #[test] fn chown_test() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); // Create a test file as root - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // First chown to 1000:1000 as root (should succeed) - fs.chown(path, Some(1000), Some(1000)) + fs.chown(ctx, path, Some(1000), Some(1000)) .expect("Failed to chown as root"); }); // Switch to user 1000 and test that owner can chown (should succeed) let path = "/testfile"; - in_mem::with_user(&mut fs, 1000, 1000, |fs| { - fs.chown(path, Some(123), Some(456)) + in_mem::with_user(&mut fs, &mut ctx, 1000, 1000, |fs, ctx| { + fs.chown(ctx, path, Some(123), Some(456)) .expect("Failed to chown as owner"); }); // Switch to a different user and test that non-owner cannot chown (should fail) - in_mem::with_user(&mut fs, 500, 500, |fs| { - match fs.chown(path, Some(789), Some(101)) { + in_mem::with_user(&mut fs, &mut ctx, 500, 500, |fs, ctx| { + match fs.chown(ctx, path, Some(789), Some(101)) { Err(crate::fs::errors::ChownError::NotTheOwner) => { // Expected behavior } @@ -473,7 +513,7 @@ mod in_mem { }); // Test chown on non-existent file (should fail) - match fs.chown("/nonexistent", Some(123), Some(456)) { + match fs.chown(&ctx, "/nonexistent", Some(123), Some(456)) { Err(crate::fs::errors::ChownError::PathError( crate::fs::errors::PathError::NoSuchFileOrDirectory, )) => { @@ -484,39 +524,46 @@ mod in_mem { } // Test partial chown (change only user, leave group unchanged) - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chown(path, Some(999), None) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chown(ctx, path, Some(999), None) .expect("Failed to chown user only"); }); // Test partial chown (change only group, leave user unchanged) - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chown(path, None, Some(888)) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chown(ctx, path, None, Some(888)) .expect("Failed to chown group only"); }); } #[test] fn o_directory_flag_tests() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create test directory and file - fs.mkdir("/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(&ctx, "/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create directory"); let fd = fs - .open("/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/testfile", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // Test O_DIRECTORY on a directory (should succeed) let fd = fs .open( + &ctx, "/testdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -527,6 +574,7 @@ mod in_mem { // Test O_DIRECTORY on a regular file (should fail) assert!(matches!( fs.open( + &ctx, "/testfile", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() @@ -539,6 +587,7 @@ mod in_mem { // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( + &ctx, "/nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() @@ -552,6 +601,7 @@ mod in_mem { // According to the implementation, O_DIRECTORY should be ignored when O_CREAT is specified let fd = fs .open( + &ctx, "/newfile", OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, Mode::RWXU, @@ -561,7 +611,7 @@ mod in_mem { // Verify it created a regular file, not a directory let stat = fs - .file_status("/newfile") + .file_status(&ctx, "/newfile") .expect("Failed to get file status"); assert_eq!(stat.file_type, crate::fs::FileType::RegularFile); @@ -572,17 +622,19 @@ mod in_mem { #[test] fn o_excl_flag_tests() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Test O_CREAT | O_EXCL on non-existent file (should succeed) let fd = fs .open( + &ctx, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -597,6 +649,7 @@ mod in_mem { // Test O_CREAT | O_EXCL on existing file (should fail) assert!(matches!( fs.open( + &ctx, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -606,7 +659,12 @@ mod in_mem { // Test O_EXCL without O_CREAT (should be ignored and succeed) let fd = fs - .open("/newfile", OFlags::EXCL | OFlags::RDONLY, Mode::empty()) + .open( + &ctx, + "/newfile", + OFlags::EXCL | OFlags::RDONLY, + Mode::empty(), + ) .expect("Failed to open existing file with O_EXCL (without O_CREAT)"); // Verify we can read the data @@ -619,15 +677,16 @@ mod in_mem { // Test O_CREAT without O_EXCL on existing file (should succeed) let fd = fs - .open("/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); fs.close(&fd).expect("Failed to close file"); // Test O_CREAT | O_EXCL on directory (should fail) - fs.mkdir("/testdir", Mode::RWXU) + fs.mkdir(&ctx, "/testdir", Mode::RWXU) .expect("Failed to create directory"); assert!(matches!( fs.open( + &ctx, "/testdir", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -638,18 +697,19 @@ mod in_mem { #[test] fn open_with_trunc() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file and write some initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let initial_data = b"Hello, world! This is initial content."; fs.write(&fd, initial_data, None) @@ -658,7 +718,7 @@ mod in_mem { // Verify initial content was written let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -670,7 +730,7 @@ mod in_mem { // Test O_TRUNC with O_WRONLY - should truncate file let fd = fs - .open(path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) + .open(&ctx, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC | O_WRONLY"); // Write new content to the truncated file @@ -681,7 +741,7 @@ mod in_mem { // Verify the file was truncated and contains only new content let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for verification"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -693,16 +753,16 @@ mod in_mem { // Test O_TRUNC with O_RDWR - should also truncate fs.write( - &fs.open(path, OFlags::WRONLY, Mode::empty()).unwrap(), + &fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()).unwrap(), b"More content to truncate", None, ) .unwrap(); - fs.close(&fs.open(path, OFlags::WRONLY, Mode::empty()).unwrap()) + fs.close(&fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()).unwrap()) .unwrap(); let fd = fs - .open(path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .open(&ctx, path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC | O_RDWR"); // File should be empty after truncation @@ -731,16 +791,19 @@ mod in_mem { fn write_position_after_seek() { use crate::fs::SeekWhence; + let mut ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { // Allow regular user to create in root for this focused test - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("chmod / failed"); }); let fd = fs .open( + &ctx, "/posfile", OFlags::CREAT | OFlags::RDWR, Mode::RWXU | Mode::RWXG | Mode::RWXO, @@ -786,18 +849,19 @@ mod in_mem { #[test] fn o_append_flag_basic() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file and write some initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let initial_data = b"Hello"; fs.write(&fd, initial_data, None) @@ -806,7 +870,7 @@ mod in_mem { // Re-open with O_APPEND and write more data let fd = fs - .open(path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); let append_data = b" World"; fs.write(&fd, append_data, None) @@ -815,7 +879,7 @@ mod in_mem { // Verify the file contains both pieces of data concatenated let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; 11]; let bytes_read = fs @@ -830,18 +894,20 @@ mod in_mem { fn o_append_flag_seek_ignored_for_write() { use crate::fs::SeekWhence; + let mut ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file and write some initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.write(&fd, b"ABCDEF", None) .expect("Failed to write initial content"); @@ -849,7 +915,7 @@ mod in_mem { // Re-open with O_APPEND let fd = fs - .open(path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); // Seek to beginning - this should succeed but writes should still append @@ -863,7 +929,7 @@ mod in_mem { // Verify the file content: original data followed by appended data let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; 20]; let bytes_read = fs @@ -878,18 +944,20 @@ mod in_mem { fn o_append_flag_with_rdwr() { use crate::fs::SeekWhence; + let mut ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file with initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.write(&fd, b"Hello", None) .expect("Failed to write initial content"); @@ -897,7 +965,7 @@ mod in_mem { // Re-open with O_RDWR | O_APPEND let fd = fs - .open(path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) + .open(&ctx, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_RDWR | O_APPEND"); // Read should work normally from the beginning @@ -930,18 +998,19 @@ mod in_mem { #[test] fn o_append_pwrite_ignores_append_mode() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file with initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.write(&fd, b"ABCDEF", None) .expect("Failed to write initial content"); @@ -949,7 +1018,7 @@ mod in_mem { // Re-open with O_APPEND let fd = fs - .open(path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); // pwrite (write with explicit offset) should ignore O_APPEND per POSIX @@ -958,7 +1027,7 @@ mod in_mem { // Verify the file content: XX should be at position 2, not appended let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; 10]; let bytes_read = fs @@ -971,18 +1040,19 @@ mod in_mem { #[test] fn o_append_with_trunc() { + let mut ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); // Create a file with initial content let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.write(&fd, b"Original content", None) .expect("Failed to write initial content"); @@ -991,6 +1061,7 @@ mod in_mem { // Re-open with O_TRUNC | O_APPEND let fd = fs .open( + &ctx, path, OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, Mode::empty(), @@ -1006,7 +1077,7 @@ mod in_mem { // Verify the file content let fd = fs - .open(path, OFlags::RDONLY, Mode::empty()) + .open(&ctx, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); let mut buffer = vec![0; 20]; let bytes_read = fs @@ -1030,10 +1101,11 @@ mod tar_ro { #[test] fn file_read() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); let fd = fs - .open("foo", OFlags::RDONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1042,7 +1114,7 @@ mod tar_ro { assert_eq!(&buffer[..bytes_read], b"testfoo\n"); fs.close(&fd).expect("Failed to close file"); let fd = fs - .open("bar/baz", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1054,34 +1126,46 @@ mod tar_ro { #[test] fn dir_and_nonexist_checks() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); assert!(matches!( - fs.open("bar/ba", OFlags::RDONLY, Mode::empty()), + fs.open(&ctx, "bar/ba", OFlags::RDONLY, Mode::empty()), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::NoSuchFileOrDirectory )), )); let fd = fs - .open("bar", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open dir"); fs.close(&fd).expect("Failed to close dir"); } #[test] fn o_directory_flag_tests() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); // Test O_DIRECTORY on a directory (should succeed) let fd = fs - .open("bar", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + &ctx, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) .expect("Failed to open directory with O_DIRECTORY"); fs.close(&fd).expect("Failed to close directory"); // Test O_DIRECTORY on a regular file (should fail) assert!(matches!( - fs.open("foo", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()), + fs.open( + &ctx, + "foo", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::ComponentNotADirectory )) @@ -1090,6 +1174,7 @@ mod tar_ro { // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( + &ctx, "nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() @@ -1101,7 +1186,12 @@ mod tar_ro { // Test O_DIRECTORY on nested file (should fail) assert!(matches!( - fs.open("bar/baz", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()), + fs.open( + &ctx, + "bar/baz", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::ComponentNotADirectory )) @@ -1110,12 +1200,13 @@ mod tar_ro { #[test] fn write_or_truncate_open_of_directory_fails() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); for flags in [OFlags::WRONLY, OFlags::RDWR, OFlags::TRUNC] { assert!(matches!( - fs.open("bar", flags, Mode::empty()), + fs.open(&ctx, "bar", flags, Mode::empty()), Err(crate::fs::errors::OpenError::ReadOnlyFileSystem) )); } @@ -1123,12 +1214,13 @@ mod tar_ro { #[test] fn read_dir_subdirectory() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); // Read root directory let fd = fs - .open("/", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); fs.close(&fd).expect("Failed to close root directory"); @@ -1159,7 +1251,7 @@ mod tar_ro { // Read `bar` directory let fd = fs - .open("bar", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); fs.close(&fd).expect("Failed to close bar directory"); @@ -1172,11 +1264,12 @@ mod tar_ro { #[test] fn read_dir_file_not_directory() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); let fd = fs - .open("foo", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to open foo file"); let result = fs.read_dir(&fd); fs.close(&fd).expect("Failed to close foo file"); @@ -1232,10 +1325,11 @@ mod overlay { #[test] fn file_read_from_lower() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd = fs - .open("foo", OFlags::RDONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1247,12 +1341,12 @@ mod overlay { assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); fs.close(&fd).expect("Failed to close file"); - let stat = fs.file_status("bar").expect("Failed to file stat"); + let stat = fs.file_status(&ctx, "bar").expect("Failed to file stat"); assert_eq!(stat.file_type, FileType::Directory); assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); let fd = fs - .open("bar/baz", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1267,16 +1361,17 @@ mod overlay { #[test] fn dir_and_nonexist_checks() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); assert!(matches!( - fs.open("bar/ba", OFlags::RDONLY, Mode::empty()), + fs.open(&ctx, "bar/ba", OFlags::RDONLY, Mode::empty()), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::NoSuchFileOrDirectory )), )); let fd = fs - .open("bar", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open dir"); fs.close(&fd).expect("Failed to close dir"); } @@ -1285,13 +1380,14 @@ mod overlay { /// it up and redirects handles already open on it, so every descriptor sees the update. #[test] fn file_read_write_copy_up() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd1 = fs - .open("foo", OFlags::RDONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let fd2 = fs - .open("foo", OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; @@ -1319,13 +1415,14 @@ mod overlay { /// maintained. #[test] fn file_read_write_copy_up_keeps_position() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd1 = fs - .open("foo", OFlags::RDONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let fd2 = fs - .open("foo", OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1349,10 +1446,11 @@ mod overlay { #[test] fn file_deletion() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd = fs - .open("foo", OFlags::RDONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1364,7 +1462,7 @@ mod overlay { assert_eq!(&buffer[..bytes_read], b"test"); // Then we delete it - fs.unlink("foo").unwrap(); + fs.unlink(&ctx, "foo").unwrap(); // This should not really impact the readability; file is fine. let bytes_read = fs @@ -1375,7 +1473,7 @@ mod overlay { // But if we close and attempt to re-open, it should not exist fs.close(&fd).expect("Failed to close file"); assert!(matches!( - fs.open("foo", OFlags::RDONLY, Mode::empty()), + fs.open(&ctx, "foo", OFlags::RDONLY, Mode::empty()), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::NoSuchFileOrDirectory )), @@ -1384,6 +1482,7 @@ mod overlay { #[test] fn o_directory_flag_tests() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs( &litebox, @@ -1408,13 +1507,19 @@ mod overlay { // Test O_DIRECTORY on directory from lower layer (tar) let fd = fs - .open("bar", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + &ctx, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) .expect("Failed to open lower layer directory with O_DIRECTORY"); fs.close(&fd).expect("Failed to close directory"); // Test O_DIRECTORY on directory from upper layer (in_mem) let fd = fs .open( + &ctx, "/upperdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), @@ -1424,7 +1529,12 @@ mod overlay { // Test O_DIRECTORY on file from lower layer (should fail) assert!(matches!( - fs.open("foo", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()), + fs.open( + &ctx, + "foo", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::ComponentNotADirectory )) @@ -1433,6 +1543,7 @@ mod overlay { // Test O_DIRECTORY on file from upper layer (should fail) assert!(matches!( fs.open( + &ctx, "/upperfile", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() @@ -1444,7 +1555,12 @@ mod overlay { // Test O_DIRECTORY on nested file from lower layer (should fail) assert!(matches!( - fs.open("bar/baz", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()), + fs.open( + &ctx, + "bar/baz", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), Err(crate::fs::errors::OpenError::PathError( crate::fs::errors::PathError::ComponentNotADirectory )) @@ -1453,6 +1569,7 @@ mod overlay { // Test O_DIRECTORY on non-existent path (should fail) assert!(matches!( fs.open( + &ctx, "nonexistent", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() @@ -1467,10 +1584,11 @@ mod overlay { // Regression test for #250: a file that already exists in the lower layer should not be // shadowed by an attempt to create a file. fn file_create_exist_in_lower() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd = fs - .open("foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) + .open(&ctx, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1483,12 +1601,13 @@ mod overlay { #[test] fn read_dir_from_lower_layer() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Read bar subdirectory let fd = fs - .open("bar", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); fs.close(&fd).expect("Failed to close bar directory"); @@ -1505,6 +1624,7 @@ mod overlay { #[test] fn read_dir_from_upper_layer() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs( &litebox, @@ -1529,7 +1649,7 @@ mod overlay { // Read root directory (should contain entries from both layers) let fd = fs - .open("/", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); fs.close(&fd).expect("Failed to close root directory"); @@ -1565,7 +1685,7 @@ mod overlay { // Read upperdir directory (should be from upper layer) let fd = fs - .open("/upperdir", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/upperdir", OFlags::RDONLY, Mode::empty()) .expect("Failed to open upperdir"); let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); fs.close(&fd).expect("Failed to close upperdir"); @@ -1576,6 +1696,7 @@ mod overlay { #[test] fn o_excl_tests() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); @@ -1583,6 +1704,7 @@ mod overlay { // "foo" exists in the tar file assert!(matches!( fs.open( + &ctx, "foo", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1593,6 +1715,7 @@ mod overlay { // Test O_CREAT | O_EXCL on file that doesn't exist anywhere (should succeed) let fd = fs .open( + &ctx, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1606,6 +1729,7 @@ mod overlay { // Test O_CREAT | O_EXCL on file that now exists in upper layer (should fail) assert!(matches!( fs.open( + &ctx, "/newfile", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1617,6 +1741,7 @@ mod overlay { // "bar" is a directory in the tar file assert!(matches!( fs.open( + &ctx, "bar", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1626,11 +1751,13 @@ mod overlay { // Test O_CREAT | O_EXCL on file that was deleted (tombstoned) should succeed // First delete a file from lower layer - fs.unlink("foo").expect("Failed to unlink lower layer file"); + fs.unlink(&ctx, "foo") + .expect("Failed to unlink lower layer file"); // Now try to create it with O_EXCL (should succeed since it's tombstoned) let fd = fs .open( + &ctx, "foo", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1643,7 +1770,7 @@ mod overlay { // Verify the new content let fd = fs - .open("foo", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to open recreated file"); let mut buffer = vec![0; 15]; let bytes_read = fs @@ -1656,6 +1783,7 @@ mod overlay { // Create a file in upper layer first let fd = fs .open( + &ctx, "/upper_only_file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -1668,6 +1796,7 @@ mod overlay { // Now try O_CREAT | O_EXCL on the same file (should fail) assert!(matches!( fs.open( + &ctx, "/upper_only_file", OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, @@ -1678,22 +1807,23 @@ mod overlay { #[test] fn dir_creation_inside_lower_existing_dir() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Create the directory /bar/test (where /bar already exists inside the tar file) - fs.mkdir("/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(&ctx, "/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /bar/test directory"); // Verify the directory was created let stat = fs - .file_status("/bar/test") + .file_status(&ctx, "/bar/test") .expect("Failed to get status of /bar/test"); assert_eq!(stat.file_type, FileType::Directory); // Verify we can open the directory let fd = fs - .open("/bar/test", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/bar/test", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /bar/test directory"); let entries = fs .read_dir(&fd) @@ -1709,13 +1839,14 @@ mod overlay { #[test] fn file_creation_materializes_ancestor_dirs() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Open bar/test for writing (where bar exists in lower layer but test doesn't exist) // This should create ancestor directories and allow file creation let fd = fs - .open("bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/test for writing"); // Write data to the file @@ -1726,7 +1857,7 @@ mod overlay { // Read the file back let fd = fs - .open("bar/test", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar/test", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar/test for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1737,20 +1868,21 @@ mod overlay { // Verify the file exists and has correct type let stat = fs - .file_status("bar/test") + .file_status(&ctx, "bar/test") .expect("Failed to get status of bar/test"); assert_eq!(stat.file_type, FileType::RegularFile); } #[test] fn file_modification_materializes_ancestor_dirs() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Open bar/baz for writing (both bar and baz exist in lower layer) // This copies up the ancestor directories and allows the file to be modified let fd = fs - .open("bar/baz", OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "bar/baz", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/baz for writing"); // Write new data to the file (overwriting existing content) @@ -1761,7 +1893,7 @@ mod overlay { // Read the file back to verify it was modified let fd = fs - .open("bar/baz", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open bar/baz for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1773,19 +1905,20 @@ mod overlay { // Verify the file still exists and has correct type let stat = fs - .file_status("bar/baz") + .file_status(&ctx, "bar/baz") .expect("Failed to get status of bar/baz"); assert_eq!(stat.file_type, FileType::RegularFile); } #[test] fn open_with_trunc() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Open with O_TRUNC should copy the file up into the upper backend, empty let fd = fs - .open("foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .open(&ctx, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC"); // File should be truncated (empty) @@ -1802,7 +1935,7 @@ mod overlay { // Verify the content persists let fd = fs - .open("foo", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to reopen file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1816,20 +1949,22 @@ mod overlay { fn rmdir_upper_only_directory() { use crate::fs::errors::{PathError, RmdirError}; + let ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Create an empty directory only in upper layer - fs.mkdir("/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(&ctx, "/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("mkdir upper_empty failed"); // Remove it - fs.rmdir("/upper_empty") + fs.rmdir(&ctx, "/upper_empty") .expect("rmdir upper_empty should succeed"); // Verify it no longer exists assert!(matches!( - fs.file_status("/upper_empty"), + fs.file_status(&ctx, "/upper_empty"), Err(crate::fs::errors::FileStatusError::PathError( PathError::NoSuchFileOrDirectory )) @@ -1837,7 +1972,7 @@ mod overlay { // Second removal should yield NoSuchFileOrDirectory (path error) assert!(matches!( - fs.rmdir("/upper_empty"), + fs.rmdir(&ctx, "/upper_empty"), Err(RmdirError::PathError(PathError::NoSuchFileOrDirectory)) )); } @@ -1846,15 +1981,18 @@ mod overlay { fn rmdir_upper_directory_not_empty_then_empty() { use crate::fs::errors::{PathError, RmdirError}; + let ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); - fs.mkdir("/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + fs.mkdir(&ctx, "/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("mkdir upper_dir failed"); // Create a file inside making directory non-empty let fd = fs .open( + &ctx, "/upper_dir/file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, @@ -1863,18 +2001,22 @@ mod overlay { fs.close(&fd).unwrap(); // Attempt to remove while non-empty - assert!(matches!(fs.rmdir("/upper_dir"), Err(RmdirError::NotEmpty))); + assert!(matches!( + fs.rmdir(&ctx, "/upper_dir"), + Err(RmdirError::NotEmpty) + )); // Remove inner file - fs.unlink("/upper_dir/file").expect("unlink inner failed"); + fs.unlink(&ctx, "/upper_dir/file") + .expect("unlink inner failed"); // Now should succeed - fs.rmdir("/upper_dir") + fs.rmdir(&ctx, "/upper_dir") .expect("rmdir upper_dir should succeed"); // Confirm gone assert!(matches!( - fs.file_status("/upper_dir"), + fs.file_status(&ctx, "/upper_dir"), Err(crate::fs::errors::FileStatusError::PathError( PathError::NoSuchFileOrDirectory )) @@ -1885,23 +2027,28 @@ mod overlay { fn rmdir_lower_directory_non_empty() { use crate::fs::errors::RmdirError; + let ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // "bar" exists in lower layer and contains "baz" (non-empty) - assert!(matches!(fs.rmdir("bar"), Err(RmdirError::NotEmpty))); + assert!(matches!(fs.rmdir(&ctx, "bar"), Err(RmdirError::NotEmpty))); } #[test] fn rmdir_not_a_directory() { use crate::fs::errors::RmdirError; + let ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Create a regular file (upper only) let fd = fs .open( + &ctx, "/regular_file", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, @@ -1911,7 +2058,7 @@ mod overlay { // rmdir should fail with NotADirectory assert!(matches!( - fs.rmdir("/regular_file"), + fs.rmdir(&ctx, "/regular_file"), Err(RmdirError::NotADirectory) )); } @@ -1922,16 +2069,18 @@ mod overlay { use std::thread; use std::time::Duration; + let ctx = crate::fs::resolver::Context::new(); + let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); - fs.file_status("foo").expect("Failed to stat foo"); + fs.file_status(&ctx, "foo").expect("Failed to stat foo"); // Writing to the lower-layer file triggers copy-up. Run it on a worker thread. let (tx, rx) = mpsc::channel(); thread::spawn(move || { let fd = fs - .open("foo", OFlags::WRONLY, Mode::RWXU) + .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file for writing"); fs.write(&fd, b"x", None).expect("Failed to write to file"); fs.close(&fd).expect("Failed to close file"); @@ -1954,6 +2103,7 @@ mod stdio { #[test] fn stdio_open_read_write() { + let ctx = crate::fs::resolver::Context::new(); let platform = MockPlatform::new(); let litebox = LiteBox::new(platform); let fs = Resolver::new( @@ -1966,7 +2116,7 @@ mod stdio { // Test opening and writing to /dev/stdout let fd_stdout = fs - .open("/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stdout"); let data = b"Hello, stdout!"; fs.write(&fd_stdout, data, None) @@ -1977,7 +2127,7 @@ mod stdio { // Test opening and writing to /dev/stderr let fd_stderr = fs - .open("/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stderr"); let data = b"Hello, stderr!"; fs.write(&fd_stderr, data, None) @@ -1993,7 +2143,7 @@ mod stdio { .unwrap() .push_back(b"Hello, stdin!".to_vec()); let fd_stdin = fs - .open("/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); let mut buffer = vec![0; 13]; let bytes_read = fs @@ -2006,6 +2156,7 @@ mod stdio { #[test] fn non_dev_path_fails() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = Resolver::new( &litebox, @@ -2016,7 +2167,7 @@ mod stdio { ); // Attempt to open a non-/dev/* path - let result = fs.open("foo", OFlags::RDONLY, Mode::empty()); + let result = fs.open(&ctx, "foo", OFlags::RDONLY, Mode::empty()); assert!(matches!( result, Err(crate::fs::errors::OpenError::PathError( @@ -2060,13 +2211,14 @@ mod composed_stdio { #[test] fn stdio_open_read_write() { + let ctx = crate::fs::resolver::Context::new(); let platform = MockPlatform::new(); let litebox = LiteBox::new(platform); let fs = composed_fs(&litebox); // Test opening and writing to /dev/stdout let fd_stdout = fs - .open("/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stdout"); let data = b"Hello, composed stdout!"; fs.write(&fd_stdout, data, None) @@ -2077,7 +2229,7 @@ mod composed_stdio { // Test opening and writing to /dev/stderr let fd_stderr = fs - .open("/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stderr"); let data = b"Hello, composed stderr!"; fs.write(&fd_stderr, data, None) @@ -2093,7 +2245,7 @@ mod composed_stdio { .unwrap() .push_back(b"Hello, composed stdin!".to_vec()); let fd_stdin = fs - .open("/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -2105,21 +2257,22 @@ mod composed_stdio { #[test] fn write_to_non_dev() { + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let fs = composed_fs(&litebox); // Test file creation let path = "/testfile"; let fd = fs - .open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.close(&fd).expect("Failed to close file"); // Test file deletion - fs.unlink(path).expect("Failed to unlink file"); + fs.unlink(&ctx, path).expect("Failed to unlink file"); assert!( - fs.open(path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs index aadd353a6..07a8ea88d 100644 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs @@ -12,6 +12,7 @@ pub struct TestLauncher { platform: &'static Platform, shim_builder: litebox_shim_linux::LinuxShimBuilder, fs: litebox_shim_linux::DefaultFS, + context: litebox::fs::resolver::Context, } impl TestLauncher { @@ -40,6 +41,7 @@ impl TestLauncher { platform, shim_builder, fs, + context: litebox::fs::resolver::Context::new(), }; for each in initial_dirs { @@ -55,7 +57,7 @@ impl TestLauncher { pub fn install_dir(&mut self, path: &str) { self.fs - .mkdir(path, Mode::RWXU | Mode::RWXG | Mode::RWXO) + .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create directory"); } @@ -63,6 +65,7 @@ impl TestLauncher { let fd = self .fs .open( + &self.context, out, OFlags::CREAT | OFlags::WRONLY, Mode::RWXG | Mode::RWXO | Mode::RWXU, diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index c5a8a82ec..00159e1fd 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -13,6 +13,7 @@ struct TestLauncher { platform: &'static Platform, shim_builder: litebox_shim_linux::LinuxShimBuilder, fs: litebox_shim_linux::DefaultFS, + context: litebox::fs::resolver::Context, } impl TestLauncher { @@ -41,6 +42,7 @@ impl TestLauncher { platform, shim_builder, fs, + context: litebox::fs::resolver::Context::new(), }; for each in initial_files { @@ -70,13 +72,15 @@ impl TestLauncher { } fn install_dir(&mut self, path: &str) -> Result<(), litebox::fs::errors::MkdirError> { - self.fs.mkdir(path, Mode::RWXU | Mode::RWXG | Mode::RWXO) + self.fs + .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) } fn install_file(&mut self, contents: Vec, out: &str) { let fd = self .fs .open( + &self.context, out, OFlags::CREAT | OFlags::WRONLY, Mode::RWXG | Mode::RWXO | Mode::RWXU, diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 6c3e3fec1..8b059e9da 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -265,7 +265,8 @@ impl LinuxShim { let files = syscalls::file::FilesState::new(fs); files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR - 1); let files = Arc::new(files); - files.initialize_stdio_in_shared_descriptors_table(&self.0); + let fs_state = Arc::new(syscalls::file::FsState::new()); + files.initialize_stdio_in_shared_descriptors_table(&self.0, &fs_state.context); let entrypoints = crate::LinuxShimEntrypoints { _not_send: core::marker::PhantomData, @@ -284,7 +285,7 @@ impl LinuxShim { } .into(), comm: [0; litebox_common_linux::TASK_COMM_LEN].into(), // set at load time - fs: Arc::new(syscalls::file::FsState::new()).into(), + fs: fs_state.into(), files: files.into(), signals: syscalls::signal::SignalState::new_process(), }, @@ -393,19 +394,23 @@ fn default_fs( pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); impl syscalls::file::FilesState { - fn initialize_stdio_in_shared_descriptors_table(&self, global: &GlobalState) { + fn initialize_stdio_in_shared_descriptors_table( + &self, + global: &GlobalState, + context: &litebox::fs::resolver::Context, + ) { use litebox::fs::{Mode, OFlags}; let stdin = self .fs - .open("/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .unwrap(); let stdout = self .fs - .open("/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .unwrap(); let stderr = self .fs - .open("/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .unwrap(); let mut dt = global.litebox.descriptor_table_mut(); let mut rds = self.raw_descriptor_store.write(); @@ -1223,7 +1228,8 @@ mod test_utils { .next_thread_id .fetch_add(1, core::sync::atomic::Ordering::Relaxed); let files = Arc::new(syscalls::file::FilesState::new(fs)); - files.initialize_stdio_in_shared_descriptors_table(&self); + let fs_state = Arc::new(syscalls::file::FsState::new()); + files.initialize_stdio_in_shared_descriptors_table(&self, &fs_state.context); Task { wait_state: wait::WaitState::new(self.platform), thread: syscalls::process::ThreadState::new_process(pid), @@ -1237,7 +1243,7 @@ mod test_utils { egid: 0, }), comm: Cell::new(*b"test\0\0\0\0\0\0\0\0\0\0\0\0"), - fs: Arc::new(syscalls::file::FsState::new()).into(), + fs: fs_state.into(), files: files.into(), signals: syscalls::signal::SignalState::new_process(), global: self, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 3b1190331..4101e37fd 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -51,6 +51,8 @@ pub(crate) struct FsState { /// /// Must end with a '/'. cwd: litebox::sync::RwLock, + // TODO(jayb): simplify by removing [`FsState::cwd`] and instead use this + pub(crate) context: litebox::fs::resolver::Context, } impl Clone for FsState { @@ -58,6 +60,7 @@ impl Clone for FsState { Self { umask: self.umask.load(Ordering::Relaxed).into(), cwd: litebox::sync::RwLock::new(self.cwd.read().clone()), + context: self.context.clone(), } } } @@ -67,6 +70,7 @@ impl FsState { Self { umask: (Mode::WGRP | Mode::WOTH).bits().into(), cwd: litebox::sync::RwLock::new(String::from("/")), + context: litebox::fs::resolver::Context::new(), } } @@ -230,10 +234,15 @@ impl Task { mode: Mode, ) -> Result, Errno> { let mode = mode & !self.get_umask(); - self.files - .borrow() + let files = self.files.borrow(); + files .fs - .open(path, flags - OFlags::CLOEXEC, mode) + .open( + &self.fs.borrow().context, + path, + flags - OFlags::CLOEXEC, + mode, + ) .map_err(Errno::from) } @@ -368,10 +377,17 @@ impl Task { } let path = self.resolve_path_at(dirfd, pathname)?; + let files = self.files.borrow(); if flags.contains(AtFlags::AT_REMOVEDIR) { - self.files.borrow().fs.rmdir(path).map_err(Errno::from) + files + .fs + .rmdir(&self.fs.borrow().context, path) + .map_err(Errno::from) } else { - self.files.borrow().fs.unlink(path).map_err(Errno::from) + files + .fs + .unlink(&self.fs.borrow().context, path) + .map_err(Errno::from) } } @@ -729,10 +745,10 @@ impl Task { fn do_mkdir(&self, pathname: impl path::Arg, mode: Mode) -> Result<(), Errno> { let mode = mode & !self.get_umask(); - self.files - .borrow() + let files = self.files.borrow(); + files .fs - .mkdir(pathname, mode) + .mkdir(&self.fs.borrow().context, pathname, mode) .map_err(Errno::from) } @@ -1152,7 +1168,10 @@ impl Task { mode: AccessFlags, caller: AccessUserInfo, ) -> Result<(), Errno> { - let status = self.files.borrow().fs.file_status(pathname)?; + let status = { + let files = self.files.borrow(); + files.fs.file_status(&self.fs.borrow().context, pathname)? + }; let owner = status.owner.into(); Self::do_access_mode(status.mode, owner, caller, &mode) } @@ -1368,7 +1387,10 @@ impl Task { } else { normalized_path }; - let status = self.files.borrow().fs.file_status(path)?; + let status = { + let files = self.files.borrow(); + files.fs.file_status(&self.fs.borrow().context, path)? + }; Ok(T::from(status)) } @@ -1412,7 +1434,10 @@ impl Task { self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW)) } FsPath::Cwd if flags.contains(AtFlags::AT_EMPTY_PATH) => { - Ok(T::from(self.files.borrow().fs.file_status(get_cwd())?)) + let files = self.files.borrow(); + Ok(T::from( + files.fs.file_status(&self.fs.borrow().context, get_cwd())?, + )) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { descriptor_stat(fd as usize, self) @@ -1705,7 +1730,11 @@ impl Task { let abs_path = resolved.normalized().map_err(|_| Errno::EINVAL)?; // Verify the path exists and is a directory. - match self.files.borrow().fs.file_status(abs_path.as_str()) { + let files = self.files.borrow(); + match files + .fs + .file_status(&self.fs.borrow().context, abs_path.as_str()) + { Ok(status) => { if status.file_type != FileType::Directory { return Err(Errno::ENOTDIR); diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index ab91a0c42..5f3e1d519 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -117,19 +117,21 @@ impl UnixSocketAddr { OFlags::RDWR }; // TODO: extend fs to support creating sock file (i.e., with type `InodeType::Socket`) - let file = task - .files - .borrow() - .fs - .open( - path.as_str(), - flags, - Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, - ) - .map_err(|err| match err { - OpenError::AlreadyExists => Errno::EADDRINUSE, - other => Errno::from(other), - })?; + let file = { + let files = task.files.borrow(); + files + .fs + .open( + &task.fs.borrow().context, + path.as_str(), + flags, + Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, + ) + .map_err(|err| match err { + OpenError::AlreadyExists => Errno::EADDRINUSE, + other => Errno::from(other), + })? + }; Ok(UnixBoundSocketAddr::Path(( path, file, diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs index 10f4b87d6..028643fa9 100644 --- a/litebox_shim_linux/src/transport.rs +++ b/litebox_shim_linux/src/transport.rs @@ -293,6 +293,7 @@ mod tests { #[test] fn test_tun_nine_p_create_and_read_file() { + let ctx = litebox::fs::resolver::Context::new(); let task = init_platform(Some(TUN_DEVICE_NAME)); let server = DiodServer::start(); @@ -300,7 +301,12 @@ mod tests { // Create a file and write to it. let fd = fs - .open("/hello.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + &ctx, + "/hello.txt", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) .expect("failed to create file via 9P"); let data = b"Hello from litebox shim 9P!"; @@ -316,7 +322,7 @@ mod tests { // Read back through 9P. let fd = fs - .open("/hello.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/hello.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); let mut buf = alloc::vec![0u8; 256]; @@ -327,6 +333,7 @@ mod tests { #[test] fn test_tun_nine_p_host_files_visible() { + let ctx = litebox::fs::resolver::Context::new(); let task = init_platform(Some(TUN_DEVICE_NAME)); let server = DiodServer::start(); @@ -344,7 +351,7 @@ mod tests { // Read file created on the host through 9P. let fd = fs - .open("/host_file.txt", OFlags::RDONLY, Mode::empty()) + .open(&ctx, "/host_file.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open host file via 9P"); let mut buf = alloc::vec![0u8; 256]; let n = fs.read(&fd, &mut buf, None).unwrap(); @@ -354,6 +361,7 @@ mod tests { // List host directory through 9P. let fd = fs .open( + &ctx, "/host_dir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), From 06404de994fecc950a36471d757aef7fefe31017 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Tue, 25 Aug 2026 15:28:05 -0700 Subject: [PATCH 2/8] Allow public manipulation of contexts --- litebox/src/fs/in_mem.rs | 12 ++--- litebox/src/fs/resolver.rs | 69 +++++++++++++++++++-------- litebox/src/fs/tests.rs | 98 +++++++++++++++++++------------------- 3 files changed, 105 insertions(+), 74 deletions(-) diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index ac8ef14d2..c2c7b9b8f 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -702,7 +702,7 @@ struct Permissions { #[cfg(test)] pub(super) fn with_root_privileges( fs: &mut super::resolver::Resolver>, - context: &mut super::resolver::Context, + context: &super::resolver::Context, f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), ) { with_user(fs, context, UserInfo::ROOT.user, UserInfo::ROOT.group, f); @@ -712,16 +712,16 @@ pub(super) fn with_root_privileges( #[cfg(test)] pub(super) fn with_user( fs: &mut super::resolver::Resolver>, - context: &mut super::resolver::Context, + context: &super::resolver::Context, user: u16, group: u16, f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), ) { let user = UserInfo { user, group }; - let original_user = context.swap_acting_user(user); + let original_user = context.acting_user(); + let mut context = context.clone(); + context.set_acting_user(user); fs.backend_mut().current_user = user; - f(fs, context); - let user_again = context.swap_acting_user(original_user); + f(fs, &context); fs.backend_mut().current_user = original_user; - assert!(user_again.user == user.user && user_again.group == user.group); } diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index 297c4a541..c2e1c1b2b 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -4,6 +4,7 @@ //! The path-management/permissions/... layer, that sits above [`super::backend`]. use alloc::string::String; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -57,35 +58,48 @@ impl, + cwd: Arc, /// Effective user for permission checks. user_info: UserInfo, } impl Context { - /// Set the acting user for all subsequent operations on this context, returning the previous - /// one. - /// - /// Non-test callers set up whatever needs a different user while constructing the backend; - /// this exists so that the tests can exercise operations that depend on the acting user. - /// - /// TODO(DO NOT COMMIT): Temporarily still kept as test-only, but within the same PR, we should - /// expose getters/setters for the context, or maybe make the fields public, while also making - /// it non-exhaustive or something? - #[cfg(test)] - pub(super) fn swap_acting_user(&mut self, user: UserInfo) -> UserInfo { - core::mem::replace(&mut self.user_info, user) + /// The user that operations on this context act as. + #[must_use] + pub fn acting_user(&self) -> UserInfo { + self.user_info + } + + /// Set the user that operations on this context act as. + pub fn set_acting_user(&mut self, user: UserInfo) { + self.user_info = user; + } + + /// The current working directory. + #[must_use] + pub fn cwd(&self) -> &ResolvedPath { + &self.cwd + } + + /// Set the current working directory. + pub fn set_cwd(&mut self, cwd: ResolvedPath) { + self.cwd = Arc::new(cwd); } /// A new default context, anchored at `/` for a non-root user. pub fn new() -> Context { Self { - cwd: vec![], + cwd: Arc::new(ResolvedPath { components: vec![] }), user_info: UserInfo { user: 1000, group: 1000, @@ -98,11 +112,11 @@ impl Context { // outside the chrooted part. // XXX(jayb): since we are migrating all resolution into the resolver, we probably don't need // `Arg` anymore, so could get rid of it in the future. - fn resolve(&self, path: impl Arg) -> Result { + pub fn resolve(&self, path: impl Arg) -> Result { let mut components = if path.as_rust_str()?.starts_with('/') { vec![] } else { - self.cwd.clone() + self.cwd.components.clone() }; for component in path.components()? { match component { @@ -156,10 +170,27 @@ impl Default for Context { } /// Absolute normalized path, must only be created from [`Context::resolve`]. -struct ResolvedPath { +/// +/// Note that a resolved path does not imply that it exists within the file system, merely that it +/// is an absolute normalized path. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedPath { + // Note: an empty path is equivalent to `/`. components: Vec, } +impl core::fmt::Display for ResolvedPath { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for component in &self.components { + write!(f, "/{component}")?; + } + if self.components.is_empty() { + f.write_str("/")?; + } + Ok(()) + } +} + impl ResolvedPath { fn parent_and_name(&self) -> Option<(Vec<&str>, &str)> { let (name, parent) = self.components.split_last()?; diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 5debe49ec..8cacc53dc 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -62,10 +62,10 @@ mod in_mem { #[test] fn root_file_creation_and_deletion() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Test file creation let path = "/testfile"; let fd = fs @@ -85,10 +85,10 @@ mod in_mem { #[test] fn root_file_read_write() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create and write to a file let path = "/testfile"; let fd = fs @@ -114,10 +114,10 @@ mod in_mem { #[test] fn write_only_open_does_not_require_read_permission() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); @@ -143,10 +143,10 @@ mod in_mem { #[test] fn newly_created_file_does_not_require_its_own_permissions() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); @@ -168,10 +168,10 @@ mod in_mem { #[test] fn root_directory_creation_and_removal() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Test directory creation let path = "/testdir"; fs.mkdir(ctx, path, Mode::RWXU) @@ -188,10 +188,10 @@ mod in_mem { #[test] fn file_creation_and_deletion() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -215,10 +215,10 @@ mod in_mem { #[test] fn file_read_write() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -254,10 +254,10 @@ mod in_mem { #[test] fn directory_creation_and_removal() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -278,10 +278,10 @@ mod in_mem { #[test] fn read_dir_empty() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { let fd = fs .open(ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); @@ -302,10 +302,10 @@ mod in_mem { #[test] fn read_dir_with_files_and_dirs() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create a directory structure fs.mkdir(ctx, "/testdir", Mode::RWXU) .expect("Failed to create directory"); @@ -378,10 +378,10 @@ mod in_mem { #[test] fn read_dir_file_not_directory() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create a file let fd = fs .open(ctx, "/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) @@ -404,11 +404,11 @@ mod in_mem { #[test] fn parent_dir_write_permissions_are_enforced() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { // A root-owned 0755 directory, holding a file and a directory to try to remove. fs.mkdir( ctx, @@ -433,7 +433,7 @@ mod in_mem { .expect("Failed to create directory"); }); - in_mem::with_user(&mut fs, &mut ctx, 1000, 1000, |fs, ctx| { + in_mem::with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { assert!(matches!( fs.open( ctx, @@ -477,12 +477,12 @@ mod in_mem { #[test] fn chown_test() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); // Create a test file as root - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { let path = "/testfile"; let fd = fs .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) @@ -496,13 +496,13 @@ mod in_mem { // Switch to user 1000 and test that owner can chown (should succeed) let path = "/testfile"; - in_mem::with_user(&mut fs, &mut ctx, 1000, 1000, |fs, ctx| { + in_mem::with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { fs.chown(ctx, path, Some(123), Some(456)) .expect("Failed to chown as owner"); }); // Switch to a different user and test that non-owner cannot chown (should fail) - in_mem::with_user(&mut fs, &mut ctx, 500, 500, |fs, ctx| { + in_mem::with_user(&mut fs, &ctx, 500, 500, |fs, ctx| { match fs.chown(ctx, path, Some(789), Some(101)) { Err(crate::fs::errors::ChownError::NotTheOwner) => { // Expected behavior @@ -524,13 +524,13 @@ mod in_mem { } // Test partial chown (change only user, leave group unchanged) - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chown(ctx, path, Some(999), None) .expect("Failed to chown user only"); }); // Test partial chown (change only group, leave user unchanged) - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chown(ctx, path, None, Some(888)) .expect("Failed to chown group only"); }); @@ -538,11 +538,11 @@ mod in_mem { #[test] fn o_directory_flag_tests() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -622,11 +622,11 @@ mod in_mem { #[test] fn o_excl_flag_tests() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -697,11 +697,11 @@ mod in_mem { #[test] fn open_with_trunc() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -791,11 +791,11 @@ mod in_mem { fn write_position_after_seek() { use crate::fs::SeekWhence; - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Allow regular user to create in root for this focused test fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("chmod / failed"); @@ -849,11 +849,11 @@ mod in_mem { #[test] fn o_append_flag_basic() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -894,12 +894,12 @@ mod in_mem { fn o_append_flag_seek_ignored_for_write() { use crate::fs::SeekWhence; - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -944,12 +944,12 @@ mod in_mem { fn o_append_flag_with_rdwr() { use crate::fs::SeekWhence; - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -998,11 +998,11 @@ mod in_mem { #[test] fn o_append_pwrite_ignores_append_mode() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -1040,11 +1040,11 @@ mod in_mem { #[test] fn o_append_with_trunc() { - let mut ctx = crate::fs::resolver::Context::new(); + let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &mut ctx, |fs, ctx| { + in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); From 7886f6f265572a8bfbb92a270d4d95a55f4bfdad Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Tue, 25 Aug 2026 15:46:45 -0700 Subject: [PATCH 3/8] Use context rather than roll cwd in shim --- litebox_shim_linux/src/lib.rs | 4 +- litebox_shim_linux/src/syscalls/file.rs | 137 ++++++++++++------------ litebox_shim_linux/src/syscalls/unix.rs | 4 +- 3 files changed, 76 insertions(+), 69 deletions(-) diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 8b059e9da..e9f7b32ae 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -266,7 +266,7 @@ impl LinuxShim { files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR - 1); let files = Arc::new(files); let fs_state = Arc::new(syscalls::file::FsState::new()); - files.initialize_stdio_in_shared_descriptors_table(&self.0, &fs_state.context); + files.initialize_stdio_in_shared_descriptors_table(&self.0, &fs_state.context.read()); let entrypoints = crate::LinuxShimEntrypoints { _not_send: core::marker::PhantomData, @@ -1229,7 +1229,7 @@ mod test_utils { .fetch_add(1, core::sync::atomic::Ordering::Relaxed); let files = Arc::new(syscalls::file::FilesState::new(fs)); let fs_state = Arc::new(syscalls::file::FsState::new()); - files.initialize_stdio_in_shared_descriptors_table(&self, &fs_state.context); + files.initialize_stdio_in_shared_descriptors_table(&self, &fs_state.context.read()); Task { wait_state: wait::WaitState::new(self.platform), thread: syscalls::process::ThreadState::new_process(pid), diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 4101e37fd..ef713bd90 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -47,20 +47,16 @@ impl From for AccessUserInfo { /// Task state shared by `CLONE_FS`. pub(crate) struct FsState { umask: core::sync::atomic::AtomicU32, - /// The current working directory - /// - /// Must end with a '/'. - cwd: litebox::sync::RwLock, - // TODO(jayb): simplify by removing [`FsState::cwd`] and instead use this - pub(crate) context: litebox::fs::resolver::Context, + // XXX: the context also stores credentials, might need to reconsider design when implementing + // `setuid` and similar. + pub(crate) context: litebox::sync::RwLock, } impl Clone for FsState { fn clone(&self) -> Self { Self { umask: self.umask.load(Ordering::Relaxed).into(), - cwd: litebox::sync::RwLock::new(self.cwd.read().clone()), - context: self.context.clone(), + context: litebox::sync::RwLock::new(self.context.read().clone()), } } } @@ -69,8 +65,7 @@ impl FsState { pub fn new() -> Self { Self { umask: (Mode::WGRP | Mode::WOTH).bits().into(), - cwd: litebox::sync::RwLock::new(String::from("/")), - context: litebox::fs::resolver::Context::new(), + context: litebox::sync::RwLock::new(litebox::fs::resolver::Context::new()), } } @@ -196,6 +191,17 @@ impl Task { self.fs.borrow().umask() } + /// The current working directory, as a prefix that a relative path can be appended to. + /// + /// Always ends with a `/`. + fn cwd_prefix(&self) -> String { + let mut cwd = self.fs.borrow().context.read().cwd().to_string(); + if !cwd.ends_with('/') { + cwd.push('/'); + } + cwd + } + /// Resolve a path against the current working directory. pub(crate) fn resolve_path(&self, path: impl path::Arg) -> Result { let path_str = path.as_rust_str().map_err(|_| Errno::EINVAL)?; @@ -205,7 +211,7 @@ impl Task { if path_str.starts_with('/') { CString::new(path_str.to_string()).map_err(|_| Errno::EINVAL) } else { - let mut cwd = self.fs.borrow().cwd.read().clone(); + let mut cwd = self.cwd_prefix(); cwd.push_str(path_str); CString::new(cwd).map_err(|_| Errno::EINVAL) } @@ -215,7 +221,7 @@ impl Task { /// /// Note that an empty path is not valid for this function, and will be rejected with `ENOENT`. fn resolve_path_at(&self, dirfd: i32, pathname: impl path::Arg) -> Result { - let get_cwd = || self.fs.borrow().cwd.read().clone(); + let get_cwd = || self.cwd_prefix(); let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; match fs_path { FsPath::Absolute { path } => Ok(path), @@ -235,14 +241,11 @@ impl Task { ) -> Result, Errno> { let mode = mode & !self.get_umask(); let files = self.files.borrow(); + let fs = self.fs.borrow(); + let context = fs.context.read(); files .fs - .open( - &self.fs.borrow().context, - path, - flags - OFlags::CLOEXEC, - mode, - ) + .open(&context, path, flags - OFlags::CLOEXEC, mode) .map_err(Errno::from) } @@ -378,16 +381,12 @@ impl Task { let path = self.resolve_path_at(dirfd, pathname)?; let files = self.files.borrow(); + let fs = self.fs.borrow(); + let context = fs.context.read(); if flags.contains(AtFlags::AT_REMOVEDIR) { - files - .fs - .rmdir(&self.fs.borrow().context, path) - .map_err(Errno::from) + files.fs.rmdir(&context, path).map_err(Errno::from) } else { - files - .fs - .unlink(&self.fs.borrow().context, path) - .map_err(Errno::from) + files.fs.unlink(&context, path).map_err(Errno::from) } } @@ -746,9 +745,11 @@ impl Task { fn do_mkdir(&self, pathname: impl path::Arg, mode: Mode) -> Result<(), Errno> { let mode = mode & !self.get_umask(); let files = self.files.borrow(); + let fs = self.fs.borrow(); + let context = fs.context.read(); files .fs - .mkdir(&self.fs.borrow().context, pathname, mode) + .mkdir(&context, pathname, mode) .map_err(Errno::from) } @@ -1170,7 +1171,9 @@ impl Task { ) -> Result<(), Errno> { let status = { let files = self.files.borrow(); - files.fs.file_status(&self.fs.borrow().context, pathname)? + let fs = self.fs.borrow(); + let context = fs.context.read(); + files.fs.file_status(&context, pathname)? }; let owner = status.owner.into(); Self::do_access_mode(status.mode, owner, caller, &mode) @@ -1194,7 +1197,7 @@ impl Task { Self::validate_access_mode(&mode)?; let caller = self.access_user(&flags); - let get_cwd = || self.fs.borrow().cwd.read().clone(); + let get_cwd = || self.cwd_prefix(); let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; match fs_path { FsPath::Absolute { path } => self.do_access(path, mode, caller), @@ -1389,7 +1392,9 @@ impl Task { }; let status = { let files = self.files.borrow(); - files.fs.file_status(&self.fs.borrow().context, path)? + let fs = self.fs.borrow(); + let context = fs.context.read(); + files.fs.file_status(&context, path)? }; Ok(T::from(status)) } @@ -1427,17 +1432,20 @@ impl Task { where T: From + From, { - let get_cwd = || self.fs.borrow().cwd.read().clone(); + let get_cwd = || self.cwd_prefix(); let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; match fs_path { FsPath::Absolute { path } => { self.do_stat(path, !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW)) } FsPath::Cwd if flags.contains(AtFlags::AT_EMPTY_PATH) => { + // Take the cwd before locking the context: this lock is not recursive, so a + // waiting writer would deadlock a nested read. + let cwd = get_cwd(); let files = self.files.borrow(); - Ok(T::from( - files.fs.file_status(&self.fs.borrow().context, get_cwd())?, - )) + let fs = self.fs.borrow(); + let context = fs.context.read(); + Ok(T::from(files.fs.file_status(&context, cwd)?)) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { descriptor_stat(fd as usize, self) @@ -1705,7 +1713,7 @@ impl Task { /// Handle syscall `getcwd` pub fn sys_getcwd(&self, buf: &mut [u8]) -> Result { - let cwd = self.fs.borrow().cwd.read().clone(); + let cwd = self.fs.borrow().context.read().cwd().to_string(); // need to account for the null terminator if cwd.len() >= buf.len() { return Err(Errno::ERANGE); @@ -1723,41 +1731,38 @@ impl Task { pub fn sys_chdir(&self, pathname: impl path::Arg) -> Result<(), Errno> { use litebox::fs::FileType; use litebox::fs::errors::{FileStatusError, PathError}; - use litebox::path::Arg as _; - // Resolve relative paths against CWD, then normalize (handle `.` / `..`). - let resolved = self.resolve_path(pathname)?; - let abs_path = resolved.normalized().map_err(|_| Errno::EINVAL)?; + let fs = self.fs.borrow(); + // Resolve relative paths against the CWD, and normalize (handle `.` / `..`). + let target = fs + .context + .read() + .resolve(pathname) + .map_err(|_| Errno::EINVAL)?; // Verify the path exists and is a directory. - let files = self.files.borrow(); - match files - .fs - .file_status(&self.fs.borrow().context, abs_path.as_str()) { - Ok(status) => { - if status.file_type != FileType::Directory { - return Err(Errno::ENOTDIR); + let files = self.files.borrow(); + let context = fs.context.read(); + match files.fs.file_status(&context, target.to_string()) { + Ok(status) => { + if status.file_type != FileType::Directory { + return Err(Errno::ENOTDIR); + } + } + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { + return Err(Errno::ENOENT); + } + Err(FileStatusError::PathError(_)) => { + return Err(Errno::EACCES); + } + Err(_) => { + return Err(Errno::ENOENT); } } - Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { - return Err(Errno::ENOENT); - } - Err(FileStatusError::PathError(_)) => { - return Err(Errno::EACCES); - } - Err(_) => { - return Err(Errno::ENOENT); - } - } - - // Ensure the CWD ends with '/'. - let mut new_cwd = abs_path; - if !new_cwd.ends_with('/') { - new_cwd.push('/'); } - *self.fs.borrow().cwd.write() = new_cwd; + fs.context.write().set_cwd(target); Ok(()) } } @@ -2842,7 +2847,7 @@ mod tests { task.sys_chdir("/test_chdir_dir").unwrap(); let len = task.sys_getcwd(&mut buf).unwrap(); let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/test_chdir_dir/"); + assert_eq!(cwd, "/test_chdir_dir"); // chdir to nonexistent path → ENOENT. assert_eq!( @@ -2890,13 +2895,13 @@ mod tests { let mut buf = [0u8; 256]; let len = task.sys_getcwd(&mut buf).unwrap(); let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/rel_parent/rel_child/"); + assert_eq!(cwd, "/rel_parent/rel_child"); - // chdir("..") should normalize back to /rel_parent/. + // chdir("..") should normalize back to /rel_parent. task.sys_chdir("..").unwrap(); let len = task.sys_getcwd(&mut buf).unwrap(); let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/rel_parent/"); + assert_eq!(cwd, "/rel_parent"); } #[test] diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index 5f3e1d519..fcf7cce33 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -119,10 +119,12 @@ impl UnixSocketAddr { // TODO: extend fs to support creating sock file (i.e., with type `InodeType::Socket`) let file = { let files = task.files.borrow(); + let fs = task.fs.borrow(); + let context = fs.context.read(); files .fs .open( - &task.fs.borrow().context, + &context, path.as_str(), flags, Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, From c81f2f925feb8f2752bce47c7f2b951f910f67cb Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Tue, 25 Aug 2026 19:30:10 -0700 Subject: [PATCH 4/8] Use resolver user rather than in-mem internal --- litebox/src/fs/backend.rs | 18 +++++-- litebox/src/fs/composer.rs | 12 ++--- litebox/src/fs/devices.rs | 13 +++-- litebox/src/fs/in_mem.rs | 40 +++----------- litebox/src/fs/nine_p/mod.rs | 25 ++++++--- litebox/src/fs/overlay.rs | 93 ++++++++++++++------------------ litebox/src/fs/resolver.rs | 101 +++++++++++++++++++++++++---------- litebox/src/fs/tar_ro.rs | 11 ++-- 8 files changed, 176 insertions(+), 137 deletions(-) diff --git a/litebox/src/fs/backend.rs b/litebox/src/fs/backend.rs index b4bb17e05..1971dfa65 100644 --- a/litebox/src/fs/backend.rs +++ b/litebox/src/fs/backend.rs @@ -129,16 +129,16 @@ pub trait Backend: private::Sealed + Send + Sync + Any { /// Status of an open file or directory handle. fn status(&self, h: HandleRef<'_>) -> Result; - /// Create a new file at `parent` with the given `name` and `mode`. + /// Create a new file at `parent` with the given `name` and metadata. fn create_file_at( &self, dir: DirHandle, name: &str, - mode: Mode, + node: NewNode, ) -> Result; - /// Create a new directory at `parent` with the given `name` and `mode`. - fn mkdir_at(&self, dir: DirHandle, name: &str, mode: Mode) -> Result; + /// Create a new directory at `parent` with the given `name` and metadata. + fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result; /// Remove the file `name` at `parent`. fn unlink_at(&self, dir: DirHandle, name: &str) -> Result<(), UnlinkError>; @@ -334,6 +334,16 @@ pub(super) enum WalkStopReason { Continue, } +/// The metadata a backend stamps onto a newly created file or directory. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub struct NewNode { + /// Permission bits for the new node. + pub mode: Mode, + /// Owner of the new node. + pub owner: UserInfo, +} + /// A backend item plus permission metadata for resolver-side checks. pub struct Permissioned { pub(super) item: H, diff --git a/litebox/src/fs/composer.rs b/litebox/src/fs/composer.rs index 5453c3f12..558eefdca 100644 --- a/litebox/src/fs/composer.rs +++ b/litebox/src/fs/composer.rs @@ -10,8 +10,8 @@ use alloc::vec; use alloc::vec::Vec; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, - SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, + Backend, BackendHandles, DirHandle, FileHandle, HandleRef, NewNode, PermissionCheck, + Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, }; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, @@ -701,7 +701,7 @@ impl Backend for Composer { &self, dir: DirHandle, name: &str, - mode: Mode, + node: NewNode, ) -> Result { let dir = dir.into_typed::(); match dir.inner { @@ -714,7 +714,7 @@ impl Backend for Composer { self.checked_child_path(path, name, OpenError::ReadOnlyFileSystem)?; self.mounts[mount_index] .backend - .create_file_at(handle, name, mode) + .create_file_at(handle, name, node) .map(|handle| { FileHandle::from_typed::(ComposerFileHandle { mount_index, @@ -725,7 +725,7 @@ impl Backend for Composer { } } - fn mkdir_at(&self, dir: DirHandle, name: &str, mode: Mode) -> Result { + fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result { let dir = dir.into_typed::(); match dir.inner { ComposerDirHandleInner::Virtual { .. } => Err(MkdirError::ReadOnlyFileSystem), @@ -737,7 +737,7 @@ impl Backend for Composer { let path = self.checked_child_path(path, name, MkdirError::ReadOnlyFileSystem)?; self.mounts[mount_index] .backend - .mkdir_at(handle, name, mode) + .mkdir_at(handle, name, node) .map(|handle| { DirHandle::from_typed::( ComposerDirHandleInner::Mounted { diff --git a/litebox/src/fs/devices.rs b/litebox/src/fs/devices.rs index df24df2da..43680453f 100644 --- a/litebox/src/fs/devices.rs +++ b/litebox/src/fs/devices.rs @@ -13,8 +13,8 @@ use crate::LiteBox; use crate::sync::RawSyncPrimitivesProvider; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, - SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, + Backend, BackendHandles, DirHandle, FileHandle, HandleRef, NewNode, PermissionCheck, + Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, @@ -359,12 +359,17 @@ where &self, _dir: DirHandle, _name: &str, - _mode: Mode, + _node: NewNode, ) -> Result { Err(OpenError::ReadOnlyFileSystem) } - fn mkdir_at(&self, _dir: DirHandle, _name: &str, _mode: Mode) -> Result { + fn mkdir_at( + &self, + _dir: DirHandle, + _name: &str, + _node: NewNode, + ) -> Result { Err(MkdirError::ReadOnlyFileSystem) } diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index c2c7b9b8f..226302dd4 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -27,10 +27,6 @@ pub struct InMem { // TODO: Possibly support a single-threaded variant that doesn't have the cost of requiring a // sync-primitives platform, as well as cost of mutexes and such? root: DirNode, - // TODO(jayb): This duplicates the resolver's `Context::user_info`, which is supposed to own - // this. This exists as a transition until we update callers to either manage the perm checks or - // pass down the UserInfo. - current_user: UserInfo, inode_allocator: InodeAllocator, } @@ -48,10 +44,6 @@ impl InMem { })); Self { root, - current_user: UserInfo { - user: 1000, - group: 1000, - }, inode_allocator, } } @@ -487,7 +479,7 @@ impl super::backend::Backend for InMe &self, dir: super::backend::DirHandle, name: &str, - mode: Mode, + node: super::backend::NewNode, ) -> Result { // TODO(jayb): Nothing checks write permission on the parent directory before creating; // the resolver should do so before calling this. @@ -498,8 +490,8 @@ impl super::backend::Backend for InMe } let file = Arc::new(sync::RwLock::new(FileData { perms: Permissions { - mode, - userinfo: self.current_user, + mode: node.mode, + userinfo: node.owner, }, data: Vec::new().into(), node_info: self.inode_allocator.next(), @@ -517,7 +509,7 @@ impl super::backend::Backend for InMe &self, dir: super::backend::DirHandle, name: &str, - mode: Mode, + node: super::backend::NewNode, ) -> Result { // TODO(jayb): Nothing checks write permission on the parent directory before creating; // the resolver should do so before calling this. @@ -528,8 +520,8 @@ impl super::backend::Backend for InMe } let child = Arc::new(sync::RwLock::new(DirData { perms: Permissions { - mode, - userinfo: self.current_user, + mode: node.mode, + userinfo: node.owner, }, children: HashMap::default(), node_info: self.inode_allocator.next(), @@ -580,8 +572,6 @@ impl super::backend::Backend for InMe } fn chmod(&self, h: super::backend::HandleRef<'_>, mode: Mode) -> Result<(), ChmodError> { - // TODO(jayb): This checks ownership against the backend's own `current_user`, rather than - // the resolver's context user. let mut perms = match h { super::backend::HandleRef::File(h) => { sync::RwLockWriteGuard::map(h.get_typed::().file.write(), |f| &mut f.perms) @@ -590,11 +580,6 @@ impl super::backend::Backend for InMe sync::RwLockWriteGuard::map(h.get_typed::().dir.write(), |d| &mut d.perms) } }; - if !(self.current_user.user == UserInfo::ROOT.user - || self.current_user.user == perms.userinfo.user) - { - return Err(ChmodError::NotTheOwner); - } perms.mode = mode; Ok(()) } @@ -605,8 +590,6 @@ impl super::backend::Backend for InMe user: Option, group: Option, ) -> Result<(), ChownError> { - // TODO(jayb): This checks ownership against the backend's own `current_user`, rather than - // the resolver's context user. let mut perms = match h { super::backend::HandleRef::File(h) => { sync::RwLockWriteGuard::map(h.get_typed::().file.write(), |f| &mut f.perms) @@ -615,11 +598,6 @@ impl super::backend::Backend for InMe sync::RwLockWriteGuard::map(h.get_typed::().dir.write(), |d| &mut d.perms) } }; - if !(self.current_user.user == UserInfo::ROOT.user - || self.current_user.user == perms.userinfo.user) - { - return Err(ChownError::NotTheOwner); - } if let Some(new_user) = user { perms.userinfo.user = new_user; } @@ -717,11 +695,7 @@ pub(super) fn with_user( group: u16, f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), ) { - let user = UserInfo { user, group }; - let original_user = context.acting_user(); let mut context = context.clone(); - context.set_acting_user(user); - fs.backend_mut().current_user = user; + context.set_acting_user(UserInfo { user, group }); f(fs, &context); - fs.backend_mut().current_user = original_user; } diff --git a/litebox/src/fs/nine_p/mod.rs b/litebox/src/fs/nine_p/mod.rs index 96e026854..09317ac32 100644 --- a/litebox/src/fs/nine_p/mod.rs +++ b/litebox/src/fs/nine_p/mod.rs @@ -501,16 +501,23 @@ where &self, dir: DirHandle, name: &str, - mode: super::Mode, + node: super::backend::NewNode, ) -> Result { // `Tlcreate` turns the directory fid into the new file's fid server-side, so it must be // handed a private clone rather than the caller's directory handle. let fid = self.client.clone_fid(&dir.get_typed::().fid.fid)?; // NOTE: 9P needs to commit to an access mode at creation time. The resolver still enforces // the caller's read/write intent via its own `read_allowed`/`write_allowed`. - let (_, fid) = self - .client - .create(fid, name, fcall::LOpenFlags::O_RDWR, mode.bits(), 0)?; + // + // XXX: `Tlcreate` only carries a gid; the owning uid is whichever user the connection + // attached as, so `node.owner.user` cannot be honored here. + let (_, fid) = self.client.create( + fid, + name, + fcall::LOpenFlags::O_RDWR, + node.mode.bits(), + u32::from(node.owner.group), + )?; Ok(FileHandle::from_typed::(NinePFileHandle { fid: self.own(fid), })) @@ -520,10 +527,16 @@ where &self, dir: DirHandle, name: &str, - mode: super::Mode, + node: super::backend::NewNode, ) -> Result { let dir = dir.into_typed::(); - self.client.mkdir(&dir.fid.fid, name, mode.bits(), 0)?; + // XXX: as in `create_file_at`, `Tmkdir` cannot set the owning uid. + self.client.mkdir( + &dir.fid.fid, + name, + node.mode.bits(), + u32::from(node.owner.group), + )?; // `Tmkdir` only reports the new directory's qid, so a walk is needed to address it. // // TODO(jayb): the resolver discards this handle, so the walk is pure overhead, and worse, a diff --git a/litebox/src/fs/overlay.rs b/litebox/src/fs/overlay.rs index ea1513554..b177768be 100644 --- a/litebox/src/fs/overlay.rs +++ b/litebox/src/fs/overlay.rs @@ -24,7 +24,7 @@ use crate::LiteBox; use crate::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, Handle, HandleRef, PermissionCheck, + Backend, BackendHandles, DirHandle, FileHandle, Handle, HandleRef, NewNode, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, }; @@ -33,7 +33,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags}; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; /// The reserved namespace prefix; no overlay-visible name may start with it. const MARKER_PREFIX: &str = ".litebox-overlay-"; @@ -268,28 +268,19 @@ impl Overlay { truncate: bool, ) -> Result { let (layer, lower) = lower; - let upper = self - .upper - .create_file_at(upper_dir.clone(), name, status.mode)?; - let copied = self - .upper - .chown( - HandleRef::File(&upper), - Some(status.owner.user), - Some(status.owner.group), - ) - .map_err(|error| match error { - ChownError::PathError(error) => OpenError::PathError(error), - ChownError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, - _ => OpenError::Io, - }); - - let copied = copied.and_then(|()| { - if truncate { - return Ok(()); - } + let upper = self.upper.create_file_at( + upper_dir.clone(), + name, + NewNode { + mode: status.mode, + owner: status.owner, + }, + )?; + let copied = if truncate { + Ok(()) + } else { self.copy_bytes(layer, lower, &upper) - }); + }; if let Err(error) = copied { // Ancestor directories materialised for this copy-up deliberately stay behind. @@ -400,8 +391,16 @@ impl Overlay { { return Ok(()); } - self.upper - .create_file_at(dir.clone(), marker, Mode::empty())?; + // Markers are overlay-internal bookkeeping, never visible to callers, so they are owned by + // root rather than by whoever happened to trigger the write. + self.upper.create_file_at( + dir.clone(), + marker, + NewNode { + mode: Mode::empty(), + owner: UserInfo::ROOT, + }, + )?; Ok(()) } @@ -474,7 +473,14 @@ impl Overlay { .map_err(file_status_to_open_error)?; let child = self .upper - .mkdir_at(parent.clone(), name, status.mode) + .mkdir_at( + parent.clone(), + name, + NewNode { + mode: status.mode, + owner: status.owner, + }, + ) .map_err(|error| match error { MkdirError::PathError(error) => OpenError::PathError(error), MkdirError::AlreadyExists => OpenError::AlreadyExists, @@ -482,30 +488,11 @@ impl Overlay { MkdirError::NoWritePerms => OpenError::NoWritePerms, _ => OpenError::Io, })?; - // XXX(jayb): an atomic create-with-metadata `Backend` operation would avoid this - // best-effort rollback path. - match self.upper.chown( - HandleRef::Dir(&child), - Some(status.owner.user), - Some(status.owner.group), - ) { - Ok(()) => { - // A materialised directory stands in for the lower one, so it keeps its identity. - if let (Some(layer), Ok(upper)) = (layer, self.upper.status(HandleRef::Dir(&child))) - { - self.bind_copy_up(layer, status.node_info, upper.node_info, None); - } - Ok(child) - } - Err(error) => { - let _rollback_result = self.upper.rmdir_at(parent.clone(), name); - Err(match error { - ChownError::PathError(error) => OpenError::PathError(error), - ChownError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, - _ => OpenError::Io, - }) - } + // A materialised directory stands in for the lower one, so it keeps its identity. + if let (Some(layer), Ok(upper)) = (layer, self.upper.status(HandleRef::Dir(&child))) { + self.bind_copy_up(layer, status.node_info, upper.node_info, None); } + Ok(child) } /// The layer that owns a resolved directory, and its handle within that layer: the upper @@ -984,7 +971,7 @@ impl Backend for Overlay { &self, dir: DirHandle, name: &str, - mode: Mode, + node: NewNode, ) -> Result { if !valid(name) { return Err(PathError::InvalidPathname.into()); @@ -995,7 +982,7 @@ impl Backend for Overlay { return Err(OpenError::AlreadyExists); } let upper = self.ensure_upper_dir(&locked, &path)?; - let file = self.upper.create_file_at(upper.clone(), name, mode)?; + let file = self.upper.create_file_at(upper.clone(), name, node)?; if let Err(error) = self.remove_marker(&locked, &upper, &whiteout(name)) { let _rollback_result = self.upper.unlink_at(upper, name); return Err(unlink_to_open_error(error)); @@ -1007,7 +994,7 @@ impl Backend for Overlay { })) } - fn mkdir_at(&self, dir: DirHandle, name: &str, mode: Mode) -> Result { + fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result { fn open_to_mkdir_error(error: OpenError) -> MkdirError { match error { OpenError::PathError(error) => MkdirError::PathError(error), @@ -1033,7 +1020,7 @@ impl Backend for Overlay { let recreated = self .marker_present(&upper, &whiteout) .map_err(|_| MkdirError::Io)?; - let child = self.upper.mkdir_at(upper.clone(), name, mode)?; + let child = self.upper.mkdir_at(upper.clone(), name, node)?; // A directory recreated over a whiteout must not re-merge with the lower directory it // replaces, so it starts out opaque. diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index c2e1c1b2b..f3c884843 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -20,8 +20,8 @@ use super::errors::{ use super::{ FileType, Mode, OFlags, backend::{ - DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, SeekBehavior, WalkOutcome, - WalkStopReason, WalkingDirHandle, + DirHandle, Handle, HandleRef, NewNode, PermissionCheck, PermissionInfo, Permissioned, + SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }, }; @@ -45,16 +45,6 @@ impl &mut Backend { - &mut self.backend - } } /// Per-call resolution context. The user may hold and mutate this as they wish. @@ -269,11 +259,16 @@ impl Result { + fn path_handle( + &self, + context: &Context, + path: &ResolvedPath, + ) -> Result, WalkError> { let map_open_error = |error| match error { OpenError::PathError(error) => WalkError::PathError(error), _ => WalkError::Io, @@ -284,7 +279,12 @@ impl Ok(Handle::Dir( - self.backend + WalkStopReason::CompleteDirectory => { + let permissions = outcome + .components + .last() + .map_or(PermissionCheck::ByBackend, |component| { + component.permissions.clone() + }); + let dir = self + .backend .owned_dir_at(outcome.last, OFlags::PATH) - .map_err(map_open_error)?, - )), - WalkStopReason::StoppedAtNonDirectory => Ok(Handle::File( - self.backend + .map_err(map_open_error)?; + Ok(Permissioned { + item: Handle::Dir(dir), + permissions, + }) + } + WalkStopReason::StoppedAtNonDirectory => { + let file = self + .backend .open_file_at(outcome.last, components[walked], OFlags::PATH) - .map_err(map_open_error)? - .item, - )), + .map_err(map_open_error)?; + Ok(Permissioned { + item: Handle::File(file.item), + permissions: file.permissions, + }) + } WalkStopReason::Continue => { // `walk_path` validates stop reasons before returning. unreachable!() @@ -584,7 +599,14 @@ impl OpenError::Io, WalkError::PathError(error) => error.into(), })?; - let file = self.backend.create_file_at(parent, name, mode)?; + let file = self.backend.create_file_at( + parent, + name, + NewNode { + mode, + owner: context.acting_user(), + }, + )?; let seek_behavior = self.backend.seek_behavior(&file); Ok(insert(Handle::File(file), seek_behavior)) } @@ -799,6 +821,14 @@ impl bool { + let PermissionCheck::ByResolver(permissions) = permissions else { + return true; + }; + let acting = context.acting_user(); + acting.user == UserInfo::ROOT.user || acting.user == permissions.owner.user + } + /// Change the permissions of a file pub fn chmod(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), ChmodError> { let path = context.resolve(path)?; @@ -808,7 +838,10 @@ impl ChmodError::Io, WalkError::PathError(error) => error.into(), })?; - self.backend.chmod(handle.as_ref(), mode) + if !Self::may_change_metadata(context, &handle.permissions) { + return Err(ChmodError::NotTheOwner); + } + self.backend.chmod(handle.item.as_ref(), mode) } /// Change the owner of a file @@ -826,7 +859,10 @@ impl ChownError::Io, WalkError::PathError(error) => error.into(), })?; - self.backend.chown(handle.as_ref(), user, group) + if !Self::may_change_metadata(context, &handle.permissions) { + return Err(ChownError::NotTheOwner); + } + self.backend.chown(handle.item.as_ref(), user, group) } /// Unlink a file @@ -874,7 +910,16 @@ impl MkdirError::Io, WalkError::PathError(error) => error.into(), })?; - self.backend.mkdir_at(parent, name, mode).map(|_| ()) + self.backend + .mkdir_at( + parent, + name, + NewNode { + mode, + owner: context.acting_user(), + }, + ) + .map(|_| ()) } /// Remove a directory diff --git a/litebox/src/fs/tar_ro.rs b/litebox/src/fs/tar_ro.rs index 8aad4e5b9..0e89e965e 100644 --- a/litebox/src/fs/tar_ro.rs +++ b/litebox/src/fs/tar_ro.rs @@ -33,7 +33,7 @@ use crate::fs::{DirEntry, FileType}; use super::{ Mode, NodeInfo, OFlags, UserInfo, - backend::{DirHandle, FileHandle, HandleRef, WalkingDirHandle}, + backend::{DirHandle, FileHandle, HandleRef, NewNode, WalkingDirHandle}, errors::{ ChmodError, ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, @@ -259,12 +259,17 @@ impl super::backend::Backend for TarRo { &self, _dir: DirHandle, _name: &str, - _mode: Mode, + _node: NewNode, ) -> Result { Err(OpenError::ReadOnlyFileSystem) } - fn mkdir_at(&self, _dir: DirHandle, _name: &str, _mode: Mode) -> Result { + fn mkdir_at( + &self, + _dir: DirHandle, + _name: &str, + _node: NewNode, + ) -> Result { Err(MkdirError::ReadOnlyFileSystem) } From 027c021b380df0d73911682fbf3f0343b44cec5b Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Tue, 25 Aug 2026 19:09:57 -0700 Subject: [PATCH 5/8] Use user credentials for context --- litebox_shim_linux/src/lib.rs | 31 +++++++++++++------------ litebox_shim_linux/src/syscalls/file.rs | 15 ++++++++++-- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index e9f7b32ae..eb2075695 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -265,7 +265,13 @@ impl LinuxShim { let files = syscalls::file::FilesState::new(fs); files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR - 1); let files = Arc::new(files); - let fs_state = Arc::new(syscalls::file::FsState::new()); + let credentials = Arc::new(syscalls::process::Credentials { + uid, + euid, + gid, + egid, + }); + let fs_state = Arc::new(syscalls::file::FsState::new(&credentials)); files.initialize_stdio_in_shared_descriptors_table(&self.0, &fs_state.context.read()); let entrypoints = crate::LinuxShimEntrypoints { @@ -277,13 +283,7 @@ impl LinuxShim { pid, ppid, tid: pid, - credentials: syscalls::process::Credentials { - uid, - euid, - gid, - egid, - } - .into(), + credentials, comm: [0; litebox_common_linux::TASK_COMM_LEN].into(), // set at load time fs: fs_state.into(), files: files.into(), @@ -1228,7 +1228,13 @@ mod test_utils { .next_thread_id .fetch_add(1, core::sync::atomic::Ordering::Relaxed); let files = Arc::new(syscalls::file::FilesState::new(fs)); - let fs_state = Arc::new(syscalls::file::FsState::new()); + let credentials = Arc::new(syscalls::process::Credentials { + uid: 0, + euid: 0, + gid: 0, + egid: 0, + }); + let fs_state = Arc::new(syscalls::file::FsState::new(&credentials)); files.initialize_stdio_in_shared_descriptors_table(&self, &fs_state.context.read()); Task { wait_state: wait::WaitState::new(self.platform), @@ -1236,12 +1242,7 @@ mod test_utils { pid, ppid: 0, tid: pid, - credentials: Arc::new(syscalls::process::Credentials { - uid: 0, - euid: 0, - gid: 0, - egid: 0, - }), + credentials, comm: Cell::new(*b"test\0\0\0\0\0\0\0\0\0\0\0\0"), fs: fs_state.into(), files: files.into(), diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index ef713bd90..6fde43406 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -62,10 +62,21 @@ impl Clone for FsState { } impl FsState { - pub fn new() -> Self { + /// Create the state for a task running as `credentials`. + pub fn new(credentials: &super::process::Credentials) -> Self { + let user_info = litebox::fs::UserInfo { + // XXX: Linux ids are 32-bit, but the core litebox file system uses 16-bit ones, so we + // may need to widen `UserInfo`. + user: u16::try_from(credentials.euid) + .unwrap_or_else(|_| unimplemented!("{}", credentials.euid)), + group: u16::try_from(credentials.egid) + .unwrap_or_else(|_| unimplemented!("{}", credentials.egid)), + }; + let mut context = litebox::fs::resolver::Context::new(); + context.set_acting_user(user_info); Self { umask: (Mode::WGRP | Mode::WOTH).bits().into(), - context: litebox::sync::RwLock::new(litebox::fs::resolver::Context::new()), + context: litebox::sync::RwLock::new(context), } } From 7b60bba3a359176982e8dfda5acf156defd45385 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Thu, 27 Aug 2026 13:44:54 -0700 Subject: [PATCH 6/8] Move test-only functions out of in-mem --- litebox/src/fs/in_mem.rs | 27 ------------- litebox/src/fs/tests.rs | 84 ++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index 226302dd4..668a6f8fe 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -672,30 +672,3 @@ struct Permissions { mode: Mode, userinfo: UserInfo, } - -/// Run `f` with the acting user set to root. -/// -/// Non-test callers set up root-owned state via [`InMem::new_initialized`] instead; this exists so -/// that the tests can exercise operations that depend on the acting user. -#[cfg(test)] -pub(super) fn with_root_privileges( - fs: &mut super::resolver::Resolver>, - context: &super::resolver::Context, - f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), -) { - with_user(fs, context, UserInfo::ROOT.user, UserInfo::ROOT.group, f); -} - -/// Run `f` with the acting user set to `user`/`group`. See [`with_root_privileges`]. -#[cfg(test)] -pub(super) fn with_user( - fs: &mut super::resolver::Resolver>, - context: &super::resolver::Context, - user: u16, - group: u16, - f: impl FnOnce(&mut super::resolver::Resolver>, &super::resolver::Context), -) { - let mut context = context.clone(); - context.set_acting_user(UserInfo { user, group }); - f(fs, &context); -} diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 8cacc53dc..3bbaf03cc 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -26,6 +26,33 @@ fn in_mem_fs(litebox: &crate::LiteBox) -> I ) } +/// Run `f` with the acting user set to root. +fn with_root_privileges< + Platform: crate::sync::RawSyncPrimitivesProvider, + B: crate::fs::backend::Backend, +>( + fs: &mut crate::fs::resolver::Resolver, + context: &crate::fs::resolver::Context, + f: impl FnOnce(&mut crate::fs::resolver::Resolver, &crate::fs::resolver::Context), +) { + let root = crate::fs::UserInfo::ROOT; + with_user(fs, context, root.user, root.group, f); +} + +/// Run `f` with the acting user set to `user`/`group`, so that tests can exercise operations +/// whose outcome depends on the acting user. +fn with_user( + fs: &mut crate::fs::resolver::Resolver, + context: &crate::fs::resolver::Context, + user: u16, + group: u16, + f: impl FnOnce(&mut crate::fs::resolver::Resolver, &crate::fs::resolver::Context), +) { + let mut context = context.clone(); + context.set_acting_user(crate::fs::UserInfo { user, group }); + f(fs, &context); +} + type OverlayFs = crate::fs::resolver::Resolver< crate::platform::mock::MockPlatform, crate::fs::overlay::Overlay, @@ -53,19 +80,20 @@ fn overlay_fs( mod in_mem { use crate::LiteBox; - use crate::fs::in_mem; use crate::fs::{Mode, OFlags}; use crate::platform::mock::MockPlatform; use alloc::vec; use alloc::vec::Vec; extern crate std; + use super::{with_root_privileges, with_user}; + #[test] fn root_file_creation_and_deletion() { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Test file creation let path = "/testfile"; let fd = fs @@ -88,7 +116,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create and write to a file let path = "/testfile"; let fd = fs @@ -117,7 +145,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); @@ -146,7 +174,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); }); @@ -171,7 +199,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Test directory creation let path = "/testdir"; fs.mkdir(ctx, path, Mode::RWXU) @@ -191,7 +219,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -218,7 +246,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -257,7 +285,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Make `/tmp` and set up with reasonable privs so normal users can do things in there. fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to create /tmp"); @@ -281,7 +309,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { let fd = fs .open(ctx, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); @@ -305,7 +333,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create a directory structure fs.mkdir(ctx, "/testdir", Mode::RWXU) .expect("Failed to create directory"); @@ -381,7 +409,7 @@ mod in_mem { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - in_mem::with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { + with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { // Create a file let fd = fs .open(ctx, "/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) @@ -408,7 +436,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { // A root-owned 0755 directory, holding a file and a directory to try to remove. fs.mkdir( ctx, @@ -433,7 +461,7 @@ mod in_mem { .expect("Failed to create directory"); }); - in_mem::with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { + with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { assert!(matches!( fs.open( ctx, @@ -482,7 +510,7 @@ mod in_mem { let mut fs = super::in_mem_fs(&litebox); // Create a test file as root - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { let path = "/testfile"; let fd = fs .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) @@ -496,13 +524,13 @@ mod in_mem { // Switch to user 1000 and test that owner can chown (should succeed) let path = "/testfile"; - in_mem::with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { + with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { fs.chown(ctx, path, Some(123), Some(456)) .expect("Failed to chown as owner"); }); // Switch to a different user and test that non-owner cannot chown (should fail) - in_mem::with_user(&mut fs, &ctx, 500, 500, |fs, ctx| { + with_user(&mut fs, &ctx, 500, 500, |fs, ctx| { match fs.chown(ctx, path, Some(789), Some(101)) { Err(crate::fs::errors::ChownError::NotTheOwner) => { // Expected behavior @@ -524,13 +552,13 @@ mod in_mem { } // Test partial chown (change only user, leave group unchanged) - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chown(ctx, path, Some(999), None) .expect("Failed to chown user only"); }); // Test partial chown (change only group, leave user unchanged) - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chown(ctx, path, None, Some(888)) .expect("Failed to chown group only"); }); @@ -542,7 +570,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -626,7 +654,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -701,7 +729,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -795,7 +823,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { // Allow regular user to create in root for this focused test fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("chmod / failed"); @@ -853,7 +881,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -899,7 +927,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -949,7 +977,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -1002,7 +1030,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); @@ -1044,7 +1072,7 @@ mod in_mem { let litebox = LiteBox::new(MockPlatform::new()); let mut fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut fs, &ctx, |fs, ctx| { + with_root_privileges(&mut fs, &ctx, |fs, ctx| { fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("Failed to chmod /"); }); From 41701c301f080607570323b28db871dde8a4acb8 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Fri, 28 Aug 2026 18:56:56 -0700 Subject: [PATCH 7/8] Rename NewNode to CreationMetadata --- litebox/src/fs/backend.rs | 11 ++++++++--- litebox/src/fs/composer.rs | 15 ++++++++++----- litebox/src/fs/devices.rs | 6 +++--- litebox/src/fs/in_mem.rs | 12 ++++++------ litebox/src/fs/nine_p/mod.rs | 14 +++++++------- litebox/src/fs/overlay.rs | 25 +++++++++++++++---------- litebox/src/fs/resolver.rs | 8 ++++---- litebox/src/fs/tar_ro.rs | 6 +++--- 8 files changed, 56 insertions(+), 41 deletions(-) diff --git a/litebox/src/fs/backend.rs b/litebox/src/fs/backend.rs index 1971dfa65..79adb0430 100644 --- a/litebox/src/fs/backend.rs +++ b/litebox/src/fs/backend.rs @@ -134,11 +134,16 @@ pub trait Backend: private::Sealed + Send + Sync + Any { &self, dir: DirHandle, name: &str, - node: NewNode, + metadata: CreationMetadata, ) -> Result; /// Create a new directory at `parent` with the given `name` and metadata. - fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result; + fn mkdir_at( + &self, + dir: DirHandle, + name: &str, + metadata: CreationMetadata, + ) -> Result; /// Remove the file `name` at `parent`. fn unlink_at(&self, dir: DirHandle, name: &str) -> Result<(), UnlinkError>; @@ -337,7 +342,7 @@ pub(super) enum WalkStopReason { /// The metadata a backend stamps onto a newly created file or directory. #[derive(Clone, Copy, Debug)] #[non_exhaustive] -pub struct NewNode { +pub struct CreationMetadata { /// Permission bits for the new node. pub mode: Mode, /// Owner of the new node. diff --git a/litebox/src/fs/composer.rs b/litebox/src/fs/composer.rs index 558eefdca..8abc1ece1 100644 --- a/litebox/src/fs/composer.rs +++ b/litebox/src/fs/composer.rs @@ -10,7 +10,7 @@ use alloc::vec; use alloc::vec::Vec; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, HandleRef, NewNode, PermissionCheck, + Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, }; use super::errors::{ @@ -701,7 +701,7 @@ impl Backend for Composer { &self, dir: DirHandle, name: &str, - node: NewNode, + metadata: CreationMetadata, ) -> Result { let dir = dir.into_typed::(); match dir.inner { @@ -714,7 +714,7 @@ impl Backend for Composer { self.checked_child_path(path, name, OpenError::ReadOnlyFileSystem)?; self.mounts[mount_index] .backend - .create_file_at(handle, name, node) + .create_file_at(handle, name, metadata) .map(|handle| { FileHandle::from_typed::(ComposerFileHandle { mount_index, @@ -725,7 +725,12 @@ impl Backend for Composer { } } - fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result { + fn mkdir_at( + &self, + dir: DirHandle, + name: &str, + metadata: CreationMetadata, + ) -> Result { let dir = dir.into_typed::(); match dir.inner { ComposerDirHandleInner::Virtual { .. } => Err(MkdirError::ReadOnlyFileSystem), @@ -737,7 +742,7 @@ impl Backend for Composer { let path = self.checked_child_path(path, name, MkdirError::ReadOnlyFileSystem)?; self.mounts[mount_index] .backend - .mkdir_at(handle, name, node) + .mkdir_at(handle, name, metadata) .map(|handle| { DirHandle::from_typed::( ComposerDirHandleInner::Mounted { diff --git a/litebox/src/fs/devices.rs b/litebox/src/fs/devices.rs index 43680453f..40c0a8b21 100644 --- a/litebox/src/fs/devices.rs +++ b/litebox/src/fs/devices.rs @@ -13,7 +13,7 @@ use crate::LiteBox; use crate::sync::RawSyncPrimitivesProvider; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, HandleRef, NewNode, PermissionCheck, + Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }; use super::errors::{ @@ -359,7 +359,7 @@ where &self, _dir: DirHandle, _name: &str, - _node: NewNode, + _metadata: CreationMetadata, ) -> Result { Err(OpenError::ReadOnlyFileSystem) } @@ -368,7 +368,7 @@ where &self, _dir: DirHandle, _name: &str, - _node: NewNode, + _metadata: CreationMetadata, ) -> Result { Err(MkdirError::ReadOnlyFileSystem) } diff --git a/litebox/src/fs/in_mem.rs b/litebox/src/fs/in_mem.rs index 668a6f8fe..68efcfe67 100644 --- a/litebox/src/fs/in_mem.rs +++ b/litebox/src/fs/in_mem.rs @@ -479,7 +479,7 @@ impl super::backend::Backend for InMe &self, dir: super::backend::DirHandle, name: &str, - node: super::backend::NewNode, + metadata: super::backend::CreationMetadata, ) -> Result { // TODO(jayb): Nothing checks write permission on the parent directory before creating; // the resolver should do so before calling this. @@ -490,8 +490,8 @@ impl super::backend::Backend for InMe } let file = Arc::new(sync::RwLock::new(FileData { perms: Permissions { - mode: node.mode, - userinfo: node.owner, + mode: metadata.mode, + userinfo: metadata.owner, }, data: Vec::new().into(), node_info: self.inode_allocator.next(), @@ -509,7 +509,7 @@ impl super::backend::Backend for InMe &self, dir: super::backend::DirHandle, name: &str, - node: super::backend::NewNode, + metadata: super::backend::CreationMetadata, ) -> Result { // TODO(jayb): Nothing checks write permission on the parent directory before creating; // the resolver should do so before calling this. @@ -520,8 +520,8 @@ impl super::backend::Backend for InMe } let child = Arc::new(sync::RwLock::new(DirData { perms: Permissions { - mode: node.mode, - userinfo: node.owner, + mode: metadata.mode, + userinfo: metadata.owner, }, children: HashMap::default(), node_info: self.inode_allocator.next(), diff --git a/litebox/src/fs/nine_p/mod.rs b/litebox/src/fs/nine_p/mod.rs index 09317ac32..721c488a0 100644 --- a/litebox/src/fs/nine_p/mod.rs +++ b/litebox/src/fs/nine_p/mod.rs @@ -501,7 +501,7 @@ where &self, dir: DirHandle, name: &str, - node: super::backend::NewNode, + metadata: super::backend::CreationMetadata, ) -> Result { // `Tlcreate` turns the directory fid into the new file's fid server-side, so it must be // handed a private clone rather than the caller's directory handle. @@ -510,13 +510,13 @@ where // the caller's read/write intent via its own `read_allowed`/`write_allowed`. // // XXX: `Tlcreate` only carries a gid; the owning uid is whichever user the connection - // attached as, so `node.owner.user` cannot be honored here. + // attached as, so `metadata.owner.user` cannot be honored here. let (_, fid) = self.client.create( fid, name, fcall::LOpenFlags::O_RDWR, - node.mode.bits(), - u32::from(node.owner.group), + metadata.mode.bits(), + u32::from(metadata.owner.group), )?; Ok(FileHandle::from_typed::(NinePFileHandle { fid: self.own(fid), @@ -527,15 +527,15 @@ where &self, dir: DirHandle, name: &str, - node: super::backend::NewNode, + metadata: super::backend::CreationMetadata, ) -> Result { let dir = dir.into_typed::(); // XXX: as in `create_file_at`, `Tmkdir` cannot set the owning uid. self.client.mkdir( &dir.fid.fid, name, - node.mode.bits(), - u32::from(node.owner.group), + metadata.mode.bits(), + u32::from(metadata.owner.group), )?; // `Tmkdir` only reports the new directory's qid, so a walk is needed to address it. // diff --git a/litebox/src/fs/overlay.rs b/litebox/src/fs/overlay.rs index b177768be..845889af9 100644 --- a/litebox/src/fs/overlay.rs +++ b/litebox/src/fs/overlay.rs @@ -24,9 +24,9 @@ use crate::LiteBox; use crate::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; use super::backend::{ - Backend, BackendHandles, DirHandle, FileHandle, Handle, HandleRef, NewNode, PermissionCheck, - PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, - WalkingDirHandle, + Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, Handle, HandleRef, + PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, + WalkedComponent, WalkingDirHandle, }; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, @@ -271,7 +271,7 @@ impl Overlay { let upper = self.upper.create_file_at( upper_dir.clone(), name, - NewNode { + CreationMetadata { mode: status.mode, owner: status.owner, }, @@ -396,7 +396,7 @@ impl Overlay { self.upper.create_file_at( dir.clone(), marker, - NewNode { + CreationMetadata { mode: Mode::empty(), owner: UserInfo::ROOT, }, @@ -476,7 +476,7 @@ impl Overlay { .mkdir_at( parent.clone(), name, - NewNode { + CreationMetadata { mode: status.mode, owner: status.owner, }, @@ -971,7 +971,7 @@ impl Backend for Overlay { &self, dir: DirHandle, name: &str, - node: NewNode, + metadata: CreationMetadata, ) -> Result { if !valid(name) { return Err(PathError::InvalidPathname.into()); @@ -982,7 +982,7 @@ impl Backend for Overlay { return Err(OpenError::AlreadyExists); } let upper = self.ensure_upper_dir(&locked, &path)?; - let file = self.upper.create_file_at(upper.clone(), name, node)?; + let file = self.upper.create_file_at(upper.clone(), name, metadata)?; if let Err(error) = self.remove_marker(&locked, &upper, &whiteout(name)) { let _rollback_result = self.upper.unlink_at(upper, name); return Err(unlink_to_open_error(error)); @@ -994,7 +994,12 @@ impl Backend for Overlay { })) } - fn mkdir_at(&self, dir: DirHandle, name: &str, node: NewNode) -> Result { + fn mkdir_at( + &self, + dir: DirHandle, + name: &str, + metadata: CreationMetadata, + ) -> Result { fn open_to_mkdir_error(error: OpenError) -> MkdirError { match error { OpenError::PathError(error) => MkdirError::PathError(error), @@ -1020,7 +1025,7 @@ impl Backend for Overlay { let recreated = self .marker_present(&upper, &whiteout) .map_err(|_| MkdirError::Io)?; - let child = self.upper.mkdir_at(upper.clone(), name, node)?; + let child = self.upper.mkdir_at(upper.clone(), name, metadata)?; // A directory recreated over a whiteout must not re-merge with the lower directory it // replaces, so it starts out opaque. diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index f3c884843..225f44026 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -20,8 +20,8 @@ use super::errors::{ use super::{ FileType, Mode, OFlags, backend::{ - DirHandle, Handle, HandleRef, NewNode, PermissionCheck, PermissionInfo, Permissioned, - SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, + CreationMetadata, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, + Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }, }; @@ -602,7 +602,7 @@ impl Result { Err(OpenError::ReadOnlyFileSystem) } @@ -268,7 +268,7 @@ impl super::backend::Backend for TarRo { &self, _dir: DirHandle, _name: &str, - _node: NewNode, + _metadata: CreationMetadata, ) -> Result { Err(MkdirError::ReadOnlyFileSystem) } From 8b0887771efda18cfeba2f74b0acee8d56a69f3d Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Fri, 28 Aug 2026 19:00:09 -0700 Subject: [PATCH 8/8] chdir("") == ENOENT --- litebox_shim_linux/src/syscalls/file.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 6fde43406..af9a5e85a 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1744,6 +1744,14 @@ impl Task { use litebox::fs::errors::{FileStatusError, PathError}; let fs = self.fs.borrow(); + if pathname + .as_rust_str() + .map_err(|_| Errno::EINVAL)? + .is_empty() + { + return Err(Errno::ENOENT); + } + // Resolve relative paths against the CWD, and normalize (handle `.` / `..`). let target = fs .context @@ -2865,6 +2873,7 @@ mod tests { task.sys_chdir("/does_not_exist").unwrap_err(), Errno::ENOENT ); + assert_eq!(task.sys_chdir("").unwrap_err(), Errno::ENOENT); // chdir to a regular file → ENOTDIR. let fd = task