Converting size data from ioctl() fails only in release build

Hi!

I have a program on Linux that reads information through some ioctls(). I'm using Rust 1.81.0 and libc crate 0.2.159. I also used rust-bindgen 0.69.4 on the header I needed and generated the ioctl number with the nix crate. Everything works perfectly with a dev build, but when I tried a release build it started to throw malloc() errors like "invalid size (unsorted)" and others which I understand to be some corruption.

I started debugging and isolated the problem. I'm probably doing something wrong, but I don't know what. The ioctl() in question needs to be submitted, then the size of the information is returned, which I use to allocate that amount of memory and call the ioctl() again to get the actual information. Of course, if the allocation is wrong on the second call to the ioctl() it'll overwrite memory it shouldn't.

The code below is a simplification of what I have in my program plus some additional debugging code that helped narrow down the weird behavior.

  • If I run the program with a dev build, everything works as expected. If I run it with debug logging enabled, I also get the correct size in my debug print ("dq.size = 48").
  • If I run the program with a release build, it panics with the message "on no! dq.size = 0, size_of(query) = 8".
  • If I run the program with a release build and with logging enabled then it also works and prints the correct size ("dq.size = 48")! If I remove that exact debug!() line that prints dc.size it panics again with the same message as before.

I understand the size returned by the ioctl() is a u32 and the memory APIs in Rust want an usize. I did try to convert with a ".try_into().unwrap()" but it gives me the same behavior. I also tried to switch from using std::alloc::{Layout, alloc, dealloc} to libc::{malloc, free} but I get the same problems with converting dq.size (that exercise actually helped me spot the dq.size issue).

I'm puzzled as to what's happening here and I'd appreciate any help/pointers. Thanks!

#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct query {
    extensions: u64,
    query: u32,
    size: u32,
    data: u64,
    reserved: [u64; 2usize],
}

#[repr(C)]
#[derive(Debug)]
struct query_config {
    num_params: u32,
    pad: u32,
    info: __IncompleteArrayField<u64>,
}

fn get_info(&mut self) -> Result<InfoType>
{
    let mut dq = query {
        extensions: 0,
        query: 2,
        size: 0,
        data: 0,
        reserved: [0, 0],
    };

    let res = unsafe {
        libc::ioctl(self.dn_fd, 3223872576, &dq) };
    if res < 0 {
        return Err(io::Error::last_os_error().into());
    }

    // >> added to debug the problem
    debug!("dq.size = {:?}", dq.size);
    if (dq.size as usize) < mem::size_of::<query_config>() {
        panic!("on no! dq.size = {:?}, size_of(query) = {:?}", dq.size as usize, mem::size_of::<query_config>());
    }
    // << ends here

    let layout = alloc::Layout::from_size_align(dq.size as usize,
        mem::align_of::<u64>())?;

    // allocs dc.size bytes, puts pointer in dc.data and calls same ioctl
    // again to get the information. I then use the as_slice() method from info field 
    // (it's an __IncompleteArrayField) with the num_params and read what I need.
    // 
    // returns information
}
```

Replace &dq with &mut dq. It's UB to modify anything under shared reference without using interior mutability.

4 Likes

@Hyeonu Ugh, that's subtle, but makes sense. And inside the unsafe {} where it goes unnoticed. Thanks!!