Performing I/O on ```BorrowedFd``` or ```BorrowedHandle``` objects with ```ManuallyDrop<std::fs::File>```

Hello everyone!

The OS-specific part of the Rust standard library contains types to contain a kernel object handle to work on raw OS resources and close them on drop (OwnedHandle and OwnedSocket for Windows and OwnedFd for Unix-like systems). These types have "borrowed" counterparts, which do not close the resource on drop and are lifetime-tied to the "resource-owning" object (BorrowedHandle and BorrowedSocket for Windows and BorrowedFd for Linux). I wonder, is it safe to perform I/O on BorrowedFd or BorrowedHandle object by first converting it to RawFd or RawHandle, then wrapping it in a ManuallyDrop<OwnedFd> or ManuallyDrop<OwnedHandle> value, and then converting it into ManuallyDrop<std::fs::File> object? Or is it better to use the OS API through FFI directly? As far as I understand, the problem with putting a borrowed handle into a std::fs::File is that it may close the file handle that is still in use, but ManuallyDrop prevents it.

if I understand it correctly, you have a non-owning os handle, and you want to avoid low level apis (such as libc) and instead use high level apis of rust's standard library to perform io?

no, it's not safe.

you can only wrap a raw os file handle into a File (or OwnedFd) object using FromRawFd (or FromRawHandle on windows). the function FromRawFd::from_raw_fd() is unsafe, and it requires the raw handle to be owned. wrapping the result object into ManuallyDrop does not change the safety requirement.

the problem is not just to prevent closing the file. the type File assumes exclusive ownership of the underlying file descriptor, so that it cannot be invalidated accidentally. see the IO safety section in the standard library:


so what to do then?

the low level api in libc is good enough for simple cases if you only need a handful of read() and/or write().

up a level, some libraries like rustix and nix wrap libc with safe apis, which are also a bit more "rusty" [1], while still stay faithful to the low level api.

if you want the full experience of high level abstraction of the rust standard library, you can always dup() the file descriptor, or use BorrowedFd::try_clone_into_owned() if you already wrapped the raw fd into a BorrowedFd. (note the difference between a file descriptor and the open file description kernel resource: duplicated descriptors share the same open file description, such as seek position, dirty flag, block cache, etc)

in practice, even if you do wrap a non-owning file descriptor into a File object, I think it's probably gonna be fine if you are careful [2], but strictly speaking, it's still library UB (not language UB though), I wouldn't avoid it.


  1. e.g. using rust slices for the buffer instead of raw pointers, returning a Result<T, Errno> instead of a plain c_int where a negative value signals an error, etc ↩︎

  2. e.g. using a phantom data to link the lifetime of the BorrowedFd ↩︎

There's a WIP change so you can do:

let mut f: &File = borrowed_fd.as_ref();
writeln!(f, "foo bar")?;