Problem with statvfs (NetBSD)

I'm trying to run yazi on NetBSD. It basically works fine, but deletion behaves badly. I filed a bug report with yazi, they referred me to trash-rs, I ran its self tests and one of them immediately dumps core on NetBSD.

I've extracted the code (removing the locking to simplify it, my test is simple-threaded anyway), it generates the same backtrace in gdb.

Here's the code:

use std::path::PathBuf;

#[derive(Debug)]
struct MountPoint {
    mnt_dir: PathBuf,
    _mnt_type: String,
    _mnt_fsname: String,
}

fn get_mount_points() -> Vec<MountPoint> {
    fn c_buf_to_str(buf: &[libc::c_char]) -> Option<&str> {
        let buf: &[u8] = unsafe { std::slice::from_raw_parts(buf.as_ptr() as _, buf.len()) };
        if let Some(pos) = buf.iter().position(|x| *x == 0) {
            // Shrink buffer to omit the null bytes
            std::str::from_utf8(&buf[..pos]).ok()
        } else {
            std::str::from_utf8(buf).ok()
        }
    }
    let mut fs_infos: *mut libc::statvfs = std::ptr::null_mut();
    let count = unsafe { libc::getmntinfo(&mut fs_infos, libc::MNT_WAIT) };
    if count < 1 {
        return Vec::new();
    }
    let fs_infos: &[libc::statvfs] = unsafe { std::slice::from_raw_parts(fs_infos as _, count as _) };

    let mut result = Vec::new();
    for fs_info in fs_infos {
        if fs_info.f_mntfromname[0] == 0 || fs_info.f_mntonname[0] == 0 {
            // If we have missing information, no need to look any further...
            continue;
        }
        let fs_type = c_buf_to_str(&fs_info.f_fstypename).unwrap_or_default();
        let mount_to = match c_buf_to_str(&fs_info.f_mntonname) {
            Some(m) => m,
            None => {
                continue;
            }
        };
        let mount_from = c_buf_to_str(&fs_info.f_mntfromname).unwrap_or_default();

        let mount_point =
            MountPoint { mnt_dir: mount_to.into(), _mnt_fsname: mount_from.into(), _mnt_type: fs_type.into() };
        result.push(mount_point);
    }
    result
}

fn main() {
    let result = get_mount_points();
    println!("{:?}", result);
}

In gdb, i get:

