deno/ext/io/winpipe.rs
Matt Mastracci 7e4ee02e2e
fix(ext/io): Fix NUL termination error in windows named pipes (#23379)
Due to a terminating NUL that was placed in a `r#` string, we were not
actually NUL-terminating pipe names on Windows. While this has no
security implications due to the random nature of the prefix, it would
occasionally cause random failures when the trailing garbage would make
the pipe name invalid.
2024-04-15 14:10:09 -06:00

177 lines
5.8 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use rand::thread_rng;
use rand::RngCore;
use std::io;
use std::os::windows::io::RawHandle;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use winapi::shared::minwindef::DWORD;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::fileapi::CreateFileA;
use winapi::um::fileapi::OPEN_EXISTING;
use winapi::um::handleapi::CloseHandle;
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::minwinbase::SECURITY_ATTRIBUTES;
use winapi::um::winbase::CreateNamedPipeA;
use winapi::um::winbase::FILE_FLAG_FIRST_PIPE_INSTANCE;
use winapi::um::winbase::FILE_FLAG_OVERLAPPED;
use winapi::um::winbase::PIPE_ACCESS_DUPLEX;
use winapi::um::winbase::PIPE_READMODE_BYTE;
use winapi::um::winbase::PIPE_TYPE_BYTE;
use winapi::um::winnt::GENERIC_READ;
use winapi::um::winnt::GENERIC_WRITE;
/// Create a pair of file descriptors for a named pipe with non-inheritable handles. We cannot use
/// the anonymous pipe from `os_pipe` because that does not support OVERLAPPED (aka async) I/O.
///
/// This is the same way that Rust and pretty much everyone else does it.
///
/// For more information, there is an interesting S.O. question that explains the history, as
/// well as offering a complex NTAPI solution if we decide to try to make these pipes truely
/// anonymous: https://stackoverflow.com/questions/60645/overlapped-i-o-on-anonymous-pipe
pub fn create_named_pipe() -> io::Result<(RawHandle, RawHandle)> {
create_named_pipe_inner()
}
fn create_named_pipe_inner() -> io::Result<(RawHandle, RawHandle)> {
static NEXT_ID: AtomicU32 = AtomicU32::new(0);
// Create an extremely-likely-unique pipe name from randomness, identity and a serial counter.
let pipe_name = format!(
concat!(r#"\\.\pipe\deno_pipe_{:x}.{:x}.{:x}"#, "\0"),
thread_rng().next_u64(),
std::process::id(),
NEXT_ID.fetch_add(1, Ordering::SeqCst),
);
// Create security attributes to make the pipe handles non-inheritable
let mut security_attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as DWORD,
lpSecurityDescriptor: std::ptr::null_mut(),
bInheritHandle: 0,
};
// SAFETY: Create the pipe server with non-inheritable handle
let server_handle = unsafe {
CreateNamedPipeA(
pipe_name.as_ptr() as *const i8,
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE,
// Read and write bytes, not messages
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
// The maximum number of instances that can be created for this pipe.
1,
// 4kB buffer sizes
4096,
4096,
// "The default time-out value, in milliseconds, if the WaitNamedPipe function specifies NMPWAIT_USE_DEFAULT_WAIT.
// Each instance of a named pipe must specify the same value. A value of zero will result in a default time-out of
// 50 milliseconds."
0,
&mut security_attributes,
)
};
if server_handle == INVALID_HANDLE_VALUE {
// This should not happen, so we would like to get some better diagnostics here.
// SAFETY: Printing last error for diagnostics
unsafe {
eprintln!(
"*** Unexpected server pipe failure '{pipe_name:?}': {:x}",
GetLastError()
);
}
return Err(io::Error::last_os_error());
}
// SAFETY: Create the pipe client with non-inheritable handle
let client_handle = unsafe {
CreateFileA(
pipe_name.as_ptr() as *const i8,
GENERIC_READ | GENERIC_WRITE,
0,
&mut security_attributes,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
std::ptr::null_mut(),
)
};
if client_handle == INVALID_HANDLE_VALUE {
// SAFETY: Getting last error for diagnostics
let error = unsafe { GetLastError() };
// This should not happen, so we would like to get some better diagnostics here.
eprintln!(
"*** Unexpected client pipe failure '{pipe_name:?}': {:x}",
error
);
let err = io::Error::last_os_error();
// SAFETY: Close the handles if we failed
unsafe {
CloseHandle(server_handle);
}
return Err(err);
}
Ok((server_handle, client_handle))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Read;
use std::io::Write;
use std::os::windows::io::FromRawHandle;
use std::sync::Arc;
use std::sync::Barrier;
#[test]
fn make_named_pipe() {
let (server, client) = create_named_pipe().unwrap();
// SAFETY: For testing
let mut server = unsafe { File::from_raw_handle(server) };
// SAFETY: For testing
let mut client = unsafe { File::from_raw_handle(client) };
// Write to the server and read from the client
server.write_all(b"hello").unwrap();
let mut buf: [u8; 5] = Default::default();
client.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello");
}
#[test]
fn make_many_named_pipes_serial() {
let mut handles = vec![];
for _ in 0..100 {
let (server, client) = create_named_pipe().unwrap();
// SAFETY: For testing
let server = unsafe { File::from_raw_handle(server) };
// SAFETY: For testing
let client = unsafe { File::from_raw_handle(client) };
handles.push((server, client))
}
}
#[test]
fn make_many_named_pipes_parallel() {
let mut handles = vec![];
let barrier = Arc::new(Barrier::new(50));
for _ in 0..50 {
let barrier = barrier.clone();
handles.push(std::thread::spawn(move || {
barrier.wait();
let (server, client) = create_named_pipe().unwrap();
// SAFETY: For testing
let server = unsafe { File::from_raw_handle(server) };
// SAFETY: For testing
let client = unsafe { File::from_raw_handle(client) };
std::thread::sleep(std::time::Duration::from_millis(100));
drop((server, client));
}));
}
for handle in handles.drain(..) {
handle.join().unwrap();
}
}
}