From 88f769900a9042df1378b581febf412ac77cde09 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Tue, 2 Jun 2026 21:57:06 +0200 Subject: [PATCH] fix(sandbox): allow read access to /sys for interface stats under Landlock The Landlock ruleset handles all filesystem reads but only added read allow-rules for /proc and the GeoIP read_paths. The interface-stats poller enumerates interfaces via read_dir("/sys/class/net") and reads each /sys/class/net//statistics/* counter, which got denied with EACCES once the sandbox was enforced. The error was swallowed at debug level, so the Interfaces panel silently showed "No interface stats available" (independent of -i any vs auto-detected interface). Add read-only allow-rules for /sys/class/net (enumeration) and /sys/devices (the per-interface statistics entries are symlinks into /sys/devices and Landlock evaluates the resolved path). Sandbox stays fully enforced; this only grants read. Also surface the stats-collection failure once at warn! instead of a silent debug!, so a future sandbox/permission regression is visible at the default log level. Verified in OrbStack (Landlock ABI 7, FullyEnforced): without the rule read_dir("/sys/class/net") fails with EACCES; with it all interfaces are readable. --- src/app.rs | 14 +++++++++++++- src/network/platform/linux/sandbox/landlock.rs | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index a0197ce8..95469126 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1449,6 +1449,10 @@ impl App { let provider = PlatformStatsProvider; let mut previous_stats: HashMap = HashMap::new(); + // Warn once if stat collection ever fails so a permission/sandbox + // regression (e.g. Landlock denying /sys) is visible at the + // default log level instead of being silently swallowed. + let mut warned_collect_failure = false; loop { if should_stop.load(Ordering::Relaxed) { @@ -1477,7 +1481,15 @@ impl App { } } Err(e) => { - debug!("Failed to collect interface stats: {}", e); + if !warned_collect_failure { + warn!( + "Failed to collect interface stats: {} (interface panel will be empty; on Linux this is often a sandbox/permission issue reading /sys/class/net)", + e + ); + warned_collect_failure = true; + } else { + debug!("Failed to collect interface stats: {}", e); + } } } diff --git a/src/network/platform/linux/sandbox/landlock.rs b/src/network/platform/linux/sandbox/landlock.rs index 6dc764db..1da03478 100644 --- a/src/network/platform/linux/sandbox/landlock.rs +++ b/src/network/platform/linux/sandbox/landlock.rs @@ -182,6 +182,20 @@ pub fn apply_landlock(config: &SandboxConfig) -> Result { log::warn!("Could not add /proc rule: {}", e); } + // Add rules for sysfs (read-only). The interface-stats poller enumerates + // interfaces via read_dir("/sys/class/net") and then reads each + // /sys/class/net//statistics/* counter. Those per-interface entries + // are symlinks into /sys/devices/.../net/, and Landlock evaluates the + // *resolved* path, so both subtrees need an allow-rule — without them the + // reads fail with EACCES and the Interfaces panel shows + // "No interface stats available". sysfs is not process-sensitive the way + // /proc is, and this is read-only, so granting the two subtrees is fine. + for sysfs_path in ["/sys/class/net", "/sys/devices"] { + if let Err(e) = add_path_rule(&mut ruleset_created, sysfs_path, read_access) { + log::warn!("Could not add {} rule: {}", sysfs_path, e); + } + } + // Add rules for read-only paths (e.g., GeoIP databases) for path in &config.read_paths { if path.exists()