use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HostProcess {
pub pid: u32,
pub uid: Option<u32>,
pub sid: Option<String>,
pub executable: PathBuf,
pub audit_token: Option<[u32; 8]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum HostIdentityReason {
NoExpectation,
UidMismatch { host: Option<u32>, client: u32 },
PidUnavailable,
Unreadable(String),
ExecutableMismatch,
HostChanged(String),
Refused(String),
Unverified {
platform: &'static str,
detail: &'static str,
},
Query(String),
NotLocalPeer,
}
impl fmt::Display for HostIdentityReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostIdentityReason::NoExpectation => {
write!(
f,
"no expected host executable was supplied, so no host can match"
)
}
HostIdentityReason::UidMismatch { host, client } => match host {
Some(host) => write!(
f,
"the host runs as uid {host}, this client as uid {client}"
),
None => write!(
f,
"the host's uid is unknown, this client runs as uid {client}"
),
},
HostIdentityReason::PidUnavailable => {
write!(f, "the OS did not report the host's process id")
}
HostIdentityReason::Unreadable(why) => {
write!(f, "the OS would not describe the host process ({why})")
}
HostIdentityReason::ExecutableMismatch => {
write!(
f,
"the host's executable is not one of the expected executables"
)
}
HostIdentityReason::HostChanged(why) => {
write!(
f,
"the host process changed while it was identified ({why})"
)
}
HostIdentityReason::Refused(why) => {
write!(f, "the caller's extra host verification refused it ({why})")
}
HostIdentityReason::Unverified { platform, detail } => {
write!(
f,
"host verification is unavailable on {platform} ({detail})"
)
}
HostIdentityReason::Query(why) => {
write!(f, "the OS could not identify the peer ({why})")
}
HostIdentityReason::NotLocalPeer => {
write!(
f,
"the caller is not a local app, so it has no process to identify"
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HostIdentityError {
pub expected: Vec<PathBuf>,
pub found_pid: Option<u32>,
pub found_executable: Option<PathBuf>,
pub reason: HostIdentityReason,
}
impl fmt::Display for HostIdentityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"the service host failed identity verification: {}",
self.reason
)?;
if let Some(pid) = self.found_pid {
write!(f, "; host pid {pid}")?;
}
if let Some(exe) = &self.found_executable {
write!(f, "; host executable {}", exe.display())?;
}
if self.expected.is_empty() {
write!(f, "; expected: none supplied")
} else {
write!(f, "; expected ")?;
for (i, path) in self.expected.iter().enumerate() {
if i > 0 {
write!(f, " or ")?;
}
write!(f, "{}", path.display())?;
}
Ok(())
}
}
}
impl std::error::Error for HostIdentityError {}
#[derive(Debug)]
pub(crate) enum HostConnectError {
Io(io::Error),
Identity(HostIdentityError),
}
type Verifier = Arc<dyn Fn(&HostProcess) -> Result<(), String> + Send + Sync>;
#[derive(Clone, Default)]
pub struct HostExpectation {
executables: Vec<PathBuf>,
verifier: Option<Verifier>,
any_executable: bool,
}
impl fmt::Debug for HostExpectation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HostExpectation")
.field("executables", &self.executables)
.field("verifier", &self.verifier.as_ref().map(|_| "<fn>"))
.finish()
}
}
fn invalid(what: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, what.to_string())
}
fn current_exe_canonical() -> io::Result<PathBuf> {
std::fs::canonicalize(std::env::current_exe()?)
}
fn is_single_component(name: &OsStr) -> bool {
let path = Path::new(name);
path.file_name() == Some(name) && path.components().count() == 1
}
fn up_and_join(exe: &Path, levels_up: usize, relative: &Path) -> Option<PathBuf> {
use std::path::Component;
let leaves_base = relative
.components()
.any(|part| matches!(part, Component::Prefix(_) | Component::RootDir));
if levels_up == 0 || relative.is_absolute() || leaves_base {
return None;
}
exe.ancestors()
.nth(levels_up)
.map(|base| base.join(relative))
}
fn bundle_main(exe: &Path, file_name: &OsStr) -> Option<PathBuf> {
exe.ancestors()
.skip(1)
.find(|dir| dir.extension() == Some(OsStr::new("app")))
.map(|bundle| bundle.join("Contents").join("MacOS").join(file_name))
}
impl HostExpectation {
pub fn executable(path: impl Into<PathBuf>) -> Self {
Self {
executables: vec![path.into()],
verifier: None,
any_executable: false,
}
}
pub fn any_of<I, P>(paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
Self {
executables: paths.into_iter().map(Into::into).collect(),
verifier: None,
any_executable: false,
}
}
pub fn or_executable(mut self, path: impl Into<PathBuf>) -> Self {
self.executables.push(path.into());
self
}
pub fn sibling_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
let name = file_name.as_ref();
if !is_single_component(name) {
return Err(invalid("the host file name must be a bare file name"));
}
Self::relative_to_current_exe(1, name)
}
pub fn bundle_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
let name = file_name.as_ref();
if !is_single_component(name) {
return Err(invalid("the host file name must be a bare file name"));
}
let exe = current_exe_canonical()?;
let path = bundle_main(&exe, name).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"the running executable is not inside an .app bundle",
)
})?;
Ok(Self::executable(path))
}
pub fn relative_to_current_exe(
levels_up: usize,
relative: impl AsRef<Path>,
) -> io::Result<Self> {
let exe = current_exe_canonical()?;
let path = up_and_join(&exe, levels_up, relative.as_ref()).ok_or_else(|| {
invalid("no such directory above the running executable, or the path is absolute")
})?;
Ok(Self::executable(path))
}
pub fn with_verifier(
mut self,
check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
self.verifier = Some(Arc::new(check));
self
}
pub fn executables(&self) -> &[PathBuf] {
&self.executables
}
pub(crate) fn any_executable_verified(
check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
Self {
executables: Vec::new(),
verifier: Some(Arc::new(check)),
any_executable: true,
}
}
}
pub(crate) fn verify_peer_process(
peer: Option<&crate::peer::PeerIdentity>,
expect: &HostExpectation,
) -> Result<HostProcess, HostIdentityError> {
let Some(peer) = peer else {
return Err(HostIdentityError {
expected: expect.executables.clone(),
found_pid: None,
found_executable: None,
reason: HostIdentityReason::NotLocalPeer,
});
};
#[cfg(unix)]
{
verify_unix_pid(peer, expect)
}
#[cfg(windows)]
{
verify_pid_host(peer, expect)
}
#[cfg(not(any(unix, windows)))]
{
Err(HostIdentityError {
expected: expect.executables.clone(),
found_pid: peer.pid,
found_executable: None,
reason: HostIdentityReason::Unverified {
platform: std::env::consts::OS,
detail: "no process identification is implemented for this platform",
},
})
}
}
pub(crate) fn process_start_marker(pid: u32) -> Option<(u64, u64)> {
#[cfg(unix)]
{
unix_impl::start_marker_of(pid)
}
#[cfg(windows)]
{
windows_impl::start_marker_of(pid)
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
None
}
}
#[cfg(unix)]
pub(crate) use unix_impl::{verify_unix_host, verify_unix_pid};
#[cfg(all(
unix,
any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)
))]
pub(crate) use unix_impl::peer_audit_token;
#[cfg(unix)]
mod unix_impl {
use super::{
HostConnectError, HostExpectation, HostIdentityError, HostIdentityReason, HostProcess,
};
use crate::peer::{current_euid, verify_peer_is_current_user, PeerError, PeerIdentity};
use std::io;
use std::os::unix::fs::MetadataExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
pub type FileId = (u64, u64);
pub fn file_id(path: &Path) -> Option<FileId> {
let md = std::fs::metadata(path).ok()?;
Some((md.dev(), md.ino()))
}
pub struct ExeInfo {
pub path: PathBuf,
pub id: Option<FileId>,
}
#[derive(PartialEq, Eq)]
struct Witness {
start: (u64, u64),
token: Option<[u32; 8]>,
}
enum ObserveError {
Gone,
Unreadable(String),
}
impl From<ObserveError> for HostIdentityReason {
fn from(e: ObserveError) -> Self {
match e {
ObserveError::Gone => HostIdentityReason::HostChanged(
"it exited before it could be identified".to_string(),
),
ObserveError::Unreadable(why) => HostIdentityReason::Unreadable(why),
}
}
}
#[cfg(target_os = "macos")]
fn missing_or_err<T>() -> io::Result<Option<T>> {
let e = io::Error::last_os_error();
match e.raw_os_error() {
Some(3) | Some(0) | None => Ok(None),
_ => Err(e),
}
}
fn witness(stream: &UnixStream, pid: u32) -> Result<Witness, ObserveError> {
let token = os::audit_token(stream);
let start = match os::start_marker(pid) {
Ok(Some(start)) => start,
Ok(None) => return Err(ObserveError::Gone),
Err(e) => return Err(ObserveError::Unreadable(format!("process start time: {e}"))),
};
Ok(Witness { start, token })
}
pub fn same_executable(expected: &Path, found: &Path, found_id: Option<FileId>) -> bool {
let Ok(canonical) = std::fs::canonicalize(expected) else {
return false;
};
if canonical == found {
return true;
}
match (found_id, file_id(&canonical)) {
(Some(running), Some(named)) => running == named,
_ => false,
}
}
pub(crate) fn verify_unix_host(
stream: &UnixStream,
expect: &HostExpectation,
) -> Result<HostProcess, HostConnectError> {
let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
HostConnectError::Identity(HostIdentityError {
expected: expect.executables.clone(),
found_pid: pid,
found_executable: exe,
reason,
})
};
if expect.executables.is_empty() {
return Err(refuse(None, None, HostIdentityReason::NoExpectation));
}
if !os::SUPPORTED {
return Err(refuse(
None,
None,
HostIdentityReason::Unverified {
platform: std::env::consts::OS,
detail: "reading a process's executable path is not implemented for this Unix",
},
));
}
let first = match verify_peer_is_current_user(stream) {
Ok(id) => id,
Err(PeerError::Mismatch(id)) => {
return Err(refuse(
id.pid,
None,
HostIdentityReason::UidMismatch {
host: id.uid,
client: current_euid(),
},
))
}
Err(PeerError::Unsupported(detail)) => {
return Err(refuse(
None,
None,
HostIdentityReason::Unverified {
platform: std::env::consts::OS,
detail,
},
))
}
Err(e) => {
if e.peer_gone() {
return Err(HostConnectError::Io(io::Error::from(e)));
}
return Err(refuse(None, None, HostIdentityReason::Query(e.to_string())));
}
};
let Some(pid) = first.pid else {
return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
};
let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
let changed = |why: &str| {
refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(why.to_string()),
)
};
let before = witness(stream, pid).map_err(observe)?;
let exe = match os::executable_of(pid) {
Ok(Some(exe)) => exe,
Ok(None) => return Err(observe(ObserveError::Gone)),
Err(e) => {
return Err(observe(ObserveError::Unreadable(format!(
"executable path: {e}"
))))
}
};
let after = witness(stream, pid).map_err(observe)?;
if before != after {
return Err(changed(
"its identity differed before and after its executable path was read",
));
}
match verify_peer_is_current_user(stream) {
Ok(again) if again.pid == Some(pid) => {}
_ => return Err(changed("the socket no longer reports the same process id")),
}
conclude(expect, pid, exe, after.token).map_err(HostConnectError::Identity)
}
fn conclude(
expect: &HostExpectation,
pid: u32,
exe: ExeInfo,
audit_token: Option<[u32; 8]>,
) -> Result<HostProcess, HostIdentityError> {
let refuse = |exe: Option<PathBuf>, reason: HostIdentityReason| HostIdentityError {
expected: expect.executables.clone(),
found_pid: Some(pid),
found_executable: exe,
reason,
};
let ExeInfo { path, id } = exe;
let found = std::fs::canonicalize(&path).unwrap_or(path);
if !expect.any_executable
&& !expect
.executables
.iter()
.any(|expected| same_executable(expected, &found, id))
{
return Err(refuse(Some(found), HostIdentityReason::ExecutableMismatch));
}
let host = HostProcess {
pid,
uid: Some(current_euid()),
sid: None,
executable: found,
audit_token,
};
if let Some(verifier) = &expect.verifier {
if let Err(why) = verifier(&host) {
return Err(refuse(
Some(host.executable.clone()),
HostIdentityReason::Refused(why),
));
}
}
Ok(host)
}
pub(crate) fn verify_unix_pid(
peer: &PeerIdentity,
expect: &HostExpectation,
) -> Result<HostProcess, HostIdentityError> {
let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
HostIdentityError {
expected: expect.executables.clone(),
found_pid: pid,
found_executable: exe,
reason,
}
};
if expect.executables.is_empty() && !expect.any_executable {
return Err(refuse(None, None, HostIdentityReason::NoExpectation));
}
if !os::SUPPORTED {
return Err(refuse(
None,
None,
HostIdentityReason::Unverified {
platform: std::env::consts::OS,
detail: "reading a process's executable path is not implemented for this Unix",
},
));
}
if peer.uid != Some(current_euid()) {
return Err(refuse(
peer.pid,
None,
HostIdentityReason::UidMismatch {
host: peer.uid,
client: current_euid(),
},
));
}
let Some(pid) = peer.pid else {
return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
};
if let Some(token) = peer.audit_token {
if token[5] != pid {
return Err(refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(
"the audit token and the process id name different processes".to_string(),
),
));
}
}
let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
let start = |pid| match os::start_marker(pid) {
Ok(Some(start)) => Ok(start),
Ok(None) => Err(ObserveError::Gone),
Err(e) => Err(ObserveError::Unreadable(format!("process start time: {e}"))),
};
let before = start(pid).map_err(observe)?;
if peer.started != Some(before) {
return Err(refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(
concat!(
"the process is not the one that connected (its start time differs ",
"from the one recorded at connect, or none was recorded)"
)
.to_string(),
),
));
}
let exe = match os::executable_of(pid) {
Ok(Some(exe)) => exe,
Ok(None) => return Err(observe(ObserveError::Gone)),
Err(e) => {
return Err(observe(ObserveError::Unreadable(format!(
"executable path: {e}"
))))
}
};
let after = start(pid).map_err(observe)?;
if before != after {
return Err(refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(
"its start time differed before and after its executable path was read"
.to_string(),
),
));
}
conclude(expect, pid, exe, peer.audit_token)
}
pub(crate) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
os::start_marker(pid).ok().flatten()
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
pub(crate) fn peer_audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
os::audit_token(stream)
}
#[cfg(target_os = "macos")]
mod os {
use super::{file_id, missing_or_err, ExeInfo};
use std::ffi::{c_void, OsString};
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
pub const SUPPORTED: bool = true;
#[repr(C)]
#[allow(dead_code)]
struct ProcBsdInfo {
pbi_flags: u32,
pbi_status: u32,
pbi_xstatus: u32,
pbi_pid: u32,
pbi_ppid: u32,
pbi_uid: u32,
pbi_gid: u32,
pbi_ruid: u32,
pbi_rgid: u32,
pbi_svuid: u32,
pbi_svgid: u32,
rfu_1: u32,
pbi_comm: [u8; 16],
pbi_name: [u8; 32],
pbi_nfiles: u32,
pbi_pgid: u32,
pbi_pjobc: u32,
e_tdev: u32,
e_tpgid: u32,
pbi_nice: i32,
pbi_start_tvsec: u64,
pbi_start_tvusec: u64,
}
const _: () = assert!(std::mem::size_of::<ProcBsdInfo>() == 136);
const PROC_PIDTBSDINFO: i32 = 3;
const PROC_PIDPATHINFO_MAXSIZE: usize = 4 * 1024;
const SOL_LOCAL: i32 = 0;
const LOCAL_PEERTOKEN: i32 = 0x006;
extern "C" {
fn proc_pidinfo(
pid: i32,
flavor: i32,
arg: u64,
buffer: *mut c_void,
buffersize: i32,
) -> i32;
fn proc_pidpath(pid: i32, buffer: *mut c_void, buffersize: u32) -> i32;
fn getsockopt(fd: i32, level: i32, name: i32, val: *mut c_void, len: *mut u32) -> i32;
}
fn to_pid(pid: u32) -> io::Result<i32> {
i32::try_from(pid)
.ok()
.filter(|p| *p > 0)
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
}
pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
let raw_pid = to_pid(pid)?;
let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
let size = std::mem::size_of::<ProcBsdInfo>() as i32;
let written = unsafe {
proc_pidinfo(
raw_pid,
PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast::<c_void>(),
size,
)
};
if written != size {
return missing_or_err();
}
let info = unsafe { info.assume_init() };
Ok((info.pbi_pid == pid).then_some((info.pbi_start_tvsec, info.pbi_start_tvusec)))
}
pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
let raw_pid = to_pid(pid)?;
let mut buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE];
let n = unsafe {
proc_pidpath(raw_pid, buf.as_mut_ptr().cast::<c_void>(), buf.len() as u32)
};
if n <= 0 {
return missing_or_err();
}
buf.truncate(n as usize);
if let Some(nul) = buf.iter().position(|b| *b == 0) {
buf.truncate(nul);
}
let path = PathBuf::from(OsString::from_vec(buf));
let id = file_id(&path);
Ok(Some(ExeInfo { path, id }))
}
pub fn audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
let mut token = [0u32; 8];
let mut len = std::mem::size_of_val(&token) as u32;
let rc = unsafe {
getsockopt(
stream.as_raw_fd(),
SOL_LOCAL,
LOCAL_PEERTOKEN,
token.as_mut_ptr().cast::<c_void>(),
&mut len,
)
};
(rc == 0 && len as usize == std::mem::size_of_val(&token)).then_some(token)
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod os {
use super::{is_no_such_process, ExeInfo};
use std::io;
use std::os::unix::fs::MetadataExt;
use std::os::unix::net::UnixStream;
pub const SUPPORTED: bool = true;
pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
let text = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Ok(text) => text,
Err(e) if is_no_such_process(&e) => return Ok(None),
Err(e) => return Err(e),
};
let malformed = || io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc stat");
let rest = text.rsplit_once(')').ok_or_else(malformed)?.1;
let ticks = rest
.split_whitespace()
.nth(19)
.and_then(|field| field.parse::<u64>().ok())
.ok_or_else(malformed)?;
Ok(Some((ticks, 0)))
}
pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
let link = format!("/proc/{pid}/exe");
let path = match std::fs::read_link(&link) {
Ok(path) => path,
Err(e) if is_no_such_process(&e) => return Ok(None),
Err(e) => return Err(e),
};
let id = std::fs::metadata(&link).ok().map(|md| (md.dev(), md.ino()));
Ok(Some(ExeInfo { path, id }))
}
pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
None
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android")))]
mod os {
use super::ExeInfo;
use std::io;
use std::os::unix::net::UnixStream;
pub const SUPPORTED: bool = false;
pub fn start_marker(_pid: u32) -> io::Result<Option<(u64, u64)>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
pub fn executable_of(_pid: u32) -> io::Result<Option<ExeInfo>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
None
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn is_no_such_process(e: &io::Error) -> bool {
e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(3)
}
}
#[cfg(windows)]
pub(crate) use windows_impl::{verify_pid_host, verify_pipe_host};
#[cfg(windows)]
mod windows_impl {
use super::*;
use crate::peer::{verify_pipe_server_is_current_user, PeerError, PeerIdentity};
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::io::{AsRawHandle, RawHandle};
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const SYNCHRONIZE: u32 = 0x0010_0000;
const WAIT_TIMEOUT: u32 = 258;
const ERROR_INVALID_PARAMETER: i32 = 87;
const LONGEST_PATH: usize = 32_768;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[allow(dead_code)]
struct FileTime {
low: u32,
high: u32,
}
#[repr(C)]
#[derive(Default)]
#[allow(dead_code)]
struct ByHandleFileInformation {
attributes: u32,
creation: FileTime,
access: FileTime,
write: FileTime,
volume_serial: u32,
size_high: u32,
size_low: u32,
links: u32,
index_high: u32,
index_low: u32,
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetNamedPipeServerProcessId(pipe: RawHandle, pid: *mut u32) -> i32;
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> RawHandle;
fn GetProcessTimes(
process: RawHandle,
creation: *mut FileTime,
exit: *mut FileTime,
kernel: *mut FileTime,
user: *mut FileTime,
) -> i32;
fn QueryFullProcessImageNameW(
process: RawHandle,
flags: u32,
name: *mut u16,
size: *mut u32,
) -> i32;
fn WaitForSingleObject(handle: RawHandle, milliseconds: u32) -> u32;
fn CloseHandle(handle: RawHandle) -> i32;
fn GetFileInformationByHandle(file: RawHandle, info: *mut ByHandleFileInformation) -> i32;
}
struct Process(RawHandle);
impl Drop for Process {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
}
impl Process {
fn open(pid: u32) -> io::Result<Process> {
let handle =
unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
Ok(Process(handle))
}
fn created(&self) -> io::Result<FileTime> {
let mut times = [FileTime::default(); 4];
let [creation, exit, kernel, user] = &mut times;
if unsafe { GetProcessTimes(self.0, creation, exit, kernel, user) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(times[0])
}
fn running(&self) -> bool {
unsafe { WaitForSingleObject(self.0, 0) == WAIT_TIMEOUT }
}
fn image(&self) -> io::Result<PathBuf> {
let mut buffer = vec![0u16; LONGEST_PATH];
let mut length = buffer.len() as u32;
if unsafe { QueryFullProcessImageNameW(self.0, 0, buffer.as_mut_ptr(), &mut length) }
== 0
{
return Err(io::Error::last_os_error());
}
Ok(PathBuf::from(OsString::from_wide(
&buffer[..length as usize],
)))
}
}
fn server_pid(pipe: RawHandle) -> io::Result<u32> {
let mut pid = 0u32;
if unsafe { GetNamedPipeServerProcessId(pipe, &mut pid) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(pid)
}
pub(super) fn file_id(path: &Path) -> Option<(u32, u64)> {
let file = std::fs::File::open(path).ok()?;
let mut info = ByHandleFileInformation::default();
if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
return None;
}
Some((
info.volume_serial,
(u64::from(info.index_high) << 32) | u64::from(info.index_low),
))
}
pub(super) fn same_executable(
expected: &Path,
found: &Path,
found_id: Option<(u32, u64)>,
) -> bool {
let Ok(expected) = std::fs::canonicalize(expected) else {
return false;
};
let lower = |path: &Path| path.to_string_lossy().to_lowercase();
if lower(&expected) == lower(found) {
return true;
}
found_id.is_some() && file_id(&expected) == found_id
}
fn plain(path: PathBuf) -> PathBuf {
let text = path.to_string_lossy();
match text.strip_prefix(r"\\?\UNC\") {
Some(rest) => PathBuf::from(format!(r"\\{rest}")),
None => match text.strip_prefix(r"\\?\") {
Some(rest) => PathBuf::from(rest),
None => path,
},
}
}
pub(crate) fn verify_pipe_host(
pipe: RawHandle,
expect: &HostExpectation,
) -> Result<HostProcess, HostConnectError> {
let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
HostConnectError::Identity(HostIdentityError {
expected: expect.executables.clone(),
found_pid: pid,
found_executable: exe,
reason,
})
};
if expect.executables.is_empty() {
return Err(refuse(None, None, HostIdentityReason::NoExpectation));
}
let first = match verify_pipe_server_is_current_user(pipe) {
Ok(id) => id,
Err(e) => {
if e.peer_gone() {
return Err(HostConnectError::Io(io::Error::from(e)));
}
let pid = match &e {
PeerError::Mismatch(id) => id.pid,
_ => None,
};
return Err(refuse(pid, None, HostIdentityReason::Query(e.to_string())));
}
};
let Some(pid) = first.pid else {
return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
};
let unreadable = |what: &str, e: io::Error| {
refuse(
Some(pid),
None,
HostIdentityReason::Unreadable(format!("{what}: {e}")),
)
};
let changed = |why: &str| {
refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(why.to_string()),
)
};
let process = match Process::open(pid) {
Ok(process) => process,
Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
return Err(changed("it exited before it could be opened"))
}
Err(e) => return Err(unreadable("open process", e)),
};
let before = process
.created()
.map_err(|e| unreadable("creation time", e))?;
let image = process
.image()
.map_err(|e| unreadable("executable path", e))?;
let after = process
.created()
.map_err(|e| unreadable("creation time", e))?;
if before != after {
return Err(changed(
"its creation time differed before and after its executable path was read",
));
}
if !process.running() {
return Err(changed("it exited while it was being identified"));
}
match server_pid(pipe) {
Ok(again) if again == pid => {}
_ => return Err(changed("the pipe no longer reports the same process id")),
}
let found = std::fs::canonicalize(&image).unwrap_or(image);
let id = file_id(&found);
if !expect
.executables
.iter()
.any(|expected| same_executable(expected, &found, id))
{
return Err(refuse(
Some(pid),
Some(plain(found)),
HostIdentityReason::ExecutableMismatch,
));
}
let host = HostProcess {
pid,
uid: None,
sid: first.sid,
executable: plain(found),
audit_token: None,
};
if let Some(verifier) = &expect.verifier {
if let Err(why) = verifier(&host) {
return Err(refuse(
Some(pid),
Some(host.executable.clone()),
HostIdentityReason::Refused(why),
));
}
}
drop(process);
Ok(host)
}
pub(super) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
let t = Process::open(pid).ok()?.created().ok()?;
Some(((u64::from(t.high) << 32) | u64::from(t.low), 0))
}
pub(crate) fn verify_pid_host(
peer: &PeerIdentity,
expect: &HostExpectation,
) -> Result<HostProcess, HostIdentityError> {
let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
HostIdentityError {
expected: expect.executables.clone(),
found_pid: pid,
found_executable: exe,
reason,
}
};
if expect.executables.is_empty() && !expect.any_executable {
return Err(refuse(None, None, HostIdentityReason::NoExpectation));
}
let Some(pid) = peer.pid else {
return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
};
let Some(sid) = peer.sid.clone() else {
return Err(refuse(
Some(pid),
None,
HostIdentityReason::Query("no user SID was recorded for the peer".to_string()),
));
};
let unreadable = |what: &str, e: io::Error| {
refuse(
Some(pid),
None,
HostIdentityReason::Unreadable(format!("{what}: {e}")),
)
};
let changed = |why: &str| {
refuse(
Some(pid),
None,
HostIdentityReason::HostChanged(why.to_string()),
)
};
let process = match Process::open(pid) {
Ok(process) => process,
Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
return Err(changed("it exited before it could be opened"))
}
Err(e) => return Err(unreadable("open process", e)),
};
let before = process
.created()
.map_err(|e| unreadable("creation time", e))?;
let marker = ((u64::from(before.high) << 32) | u64::from(before.low), 0);
if peer.started != Some(marker) {
return Err(changed(concat!(
"the process is not the one that connected (its creation time differs ",
"from the one recorded at connect, or none was recorded)"
)));
}
let image = process
.image()
.map_err(|e| unreadable("executable path", e))?;
let after = process
.created()
.map_err(|e| unreadable("creation time", e))?;
if before != after {
return Err(changed(
"its creation time differed before and after its executable path was read",
));
}
if !process.running() {
return Err(changed("it exited while it was being identified"));
}
let found = std::fs::canonicalize(&image).unwrap_or(image);
let id = file_id(&found);
if !expect.any_executable
&& !expect
.executables
.iter()
.any(|expected| same_executable(expected, &found, id))
{
return Err(refuse(
Some(pid),
Some(plain(found)),
HostIdentityReason::ExecutableMismatch,
));
}
let host = HostProcess {
pid,
uid: None,
sid: Some(sid),
executable: plain(found),
audit_token: None,
};
if let Some(verifier) = &expect.verifier {
if let Err(why) = verifier(&host) {
return Err(refuse(
Some(pid),
Some(host.executable.clone()),
HostIdentityReason::Refused(why),
));
}
}
drop(process);
Ok(host)
}
}