diff --git a/Cargo.lock b/Cargo.lock index 73c192236bf..36af01cf4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4493,7 +4493,7 @@ dependencies = [ [[package]] name = "mirrord-protocol" -version = "1.15.1" +version = "1.16.0" dependencies = [ "actix-codec", "bincode", diff --git a/changelog.d/statfs.added.md b/changelog.d/statfs.added.md new file mode 100644 index 00000000000..b1cea16a410 --- /dev/null +++ b/changelog.d/statfs.added.md @@ -0,0 +1 @@ +Add statfs support \ No newline at end of file diff --git a/mirrord/agent/src/file.rs b/mirrord/agent/src/file.rs index 0fa945fbd85..571b2ad9d3c 100644 --- a/mirrord/agent/src/file.rs +++ b/mirrord/agent/src/file.rs @@ -223,8 +223,12 @@ impl FileManager { Some(FileResponse::Xstat(xstat_result)) } FileRequest::XstatFs(XstatFsRequest { fd }) => { - let xstat_result = self.xstatfs(fd); - Some(FileResponse::XstatFs(xstat_result)) + let xstatfs_result = self.xstatfs(fd); + Some(FileResponse::XstatFs(xstatfs_result)) + } + FileRequest::StatFs(StatFsRequest { path }) => { + let statfs_result = self.statfs(path); + Some(FileResponse::XstatFs(statfs_result)) } // dir operations @@ -769,6 +773,18 @@ impl FileManager { }) } + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) fn statfs(&mut self, path: PathBuf) -> RemoteResult { + let path = resolve_path(path, &self.root_path)?; + + let statfs = nix::sys::statfs::statfs(&path) + .map_err(|err| std::io::Error::from_raw_os_error(err as i32))?; + + Ok(XstatFsResponse { + metadata: statfs.into(), + }) + } + #[tracing::instrument(level = Level::TRACE, skip(self), err(level = Level::DEBUG))] pub(crate) fn fdopen_dir(&mut self, fd: u64) -> RemoteResult { let path = match self diff --git a/mirrord/intproxy/protocol/src/lib.rs b/mirrord/intproxy/protocol/src/lib.rs index e51fcdf773e..7648f3d6cf6 100644 --- a/mirrord/intproxy/protocol/src/lib.rs +++ b/mirrord/intproxy/protocol/src/lib.rs @@ -387,6 +387,13 @@ impl_request!( res_path = ProxyToLayerMessage::File => FileResponse::XstatFs, ); +impl_request!( + req = StatFsRequest, + res = RemoteResult, + req_path = LayerToProxyMessage::File => FileRequest::StatFs, + res_path = ProxyToLayerMessage::File => FileResponse::XstatFs, +); + impl_request!( req = FdOpenDirRequest, res = RemoteResult, diff --git a/mirrord/intproxy/src/proxies/files.rs b/mirrord/intproxy/src/proxies/files.rs index 517c743ce12..55c1b2f98f0 100644 --- a/mirrord/intproxy/src/proxies/files.rs +++ b/mirrord/intproxy/src/proxies/files.rs @@ -6,7 +6,7 @@ use mirrord_protocol::{ file::{ CloseDirRequest, CloseFileRequest, DirEntryInternal, ReadDirBatchRequest, ReadDirResponse, ReadFileResponse, ReadLimitedFileRequest, SeekFromInternal, MKDIR_VERSION, - READDIR_BATCH_VERSION, READLINK_VERSION, RMDIR_VERSION, + READDIR_BATCH_VERSION, READLINK_VERSION, RMDIR_VERSION, STATFS_VERSION, }, ClientMessage, DaemonMessage, ErrorKindInternal, FileRequest, FileResponse, RemoteIOError, ResponseError, @@ -259,21 +259,27 @@ impl FilesProxy { match request { FileRequest::ReadLink(..) - if protocol_version.is_some_and(|version| !READLINK_VERSION.matches(version)) => + if protocol_version.is_none_or(|version| !READLINK_VERSION.matches(version)) => { Err(FileResponse::ReadLink(Err(ResponseError::NotImplemented))) } FileRequest::MakeDir(..) | FileRequest::MakeDirAt(..) - if protocol_version.is_some_and(|version| !MKDIR_VERSION.matches(version)) => + if protocol_version.is_none_or(|version| !MKDIR_VERSION.matches(version)) => { Err(FileResponse::MakeDir(Err(ResponseError::NotImplemented))) } FileRequest::RemoveDir(..) | FileRequest::Unlink(..) | FileRequest::UnlinkAt(..) if protocol_version - .is_some_and(|version: &Version| !RMDIR_VERSION.matches(version)) => + .is_none_or(|version: &Version| !RMDIR_VERSION.matches(version)) => { Err(FileResponse::RemoveDir(Err(ResponseError::NotImplemented))) } + FileRequest::StatFs(..) + if protocol_version + .is_none_or(|version: &Version| !STATFS_VERSION.matches(version)) => + { + Err(FileResponse::XstatFs(Err(ResponseError::NotImplemented))) + } _ => Ok(()), } } diff --git a/mirrord/layer/src/file/hooks.rs b/mirrord/layer/src/file/hooks.rs index 7c46165b37a..3fcfc3c1280 100644 --- a/mirrord/layer/src/file/hooks.rs +++ b/mirrord/layer/src/file/hooks.rs @@ -904,8 +904,9 @@ unsafe extern "C" fn fstatat_detour( }) } +/// Hook for `libc::fstatfs`. #[hook_guard_fn] -unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) -> c_int { +pub(crate) unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) -> c_int { if out_stat.is_null() { return HookError::BadPointer.into(); } @@ -919,6 +920,25 @@ unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) -> c_int { .unwrap_or_bypass_with(|_| FN_FSTATFS(fd, out_stat)) } +/// Hook for `libc::statfs`. +#[hook_guard_fn] +pub(crate) unsafe extern "C" fn statfs_detour( + raw_path: *const c_char, + out_stat: *mut statfs, +) -> c_int { + if out_stat.is_null() { + return HookError::BadPointer.into(); + } + + crate::file::ops::statfs(raw_path.checked_into()) + .map(|res| { + let res = res.metadata; + fill_statfs(out_stat, &res); + 0 + }) + .unwrap_or_bypass_with(|_| FN_STATFS(raw_path, out_stat)) +} + unsafe fn realpath_logic( source_path: *const c_char, output_path: *mut c_char, @@ -1333,6 +1353,8 @@ pub(crate) unsafe fn enable_file_hooks(hook_manager: &mut HookManager) { FnFstatfs, FN_FSTATFS ); + replace!(hook_manager, "statfs", statfs_detour, FnStatfs, FN_STATFS); + replace!( hook_manager, "fdopendir", @@ -1415,6 +1437,13 @@ pub(crate) unsafe fn enable_file_hooks(hook_manager: &mut HookManager) { FnFstatfs, FN_FSTATFS ); + replace!( + hook_manager, + "statfs$INODE64", + statfs_detour, + FnStatfs, + FN_STATFS + ); replace!( hook_manager, "fdopendir$INODE64", diff --git a/mirrord/layer/src/file/ops.rs b/mirrord/layer/src/file/ops.rs index fcc27507876..8ca2401101f 100644 --- a/mirrord/layer/src/file/ops.rs +++ b/mirrord/layer/src/file/ops.rs @@ -9,7 +9,8 @@ use mirrord_protocol::{ file::{ MakeDirAtRequest, MakeDirRequest, OpenFileRequest, OpenFileResponse, OpenOptionsInternal, ReadFileResponse, ReadLinkFileRequest, ReadLinkFileResponse, RemoveDirRequest, - SeekFileResponse, UnlinkAtRequest, WriteFileResponse, XstatFsResponse, XstatResponse, + SeekFileResponse, StatFsRequest, UnlinkAtRequest, WriteFileResponse, XstatFsResponse, + XstatResponse, }, ResponseError, }; @@ -736,6 +737,16 @@ pub(crate) fn xstatfs(fd: RawFd) -> Detour { Detour::Success(response) } +#[mirrord_layer_macro::instrument(level = "trace")] +pub(crate) fn statfs(path: Detour) -> Detour { + let path = path?; + let lstatfs = StatFsRequest { path }; + + let response = common::make_proxy_request_with_response(lstatfs)??; + + Detour::Success(response) +} + #[cfg(target_os = "linux")] #[mirrord_layer_macro::instrument(level = "trace")] pub(crate) fn getdents64(fd: RawFd, buffer_size: u64) -> Detour { diff --git a/mirrord/layer/src/go/linux_x64.rs b/mirrord/layer/src/go/linux_x64.rs index 18b36700cbe..622a24383d7 100644 --- a/mirrord/layer/src/go/linux_x64.rs +++ b/mirrord/layer/src/go/linux_x64.rs @@ -340,6 +340,8 @@ unsafe extern "C" fn c_abi_syscall_handler( faccessat_detour(param1 as _, param2 as _, param3 as _, 0) as i64 } libc::SYS_fstat => fstat_detour(param1 as _, param2 as _) as i64, + libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, + libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, libc::SYS_getdents64 => getdents64_detour(param1 as _, param2 as _, param3 as _) as i64, #[cfg(all(target_os = "linux", not(target_arch = "aarch64")))] libc::SYS_mkdir => mkdir_detour(param1 as _, param2 as _) as i64, diff --git a/mirrord/layer/src/go/mod.rs b/mirrord/layer/src/go/mod.rs index df810bbdcf9..6a28c3ebfd9 100644 --- a/mirrord/layer/src/go/mod.rs +++ b/mirrord/layer/src/go/mod.rs @@ -101,6 +101,8 @@ unsafe extern "C" fn c_abi_syscall6_handler( .into() } libc::SYS_fstat => fstat_detour(param1 as _, param2 as _) as i64, + libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, + libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, libc::SYS_fsync => fsync_detour(param1 as _) as i64, libc::SYS_fdatasync => fsync_detour(param1 as _) as i64, libc::SYS_openat => { diff --git a/mirrord/layer/tests/apps/fileops/go/main.go b/mirrord/layer/tests/apps/fileops/go/main.go index 5973e013d5e..69db336fb3e 100644 --- a/mirrord/layer/tests/apps/fileops/go/main.go +++ b/mirrord/layer/tests/apps/fileops/go/main.go @@ -7,10 +7,21 @@ import ( func main() { tempFile := "/tmp/test_file.txt" - syscall.Open(tempFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) + fd, _ := syscall.Open(tempFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) var stat syscall.Stat_t err := syscall.Stat(tempFile, &stat) if err != nil { panic(err) } + + var statfs syscall.Statfs_t + err = syscall.Statfs(tempFile, &statfs) + if err != nil { + panic(err) + } + + err = syscall.Fstatfs(fd, &statfs) + if err != nil { + panic(err) + } } diff --git a/mirrord/layer/tests/apps/statfs_fstatfs/statfs_fstatfs.c b/mirrord/layer/tests/apps/statfs_fstatfs/statfs_fstatfs.c new file mode 100644 index 00000000000..6c474cce4e3 --- /dev/null +++ b/mirrord/layer/tests/apps/statfs_fstatfs/statfs_fstatfs.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) && defined(__MACH__) +#include +#include +#else +#include +#endif + +/// Test `statfs / fstatfs`. +/// +/// Gets information about a mounted filesystem +/// +int main() +{ + char *tmp_test_path = "/statfs_fstatfs_test_path"; + mkdir(tmp_test_path, 0777); + + // statfs + struct statfs statfs_buf; + if (statfs(tmp_test_path, &statfs_buf) == -1) + { + perror("statfs failed"); + return EXIT_FAILURE; + } + + // fstatfs + int fd = open(tmp_test_path, O_RDONLY); + + if (fd == -1) + { + perror("Error opening tmp_test_path"); + return 1; + } + + struct statfs fstatfs_buf; + if (fstatfs(fd, &fstatfs_buf) == -1) + { + perror("fstatfs failed"); + close(fd); + return EXIT_FAILURE; + } + + close(fd); + return 0; +} diff --git a/mirrord/layer/tests/common/mod.rs b/mirrord/layer/tests/common/mod.rs index 207f1c6c7a6..a4652433768 100644 --- a/mirrord/layer/tests/common/mod.rs +++ b/mirrord/layer/tests/common/mod.rs @@ -17,7 +17,7 @@ use mirrord_intproxy::{agent_conn::AgentConnection, IntProxy}; use mirrord_protocol::{ file::{ AccessFileRequest, AccessFileResponse, OpenFileRequest, OpenOptionsInternal, - ReadFileRequest, SeekFromInternal, XstatRequest, XstatResponse, + ReadFileRequest, SeekFromInternal, XstatFsResponse, XstatRequest, XstatResponse, }, tcp::{DaemonTcp, LayerTcp, NewTcpConnection, TcpClose, TcpData}, ClientMessage, DaemonCodec, DaemonMessage, FileRequest, FileResponse, @@ -489,6 +489,48 @@ impl TestIntProxy { .unwrap(); } + /// Makes a [`FileRequest::Statefs`] and answers it. + pub async fn expect_statfs(&mut self, expected_path: &str) { + // Expecting `statfs` call with path. + assert_matches!( + self.recv().await, + ClientMessage::FileRequest(FileRequest::StatFs( + mirrord_protocol::file::StatFsRequest { path } + )) if path.to_str().unwrap() == expected_path + ); + + // Answer `statfs`. + self.codec + .send(DaemonMessage::File(FileResponse::XstatFs(Ok( + XstatFsResponse { + metadata: Default::default(), + }, + )))) + .await + .unwrap(); + } + + /// Makes a [`FileRequest::Xstatefs`] and answers it. + pub async fn expect_fstatfs(&mut self, expected_fd: u64) { + // Expecting `fstatfs` call with path. + assert_matches!( + self.recv().await, + ClientMessage::FileRequest(FileRequest::XstatFs( + mirrord_protocol::file::XstatFsRequest { fd } + )) if expected_fd == fd + ); + + // Answer `fstatfs`. + self.codec + .send(DaemonMessage::File(FileResponse::XstatFs(Ok( + XstatFsResponse { + metadata: Default::default(), + }, + )))) + .await + .unwrap(); + } + /// Makes a [`FileRequest::RemoveDir`] and answers it. pub async fn expect_remove_dir(&mut self, expected_dir_name: &str) { // Expecting `rmdir` call with path. @@ -784,6 +826,7 @@ pub enum Application { Fork, ReadLink, MakeDir, + StatfsFstatfs, RemoveDir, OpenFile, CIssue2055, @@ -841,6 +884,7 @@ impl Application { Application::Fork => String::from("tests/apps/fork/out.c_test_app"), Application::ReadLink => String::from("tests/apps/readlink/out.c_test_app"), Application::MakeDir => String::from("tests/apps/mkdir/out.c_test_app"), + Application::StatfsFstatfs => String::from("tests/apps/statfs_fstatfs/out.c_test_app"), Application::RemoveDir => String::from("tests/apps/rmdir/out.c_test_app"), Application::Realpath => String::from("tests/apps/realpath/out.c_test_app"), Application::NodeHTTP | Application::NodeIssue2283 | Application::NodeIssue2807 => { @@ -1080,6 +1124,7 @@ impl Application { | Application::Fork | Application::ReadLink | Application::MakeDir + | Application::StatfsFstatfs | Application::RemoveDir | Application::Realpath | Application::RustFileOps @@ -1159,6 +1204,7 @@ impl Application { | Application::Fork | Application::ReadLink | Application::MakeDir + | Application::StatfsFstatfs | Application::RemoveDir | Application::Realpath | Application::Go21Issue834 diff --git a/mirrord/layer/tests/fileops.rs b/mirrord/layer/tests/fileops.rs index 5daacdadc50..de26b318f40 100644 --- a/mirrord/layer/tests/fileops.rs +++ b/mirrord/layer/tests/fileops.rs @@ -345,6 +345,9 @@ async fn go_stat( )))) .await; + intproxy.expect_statfs("/tmp/test_file.txt").await; + intproxy.expect_fstatfs(fd).await; + test_process.wait_assert_success().await; test_process.assert_no_error_in_stderr().await; } diff --git a/mirrord/layer/tests/statfs_fstatfs.rs b/mirrord/layer/tests/statfs_fstatfs.rs new file mode 100644 index 00000000000..38f48c8495f --- /dev/null +++ b/mirrord/layer/tests/statfs_fstatfs.rs @@ -0,0 +1,45 @@ +#![feature(assert_matches)] +use std::{path::Path, time::Duration}; + +use rstest::rstest; + +mod common; +pub use common::*; + +/// Test for the [`libc::statfs`] and [`libc::fstatfs`] functions. +#[rstest] +#[tokio::test] +#[timeout(Duration::from_secs(60))] +async fn mkdir(dylib_path: &Path) { + let application = Application::StatfsFstatfs; + + let (mut test_process, mut intproxy) = application + .start_process_with_layer(dylib_path, Default::default(), None) + .await; + + println!("waiting for file request (mkdir)."); + intproxy + .expect_make_dir("/statfs_fstatfs_test_path", 0o777) + .await; + + println!("waiting for file request (statfs)."); + intproxy.expect_statfs("/statfs_fstatfs_test_path").await; + + println!("waiting for file request (open)."); + let fd: u64 = 1; + intproxy + .expect_file_open_for_reading("/statfs_fstatfs_test_path", fd) + .await; + + println!("waiting for file request (fstatfs)."); + intproxy.expect_fstatfs(fd).await; + + println!("waiting for file request (close)."); + intproxy.expect_file_close(fd).await; + + assert_eq!(intproxy.try_recv().await, None); + + test_process.wait_assert_success().await; + test_process.assert_no_error_in_stderr().await; + test_process.assert_no_error_in_stdout().await; +} diff --git a/mirrord/protocol/Cargo.toml b/mirrord/protocol/Cargo.toml index 7daa0201505..67eab572e62 100644 --- a/mirrord/protocol/Cargo.toml +++ b/mirrord/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mirrord-protocol" -version = "1.15.1" +version = "1.16.0" authors.workspace = true description.workspace = true documentation.workspace = true diff --git a/mirrord/protocol/src/codec.rs b/mirrord/protocol/src/codec.rs index 2b55fa43b07..8e41d9acab1 100644 --- a/mirrord/protocol/src/codec.rs +++ b/mirrord/protocol/src/codec.rs @@ -94,6 +94,7 @@ pub enum FileRequest { RemoveDir(RemoveDirRequest), Unlink(UnlinkRequest), UnlinkAt(UnlinkAtRequest), + StatFs(StatFsRequest), } /// Minimal mirrord-protocol version that allows `ClientMessage::ReadyForLogs` message. diff --git a/mirrord/protocol/src/file.rs b/mirrord/protocol/src/file.rs index 9a8622731fb..4aa25069bb3 100644 --- a/mirrord/protocol/src/file.rs +++ b/mirrord/protocol/src/file.rs @@ -34,6 +34,9 @@ pub static RMDIR_VERSION: LazyLock = pub static OPEN_LOCAL_VERSION: LazyLock = LazyLock::new(|| ">=1.13.3".parse().expect("Bad Identifier")); +pub static STATFS_VERSION: LazyLock = + LazyLock::new(|| ">=1.16.0".parse().expect("Bad Identifier")); + /// Internal version of Metadata across operating system (macOS, Linux) /// Only mutual attributes #[derive(Encode, Decode, Debug, PartialEq, Clone, Copy, Eq, Default)] @@ -413,6 +416,11 @@ pub struct XstatFsRequest { pub fd: u64, } +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct StatFsRequest { + pub path: PathBuf, +} + #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub struct XstatResponse { pub metadata: MetadataInternal, diff --git a/tests/python-e2e/ops.py b/tests/python-e2e/ops.py index c107ebb2375..36c7ba5fb8c 100644 --- a/tests/python-e2e/ops.py +++ b/tests/python-e2e/ops.py @@ -88,6 +88,19 @@ def test_mkdir_errors(self): os.close(dir) + def test_statfs_and_fstatvfs_sucess(self): + """ + Test statfs / fstatfs + """ + file_path, _ = self._create_new_tmp_file() + + statvfs_result = os.statvfs(file_path) + self.assertIsNotNone(statvfs_result) + + fd = os.open(file_path, os.O_RDONLY) + fstatvfs_result = os.fstatvfs(fd) + self.assertIsNotNone(fstatvfs_result) + def test_rmdir(self): """ Creates a new directory in "/tmp" and removes it using rmdir. @@ -106,6 +119,5 @@ def _create_new_tmp_file(self): w_file.write(TEXT) return file_path, file_name - if __name__ == "__main__": unittest.main()