@@ -385,6 +385,59 @@ fn can_use_seccomp_network_block_fallback(caps: &CapabilitySet) -> bool {
385385 )
386386}
387387
388+ /// Linux `statfs` magic number for the 9P filesystem (`v9fs`).
389+ ///
390+ /// Covers all 9P-backed mounts: WSL2 Windows host paths (`/mnt/c`, `/mnt/d`),
391+ /// QEMU virtfs, and other Plan 9 mounts. The 9P driver does not implement the
392+ /// LSM inode hooks that Landlock relies on, so `PathBeneath` rules for these
393+ /// paths are accepted by the kernel but silently have no enforcement effect.
394+ const V9FS_MAGIC : libc:: c_long = 0x0102_1997 ;
395+
396+ /// Return `true` if `f_type` (from `statfs::f_type`) identifies a filesystem
397+ /// that does not support Landlock enforcement.
398+ #[ inline]
399+ fn fs_type_unsupported ( f_type : libc:: c_long ) -> bool {
400+ f_type == V9FS_MAGIC
401+ }
402+
403+ /// Return `true` if `path` sits on a filesystem that does not support Landlock.
404+ ///
405+ /// Currently detects 9P mounts (`V9FS_MAGIC`), which includes WSL2 Windows
406+ /// host paths and QEMU virtfs. Uses `statfs(2)` on the path itself; falls back
407+ /// to `false` on any error so a detection failure never blocks sandbox startup.
408+ ///
409+ /// Skips the syscall entirely for paths that cannot be on a 9P mount
410+ /// (anything not under `/mnt`), avoiding overhead on the common case.
411+ ///
412+ /// Returns the device ID (`st_dev`) of the mount when unsupported, so the
413+ /// caller can deduplicate warnings per mount rather than per path. Returns
414+ /// `None` for supported filesystems or on any `statfs` error.
415+ fn unsupported_filesystem_dev ( path : & Path ) -> Option < u64 > {
416+ if !path. starts_with ( "/mnt" ) {
417+ return None ;
418+ }
419+ use std:: ffi:: CString ;
420+ use std:: mem:: MaybeUninit ;
421+ let Ok ( cpath) = CString :: new ( path. as_os_str ( ) . as_encoded_bytes ( ) ) else {
422+ return None ;
423+ } ;
424+ // SAFETY: `buf` is a valid out-pointer for `statfs`; we initialise it
425+ // through the syscall before reading any field.
426+ unsafe {
427+ let mut buf: MaybeUninit < libc:: statfs > = MaybeUninit :: uninit ( ) ;
428+ if libc:: statfs ( cpath. as_ptr ( ) , buf. as_mut_ptr ( ) ) != 0 {
429+ return None ;
430+ }
431+ let stat = buf. assume_init ( ) ;
432+ if fs_type_unsupported ( stat. f_type ) {
433+ // fsid_t.__val is private; transmute the 8-byte struct to u64 for dedup.
434+ Some ( std:: mem:: transmute :: < libc:: fsid_t , u64 > ( stat. f_fsid ) )
435+ } else {
436+ None
437+ }
438+ }
439+ }
440+
388441/// Check if a path is a character or block device file.
389442///
390443/// Used to selectively grant `IoctlDev` only for actual device files
@@ -684,6 +737,10 @@ pub fn apply_with_abi(caps: &CapabilitySet, abi: &DetectedAbi) -> Result<Seccomp
684737 // are not distinguishable on this Linux path until the seccomp AF_UNIX
685738 // allowlist work enforces UnixSocketCapability::covers().
686739 let ioctl_dev_available = AccessFs :: from_all ( target_abi) . contains ( AccessFs :: IoctlDev ) ;
740+ // Track device IDs of mounts already warned about to emit one warning
741+ // per mount, not one per capability path.
742+ let mut warned_unsupported_devs: std:: collections:: HashSet < u64 > =
743+ std:: collections:: HashSet :: new ( ) ;
687744
688745 for cap in caps. fs_capabilities ( ) {
689746 let result = access_to_landlock ( cap. access , target_abi) ;
@@ -715,6 +772,18 @@ pub fn apply_with_abi(caps: &CapabilitySet, abi: &DetectedAbi) -> Result<Seccomp
715772 ) ;
716773 }
717774
775+ if let Some ( dev) = unsupported_filesystem_dev ( & cap. resolved )
776+ && warned_unsupported_devs. insert ( dev)
777+ {
778+ warn ! (
779+ "Path '{}' is on a 9P filesystem (e.g. WSL2 Windows host mount, QEMU virtfs). \
780+ Landlock enforcement on 9P paths is unreliable — grants may be silently ignored \
781+ or incompletely enforced, causing unexpected access denials. \
782+ Move your working directory to a native Linux filesystem to use nono safely.",
783+ cap. resolved. display( )
784+ ) ;
785+ }
786+
718787 debug ! (
719788 "Adding rule: {} with access {:?}" ,
720789 cap. resolved. display( ) ,
@@ -4162,4 +4231,63 @@ mod tests {
41624231 ) ;
41634232 }
41644233 }
4234+
4235+ // fs_type_unsupported: pure logic, runs on any CI platform
4236+
4237+ #[ test]
4238+ fn test_fs_type_unsupported_v9fs_magic ( ) {
4239+ assert ! (
4240+ fs_type_unsupported( V9FS_MAGIC ) ,
4241+ "V9FS_MAGIC must be unsupported"
4242+ ) ;
4243+ }
4244+
4245+ #[ test]
4246+ fn test_fs_type_unsupported_known_supported_types ( ) {
4247+ const EXT4_MAGIC : libc:: c_long = 0xEF53 ;
4248+ const TMPFS_MAGIC : libc:: c_long = 0x0102_1994 ;
4249+ const PROC_MAGIC : libc:: c_long = 0x9FA0 ;
4250+ assert ! ( !fs_type_unsupported( EXT4_MAGIC ) ) ;
4251+ assert ! ( !fs_type_unsupported( TMPFS_MAGIC ) ) ;
4252+ assert ! ( !fs_type_unsupported( PROC_MAGIC ) ) ;
4253+ assert ! ( !fs_type_unsupported( 0 ) ) ;
4254+ }
4255+
4256+ // unsupported_filesystem_dev: exercises the statfs syscall path
4257+
4258+ #[ test]
4259+ fn test_unsupported_filesystem_dev_native_paths ( ) {
4260+ // /tmp and /proc are native Linux filesystems, never 9P.
4261+ assert ! (
4262+ unsupported_filesystem_dev( std:: path:: Path :: new( "/tmp" ) ) . is_none( ) ,
4263+ "/tmp should be on a supported filesystem"
4264+ ) ;
4265+ assert ! (
4266+ unsupported_filesystem_dev( std:: path:: Path :: new( "/proc" ) ) . is_none( ) ,
4267+ "/proc should be on a supported filesystem"
4268+ ) ;
4269+ }
4270+
4271+ #[ test]
4272+ fn test_unsupported_filesystem_dev_nonexistent_path ( ) {
4273+ // statfs fails on a nonexistent path — must return None, not panic.
4274+ assert ! (
4275+ unsupported_filesystem_dev( std:: path:: Path :: new( "/nonexistent-nono-test-path-xyz" ) )
4276+ . is_none( ) ,
4277+ "nonexistent path should return None, not panic"
4278+ ) ;
4279+ }
4280+
4281+ #[ test]
4282+ fn test_unsupported_filesystem_dev_wsl2_mount ( ) {
4283+ // On a real WSL2 system /mnt/c is a 9P mount and must be detected.
4284+ // Skipped on native Linux where /mnt/c doesn't exist.
4285+ let mnt_c = std:: path:: Path :: new ( "/mnt/c" ) ;
4286+ if mnt_c. exists ( ) {
4287+ assert ! (
4288+ unsupported_filesystem_dev( mnt_c) . is_some( ) ,
4289+ "/mnt/c exists but was not detected as a 9P filesystem"
4290+ ) ;
4291+ }
4292+ }
41654293}
0 commit comments