-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File implementation on Windows has unsound methods #81357
Comments
I think this is one of those cases where breaking backwards compatibility after fixing mio to use |
I'm not so sure. I think there are reasonable Rust programs, perhaps yet to be written, that will necessarily want to accept file handles from untrusted sources, possibly controlled by an attacker, via some IPC mechanism. A reasonable thing for such a program to do would be to take the handle and create a The safety requirements for |
Thanks for the detailed report @jstarks! We discussed this in our recent Libs meeting and after a quick survey found that passing We felt the best path forward would be effectively option 2, which is also what you're recommending:
We were also comfortable using |
Assigning |
I just wanted to make folks in the windows notification group aware of this, it seems like everything is in hand: @rustbot ping windows |
Hey Windows Group! This bug has been identified as a good "Windows candidate". cc @arlosi @danielframpton @gdr-at-ms @kennykerr @luqmana @lzybkr @nico-abram @retep998 @rylev @sivadeilra |
@rustbot assign @sivadeilra |
@apiraino You removed |
…oshtriplett Fix unsound `File` methods This is a draft attempt to fix rust-lang#81357. *EDIT*: this PR now tackles `read()`, `write()`, `read_at()`, `write_at()` and `read_buf`. Still needs more testing though. cc `@jstarks,` can you confirm the the Windows team is ok with the Rust stdlib using `NtReadFile` and `NtWriteFile`? ~Also, I'm provisionally using `CancelIo` in a last ditch attempt to recover but I'm not sure that this is actually a good idea. Especially as getting into this state would be a programmer error so aborting the process is justified in any case.~ *EDIT*: removed, see comments.
File
'sread
,write
,seek_read
, andseek_write
all have soundness issues in Windows under some conditions. In the below, I describe the problem and possible fixes forseek_read
andread
; the same problems and solutions apply toseek_write
andwrite
.seek_read
Using
std::os::windows::fs::OpenOptionsExt::custom_flags
, we can open aFile
withFILE_FLAG_OVERLAPPED
, which is a Win32 flag that indicates that IO should be performed asynchronously usingOVERLAPPED
structures to track the state of outstanding IO.We can then use
std::os::windows::fs::FileExt::seek_read
to initiate an IO. Internally, this usesReadFile
and specifies an OVERLAPPED structure on the stack in order to specify the offset to read from. If the IO does not completely synchronously in the kernel, this call will return the errorERROR_IO_PENDING
, indicating that the buffer and theOVERLAPPED
structure are still referenced by the kernel. The kernel can write to these buffers at any time, butseek_read
has already returned so the lifetime of both objects has expired.This example shows how
read
This can also be achieved with
read
, too, but it is harder to set up.read
usesReadFile
without passing anOVERLAPPED
structure, soReadFile
allocates one on the stack and handlesERROR_IO_PENDING
internally by waiting on the file object for an IO to complete. However,ReadFile
can still return with the IO in flight if there are multiple reads outstanding concurrently, since the IO completion notification will not necessarily wake up the right caller. Worse, this condition is not detectable--ReadFile
returns a successful completion in this case (though with zero bytes written).To demonstrate this, we need some unsafe code to create a named pipe server so that we can control IO completion timing. But this could be in a separate process or via an otherwise safe named pipe wrapper, so this does not mitigate the soundness issue in
read
itself. If you ignore this aspect, the result is more dramatic since we can demonstrate the unsound behavior in the form of a buffer magically changing contents across a sleep:Possible fixes
seek_read
There are multiple ways to fix
seek_read
. The options I can think of: ifReadFile
fails withERROR_IO_PENDING
:GetOverlappedResult()
in a loop until it succeeds or fails with something other thanERROR_IO_INCOMPLETE
, or;GetOverlappedResult()
once. If this fails withERROR_IO_INCOMPLETE
, callabort()
, or;abort()
.My recommendation is to implement option 2. This is what
ReadFile
does if you don't pass anOVERLAPPED
structure and so is whatread
does today (except ReadFile doesn't abort, it just returns--more about that below). Option 1 sounds appealing but it can easily lead to deadlocks due to some peculiarities about how IO completion signaling works in Windows. Fixing this would rely on Rust allocating a separate Windows event object for each thread issuing IO--I'm not certain this is worth the overhead.We also need to fix
read
. Since it callsReadFile
without anOVERLAPPED
structure, it can hit this same condition but with the kernel-referencedOVERLAPPED
structure (actually anIO_STATUS_BLOCK
at this layer of the stack) already popped of the stack. And sinceReadFile
returns success in this case, there's no way to reliably detect this and abort after the fact. You could argue this is a bug in Windows, but it's a bit late to fix it, especially in all the in-market releases. CallingReadFile
without anOVERLAPPED
structure is not really safe unless you carefully control the handle's source.To fix it, it's tempting to switch to allocating the
OVERLAPPED
structure on the stack and passing it toReadFile
, as is done inseek_read
. Unfortunately, this is not possible to do with perfect fidelity. When you pass anOVERLAPPED
structure, you have to provide an offset, but forread
we want the current file offset. When a file is opened withoutFILE_FLAG_OVERLAPPED
, -2 means to write at the current file offset, so we could pass that. But when the file is opened withFILE_FLAG_OVERLAPPED
, there is no current file position (in this caseread
only makes sense for streams, such as named pipes, where the offset is meaningless). In this case, -2 is considered an invalid value by the kernel, so we would probably want to pass 0 for most cases. Since we don't know which case we're in, we're stuck.But ReadFile doesn't know what case it's in, either, so how does it solve this problem? Simple: it calls the lower-level system call
NtReadFile
, which allows NULL to be passed for the offset. This has exactly the behavior we want.read
should be updated to useNtReadFile
directly.As an alternative to all this,
OpenOptionsExt::custom_flags
could strip or otherwise prohibit the use ofFILE_FLAG_OVERLAPPED
, but I suspect this would be a breaking change. This interface is probably used in various places to open async file handles for other uses (viainto_raw_handle()
). I'm pretty sure I've written such code. And of course using (unsafe)from_raw_handle
it's still possible to get aFile
whose underlying file object has this flag set, so there's still a trap set for unsuspecting devs.Sorry this is such a mess. The Windows IO model has some rough edges. We (the Windows OS team) have tried to smooth some of them out over the releases, but doing so while maintaining app compat has proven challenging.
Meta
cargo +nightly --version --verbose
:This issue has been assigned to @sivadeilra via this comment.
The text was updated successfully, but these errors were encountered: