Skip to content

Commit d412b59

Browse files
committed
fix a bug where cloned pipes were inheritable on Windows
There is a bug (I think it's a bug?) in libstd which makes File::try_clone return inheritable handles. We need to work around that by making the same system calls explictly, but with the inheritable flag set to false. I've filed a fix against libstd (rust-lang/rust#65316), but even if that lands we'll need to retain this workaround for all the past versions of Rust that have the bug.
1 parent 3b187a3 commit d412b59

File tree

3 files changed

+50
-5
lines changed

3 files changed

+50
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ edition = "2018"
1313
libc = "0.2.62"
1414

1515
[target.'cfg(windows)'.dependencies]
16-
winapi = { version = "0.3.5", features = ["namedpipeapi"] }
16+
winapi = { version = "0.3.5", features = ["handleapi", "namedpipeapi", "processthreadsapi", "winnt"] }

src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ mod tests {
340340
let (input_reader, mut input_writer) = crate::pipe().unwrap();
341341
let (mut output_reader, output_writer) = crate::pipe().unwrap();
342342

343+
// Create a bunch of duplicated copies, which we'll close later. This
344+
// tests that duplication preserves non-inheritability.
345+
let ir_dup = input_reader.try_clone().unwrap();
346+
let iw_dup = input_writer.try_clone().unwrap();
347+
let or_dup = output_reader.try_clone().unwrap();
348+
let ow_dup = output_writer.try_clone().unwrap();
349+
343350
// Spawn the child. Note that this temporary Command object takes
344351
// ownership of our copies of the child's stdin and stdout, and then
345352
// closes them immediately when it drops. That stops us from blocking
@@ -351,6 +358,12 @@ mod tests {
351358
.spawn()
352359
.unwrap();
353360

361+
// Drop all the dups now that the child is spawned.
362+
drop(ir_dup);
363+
drop(iw_dup);
364+
drop(or_dup);
365+
drop(ow_dup);
366+
354367
// Write to the child's stdin. This is a small write, so it shouldn't
355368
// block.
356369
input_writer.write_all(b"hello").unwrap();

src/windows.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ use crate::PipeReader;
22
use crate::PipeWriter;
33
use std::fs::File;
44
use std::io;
5-
use std::mem::ManuallyDrop;
65
use std::os::windows::prelude::*;
76
use std::ptr;
7+
use winapi::shared::minwindef::BOOL;
88
use winapi::shared::ntdef::{HANDLE, PHANDLE};
9+
use winapi::um::handleapi::DuplicateHandle;
910
use winapi::um::namedpipeapi;
11+
use winapi::um::processthreadsapi::GetCurrentProcess;
12+
use winapi::um::winnt::DUPLICATE_SAME_ACCESS;
1013

1114
pub(crate) fn pipe() -> io::Result<(PipeReader, PipeWriter)> {
1215
let mut read_pipe: HANDLE = ptr::null_mut();
@@ -36,9 +39,38 @@ pub(crate) fn pipe() -> io::Result<(PipeReader, PipeWriter)> {
3639
}
3740

3841
pub(crate) fn dup<T: AsRawHandle>(wrapper: T) -> io::Result<File> {
39-
let handle = wrapper.as_raw_handle();
40-
let temp_file = ManuallyDrop::new(unsafe { File::from_raw_handle(handle) });
41-
temp_file.try_clone()
42+
// We rely on ("abuse") std::fs::File for a lot of descriptor/handle
43+
// operations. (For example, setting F_DUPFD_CLOEXEC on Unix is a
44+
// compatibility mess.) However, in the particular case of try_clone on
45+
// Windows, the standard library has a bug where duplicated handles end up
46+
// inheritable when they shouldn't be. See
47+
// https://github.com/rust-lang/rust/pull/65316. This leads to races where
48+
// child processes can inherit each other's handles, which tends to cause
49+
// deadlocks when the handle in question is a stdout pipe. To get that
50+
// right, we explicitly make the necessary system calls here, just like
51+
// libstd apart from that one flag.
52+
let source_handle = wrapper.as_raw_handle() as HANDLE;
53+
let desired_access = 0; // Ignored because of DUPLICATE_SAME_ACCESS.
54+
let inherit_handle = false as BOOL; // <-- Libstd sets this to true!
55+
let options = DUPLICATE_SAME_ACCESS;
56+
let mut duplicated_handle = 0 as HANDLE;
57+
let ret = unsafe {
58+
let current_process = GetCurrentProcess();
59+
DuplicateHandle(
60+
current_process,
61+
source_handle,
62+
current_process,
63+
&mut duplicated_handle,
64+
desired_access,
65+
inherit_handle,
66+
options,
67+
)
68+
};
69+
if ret == 0 {
70+
Err(io::Error::last_os_error())
71+
} else {
72+
unsafe { Ok(File::from_raw_handle(duplicated_handle as RawHandle)) }
73+
}
4274
}
4375

4476
impl IntoRawHandle for PipeReader {

0 commit comments

Comments
 (0)