Program received signal SIGSEGV, Segmentation fault.
rust_statvfs::get_mount_points () at src/main.rs:29
29              if fs_info.f_mntfromname[0] == 0 || fs_info.f_mntonname[0] == 0 {

According to the NetBSD statvfs(5) man page statvfs(5) - NetBSD Manual Pages , the fields are strings of fixed length:

         char f_fstypename[VFS_NAMELEN];     /* fs type name */
         char f_mntonname[VFS_MNAMELEN];     /* directory on which mounted */
         char f_mntfromname[VFS_MNAMELEN];   /* mounted file system */
         char f_mntfromlabel[_VFS_MNAMELEN]; /* disk label name if avail */

so the test if the first byte is a zero should work.

Can anyone see what the bug is?

1 Like

I'd try printing out the contents of that fs_infos: *mut libc::statvfs variable, first the pointer itself and then the thing it points to.

It might be that the return value is positive, but libc::getmntinfo() either didn't update it to point at a series of newly allocated libc::statvfs objects, or the values it wrote contains some weird garbage.

Also, according to the API docs, this is the definition for libc::statvfs on x86_64-unknown-netbsd (64-bit NetBSD):

#[repr(C)]
pub struct statvfs {
    pub f_flag: c_ulong,
    pub f_bsize: c_ulong,
    pub f_frsize: c_ulong,
    pub f_iosize: c_ulong,
    pub f_blocks: fsblkcnt_t,
    pub f_bfree: fsblkcnt_t,
    pub f_bavail: fsblkcnt_t,
    pub f_bresvd: fsblkcnt_t,
    pub f_files: fsfilcnt_t,
    pub f_ffree: fsfilcnt_t,
    pub f_favail: fsfilcnt_t,
    pub f_fresvd: fsfilcnt_t,
    pub f_syncreads: u64,
    pub f_syncwrites: u64,
    pub f_asyncreads: u64,
    pub f_asyncwrites: u64,
    pub f_fsidx: fsid_t,
    pub f_fsid: c_ulong,
    pub f_namemax: c_ulong,
    pub f_owner: uid_t,
    pub f_spare: [u32; 4],
    pub f_fstypename: [c_char; 32],
    pub f_mntonname: [c_char; 1024],
    pub f_mntfromname: [c_char; 1024],
}

This definition seems a bit suspect. You can see there are only 3 name fields after the f_spare, and even then the f_fstypename has a different length to the others, which contradicts the man page you linked to.

It could be that the struct definition in libc is wrong, so when you access f_mntonname it's trying to read something past the length of the allocated libc::statvfs object and hitting a guard page, which triggers a segfault.

1 Like

That's true in the man page too (the 3 names of the lengths are just very similar). The missing field is also documented as "if avail".

Haha, I just saw a bunch of caps with VFS towards the start and ending in LEN and my brain assumed they were all using the same constant.

Either way, when you are pretty sure the FFI function you are calling is implemented correctly (which I assume is correct because it's libc) and the arguments you pass in are right, but are still seeing segfaults, a good place to start looking is at the type definitions.

1 Like

What version is your NetBSD, and what libc do you have?

Motivation for the question: there are differences between the struct @Michael-F-Bryan posted and the man page... depending on the NetBSD version and arch. For example,

  • the order of f_fsidx and f_fsid
    • I didn't find a version in the man pages with the order in libc :thinking:
  • the type/size of f_spare
    • on x86 this changed from u32s to u64s in version 10

Related. NetBSD should have a different target per version due to their ABI instability, but does not.

3 Likes

I'm using NetBSD 10.99.12/x86_64. So I'm using the new statvfs structure.
The order of the f_fsidx and f_fsid in the libc crate is correct - I compared it to the NetBSD header src/sys/sys/statvfs.h at b9d6193657b32e5340422b50b4a44018505ba685 · NetBSD/src · GitHub
I tried adapting the libc crate for the new size of f_spare and adding the new f_mntfromlabel, like this:


diff --git a/src/unix/bsd/netbsdlike/netbsd/mod.rs b/src/unix/bsd/netbsdlike/netbsd/mod.rs
index 8e3507fc7..efcaa74ac 100644
--- a/src/unix/bsd/netbsdlike/netbsd/mod.rs
+++ b/src/unix/bsd/netbsdlike/netbsd/mod.rs
@@ -877,11 +877,12 @@ s_no_extra_traits! {
         pub f_namemax: c_ulong,
         pub f_owner: crate::uid_t,

-        pub f_spare: [u32; 4],
+        pub f_spare: [u64; 4],

         pub f_fstypename: [c_char; 32],
         pub f_mntonname: [c_char; 1024],
         pub f_mntfromname: [c_char; 1024],
+        pub f_mntfromlabel: [c_char; 1024],
     }

     pub struct sockaddr_storage {

and changed trash-rs to use that:

diff --git a/Cargo.toml b/Cargo.toml
index 941cf5e..cc9b613 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,7 +45,7 @@ percent-encoding = "2.3.1"
 chrono = { version = "0.4.31", optional = true, default-features = false, features = [
     "clock",
 ] }
-libc = "0.2.149"
+libc = { path = "../libc" }
 scopeguard = "1.2.0"
 urlencoding = "2.1.3"
 once_cell = "1.18.0"

but the trash-rs self tests still fail with the same symptoms in the same place.

Can you please give some more details on that?
I usually debug my rust with println!("{:?}", object) but that doesn't work here since it segfaults :slight_smile:

Ok, the differences in the structure were a red herring. I have a test program just for statvfs now and that works with an unmodified libc crate.
The statvfs syscalls in NetBSD were versioned when the structure was modified, and rust seems to be using the old function, so the old structure is correct.
So either there is a problem in getmntinfo or in the program code itself above.

1 Like

Ok, it was a bug in the libc crate's implementation for getmntinfo for NetBSD.

Thanks for the help!

5 Likes