use std::io;
use std::sync::Mutex;
use std::time::Duration;
use teksilo_automation::wire::Endpoint;
use windows::Win32::Foundation::{
CloseHandle, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_NO_DATA, ERROR_PIPE_BUSY,
ERROR_PIPE_CONNECTED, ERROR_PIPE_NOT_CONNECTED, GENERIC_READ, GENERIC_WRITE, HANDLE, HLOCAL,
LocalFree, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
use windows::Win32::Security::{
GetTokenInformation, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
TokenUser,
};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_FLAG_OVERLAPPED, FILE_SHARE_NONE, OPEN_EXISTING, PIPE_ACCESS_DUPLEX,
ReadFile, WriteFile,
};
use windows::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED};
use windows::Win32::System::Pipes::{
ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS,
PIPE_TYPE_BYTE, PIPE_WAIT, WaitNamedPipeW,
};
use windows::Win32::System::Threading::{
CreateEventW, GetCurrentProcess, INFINITE, OpenProcessToken, ResetEvent, WaitForSingleObject,
};
use windows::core::{PCWSTR, PWSTR};
use super::{BoundTransport, TransportListener, TransportStream};
const PIPE_BUFFER: u32 = 64 * 1024;
pub(super) fn pipe_name(pid: u32) -> String {
format!(r"\\.\pipe\teksilo-automation-{pid}")
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn last_io_error(context: &str) -> io::Error {
io::Error::other(format!("{context}: {}", io::Error::last_os_error()))
}
struct OwnerOnlySecurity {
descriptor: PSECURITY_DESCRIPTOR,
}
impl OwnerOnlySecurity {
fn current_user() -> io::Result<Self> {
let sid = current_user_sid_string()?;
let sddl = wide(&format!("D:P(A;;GA;;;{sid})"));
let mut descriptor = PSECURITY_DESCRIPTOR::default();
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
PCWSTR(sddl.as_ptr()),
SDDL_REVISION_1,
&mut descriptor,
None,
)
}
.map_err(|e| io::Error::other(format!("building the pipe security descriptor: {e}")))?;
Ok(Self { descriptor })
}
fn attributes(&self) -> SECURITY_ATTRIBUTES {
SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: self.descriptor.0,
bInheritHandle: false.into(),
}
}
}
impl Drop for OwnerOnlySecurity {
fn drop(&mut self) {
if !self.descriptor.is_invalid() {
unsafe {
let _ = LocalFree(Some(HLOCAL(self.descriptor.0)));
}
}
}
}
fn current_user_sid_string() -> io::Result<String> {
unsafe {
let mut token = HANDLE::default();
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| io::Error::other(format!("opening the process token: {e}")))?;
let _guard = HandleGuard(token);
let mut needed = 0u32;
let _ = GetTokenInformation(token, TokenUser, None, 0, &mut needed);
if needed == 0 {
return Err(last_io_error("sizing the token user information"));
}
let mut buf = vec![0u8; needed as usize];
GetTokenInformation(
token,
TokenUser,
Some(buf.as_mut_ptr().cast()),
needed,
&mut needed,
)
.map_err(|e| io::Error::other(format!("reading the token user information: {e}")))?;
let token_user = &*(buf.as_ptr() as *const TOKEN_USER);
let mut sid_str = PWSTR::null();
ConvertSidToStringSidW(token_user.User.Sid, &mut sid_str)
.map_err(|e| io::Error::other(format!("formatting the user SID: {e}")))?;
let owned = sid_str.to_string().unwrap_or_default();
let _ = LocalFree(Some(HLOCAL(sid_str.0.cast())));
if owned.is_empty() {
return Err(io::Error::other(
"the user SID formatted to an empty string",
));
}
Ok(owned)
}
}
struct HandleGuard(HANDLE);
impl Drop for HandleGuard {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
}
pub struct PipeStream {
handle: HANDLE,
event: HANDLE,
read_timeout: Mutex<Option<Duration>>,
}
unsafe impl Send for PipeStream {}
impl PipeStream {
fn new(handle: HANDLE) -> io::Result<Self> {
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null()) }
.map_err(|e| io::Error::other(format!("creating the pipe I/O event: {e}")))?;
Ok(Self {
handle,
event,
read_timeout: Mutex::new(None),
})
}
fn await_overlapped(&self, ov: &mut OVERLAPPED, timeout: Option<Duration>) -> io::Result<u32> {
let millis = timeout
.map(|d| u32::try_from(d.as_millis()).unwrap_or(u32::MAX - 1))
.unwrap_or(INFINITE);
let waited = unsafe { WaitForSingleObject(self.event, millis) };
if waited == WAIT_TIMEOUT {
let mut transferred = 0u32;
let completed = unsafe {
let _ = CancelIoEx(self.handle, Some(ov as *const OVERLAPPED));
GetOverlappedResult(self.handle, ov, &mut transferred, true)
};
if completed.is_ok() && transferred > 0 {
return Ok(transferred);
}
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"the pipe read deadline expired",
));
}
if waited != WAIT_OBJECT_0 {
return Err(last_io_error("waiting on the pipe I/O event"));
}
let mut transferred = 0u32;
unsafe { GetOverlappedResult(self.handle, ov, &mut transferred, false) }.map_err(|e| {
if e.code() == ERROR_BROKEN_PIPE.into() || e.code() == ERROR_PIPE_NOT_CONNECTED.into() {
io::Error::from(io::ErrorKind::UnexpectedEof)
} else {
io::Error::other(format!("completing the pipe operation: {e}"))
}
})?;
Ok(transferred)
}
fn armed_overlapped(&self) -> io::Result<OVERLAPPED> {
unsafe { ResetEvent(self.event) }
.map_err(|e| io::Error::other(format!("resetting the pipe I/O event: {e}")))?;
Ok(OVERLAPPED {
hEvent: self.event,
..Default::default()
})
}
}
impl Drop for PipeStream {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.handle);
let _ = CloseHandle(self.event);
}
}
}
impl io::Read for PipeStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut ov = self.armed_overlapped()?;
let mut read = 0u32;
let started = unsafe {
ReadFile(
self.handle,
Some(buf),
Some(&mut read),
Some(&mut ov as *mut OVERLAPPED),
)
};
match started {
Ok(()) => {
let n = self.await_overlapped(&mut ov, None)?;
Ok(n as usize)
}
Err(e) if e.code() == ERROR_IO_PENDING.into() => {
let timeout = *self.read_timeout.lock().unwrap();
match self.await_overlapped(&mut ov, timeout) {
Ok(n) => Ok(n as usize),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(0),
Err(e) => Err(e),
}
}
Err(e)
if e.code() == ERROR_BROKEN_PIPE.into()
|| e.code() == ERROR_PIPE_NOT_CONNECTED.into() =>
{
Ok(0)
}
Err(e) => Err(io::Error::other(format!("reading from the pipe: {e}"))),
}
}
}
impl io::Write for PipeStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut ov = self.armed_overlapped()?;
let mut written = 0u32;
let started = unsafe {
WriteFile(
self.handle,
Some(buf),
Some(&mut written),
Some(&mut ov as *mut OVERLAPPED),
)
};
match started {
Ok(()) => Ok(self.await_overlapped(&mut ov, None)? as usize),
Err(e) if e.code() == ERROR_IO_PENDING.into() => {
Ok(self.await_overlapped(&mut ov, None)? as usize)
}
Err(e) => Err(io::Error::other(format!("writing to the pipe: {e}"))),
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl TransportStream for PipeStream {
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
*self.read_timeout.lock().unwrap() = timeout;
Ok(())
}
}
struct PipeListener {
name: Vec<u16>,
security: OwnerOnlySecurity,
pending: Option<HANDLE>,
}
unsafe impl Send for PipeListener {}
impl PipeListener {
fn create_instance(&self) -> io::Result<HANDLE> {
let attrs = self.security.attributes();
let handle = unsafe {
CreateNamedPipeW(
PCWSTR(self.name.as_ptr()),
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
2,
PIPE_BUFFER,
PIPE_BUFFER,
0,
Some(&attrs),
)
};
if handle.is_invalid() {
return Err(last_io_error("creating the named pipe"));
}
Ok(handle)
}
}
impl TransportListener for PipeListener {
fn accept(&mut self) -> io::Result<Box<dyn TransportStream>> {
const RECYCLE_ATTEMPTS: usize = 8;
for _ in 0..RECYCLE_ATTEMPTS {
let handle = match self.pending.take() {
Some(h) => h,
None => self.create_instance()?,
};
let stream = match PipeStream::new(handle) {
Ok(s) => s,
Err(e) => {
unsafe {
let _ = CloseHandle(handle);
}
return Err(e);
}
};
let mut ov = stream.armed_overlapped()?;
let started = unsafe { ConnectNamedPipe(handle, Some(&mut ov as *mut OVERLAPPED)) };
let outcome: Result<(), AcceptFailure> = match started {
Ok(()) => Ok(()),
Err(e) if e.code() == ERROR_PIPE_CONNECTED.into() => Ok(()),
Err(e) if e.code() == ERROR_IO_PENDING.into() => stream
.await_overlapped(&mut ov, None)
.map(|_| ())
.map_err(|err| AcceptFailure {
stale: err.kind() == io::ErrorKind::UnexpectedEof,
err,
}),
Err(e) => Err(AcceptFailure {
stale: is_stale_instance(&e),
err: io::Error::other(format!("accepting on the pipe: {e}")),
}),
};
match outcome {
Ok(()) => {
self.pending = self.create_instance().ok();
return Ok(Box::new(stream));
}
Err(f) if f.stale => {
drop(stream);
continue;
}
Err(f) => return Err(f.err),
}
}
Err(io::Error::other(
"the named pipe kept yielding stale instances; giving up on this accept",
))
}
}
impl Drop for PipeListener {
fn drop(&mut self) {
if let Some(h) = self.pending.take() {
unsafe {
let _ = CloseHandle(h);
}
}
}
}
struct AcceptFailure {
stale: bool,
err: io::Error,
}
fn is_stale_instance(e: &windows::core::Error) -> bool {
let code = e.code();
code == ERROR_NO_DATA.into()
|| code == ERROR_BROKEN_PIPE.into()
|| code == ERROR_PIPE_NOT_CONNECTED.into()
}
pub(super) fn bind(pid: u32) -> io::Result<BoundTransport> {
let name = pipe_name(pid);
let security = OwnerOnlySecurity::current_user()?;
let mut listener = PipeListener {
name: wide(&name),
security,
pending: None,
};
listener.pending = Some(listener.create_instance()?);
Ok(BoundTransport {
listener: Box::new(listener),
endpoint: Endpoint::named_pipe(name),
})
}
pub(super) fn connect(address: &str) -> io::Result<Box<dyn TransportStream>> {
connect_within(address, CONNECT_PATIENCE)
}
const CONNECT_PATIENCE: Duration = Duration::from_secs(5);
pub(super) fn connect_within(
address: &str,
patience: Duration,
) -> io::Result<Box<dyn TransportStream>> {
let name = wide(address);
let deadline = std::time::Instant::now() + patience;
let mut last_busy = false;
loop {
let handle = unsafe {
CreateFileW(
PCWSTR(name.as_ptr()),
(GENERIC_READ | GENERIC_WRITE).0,
FILE_SHARE_NONE,
None,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
None,
)
};
match handle {
Ok(h) if !h.is_invalid() => {
return match PipeStream::new(h) {
Ok(s) => Ok(Box::new(s)),
Err(e) => {
unsafe {
let _ = CloseHandle(h);
}
Err(e)
}
};
}
Ok(_) => return Err(last_io_error("opening the named pipe")),
Err(e) if e.code() == ERROR_PIPE_BUSY.into() => {
last_busy = true;
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"the automation bridge is already serving another client",
));
}
let ms = u32::try_from(remaining.as_millis()).unwrap_or(u32::MAX - 1);
if !unsafe { WaitNamedPipeW(PCWSTR(name.as_ptr()), ms.min(200)) }.as_bool() {
std::thread::sleep(Duration::from_millis(10));
}
}
Err(e) => {
if std::time::Instant::now() >= deadline {
let kind = if last_busy {
io::ErrorKind::WouldBlock
} else {
io::ErrorKind::NotFound
};
return Err(io::Error::new(
kind,
format!("connecting to {address}: {e}"),
));
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pipe_names_are_per_process_and_well_formed() {
let n = pipe_name(4242);
assert!(n.starts_with(r"\\.\pipe\"), "{n}");
assert!(n.ends_with("-4242"), "{n}");
assert!(n.len() < 256, "pipe names are capped at 256 characters");
}
#[test]
fn the_current_user_sid_is_resolvable() {
let sid = current_user_sid_string().expect("current user SID");
assert!(sid.starts_with("S-1-"), "{sid}");
}
#[test]
fn the_descriptor_is_owner_only_and_protected() {
let sec = OwnerOnlySecurity::current_user().expect("descriptor");
assert!(!sec.descriptor.is_invalid());
let attrs = sec.attributes();
assert_eq!(
attrs.nLength as usize,
std::mem::size_of::<SECURITY_ATTRIBUTES>()
);
assert!(!attrs.lpSecurityDescriptor.is_null());
assert!(
!attrs.bInheritHandle.as_bool(),
"handles must not be inheritable"
);
}
#[test]
fn a_second_instance_can_exist_while_the_first_is_in_use() {
let pid = std::process::id().wrapping_add(31);
let listener = PipeListener {
name: wide(&pipe_name(pid)),
security: OwnerOnlySecurity::current_user().expect("descriptor"),
pending: None,
};
let first = listener.create_instance().expect("first instance");
let second = listener
.create_instance()
.expect("a second instance must be creatable while the first is open");
unsafe {
let _ = CloseHandle(first);
let _ = CloseHandle(second);
}
}
#[test]
fn a_client_can_connect_after_the_previous_one_is_released() {
let pid = std::process::id().wrapping_add(11);
let mut bound = bind(pid).expect("bind");
let address = bound.endpoint.address.clone();
let probe = connect(&address).expect("probe connects");
let peer1 = bound.listener.accept().expect("server accepts the probe");
drop(probe);
drop(peer1);
let mut second =
connect(&address).expect("a client must connect after the probe is released");
let mut peer2 = bound
.listener
.accept()
.expect("server accepts the second client");
std::io::Write::write_all(&mut second, b"x").expect("client writes");
std::io::Write::flush(&mut second).ok();
let mut buf = [0u8; 1];
std::io::Read::read_exact(&mut peer2, &mut buf).expect("server reads");
assert_eq!(&buf, b"x", "the second client's bytes must arrive");
}
}