Stream: general

Topic: from_raw_fd unsafety


Jake Goulding (Jun 12 2019 at 18:49, on Zulip):

FromRawFd::from_raw_fd

This function is also unsafe as the primitives currently returned have the contract that they are the sole owner of the file descriptor they are wrapping. Usage of this function could accidentally allow violating this contract which can cause memory unsafety in code that relies on it being true.

I'm having trouble constructing an example where misuse of this function can create memory unsafety — any ideas?

RalfJ (Jun 12 2019 at 19:06, on Zulip):

I could imagine a library that has internal invariants on some file-descriptor-accessible data

RalfJ (Jun 12 2019 at 19:06, on Zulip):

and that library could rely on those invariants for safety

RalfJ (Jun 12 2019 at 19:06, on Zulip):

at that point, messing with that FD can cause unsafety

Jake Goulding (Jun 12 2019 at 19:12, on Zulip):

I think that's a hint too abstract for me to grok; what would "internal invariants on some file-descriptor-accessible data" look like?

simulacrum (Jun 12 2019 at 19:13, on Zulip):

presumably something like sqlite keeping a file open and validating it once that it's "correct" and after that transmuting from file bytes to structs and such

simulacrum (Jun 12 2019 at 19:13, on Zulip):

(this is very abstract and maybe not realistic but something like that is the best I can come up with quickly)

simulacrum (Jun 12 2019 at 19:14, on Zulip):

presumably this might be a file created via some OS-temporary file abstraction where the "path" to it doesn't exist, too, so the invariant can only be bypassed via synthesis of a file descriptor

Jake Goulding (Jun 12 2019 at 19:19, on Zulip):

transmuting from file bytes to structs

Hmm, but a file descriptor itself doesn't have any of this; so would this imply using something like mmap on the file descriptor, adding an extra layer in the middle?

simulacrum (Jun 12 2019 at 19:30, on Zulip):

sure, or just read_to_end and transmute

simulacrum (Jun 12 2019 at 19:30, on Zulip):

on a File created from the raw fd

RalfJ (Jun 12 2019 at 19:55, on Zulip):

yeah, private mmap'ed memory would be one thing. I once contorted this example for another case:
https://github.com/rust-lang/rust/issues/39575#issuecomment-437658766

RalfJ (Jun 12 2019 at 19:56, on Zulip):

pipes are another example of a private resource that can be "de-privatized" with from_raw_fd

Jake Goulding (Jun 13 2019 at 15:18, on Zulip):

So, I can see how your break encapsulation and visibility with it, but I can't see the unsafety. "stealing" someones File by picking the same fd and then calling read_to_end on it will make it so that there's no data left in the file, but... so what?

Jake Goulding (Jun 13 2019 at 15:19, on Zulip):

(gonna read Ralf's code example now)

Jake Goulding (Jun 13 2019 at 15:34, on Zulip):

So, that example creates an anonymous pipe and relies on the fact that no one but the creator of the pipe can write to it. It then says that some unsafe behavior can be predicated on the result of reading from the pipe. If someone else gets the ability to write to the pipe, then the encapsulation of the unsafety is broken and the code might cause UB.

Jake Goulding (Jun 13 2019 at 15:39, on Zulip):

However, that's already trivially broken, AFAIK, because I can just write to /proc/<rust pid>/fd/<whatever pipe is>

Jake Goulding (Jun 13 2019 at 15:39, on Zulip):

The encapsulation was false to start with (similar to how mmap cannot be contained inside of a Rust program)

rkruppe (Jun 13 2019 at 15:44, on Zulip):

If you involve /proc/<pid> then you can just as well write to /proc/<pid>/mem and all memory safety guarantees go out the window anyway

Jake Goulding (Jun 13 2019 at 15:48, on Zulip):

hmm.

Jake Goulding (Jun 13 2019 at 15:48, on Zulip):

What's the difference between that and mmap then?

simulacrum (Jun 13 2019 at 15:56, on Zulip):

I think the claim is that (at least in theory) here you can have complete control (other than proc) over the file descriptor vs. mmap'ing a file where it's "public"

RalfJ (Jun 13 2019 at 15:57, on Zulip):

So, I can see how your break encapsulation and visibility with it, but I can't see the unsafety. "stealing" someones File by picking the same fd and then calling read_to_end on it will make it so that there's no data left in the file, but... so what?

Rust's safety entirely relies on proper enforcment of encapsulation and visibility! Just imagine Vec::len would be public...

RalfJ (Jun 13 2019 at 15:59, on Zulip):

hm good point about /proc/<pid>/fd, I had forgotten about that. Though @rkruppe's point also stands.

RalfJ (Jun 13 2019 at 16:00, on Zulip):

@Jake Goulding do you mean mmap of a file on the file system? Good question. I guess one difference is whether the "attacker" is in the same process or not.

RalfJ (Jun 13 2019 at 16:00, on Zulip):

But it's a fuzzy distinction indeed.

simulacrum (Jun 13 2019 at 16:04, on Zulip):

I think the loose conclusion right now that is that mmap is inherently UB pretty much if anyone touches the file other than the Rust mmap'd process (or you always use atomic single-byte raw ptr reads maybe? not sure)

RalfJ (Jun 13 2019 at 16:05, on Zulip):

no you are fine if you consider the mmap'd file as &[AtomicU8] I think

RalfJ (Jun 13 2019 at 16:05, on Zulip):

(and larger-sized atomic types also work)

nagisa (Jun 13 2019 at 16:14, on Zulip):

@Jake Goulding a trivial example of unsafety is in from_raw_fd(file.as_raw_fd()) – which then (if you forgot to forgot one of the Files) ends up being a double free of a resource.

nagisa (Jun 13 2019 at 16:14, on Zulip):

@Jake Goulding now imagine if file was actually a wrapper over a memory map + a backing memfd file.

nagisa (Jun 13 2019 at 16:15, on Zulip):

your memory map would then get released and you would begin accessing freed memory from your wrapper which still thinks that the file is "open" and the memory map is valid.

nagisa (Jun 13 2019 at 16:15, on Zulip):

So in code:

nagisa (Jun 13 2019 at 16:18, on Zulip):
struct MemfdWrapper {
    pub file: File,
    pub map: *mut u8,
}
let memory: MemfdWrapper;
{
     let arbitrary_file = File::from_raw_fd(memory.file.as_raw_fd());
} // The backing OS resource for `memory.file` is `close`d, and by extension the memory map which is backed by this file is also (implicitly) released.

// MemfdWrapper may be exposing safe API here to access contents of `map` with an assumption that `map` is always valid if `file` resource is held alive.
Jake Goulding (Jun 13 2019 at 19:20, on Zulip):

@nagisa I think a key thing I was forgetting that I see from your example is the point that these are traits, which means anything can implement them.

Jake Goulding (Jun 13 2019 at 19:21, on Zulip):

It may be entirely impossible to get unsafety from File::{into_raw_fd,from_raw_fd}, but that doesn't mean that the trait's method is always safe

simulacrum (Jun 13 2019 at 19:55, on Zulip):

Presuming nothing else exists, perhaps, but because file descriptors are a global resource I could see that being not quite true

Last update: Nov 22 2019 at 00:40UTC