diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 55225bb62..1535e34b3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -43,8 +43,11 @@ jobs: if: ${{ matrix.arch == 'amd64' }} run: | echo user_allow_other | sudo tee --append /etc/fuse.conf - make test - make smoke-all + CARGO_HOME=${HOME}/.cargo + CARGO_BIN=$(which cargo) + sudo -E CARGO=${CARGO_BIN} make test + sudo -E CARGO=${CARGO_BIN} make smoke-all + sudo chown -R $(id -u):$(id -g) "${HOME}/.cargo" Macos-CI: runs-on: macos-latest @@ -56,7 +59,8 @@ jobs: sudo installer -pkg fuse-t-macos-installer-1.0.24.pkg -target / - uses: actions/checkout@v3 - name: build and check - run: make smoke-macos + run: | + make smoke-macos deny: name: Cargo Deny diff --git a/Makefile b/Makefile index bde1a50be..07087de16 100644 --- a/Makefile +++ b/Makefile @@ -21,42 +21,42 @@ check: build ${CARGO} clippy ${TARGET} --features="fusedev,virtiofs" --no-default-features -- -Dwarnings test: - cargo test ${TARGET} --features="fusedev" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="virtiofs" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="vhost-user-fs" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="fusedev,virtiofs" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="fusedev,async-io" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="virtiofs,async-io" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="vhost-user-fs,async-io" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="fusedev,virtiofs,async-io" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --features="fusedev,persist" --no-default-features -- --nocapture --skip integration - cargo test ${TARGET} --all-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="fusedev" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="virtiofs" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="vhost-user-fs" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="fusedev,virtiofs" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="fusedev,async-io" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="virtiofs,async-io" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="vhost-user-fs,async-io" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="fusedev,virtiofs,async-io" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --features="fusedev,persist" --no-default-features -- --nocapture --skip integration + ${CARGO} test ${TARGET} --all-features -- --nocapture --skip integration smoke: - cargo test ${TARGET} --features="fusedev,persist" -- --nocapture + ${CARGO} test ${TARGET} --features="fusedev,persist" -- --nocapture smoke-all: smoke - cargo test ${TARGET} --features="fusedev,persist" -- --nocapture --ignored + ${CARGO} test ${TARGET} --features="fusedev,persist" -- --nocapture --ignored build-macos: - cargo build --features="fusedev" - cargo build --features="fusedev,fuse-t" + ${CARGO} build --features="fusedev" + ${CARGO} build --features="fusedev,fuse-t" check-macos: build-macos - cargo fmt -- --check - cargo clippy --features="fusedev" -- -Dwarnings - cargo test --features="fusedev" -- --nocapture --skip integration - cargo clippy --features="fusedev,fuse-t" -- -Dwarnings - cargo test --features="fusedev,fuse-t" -- --nocapture --skip integration + ${CARGO} fmt -- --check + ${CARGO} clippy --features="fusedev" -- -Dwarnings + ${CARGO} test --features="fusedev" -- --nocapture --skip integration + ${CARGO} clippy --features="fusedev,fuse-t" -- -Dwarnings + ${CARGO} test --features="fusedev,fuse-t" -- --nocapture --skip integration smoke-macos: check-macos - cargo test --features="fusedev,fuse-t" -- --nocapture + ${CARGO} test --features="fusedev,fuse-t" -- --nocapture docker-smoke: docker run --env RUST_BACKTRACE=1 --rm --privileged --volume ${current_dir}:/fuse-rs rust:1.68 sh -c "rustup component add clippy rustfmt; cd /fuse-rs; make smoke-all" testoverlay: - cd tests/testoverlay && cargo build + cd tests/testoverlay && ${CARGO} build # Setup xfstests env and run. xfstests: diff --git a/src/api/server/mod.rs b/src/api/server/mod.rs index cf95455c3..e0f179c9b 100644 --- a/src/api/server/mod.rs +++ b/src/api/server/mod.rs @@ -213,6 +213,8 @@ impl<'a, F: FileSystem, S: BitmapSlice> SrvContext<'a, F, S> { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "fusedev")] + use crate::transport::FuseBuf; #[test] fn test_extract_cstrs() { @@ -249,4 +251,42 @@ mod tests { ServerUtil::extract_two_cstrs(&[0x1u8, 0x2u8, 0x0]).unwrap_err(); ServerUtil::extract_two_cstrs(&[0x1u8, 0x2u8]).unwrap_err(); } + + #[cfg(feature = "fusedev")] + #[test] + fn test_get_message_body() { + let mut read_buf = [0u8; 4096]; + + let mut r = Reader::<()>::from_fuse_buffer(FuseBuf::new(&mut read_buf)).unwrap(); + let in_header = InHeader { + len: 0x1000, + ..Default::default() + }; + let buf = ServerUtil::get_message_body(&mut r, &in_header, 0).unwrap(); + assert_eq!(buf.len(), 0x1000 - size_of::()); + + let mut r = Reader::<()>::from_fuse_buffer(FuseBuf::new(&mut read_buf)).unwrap(); + let in_header = InHeader { + len: 0x1000, + ..Default::default() + }; + let buf = ServerUtil::get_message_body(&mut r, &in_header, 0x100).unwrap(); + assert_eq!(buf.len(), 0x1000 - size_of::() - 0x100); + + let mut r = Reader::<()>::from_fuse_buffer(FuseBuf::new(&mut read_buf)).unwrap(); + let in_header = InHeader { + len: 0x1000, + ..Default::default() + }; + // shoutld fail because of invalid sub header size + assert!(ServerUtil::get_message_body(&mut r, &in_header, 0x1000).is_err()); + + let mut r = Reader::<()>::from_fuse_buffer(FuseBuf::new(&mut read_buf)).unwrap(); + let in_header = InHeader { + len: 0x1000, + ..Default::default() + }; + // shoutld fail because of invalid sub header size + assert!(ServerUtil::get_message_body(&mut r, &in_header, 0x1001).is_err()); + } } diff --git a/src/api/server/sync_io.rs b/src/api/server/sync_io.rs index 3ce15b2f3..b0cf34ffc 100644 --- a/src/api/server/sync_io.rs +++ b/src/api/server/sync_io.rs @@ -1401,3 +1401,152 @@ fn add_dirent( Ok(total_len) } } + +#[cfg(test)] +mod tests { + + #[cfg(all(feature = "fusedev", target_os = "linux"))] + mod tests_fusedev { + use super::super::*; + use crate::passthrough::{Config, PassthroughFs}; + use crate::transport::FuseBuf; + + use std::fs::File; + use std::os::unix::io::AsRawFd; + use vmm_sys_util::tempfile::TempFile; + + fn prepare_srvcontext<'a>( + read_buf: &'a mut [u8], + write_buf: &'a mut [u8], + ) -> (SrvContext<'a, PassthroughFs>, File) { + let file = TempFile::new().unwrap().into_file(); + let reader = Reader::<()>::from_fuse_buffer(FuseBuf::new(read_buf)).unwrap(); + let writer = FuseDevWriter::<()>::new(file.as_raw_fd(), write_buf).unwrap(); + let in_header = InHeader::default(); + ( + SrvContext::::new(in_header, reader, writer.into()), + file, + ) + } + + #[test] + fn test_server_init() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [ + 0x8u8, 0x0, 0x0, 0x0, // major = 0x0008 + 0x0u8, 0x0, 0x0, 0x0, // minor = 0x0008 + 0x0, 0x0, 0x0, 0x0, // max_readahead = 0x0000 + 0x0, 0x0, 0x0, 0x0, // flags = 0x0000 + ]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + let res = server.init(ctx).unwrap(); + assert_eq!(res, 80); + + let mut read_buf1 = [ + 0x7u8, 0x0, 0x0, 0x0, // major = 0x0007 + 0x0u8, 0x0, 0x0, 0x0, // minor = 0x0000 + 0x0, 0x0, 0x0, 0x0, // max_readahead = 0x0000 + 0x0, 0x0, 0x0, 0x0, // flags = 0x0000 + ]; + let mut write_buf1 = [0u8; 4096]; + let (ctx1, _file) = prepare_srvcontext(&mut read_buf1, &mut write_buf1); + + let res = server.init(ctx1).unwrap(); + assert_eq!(res, 24); + } + + #[test] + fn test_server_write() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0u8; 4096]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + let res = server.write(ctx).unwrap(); + assert_eq!(res, 16); + } + + #[test] + fn test_server_read() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0u8; 4096]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + let res = server.read(ctx).unwrap(); + assert_eq!(res, 16); + } + + #[test] + fn test_server_readdir() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0u8; 4096]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + let res = server.do_readdir(ctx, true).unwrap(); + assert_eq!(res, 16); + } + + #[test] + fn test_server_ioctl() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0u8; 4096]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + let res = server.ioctl(ctx).unwrap(); + assert!(res > 0); + + // construct IoctlIn with invalid in_size + let mut read_buf_fail = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, //fh = 0 + 0x0, 0x0, 0x0, 0x0, //flags = 0 + 0x0, 0x0, 0x0, 0x0, //cmd = 0 + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, //arg = 0 + 0x7u8, 0x3u8, 0x0, 0x0, //in_size = 0x307 + 0x0, 0x0, 0x0, 0x0, //out_size = 0 + ]; + let mut write_buf_fail = [0u8; 48]; + let (ctx_fail, _file) = prepare_srvcontext(&mut read_buf_fail, &mut write_buf_fail); + let res = server.ioctl(ctx_fail).unwrap(); + assert!(res > 0); + } + + #[test] + fn test_server_batch_forget() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0u8; 4096]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + // forget should return 0 anyway + assert_eq!(server.batch_forget(ctx).unwrap(), 0); + } + + #[test] + fn test_server_forget() { + let fs = PassthroughFs::<()>::new(Config::default()).unwrap(); + let server = Server::new(fs); + + let mut read_buf = [0x1u8, 0x2u8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]; + let mut write_buf = [0u8; 4096]; + let (ctx, _file) = prepare_srvcontext(&mut read_buf, &mut write_buf); + + assert_eq!(server.forget(ctx).unwrap(), 0); + } + } +} diff --git a/src/passthrough/sync_io.rs b/src/passthrough/sync_io.rs index 76520efe6..f399bf77e 100644 --- a/src/passthrough/sync_io.rs +++ b/src/passthrough/sync_io.rs @@ -1345,3 +1345,291 @@ impl FileSystem for PassthroughFs { } } } + +#[cfg(test)] +mod tests { + use std::convert::TryInto; + + use super::*; + use crate::abi::fuse_abi::ROOT_ID; + use std::path::Path; + use vmm_sys_util::{tempdir::TempDir, tempfile::TempFile}; + + fn prepare_fs_tmpdir() -> (PassthroughFs, TempDir) { + let source = TempDir::new().expect("Cannot create temporary directory."); + let fs_cfg = Config { + writeback: true, + do_import: true, + no_open: false, + no_readdir: false, + inode_file_handles: true, + xattr: true, + killpriv_v2: true, //enable killpriv_v2 + root_dir: source + .as_path() + .to_str() + .expect("source path to string") + .to_string(), + ..Default::default() + }; + let fs = PassthroughFs::<()>::new(fs_cfg).unwrap(); + fs.import().unwrap(); + + // enable all fuse options + let opt = FsOptions::all(); + fs.init(opt).unwrap(); + + (fs, source) + } + + fn prepare_context() -> Context { + Context { + uid: unsafe { libc::getuid() }, + gid: unsafe { libc::getgid() }, + pid: unsafe { libc::getpid() }, + ..Default::default() + } + } + + fn create_file_with_sugid(ctx: &Context, fs: &PassthroughFs<()>) -> (Entry, Handle) { + let fname = CString::new("testfile").unwrap(); + let args = CreateIn { + flags: libc::O_WRONLY as u32, + mode: 0o6777, + umask: 0, + fuse_flags: 0, + }; + let (test_entry, handle, _, _) = fs.create(&ctx, ROOT_ID, &fname, args).unwrap(); + + (test_entry, handle.unwrap()) + } + + #[test] + fn test_dir_operations() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let dir = CString::new("testdir").unwrap(); + fs.mkdir(&ctx, ROOT_ID, &dir, 0o755, 0).unwrap(); + + let (handle, _) = fs.opendir(&ctx, ROOT_ID, libc::O_RDONLY as u32).unwrap(); + + assert!(fs + .readdir(&ctx, ROOT_ID, handle.unwrap(), 10, 0, &mut |_| Ok(1)) + .is_err()); + + assert!(fs + .readdirplus(&ctx, ROOT_ID, handle.unwrap(), 10, 0, &mut |_, _| Ok(1)) + .is_err()); + + assert!(fs.fsyncdir(&ctx, ROOT_ID, true, handle.unwrap()).is_ok()); + + assert!(fs.releasedir(&ctx, ROOT_ID, 0, handle.unwrap()).is_ok()); + assert!(fs.rmdir(&ctx, ROOT_ID, &dir).is_ok()); + } + + #[test] + fn test_link_rename() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let fname = CString::new("testfile").unwrap(); + let args = CreateIn::default(); + let (test_entry, _, _, _) = fs.create(&ctx, ROOT_ID, &fname, args).unwrap(); + + let link_name = CString::new("testlink").unwrap(); + fs.link(&ctx, test_entry.inode, ROOT_ID, &link_name) + .unwrap(); + + let new_name = CString::new("newlink").unwrap(); + fs.rename(&ctx, ROOT_ID, &link_name, ROOT_ID, &new_name, 0) + .unwrap(); + + let link_entry = fs.lookup(&ctx, ROOT_ID, &new_name).unwrap(); + + assert_eq!(link_entry.inode, test_entry.inode); + } + + #[test] + fn test_unlink_delete_file() { + let (fs, source) = prepare_fs_tmpdir(); + let child_path = TempFile::new_in(source.as_path()).expect("Cannot create temporary file."); + + let ctx = prepare_context(); + + let child_str = child_path + .as_path() + .file_name() + .unwrap() + .to_str() + .expect("path to string"); + let child = CString::new(child_str).unwrap(); + + fs.unlink(&ctx, ROOT_ID, &child).unwrap(); + + assert!(!Path::new(child_str).exists()) + } + + #[test] + // test virtiofs CVE-2020-35517, should not open device file + fn test_mknod_and_open_device() { + let (fs, _source) = prepare_fs_tmpdir(); + + let ctx = prepare_context(); + + let device_name = CString::new("test_device").unwrap(); + let mode = libc::S_IFBLK; + let mask = 0o777; + let device_no = libc::makedev(0, 103) as u32; + + let device_entry = fs + .mknod(&ctx, ROOT_ID, &device_name, mode, device_no, mask) + .unwrap(); + let (d_st, _) = fs.getattr(&ctx, device_entry.inode, None).unwrap(); + + assert_eq!(d_st.st_mode & libc::S_IFMT, libc::S_IFBLK); + assert_eq!(d_st.st_rdev as u32, device_no); + + // open device should fail because of is_safe_inode check + let err = fs + .open(&ctx, device_entry.inode, libc::O_RDWR as u32, 0) + .is_err(); + assert_eq!(err, true); + } + + #[test] + fn test_create_access() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let fname = CString::new("testfile").unwrap(); + let args = CreateIn { + flags: libc::O_WRONLY as u32, + mode: 0644, + umask: 0, + fuse_flags: 0, + }; + let (test_entry, _, _, _) = fs.create(&ctx, ROOT_ID, &fname, args).unwrap(); + + let mask = (libc::R_OK | libc::W_OK) as u32; + assert_eq!(fs.access(&ctx, test_entry.inode, mask).is_ok(), true); + let mask = (libc::R_OK | libc::W_OK | libc::X_OK) as u32; + assert_eq!(fs.access(&ctx, test_entry.inode, mask).is_ok(), false); + assert!(fs + .release(&ctx, test_entry.inode, 0, 0, false, false, Some(0)) + .is_err()); + } + + #[test] + fn test_symlink_escape_root() { + let (fs, _source) = prepare_fs_tmpdir(); + let child_path = + TempFile::new_in(_source.as_path()).expect("Cannot create temporary file."); + let ctx = prepare_context(); + + let eval_sym_dest = CString::new("/root").unwrap(); + let eval_sym_name = CString::new("eval_sym").unwrap(); + let normal_sym_dest = CString::new(child_path.as_path().to_str().unwrap()).unwrap(); + let normal_sym_name = CString::new("normal_sym").unwrap(); + + let normal_sym_entry = fs + .symlink(&ctx, &normal_sym_dest, ROOT_ID, &normal_sym_name) + .unwrap(); + + let eval_sym_entry = fs + .symlink(&ctx, &eval_sym_dest, ROOT_ID, &eval_sym_name) + .unwrap(); + + let normal_buf = fs.readlink(&ctx, normal_sym_entry.inode).unwrap(); + let eval_buf = fs.readlink(&ctx, eval_sym_entry.inode).unwrap(); + let normal_dest_name = CString::new(String::from_utf8(normal_buf).unwrap()).unwrap(); + let eval_dest_name = CString::new(String::from_utf8(eval_buf).unwrap()).unwrap(); + + assert_eq!(normal_dest_name, normal_sym_dest); + assert_eq!(eval_dest_name, eval_sym_dest); + } + + #[test] + fn test_setattr_and_drop_priv() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let (test_entry, _) = create_file_with_sugid(&ctx, &fs); + + let (mut old_att, _) = fs.getattr(&ctx, test_entry.inode, None).unwrap(); + + old_att.st_size = 4096; + let mut valid = SetattrValid::SIZE | SetattrValid::KILL_SUIDGID; + let (attr_not_drop, _) = fs + .setattr(&ctx, test_entry.inode, old_att, None, valid) + .unwrap(); + // during file size change, + // suid/sgid should be dropped because of killpriv_v2 + assert_eq!(attr_not_drop.st_mode, 0o100777); + + old_att.st_size = 0; + old_att.st_uid = 1; + old_att.st_gid = 1; + old_att.st_atime = 0; + old_att.st_mtime = 0; + valid = SetattrValid::SIZE + | SetattrValid::ATIME + | SetattrValid::MTIME + | SetattrValid::UID + | SetattrValid::GID; + + let (attr, _) = fs + .setattr(&ctx, test_entry.inode, old_att, None, valid) + .unwrap(); + // suid/sgid is dropped because chmod is called + assert_eq!(attr.st_mode, 0o100777); + assert_eq!(attr.st_size, 0); + } + + #[test] + // fallocate missing killpriv logic, should be fixed + fn test_fallocate_drop_priv() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let (test_entry, handle) = create_file_with_sugid(&ctx, &fs); + + let offset = fs + .lseek( + &ctx, + test_entry.inode, + handle, + 4096, + libc::SEEK_SET.try_into().unwrap(), + ) + .unwrap(); + fs.fallocate(&ctx, test_entry.inode, handle, 0, offset, 4096) + .unwrap(); + + let (att, _) = fs.getattr(&ctx, test_entry.inode, None).unwrap(); + + assert_eq!(att.st_size, 8192); + // suid/sgid not dropped + assert_eq!(att.st_mode, 0o106777); + } + + #[test] + fn test_fsync_flush() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let (test_entry, handle) = create_file_with_sugid(&ctx, &fs); + + assert!(fs.fsync(&ctx, test_entry.inode, false, handle).is_ok()); + assert!(fs.flush(&ctx, test_entry.inode, handle, 0).is_ok()); + } + + #[test] + fn test_statfs() { + let (fs, _source) = prepare_fs_tmpdir(); + let ctx = prepare_context(); + + let statfs = fs.statfs(&ctx, ROOT_ID).unwrap(); + assert_eq!(statfs.f_namemax, 255); + } +}