Strange memory behavior when creating objects containing cstring pointers

I'm adding the nmount call to libc and ran into some behavior I don't understand and was hoping someone could shed some light. I have a function that builds an libc::iovec struct which I then pass to the nmount call, which on FreeBSD is the replacement for the mount sys call.

I must be clobbering memory somewhere but I really don't understand how. I was hoping someone might have some idea of why this is happening. I think each instance of the iov_base member of iovec should point to a different piece of memory. I thought the issue might be a naive understanding of the borrow checking, so I moved the code outside the unsafe block where I actually call the nmount sys call.

fn iovec_from_string(name: &str) -> libc::iovec {                                                                                                          
    println!("processing {}", name);                                                                                                                       
    let cstring = CString::new(name).expect("Failed to covert string to to C String");                                                                     
    let bytes = cstring.as_bytes_with_nul();                                                                                                               
    libc::iovec {                                                                                                                                          
        iov_base: bytes.as_ptr() as *mut libc::c_void,                                                                                                     
        iov_len: name.len() + 1,                                                                                                                           
    }                                                                                                                                                      
}   

I then create an array of libc::iovec objects:

let fstype_option_name = "fstype";                                                                                                                     
let fstype_option_value = "ufs";                                                                                                                       
                                                                                                                                                           
let fspath_option_name = "fspath";                                                                                                                     
let fspath_option_value = "/mnt";                                                                                                                      
                                                                                                                                                           
let from_option_name  = "from";                                                                                                                        
let from_option_value = "/dev/da7p2";                                                                                                                  
                                                                                                                                                           
let mount_options_ptr: [libc::iovec; 6] = [                                                                                                            
    iovec_from_string(&fstype_option_name),                                                                                                            
    iovec_from_string(&fstype_option_value),                                                                                                           
    iovec_from_string(&fspath_option_name),                                                                                                            
    iovec_from_string(&fspath_option_value),                                                                                                           
    iovec_from_string(&from_option_name),                                                                                                              
    iovec_from_string(&from_option_value),                                                                                                             
];                                                                                                                                                     
                                                                                                                                                           
fn print_iovec(iov: &libc::iovec) {                                                                                                                    
    println!("{} bytes", iov.iov_len);                                                                                                                 
    println!("{:X} addr", iov.iov_base as u64);                                                                                                        
}

When I run this, the output indicates it's using the same block of member for each instance of the libc::iovec, even though I had passed a different string into each call of the iovec_from_string function. Except for the last iovec instance, which points to a different piece of memory.

processing fstype
processing ufs
processing fspath
processing /mnt
processing from
processing /dev/da7p2
7 bytes
8014F8030 addr
4 bytes
8014F8030 addr
7 bytes
8014F8030 addr
5 bytes
8014F8030 addr
5 bytes
8014F8030 addr
11 bytes
80153E000 addr

I wrote a dummy library to stand in for nmount and dump the values the library saw when called from Rust. Sure enough, it's seeing the same memory contents and leading nulls have been inserted.

Calling mount
	6 iovec structs
		           7
			 00 72 6f 6d 00 68 00
			  .  r  o  m  .  h  .
		           4
			 00 72 6f 6d
			  .  r  o  m
		           7
			 00 72 6f 6d 00 68 00
			  .  r  o  m  .  h  .
		           5
			 00 72 6f 6d 00
			  .  r  o  m  .
		           5
			 00 72 6f 6d 00
			  .  r  o  m  .
		           11
			 00 64 65 76 2f 64 61 37 70 32 00
			  .  d  e  v  /  d  a  7  p  2  .
	Flag: 0

I'm sorry I don't have time to give an answer in full detail, but as_bytes_with_nul doesn't consume the CString. You're creating a raw pointer to the backing store of the CString and then dropping the CString and deallocating its backing store.

You probably want into_bytes_with_nul or into_raw.

2 Likes

This is expected behavior. When you use unsafe to use a raw pointer you assume responsibility for ensuring that it's valid whenever dereferenced. None of these pointers are valid to dereference after iovec_from_string returns.

Consider this (non-compiling) function:

fn ref_from_string(name: &str) -> &str {
    let inner = name.to_owned();
    &*inner
}

ref_from_string doesn't compile because it attempts to return a reference to a local variable.

fn ptr_from_string(name: &str) -> *const str {
    let inner = name.to_owned();
    &*inner
}

ptr_from_string does compile, because raw pointers don't have lifetimes for the borrow checker to check. But it's still UB to dereference the return value because it points to memory that has been freed (when inner was dropped at the end of the function).

You're trying to write the equivalent of ptr_from_string which is why almost all the pointers are the same. The one that's different, I'm just guessing here but probably it gets placed in a different section of memory because it's larger than the others. They're all still UB to dereference.

Awsome! I assumed the raw pointers were also being checked. That explains what I'm missing. Thanks for the answer.