use super::{Cached, LaunchNonce, LaunchNonceError, LaunchNonceSource};
use std::{
ffi::OsStr,
io,
os::windows::{
ffi::OsStrExt,
io::{AsRawHandle, FromRawHandle, OwnedHandle},
},
ptr::{null, null_mut},
sync::{
atomic::{AtomicBool, Ordering},
Arc, OnceLock,
},
time::{Duration, Instant},
};
use windows_sys::Win32::{
Foundation::{
LocalFree, ERROR_ACCESS_DENIED, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_PIPE_BUSY,
ERROR_PIPE_CONNECTED, ERROR_PIPE_NOT_CONNECTED, ERROR_SEM_TIMEOUT, GENERIC_READ, HANDLE,
INVALID_HANDLE_VALUE, WAIT_OBJECT_0, WAIT_TIMEOUT,
},
Security::Cryptography::{BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG},
Security::{
Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
},
GetTokenInformation, TokenUser, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
},
Storage::FileSystem::{
CreateFileW, FlushFileBuffers, ReadFile, WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE,
FILE_FLAG_OVERLAPPED, OPEN_EXISTING, PIPE_ACCESS_OUTBOUND, SECURITY_IDENTIFICATION,
SECURITY_SQOS_PRESENT,
},
System::{
Pipes::{
ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, GetNamedPipeClientProcessId,
PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT,
},
Threading::{
CreateEventW, GetCurrentProcess, OpenProcess, OpenProcessToken, WaitForSingleObject,
PROCESS_SYNCHRONIZE,
},
IO::{CancelIoEx, CancelSynchronousIo, GetOverlappedResult, OVERLAPPED},
},
};
const OPEN_WINDOW: Duration = Duration::from_millis(500);
const READ_WINDOW: Duration = Duration::from_secs(2);
const POLL: Duration = Duration::from_millis(5);
const MAX_NONCE_BYTES: usize = 4096;
const CLIENT_ACCESS: u32 = 0x80100100;
fn wide(text: &OsStr) -> Vec<u16> {
text.encode_wide().chain(Some(0)).collect()
}
fn owned(handle: HANDLE) -> io::Result<OwnedHandle> {
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
Err(io::Error::last_os_error())
} else {
Ok(unsafe { OwnedHandle::from_raw_handle(handle) })
}
}
pub struct LaunchNoncePipeHandoff {
pipe: OwnedHandle,
name: String,
nonce: Vec<u8>,
}
impl LaunchNoncePipeHandoff {
pub fn new(nonce: &str) -> io::Result<Self> {
Self::with_names(nonce, random_name)
}
fn with_names(nonce: &str, mut next: impl FnMut() -> io::Result<String>) -> io::Result<Self> {
if nonce.is_empty() || nonce.len() > MAX_NONCE_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid launch nonce length",
));
}
let security = user_security_descriptor()?;
let attrs = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: security.0,
bInheritHandle: 0,
};
for _ in 0..16 {
let name = next()?;
let path = wide(OsStr::new(&name));
let result = owned(unsafe {
CreateNamedPipeW(
path.as_ptr(),
PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
1,
MAX_NONCE_BYTES as u32,
0,
0,
&attrs,
)
});
match result {
Ok(pipe) => {
return Ok(Self {
pipe,
name,
nonce: nonce.as_bytes().to_vec(),
})
}
Err(error) if matches!(error.raw_os_error(), Some(code) if code == ERROR_ACCESS_DENIED as i32 || code == ERROR_PIPE_BUSY as i32) => {
continue
}
Err(error) => return Err(error),
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"could not reserve a fresh launch nonce pipe",
))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn serve(self, pid: u32, deadline: Duration) -> io::Result<LaunchNoncePipeDelivery> {
let process = owned(unsafe { OpenProcess(PROCESS_SYNCHRONIZE, 0, pid) })?;
let consumed = Arc::new(OnceLock::new());
let cancelled = Arc::new(AtomicBool::new(false));
let delivery = LaunchNoncePipeDelivery {
consumed: consumed.clone(),
cancelled: cancelled.clone(),
};
let deadline = Instant::now() + deadline;
std::thread::Builder::new()
.name("launch-nonce-pipe".into())
.spawn(move || {
let _ = self.deliver(pid, &process, deadline, &cancelled, &consumed);
})?;
Ok(delivery)
}
fn deliver(
self,
pid: u32,
process: &OwnedHandle,
deadline: Instant,
cancelled: &AtomicBool,
consumed: &Arc<OnceLock<()>>,
) -> io::Result<()> {
let handle = self.pipe.as_raw_handle();
loop {
if Instant::now() >= deadline
|| cancelled.load(Ordering::Acquire)
|| unsafe { WaitForSingleObject(process.as_raw_handle(), 0) } == WAIT_OBJECT_0
{
return Err(timeout());
}
let mut operation = Operation::new(handle)?;
let connected = unsafe { ConnectNamedPipe(handle, &mut operation.overlapped) };
if connected == 0 {
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(code) if code == ERROR_PIPE_CONNECTED as i32 => {}
Some(code) if code == ERROR_IO_PENDING as i32 => {
operation.pending = true;
operation.wait(deadline, Some(process), Some(cancelled))?;
}
_ => return Err(error),
}
}
drop(operation);
let mut client_pid = 0;
let matches = unsafe { GetNamedPipeClientProcessId(handle, &mut client_pid) } != 0
&& client_pid == pid;
if !matches {
unsafe { DisconnectNamedPipe(handle) };
if Instant::now() >= deadline || cancelled.load(Ordering::Acquire) {
return Err(timeout());
}
continue;
}
let mut operation = Operation::new(handle)?;
let result = unsafe {
WriteFile(
handle,
self.nonce.as_ptr(),
self.nonce.len() as u32,
null_mut(),
&mut operation.overlapped,
)
};
if result == 0 {
if io::Error::last_os_error().raw_os_error() != Some(ERROR_IO_PENDING as i32) {
return Err(io::Error::last_os_error());
}
operation.pending = true;
}
let written = operation.wait(deadline, Some(process), Some(cancelled))?;
if written as usize != self.nonce.len() {
return Err(io::Error::other("short launch nonce pipe write"));
}
drop(operation);
return flush_delivery(self.pipe, process, deadline, cancelled, consumed.clone());
}
}
}
pub struct LaunchNoncePipeDelivery {
consumed: Arc<OnceLock<()>>,
cancelled: Arc<AtomicBool>,
}
impl LaunchNoncePipeDelivery {
pub fn consumed(&self) -> Arc<OnceLock<()>> {
self.consumed.clone()
}
}
impl Drop for LaunchNoncePipeDelivery {
fn drop(&mut self) {
self.cancelled.store(true, Ordering::Release);
}
}
fn flush_delivery(
pipe: OwnedHandle,
process: &OwnedHandle,
deadline: Instant,
cancelled: &AtomicBool,
consumed: Arc<OnceLock<()>>,
) -> io::Result<()> {
let flush = std::thread::Builder::new()
.name("launch-nonce-flush".into())
.spawn(move || {
let result = if unsafe { FlushFileBuffers(pipe.as_raw_handle()) } == 0 {
Err(io::Error::last_os_error())
} else {
let _ = consumed.set(());
Ok(())
};
unsafe { DisconnectNamedPipe(pipe.as_raw_handle()) };
result
})?;
let mut wait_error = None;
while !flush.is_finished() {
match unsafe { WaitForSingleObject(flush.as_raw_handle(), POLL.as_millis() as u32) } {
WAIT_OBJECT_0 => break,
WAIT_TIMEOUT => {}
_ => {
wait_error = Some(io::Error::last_os_error());
std::thread::sleep(POLL);
}
}
if wait_error.is_some()
|| Instant::now() >= deadline
|| cancelled.load(Ordering::Acquire)
|| unsafe { WaitForSingleObject(process.as_raw_handle(), 0) } == WAIT_OBJECT_0
{
unsafe { CancelSynchronousIo(flush.as_raw_handle()) };
}
}
let result = flush
.join()
.map_err(|_| io::Error::other("launch nonce flush thread panicked"))?;
match wait_error {
Some(error) => Err(error),
None => result,
}
}
fn random_name() -> io::Result<String> {
let mut bytes = [0u8; 16];
let status = unsafe {
BCryptGenRandom(
null_mut(),
bytes.as_mut_ptr(),
bytes.len() as u32,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
if status != 0 {
return Err(io::Error::other(
"Windows RNG could not generate a launch pipe name",
));
}
let hex: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
Ok(format!(r"\\.\pipe\subc-launch-{hex}"))
}
struct LocalAllocation(*mut std::ffi::c_void);
impl Drop for LocalAllocation {
fn drop(&mut self) {
unsafe { LocalFree(self.0) };
}
}
fn user_security_descriptor() -> io::Result<LocalAllocation> {
let mut token = null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let token = owned(token)?;
let mut needed = 0;
unsafe { GetTokenInformation(token.as_raw_handle(), TokenUser, null_mut(), 0, &mut needed) };
let mut buffer = vec![0usize; (needed as usize).div_ceil(size_of::<usize>())];
if unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenUser,
buffer.as_mut_ptr().cast(),
needed,
&mut needed,
)
} == 0
{
return Err(io::Error::last_os_error());
}
let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
let mut sid = null_mut();
if unsafe { ConvertSidToStringSidW(user.User.Sid, &mut sid) } == 0 {
return Err(io::Error::last_os_error());
}
let sid_allocation = LocalAllocation(sid.cast());
let mut length = 0;
while unsafe { *sid.add(length) } != 0 {
length += 1;
}
let sid_text = String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(sid, length) });
drop(sid_allocation);
let sddl = wide(OsStr::new(&format!(
"D:P(A;;0x{CLIENT_ACCESS:08x};;;{sid_text})"
)));
let mut descriptor = null_mut();
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
1,
&mut descriptor,
null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(LocalAllocation(descriptor))
}
struct Operation {
handle: HANDLE,
overlapped: OVERLAPPED,
_event: OwnedHandle,
pending: bool,
}
impl Operation {
fn new(handle: HANDLE) -> io::Result<Self> {
let event = owned(unsafe { CreateEventW(null(), 1, 0, null()) })?;
let overlapped = OVERLAPPED {
hEvent: event.as_raw_handle(),
..Default::default()
};
Ok(Self {
handle,
overlapped,
_event: event,
pending: false,
})
}
fn wait(
&mut self,
deadline: Instant,
process: Option<&OwnedHandle>,
cancelled: Option<&AtomicBool>,
) -> io::Result<u32> {
loop {
if Instant::now() >= deadline
|| cancelled.is_some_and(|flag| flag.load(Ordering::Acquire))
{
return Err(timeout());
}
if process.is_some_and(|process| unsafe { WaitForSingleObject(process.as_raw_handle(), 0) } == WAIT_OBJECT_0) {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "launch nonce child exited"));
}
match unsafe { WaitForSingleObject(self.overlapped.hEvent, POLL.as_millis() as u32) } {
WAIT_OBJECT_0 => {
self.pending = false;
let mut transferred = 0;
if unsafe {
GetOverlappedResult(self.handle, &self.overlapped, &mut transferred, 0)
} == 0
{
return Err(io::Error::last_os_error());
}
return Ok(transferred);
}
WAIT_TIMEOUT => {}
_ => return Err(io::Error::last_os_error()),
}
}
}
}
impl Drop for Operation {
fn drop(&mut self) {
if !self.pending {
return;
}
unsafe {
CancelIoEx(self.handle, &self.overlapped);
let mut transferred = 0;
GetOverlappedResult(self.handle, &self.overlapped, &mut transferred, 1);
}
}
}
fn timeout() -> io::Error {
io::Error::from_raw_os_error(ERROR_SEM_TIMEOUT as i32)
}
pub(super) fn read_pipe(name: &OsStr) -> Cached {
read_open_pipe(open_pipe(name)?)
}
fn open_pipe(name: &OsStr) -> Result<OwnedHandle, LaunchNonceError> {
let path = wide(name);
if path.len() <= 1 || path[..path.len() - 1].contains(&0) {
return Err(LaunchNonceError::PipeNotOpen { errno: 123 });
}
let open_deadline = Instant::now() + OPEN_WINDOW;
loop {
let result = owned(unsafe {
CreateFileW(
path.as_ptr(),
GENERIC_READ,
0,
null(),
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION,
null_mut(),
)
});
match result {
Ok(pipe) => return Ok(pipe),
Err(error)
if error.raw_os_error() == Some(ERROR_PIPE_BUSY as i32)
&& Instant::now() < open_deadline =>
{
std::thread::sleep(POLL)
}
Err(error) => {
return Err(LaunchNonceError::PipeNotOpen {
errno: error.raw_os_error().unwrap_or(0),
})
}
}
}
}
fn read_open_pipe(pipe: OwnedHandle) -> Cached {
let deadline = Instant::now() + READ_WINDOW;
let mut bytes = Vec::new();
loop {
let mut chunk = [0u8; 256];
let mut operation = Operation::new(pipe.as_raw_handle()).map_err(pipe_unreadable)?;
let result = unsafe {
ReadFile(
pipe.as_raw_handle(),
chunk.as_mut_ptr(),
chunk.len() as u32,
null_mut(),
&mut operation.overlapped,
)
};
if result == 0 {
let error = io::Error::last_os_error();
if pipe_eof(&error) {
break;
}
if error.raw_os_error() != Some(ERROR_IO_PENDING as i32) {
return Err(pipe_unreadable(error));
}
operation.pending = true;
}
let read = match operation.wait(deadline, None, None) {
Ok(count) => count,
Err(error) if pipe_eof(&error) => break,
Err(error) => return Err(pipe_unreadable(error)),
};
if read == 0 {
break;
}
bytes.extend_from_slice(&chunk[..read as usize]);
if bytes.len() > MAX_NONCE_BYTES {
return Err(LaunchNonceError::PipeUnreadable { errno: None });
}
}
if bytes.is_empty() {
return Err(LaunchNonceError::PipeEmpty);
}
let value = String::from_utf8(bytes).map_err(|_| LaunchNonceError::PipeNotUtf8)?;
Ok(Some(LaunchNonce {
value,
source: LaunchNonceSource::Pipe,
}))
}
fn pipe_eof(error: &io::Error) -> bool {
matches!(error.raw_os_error(), Some(code) if code == ERROR_BROKEN_PIPE as i32 || code == ERROR_PIPE_NOT_CONNECTED as i32)
}
fn pipe_unreadable(error: io::Error) -> LaunchNonceError {
LaunchNonceError::PipeUnreadable {
errno: error.raw_os_error(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::launch_nonce::{
launch_nonce, LaunchNonceCell, LAUNCH_NONCE_ENV, LAUNCH_NONCE_PIPE_ENV,
LAUNCH_NONCE_PIPE_FALLBACK_ENV,
};
use std::process::{Command, Stdio};
#[test]
fn windows_launch_pipe_dacl_grants_exact_client_access() {
use windows_sys::Win32::Foundation::{GetHandleInformation, HANDLE_FLAG_INHERIT};
use windows_sys::Win32::Security::{
Authorization::{GetSecurityInfo, SE_KERNEL_OBJECT},
GetAce, GetSecurityDescriptorControl, ACCESS_ALLOWED_ACE, DACL_SECURITY_INFORMATION,
SE_DACL_PROTECTED,
};
let pipe = LaunchNoncePipeHandoff::new("secret").unwrap();
let mut flags = 0;
assert_ne!(
unsafe { GetHandleInformation(pipe.pipe.as_raw_handle(), &mut flags) },
0
);
assert_eq!(flags & HANDLE_FLAG_INHERIT, 0);
let mut dacl = null_mut();
let mut descriptor = null_mut();
assert_eq!(
unsafe {
GetSecurityInfo(
pipe.pipe.as_raw_handle(),
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
null_mut(),
null_mut(),
&mut dacl,
null_mut(),
&mut descriptor,
)
},
0
);
let _descriptor = LocalAllocation(descriptor);
assert!(!dacl.is_null());
assert_eq!(unsafe { (*dacl).AceCount }, 1);
let mut ace = null_mut();
assert_ne!(unsafe { GetAce(dacl, 0, &mut ace) }, 0);
let mask = unsafe { (*ace.cast::<ACCESS_ALLOWED_ACE>()).Mask };
assert!(
mask == 0x80100100 || mask == 0x00120189,
"unexpected client access mask: {mask:#x}"
);
assert_eq!(mask & (0x40000000 | 0x4), 0);
let mut control = 0;
let mut revision = 0;
assert_ne!(
unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) },
0
);
assert_ne!(control & SE_DACL_PROTECTED, 0);
}
#[test]
fn windows_pipe_child_fixture() {
if std::env::var_os("SUBC_PIPE_TEST_CHILD").is_none() {
return;
}
std::thread::sleep(Duration::from_millis(250));
let nonce = launch_nonce()
.expect("child pipe read")
.expect("child nonce");
assert_eq!(nonce.source(), LaunchNonceSource::Pipe);
assert_eq!(launch_nonce().unwrap(), Some(nonce.clone()));
assert_eq!(std::env::var(LAUNCH_NONCE_ENV).unwrap(), "environment-copy");
println!("child-pipe:{}", nonce.value());
}
fn spawn_reader(pipe: &LaunchNoncePipeHandoff) -> std::process::Child {
Command::new(std::env::current_exe().unwrap())
.args([
"launch_nonce::windows::tests::windows_pipe_child_fixture",
"--exact",
"--nocapture",
])
.env("SUBC_PIPE_TEST_CHILD", "1")
.env(LAUNCH_NONCE_PIPE_ENV, pipe.name())
.env(LAUNCH_NONCE_ENV, "environment-copy")
.env_remove(LAUNCH_NONCE_PIPE_FALLBACK_ENV)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
}
fn assert_child_reads(mut child: std::process::Child, evidence: &OnceLock<()>) {
let deadline = Instant::now() + Duration::from_secs(10);
while child.try_wait().unwrap().is_none() {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("child did not finish its bounded pipe handoff");
}
std::thread::sleep(POLL);
}
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("child-pipe:test-secret"),
"{}",
String::from_utf8_lossy(&output.stdout)
);
assert!(evidence.get().is_some(), "server records pipe consumption");
}
#[test]
fn windows_supervisor_child_reads_secret_through_pipe() {
let pipe = LaunchNoncePipeHandoff::new("test-secret").unwrap();
let child = spawn_reader(&pipe);
let delivery = pipe.serve(child.id(), Duration::from_secs(5)).unwrap();
assert_child_reads(child, &delivery.consumed());
}
#[test]
fn windows_wrong_pid_gets_nothing_and_real_child_still_reads() {
let pipe = LaunchNoncePipeHandoff::new("test-secret").unwrap();
let name = pipe.name().to_owned();
let child = spawn_reader(&pipe);
let delivery = pipe.serve(child.id(), Duration::from_secs(5)).unwrap();
assert_eq!(
read_pipe(OsStr::new(&name)),
Err(LaunchNonceError::PipeEmpty)
);
assert_child_reads(child, &delivery.consumed());
}
#[test]
fn windows_name_collision_picks_fresh_pipe() {
let occupied = LaunchNoncePipeHandoff::new("occupied").unwrap();
let mut calls = 0;
let pipe = LaunchNoncePipeHandoff::with_names("test-secret", || {
calls += 1;
if calls == 1 {
Ok(occupied.name().to_owned())
} else {
random_name()
}
})
.unwrap();
assert_eq!(calls, 2);
assert_ne!(pipe.name(), occupied.name());
let child = spawn_reader(&pipe);
let delivery = pipe.serve(child.id(), Duration::from_secs(5)).unwrap();
assert_child_reads(child, &delivery.consumed());
}
#[test]
fn windows_gone_pipe_never_falls_back_and_caches_failure() {
let name = LaunchNoncePipeHandoff::new("secret")
.unwrap()
.name()
.to_owned();
let cell = LaunchNonceCell::new();
let result = cell.get(|key| match key {
LAUNCH_NONCE_PIPE_ENV => Some(name.clone().into()),
LAUNCH_NONCE_ENV => panic!("must not look up environment fallback"),
_ => None,
});
assert!(matches!(result, Err(LaunchNonceError::PipeNotOpen { .. })));
assert_eq!(cell.get(|_| panic!("must use cached failure")), result);
}
#[test]
fn windows_phase_one_gone_pipe_uses_and_caches_the_environment_source() {
let name = LaunchNoncePipeHandoff::new("secret")
.unwrap()
.name()
.to_owned();
let cell = LaunchNonceCell::new();
let result = cell
.get(|key| match key {
LAUNCH_NONCE_PIPE_ENV => Some(name.clone().into()),
LAUNCH_NONCE_PIPE_FALLBACK_ENV => Some("env".into()),
LAUNCH_NONCE_ENV => Some("environment-secret".into()),
_ => None,
})
.unwrap()
.unwrap();
assert_eq!(result.value(), "environment-secret");
assert_eq!(result.source(), LaunchNonceSource::Env);
assert_eq!(
cell.get(|_| panic!("must use cached fallback")),
Ok(Some(result))
);
}
}
#[cfg(test)]
mod buffered_delivery_tests {
use super::*;
use crate::launch_nonce::{
launch_nonce, LAUNCH_NONCE_ENV, LAUNCH_NONCE_PIPE_ENV, LAUNCH_NONCE_PIPE_FALLBACK_ENV,
};
use cortexkit_test_support::ScratchDir;
use std::{
path::{Path, PathBuf},
process::{Child, Command, Stdio},
};
use windows_sys::Win32::System::Pipes::PeekNamedPipe;
struct Gates(ScratchDir);
impl Gates {
fn new() -> Self {
Self(ScratchDir::new("subc-pipe-gates"))
}
fn path(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
fn wait_for(path: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
while !path.exists() {
assert!(
Instant::now() < deadline,
"pipe child did not reach {}",
path.display()
);
std::thread::sleep(POLL);
}
}
#[test]
fn windows_buffered_pipe_child_fixture() {
let Some(root) = std::env::var_os("SUBC_PIPE_GATES") else {
return;
};
let root = PathBuf::from(root);
let name = std::env::var_os(LAUNCH_NONCE_PIPE_ENV).unwrap();
let pipe = open_pipe(&name).unwrap();
std::fs::write(root.join("connected"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let mut available = 0;
assert_ne!(
unsafe {
PeekNamedPipe(
pipe.as_raw_handle(),
null_mut(),
0,
null_mut(),
&mut available,
null_mut(),
)
},
0
);
if available != 0 {
break;
}
assert!(Instant::now() < deadline, "server never buffered the nonce");
std::thread::sleep(POLL);
}
std::fs::write(root.join("buffered"), b"ready").unwrap();
while !root.join("read").exists() {
let mut available = 0;
if unsafe {
PeekNamedPipe(
pipe.as_raw_handle(),
null_mut(),
0,
null_mut(),
&mut available,
null_mut(),
)
} == 0
{
assert!(
pipe_eof(&io::Error::last_os_error()),
"unexpected pipe failure"
);
drop(pipe);
let nonce = launch_nonce().unwrap().unwrap();
assert_eq!(nonce.source(), LaunchNonceSource::Env);
println!("child-env:{}", nonce.value());
std::fs::write(root.join("closed"), b"ready").unwrap();
return;
}
assert!(Instant::now() < deadline, "reader gate was not opened");
std::thread::sleep(POLL);
}
let nonce = read_open_pipe(pipe).unwrap().unwrap();
assert_eq!(nonce.source(), LaunchNonceSource::Pipe);
println!("child-pipe:{}", nonce.value());
}
fn spawn_buffered_reader(
pipe: &LaunchNoncePipeHandoff,
gates: &Gates,
fallback: bool,
) -> Child {
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args(["launch_nonce::windows::buffered_delivery_tests::windows_buffered_pipe_child_fixture", "--exact", "--nocapture"])
.env("SUBC_PIPE_GATES", gates.0.path())
.env(LAUNCH_NONCE_PIPE_ENV, pipe.name())
.env_remove(LAUNCH_NONCE_PIPE_FALLBACK_ENV)
.env_remove(LAUNCH_NONCE_ENV)
.stdout(Stdio::piped()).stderr(Stdio::piped());
if fallback {
command
.env(LAUNCH_NONCE_PIPE_FALLBACK_ENV, "env")
.env(LAUNCH_NONCE_ENV, "test-secret");
}
command.spawn().unwrap()
}
#[test]
fn windows_client_connected_first_is_drained_before_consumption_and_eof() {
let gates = Gates::new();
let pipe = LaunchNoncePipeHandoff::new("test-secret").unwrap();
let mut child = spawn_buffered_reader(&pipe, &gates, false);
wait_for(&gates.path("connected"));
let delivery = pipe.serve(child.id(), Duration::from_secs(5)).unwrap();
wait_for(&gates.path("buffered"));
assert!(
delivery.consumed().get().is_none(),
"buffering bytes is not client consumption"
);
std::fs::write(gates.path("read"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
while child.try_wait().unwrap().is_none() {
if Instant::now() >= deadline {
let _ = child.kill();
panic!("connected-first reader did not reach EOF");
}
std::thread::sleep(POLL);
}
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("child-pipe:test-secret"));
assert!(delivery.consumed().get().is_some());
}
#[test]
fn windows_flush_deadline_closes_pipe_without_reporting_consumption() {
let gates = Gates::new();
let pipe = LaunchNoncePipeHandoff::new("test-secret").unwrap();
let child = spawn_buffered_reader(&pipe, &gates, true);
wait_for(&gates.path("connected"));
let delivery = pipe.serve(child.id(), Duration::from_secs(1)).unwrap();
wait_for(&gates.path("buffered"));
wait_for(&gates.path("closed"));
assert!(delivery.consumed().get().is_none());
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("child-env:test-secret"));
}
}
#[cfg(all(test, windows))]
mod lifecycle_tests {
use super::*;
use std::process::{Command, Stdio};
#[test]
fn windows_pipe_idle_child_fixture() {
if std::env::var_os("SUBC_PIPE_IDLE_CHILD").is_some() {
std::thread::sleep(Duration::from_secs(1));
}
}
fn idle_child() -> std::process::Child {
Command::new(std::env::current_exe().unwrap())
.args([
"launch_nonce::windows::lifecycle_tests::windows_pipe_idle_child_fixture",
"--exact",
])
.env("SUBC_PIPE_IDLE_CHILD", "1")
.stdout(Stdio::null())
.spawn()
.unwrap()
}
#[test]
fn windows_registration_deadline_closes_unconsumed_pipe() {
let pipe = LaunchNoncePipeHandoff::new("secret").unwrap();
let name = pipe.name().to_owned();
let mut child = idle_child();
let delivery = pipe.serve(child.id(), Duration::from_millis(30)).unwrap();
std::thread::sleep(Duration::from_millis(100));
assert!(matches!(
read_pipe(OsStr::new(&name)),
Err(LaunchNonceError::PipeNotOpen { .. })
));
assert!(delivery.consumed().get().is_none());
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn windows_child_exit_closes_unconsumed_pipe() {
let pipe = LaunchNoncePipeHandoff::new("secret").unwrap();
let name = pipe.name().to_owned();
let mut child = idle_child();
let delivery = pipe.serve(child.id(), Duration::from_secs(5)).unwrap();
child.kill().unwrap();
child.wait().unwrap();
std::thread::sleep(Duration::from_millis(100));
assert!(matches!(
read_pipe(OsStr::new(&name)),
Err(LaunchNonceError::PipeNotOpen { .. })
));
assert!(delivery.consumed().get().is_none());
}
}