Wrong mapping of C struct to Rust

2019-02-25 02:15发布

问题:

For educational purpose I try to access the FILE struct in Rust:

unsafe {
    let passwd = libc::fopen("/etc/passwd".to_ptr(), &('r' as libc::c_char));
    let fp = &mut *(passwd as *mut MY_FILE);
    println!("flags={}, file={}", fp._flags, fp._file);
}

the MY_FILE struct I obtained by running bindgen on stdio.h (I'm on OS X):

bindgen /usr/include/stdio.h

Somehow _flags is always 8 for files open in write mode (4 in read mode), so this flags seems off (I tested with a C code to verify that it indeed is not 4 or 8). The file pointer however seems to be right. What could cause this? Am I extracting the binding from the wrong header file? Is there something I need to add to the #[repr(C,)] attribute?

Here is the full code including the struct.

This is a follow up question from an earlier question

回答1:

First, your implementation of ToPtr invites unsound code. Reproduced here:

// code in italics is wrong
impl ToPtr for str {
    fn to_ptr(&self) -> *const i8 {
        CString::new(self).unwrap().as_ptr()
    }
}

This allocates a new CString and returns a pointer to its contents, but the CString is dropped when to_ptr returns, so this is a dangling pointer. Any dereference of this pointer is undefined behavior. The documentation has a big warning about this, but it's still a very common mistake.

One correct way to make a *const c_char from a string literal is b"string here\0".as_ptr() as *const c_char. The string is null terminated and there is no dangling pointer because string literals live for the entire program. If you have a non-constant string to be converted, you must keep the CString alive while it is being used, like this:

let s = "foo";
let cs = CString::new(s).unwrap(); // don't call .as_ptr(), so the CString stays alive
unsafe { some_c_function(cs.as_ptr()); }
// CString is dropped here, after we're done with it

Aside: an editor "suggested" (I'm new to Stack Overflow, but it seems more polite to comment rather than try to rewrite my answer) that the above code could be written like this:

let s = "foo";
unsafe {
    // due to temporary drop rules, the CString will be dropped at the end of the statement (the `;`)
    some_c_function(CString::new(s).unwrap().as_ptr());
}

While this is technically correct (the best kind of correct), the "temporary drop rules" involved are subtle -- this works because as_ptr takes a reference to the CString (actually a &CStr, because the compiler changes the method chain to CString::new(s).unwrap().deref().as_ptr()) instead of consuming it, and because we have only one C function to call. I don't like to rely on anything that's subtle or non-obvious when writing unsafe code.


With that out of the way, I fixed that unsoundness in your code (your calls all use string literals so I just used my first strategy above). I get this output on OSX:

0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 2, 0, 0, 0, 0, 0, 0, 0
0, 0, 4, 3, 0, 0, 0, 0, 0, 0, 0

So, this matches your results, right? I also wrote the following C program:

#include <stdio.h>
#include <unistd.h>

int main() {
    struct __sFILE *fp1 = fdopen(STDIN_FILENO, "r");
    struct __sFILE *fp2 = fdopen(STDOUT_FILENO, "w");
    struct __sFILE *fp3 = fdopen(STDERR_FILENO, "w");
    struct __sFILE *passwd = fopen("/etc/passwd", "r");

    printf("%i %i %i %i\n", fp1->_flags, fp2->_flags, fp3->_flags, passwd->_flags);
}

And got the output:

4 8 8 4

Which seems to vindicate the Rust results. There is a comment at the top of /usr/include/stdio.h that says:

/*
 * The following always hold:
 *
 *  if (_flags&(__SLBF|__SWR)) == (__SLBF|__SWR),
 *      _lbfsize is -_bf._size, else _lbfsize is 0
 *  if _flags&__SRD, _w is 0
 *  if _flags&__SWR, _r is 0
 */

And looking up those constants:

#define __SLBF  0x0001      /* line buffered */
#define __SRD   0x0004      /* OK to read */
#define __SWR   0x0008      /* OK to write */

This seems to match the output we get: 4 for file opened in read mode, 8 for write. So what is the problem here?



标签: rust stdio libc