From 3d37f64fc5948e339cfa2a66918ccf42ab0b496e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 27 Mar 2026 12:49:03 +0900 Subject: [PATCH 1/4] fix(dgw): use statvfs for recording storage space on Unix Replaces the sysinfo disk-enumeration approach on Linux/macOS with a direct statvfs(2) call against the configured recording path. This fixes incorrect or missing space values for network filesystems (NFS, CIFS/Samba) and any mount point the previous heuristic could not resolve. Issue: DGW-355 --- Cargo.lock | 3 +- devolutions-gateway/Cargo.toml | 3 +- devolutions-gateway/src/api/heartbeat.rs | 87 +++++++++++++----------- devolutions-gateway/src/main.rs | 2 - 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c8852ef..ab6cff46f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,7 +1491,6 @@ dependencies = [ "devolutions-log", "dlopen", "dlopen_derive", - "dunce", "embed-resource", "etherparse", "expect-test", @@ -1512,6 +1511,7 @@ dependencies = [ "job-queue", "job-queue-libsql", "kdc", + "libc", "multibase", "network-monitor", "network-scanner", @@ -1540,7 +1540,6 @@ dependencies = [ "sysevent-codes", "sysevent-syslog", "sysevent-winevent", - "sysinfo", "tap", "tempfile", "terminal-streamer", diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 5fef872cf..40226fb8b 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -68,8 +68,6 @@ anyhow = "1.0" thiserror = "2" typed-builder = "0.21" backoff = "0.4" -sysinfo = { version = "0.35", default-features = false, features = ["disk"] } -dunce = "1.0" bitflags = "2.9" # Security, crypto… @@ -132,6 +130,7 @@ portpicker = "0.1" [target.'cfg(unix)'.dependencies] sysevent-syslog.path = "../crates/sysevent-syslog" +libc = "0.2" [target.'cfg(windows)'.dependencies] rustls-cng = { version = "0.5", default-features = false, features = ["logging", "tls12", "ring"] } diff --git a/devolutions-gateway/src/api/heartbeat.rs b/devolutions-gateway/src/api/heartbeat.rs index ffe5fcb09..46367abbe 100644 --- a/devolutions-gateway/src/api/heartbeat.rs +++ b/devolutions-gateway/src/api/heartbeat.rs @@ -180,7 +180,11 @@ pub(crate) fn recording_storage_health(recording_path: &std::path::Path) -> Reco /// This supports UNC paths (`\\server\share\...`) and mapped drive letters without needing /// to enumerate mount points or canonicalize the path. /// -/// On other platforms, enumerates disks via `sysinfo` and finds the longest-prefix mount point. +/// On Unix, calls `statvfs(2)` directly against the configured recording path. +/// This supports network filesystems (NFS, CIFS/Samba) and any mount point without needing +/// to enumerate mount points. +/// +/// On other platforms, space values are not available and `(None, None)` is returned. fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option) { static NO_DISK_STATE: NoDiskState = NoDiskState::new(); @@ -230,49 +234,54 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option } } - #[cfg(not(windows))] + #[cfg(unix)] fn query_storage_space_impl(recording_path: &std::path::Path) -> (Option, Option) { - use sysinfo::Disks; - - if !sysinfo::IS_SUPPORTED_SYSTEM { - debug!("This system does not support listing storage disks"); - return (None, None); - } - - trace!("System is supporting listing storage disks"); - - let recording_path = dunce::canonicalize(recording_path) - .inspect_err( - |error| debug!(%error, recording_path = %recording_path.display(), "Failed to canonicalize recording path"), - ) - .unwrap_or_else(|_| recording_path.to_owned()); - - let disks = Disks::new_with_refreshed_list(); - - debug!(recording_path = %recording_path.display(), "Search mount point for path"); - debug!(?disks, "Found disks"); - - let mut recording_disk = None; - let mut longest_path = 0; - - for disk in disks.list() { - let mount_point = disk.mount_point(); - let path_len = mount_point.components().count(); - if recording_path.starts_with(mount_point) && longest_path < path_len { - recording_disk = Some(disk); - longest_path = path_len; + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt as _; + + let c_path = match CString::new(recording_path.as_os_str().as_bytes()) { + Ok(p) => p, + Err(_) => { + NO_DISK_STATE.on_disk_missing(recording_path, "path contains null byte"); + return (None, None); } - } + }; + + // SAFETY: `c_path` is a valid null-terminated C string; `stat` is zeroed stack memory + // whose layout matches what the OS writes into it. + let mut stat: libc::statvfs = unsafe { std::mem::zeroed() }; + let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) }; - if let Some(disk) = recording_disk { + if ret == 0 { NO_DISK_STATE.on_disk_present(); - debug!(?disk, "Disk used to store recordings"); - (Some(disk.total_space()), Some(disk.available_space())) + // f_frsize is the fundamental block size; fall back to f_bsize if zero. + let block_size = if stat.f_frsize != 0 { + stat.f_frsize as u64 + } else { + stat.f_bsize as u64 + }; + let total = (stat.f_blocks as u64).saturating_mul(block_size); + // f_bavail is the space available to unprivileged users (vs f_bfree which is root-only). + let available = (stat.f_bavail as u64).saturating_mul(block_size); + debug!( + recording_path = %recording_path.display(), + total_bytes = total, + free_bytes_available = available, + "Retrieved disk space via statvfs" + ); + (Some(total), Some(available)) } else { - NO_DISK_STATE.on_disk_missing(&recording_path, "no matching disk mount point found"); + let error = std::io::Error::last_os_error(); + NO_DISK_STATE.on_disk_missing(recording_path, &error.to_string()); (None, None) } } + + #[cfg(not(any(windows, unix)))] + fn query_storage_space_impl(recording_path: &std::path::Path) -> (Option, Option) { + NO_DISK_STATE.on_disk_missing(recording_path, "unsupported platform"); + (None, None) + } } #[cfg(test)] @@ -290,9 +299,9 @@ mod tests { assert!(result.recording_storage_is_writeable); - // Space values may be None only on platforms where sysinfo is unsupported; - // on Windows and common Unix platforms they should be Some. - #[cfg(any(windows, target_os = "linux", target_os = "macos"))] + // Space values are Some on Windows (GetDiskFreeSpaceExW) and all Unix platforms + // (statvfs); they may be None on exotic targets that fall back to sysinfo. + #[cfg(any(windows, unix))] { assert!( result.recording_storage_total_space.is_some(), diff --git a/devolutions-gateway/src/main.rs b/devolutions-gateway/src/main.rs index 021cbd64b..8fda2c75e 100644 --- a/devolutions-gateway/src/main.rs +++ b/devolutions-gateway/src/main.rs @@ -12,7 +12,6 @@ use camino as _; use devolutions_agent_shared as _; use dlopen as _; use dlopen_derive as _; -use dunce as _; use etherparse as _; use hostname as _; use http_body_util as _; @@ -39,7 +38,6 @@ use rustls_cng as _; use serde as _; use serde_urlencoded as _; use smol_str as _; -use sysinfo as _; use thiserror as _; use time as _; use tokio_rustls as _; From 743c6fff4f35dbbc8839d156b02fe20215bf724c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 27 Mar 2026 12:59:13 +0900 Subject: [PATCH 2/4] . --- devolutions-gateway/src/api/heartbeat.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/devolutions-gateway/src/api/heartbeat.rs b/devolutions-gateway/src/api/heartbeat.rs index 46367abbe..90c74f4bf 100644 --- a/devolutions-gateway/src/api/heartbeat.rs +++ b/devolutions-gateway/src/api/heartbeat.rs @@ -247,9 +247,10 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option } }; - // SAFETY: `c_path` is a valid null-terminated C string; `stat` is zeroed stack memory - // whose layout matches what the OS writes into it. + // SAFETY: `stat` is zeroed stack memory whose layout matches what the OS writes into it. let mut stat: libc::statvfs = unsafe { std::mem::zeroed() }; + + // SAFETY: `c_path` is a valid null-terminated C string. let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) }; if ret == 0 { @@ -299,8 +300,7 @@ mod tests { assert!(result.recording_storage_is_writeable); - // Space values are Some on Windows (GetDiskFreeSpaceExW) and all Unix platforms - // (statvfs); they may be None on exotic targets that fall back to sysinfo. + // Space values are Some on Windows (GetDiskFreeSpaceExW) and all Unix platforms (statvfs). #[cfg(any(windows, unix))] { assert!( @@ -316,6 +316,13 @@ mod tests { "total space must be >= available space" ); } + + // Space values are None on other unsupported platforms. + #[cfg(not(any(windows, unix)))] + { + assert!(result.recording_storage_total_space.is_none()); + assert!(result.recording_storage_available_space.is_none()); + } } /// A non-existent path should be reported as not writeable. From d01143af62a52b9b5e5d6cdd49dd930bdb38bfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 27 Mar 2026 13:57:38 +0900 Subject: [PATCH 3/4] . --- devolutions-gateway/src/api/heartbeat.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/devolutions-gateway/src/api/heartbeat.rs b/devolutions-gateway/src/api/heartbeat.rs index 90c74f4bf..568f59a06 100644 --- a/devolutions-gateway/src/api/heartbeat.rs +++ b/devolutions-gateway/src/api/heartbeat.rs @@ -255,25 +255,35 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option if ret == 0 { NO_DISK_STATE.on_disk_present(); + // f_frsize is the fundamental block size; fall back to f_bsize if zero. + // u64::from() is used throughout as a precaution: f_frsize/f_bsize + // are c_ulong (u64 on both Linux and macOS); f_blocks/f_bavail are + // fsblkcnt_t which is u64 on Linux but u32 on macOS. + let block_size = if stat.f_frsize != 0 { - stat.f_frsize as u64 + u64::from(stat.f_frsize) } else { - stat.f_bsize as u64 + u64::from(stat.f_bsize) }; - let total = (stat.f_blocks as u64).saturating_mul(block_size); + + let total = u64::from(stat.f_blocks).saturating_mul(block_size); + // f_bavail is the space available to unprivileged users (vs f_bfree which is root-only). - let available = (stat.f_bavail as u64).saturating_mul(block_size); + let available = u64::from(stat.f_bavail).saturating_mul(block_size); + debug!( recording_path = %recording_path.display(), total_bytes = total, free_bytes_available = available, "Retrieved disk space via statvfs" ); + (Some(total), Some(available)) } else { let error = std::io::Error::last_os_error(); NO_DISK_STATE.on_disk_missing(recording_path, &error.to_string()); + (None, None) } } From 0f5a83c668141ec4fdf8844c23015b0a40e233f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 27 Mar 2026 14:00:44 +0900 Subject: [PATCH 4/4] . --- devolutions-gateway/src/api/heartbeat.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devolutions-gateway/src/api/heartbeat.rs b/devolutions-gateway/src/api/heartbeat.rs index 568f59a06..b22a0890f 100644 --- a/devolutions-gateway/src/api/heartbeat.rs +++ b/devolutions-gateway/src/api/heartbeat.rs @@ -235,6 +235,10 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option } #[cfg(unix)] + #[allow( + clippy::useless_conversion, + reason = "statvfs field types differ across Unix platforms: fsblkcnt_t is u64 on Linux but u32 on macOS" + )] fn query_storage_space_impl(recording_path: &std::path::Path) -> (Option, Option) { use std::ffi::CString; use std::os::unix::ffi::OsStrExt as _; @@ -257,10 +261,6 @@ fn query_storage_space(recording_path: &std::path::Path) -> (Option, Option NO_DISK_STATE.on_disk_present(); // f_frsize is the fundamental block size; fall back to f_bsize if zero. - // u64::from() is used throughout as a precaution: f_frsize/f_bsize - // are c_ulong (u64 on both Linux and macOS); f_blocks/f_bavail are - // fsblkcnt_t which is u64 on Linux but u32 on macOS. - let block_size = if stat.f_frsize != 0 { u64::from(stat.f_frsize) } else {