pub fn read_process_file_handles(pid: u32) -> std::io::Result<Vec<String>> {
#[cfg(target_os = "linux")]
{
linux_impl::read_process_file_handles(pid)
}
#[cfg(target_os = "macos")]
{
macos_impl::read_process_file_handles(pid)
}
#[cfg(target_os = "windows")]
{
windows_impl::read_process_file_handles(pid)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
let _ = pid;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"#539: no LaunchedProcessTree handle-snapshot backend planned for this OS",
))
}
}
#[cfg(target_os = "windows")]
mod windows_impl {
use std::ffi::c_void;
use winapi::shared::minwindef::FALSE;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcess};
use winapi::um::winnt::{DUPLICATE_SAME_ACCESS, HANDLE, PROCESS_DUP_HANDLE};
const SYSTEM_EXTENDED_HANDLE_INFORMATION: i32 = 64;
const OBJECT_TYPE_INFORMATION: i32 = 2;
const OBJECT_NAME_INFORMATION: i32 = 1;
const STATUS_SUCCESS: i32 = 0;
const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC0000004u32 as i32;
#[repr(C)]
#[derive(Copy, Clone)]
struct SystemHandleTableEntryInfoEx {
object: usize, unique_process_id: usize, handle_value: usize, granted_access: u32,
creator_back_trace_index: u16,
object_type_index: u16,
handle_attributes: u32,
reserved: u32,
}
#[repr(C)]
struct SystemHandleInformationExHeader {
number_of_handles: usize, reserved: usize, }
#[repr(C)]
#[derive(Copy, Clone)]
struct UnicodeString {
length: u16, maximum_length: u16, buffer: *mut u16,
}
#[link(name = "ntdll")]
extern "system" {
fn NtQuerySystemInformation(
system_information_class: i32,
system_information: *mut c_void,
system_information_length: u32,
return_length: *mut u32,
) -> i32;
fn NtQueryObject(
handle: HANDLE,
object_information_class: i32,
object_information: *mut c_void,
object_information_length: u32,
return_length: *mut u32,
) -> i32;
}
pub(super) fn read_process_file_handles(pid: u32) -> std::io::Result<Vec<String>> {
if pid == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"pid 0 is the system idle process — not queryable",
));
}
let raw = query_system_handles()?;
let header_size = std::mem::size_of::<SystemHandleInformationExHeader>();
if raw.len() < header_size {
return Err(std::io::Error::other("NtQuerySystemInformation returned < header bytes"));
}
let header = unsafe {
std::ptr::read(raw.as_ptr() as *const SystemHandleInformationExHeader)
};
let entry_size = std::mem::size_of::<SystemHandleTableEntryInfoEx>();
let max_entries = (raw.len() - header_size) / entry_size;
let entries_count = std::cmp::min(header.number_of_handles, max_entries);
let target_proc = unsafe { OpenProcess(PROCESS_DUP_HANDLE, FALSE, pid) };
if target_proc.is_null() {
return Err(std::io::Error::last_os_error());
}
let target_guard = ProcHandle(target_proc);
let mut handles = Vec::new();
let entries_ptr =
unsafe { raw.as_ptr().add(header_size) } as *const SystemHandleTableEntryInfoEx;
for i in 0..entries_count {
let entry = unsafe { std::ptr::read(entries_ptr.add(i)) };
if entry.unique_process_id as u32 != pid {
continue;
}
if let Some(path) = resolve_entry(target_guard.0, entry.handle_value as HANDLE) {
handles.push(path);
}
}
Ok(handles)
}
fn query_system_handles() -> std::io::Result<Vec<u8>> {
let mut size: u32 = 256 * 1024;
loop {
let mut buf = vec![0u8; size as usize];
let mut returned: u32 = 0;
let status = unsafe {
NtQuerySystemInformation(
SYSTEM_EXTENDED_HANDLE_INFORMATION,
buf.as_mut_ptr() as *mut c_void,
size,
&mut returned,
)
};
if status == STATUS_SUCCESS {
let used = returned.max(1) as usize;
buf.truncate(used.min(buf.len()));
return Ok(buf);
}
if status == STATUS_INFO_LENGTH_MISMATCH {
if size >= 256 * 1024 * 1024 {
return Err(std::io::Error::other(format!(
"NtQuerySystemInformation handle table exceeds 256 MiB (returned hint={returned})",
)));
}
size = size.saturating_mul(2).max(returned.saturating_add(64 * 1024));
continue;
}
return Err(std::io::Error::other(format!(
"NtQuerySystemInformation returned status=0x{:08x}",
status as u32,
)));
}
}
fn resolve_entry(target_proc: HANDLE, foreign_handle: HANDLE) -> Option<String> {
use winapi::um::handleapi::DuplicateHandle;
let mut local_handle: HANDLE = std::ptr::null_mut();
let ok = unsafe {
DuplicateHandle(
target_proc,
foreign_handle,
GetCurrentProcess(),
&mut local_handle,
0,
FALSE,
DUPLICATE_SAME_ACCESS,
)
};
if ok == FALSE || local_handle.is_null() {
return None;
}
let local_guard = ProcHandle(local_handle);
let type_name = query_object_string(local_guard.0, OBJECT_TYPE_INFORMATION)?;
if type_name != "File" {
return None;
}
query_object_string(local_guard.0, OBJECT_NAME_INFORMATION).filter(|s| !s.is_empty())
}
fn query_object_string(handle: HANDLE, info_class: i32) -> Option<String> {
let mut size: u32 = 4 * 1024;
loop {
let mut buf = vec![0u8; size as usize];
let mut returned: u32 = 0;
let status = unsafe {
NtQueryObject(
handle,
info_class,
buf.as_mut_ptr() as *mut c_void,
size,
&mut returned,
)
};
if status == STATUS_SUCCESS {
buf.truncate((returned as usize).min(buf.len()));
return parse_leading_unicode_string(&buf);
}
if status == STATUS_INFO_LENGTH_MISMATCH {
if size >= 1024 * 1024 {
return None;
}
size = size.saturating_mul(2).max(returned);
continue;
}
return None;
}
}
fn parse_leading_unicode_string(buf: &[u8]) -> Option<String> {
let header_size = std::mem::size_of::<UnicodeString>();
if buf.len() < header_size {
return None;
}
let us = unsafe { std::ptr::read(buf.as_ptr() as *const UnicodeString) };
let len_bytes = us.length as usize;
if len_bytes == 0 || us.buffer.is_null() {
return Some(String::new());
}
let buf_start = buf.as_ptr() as usize;
let buf_end = buf_start + buf.len();
let buffer_addr = us.buffer as usize;
if buffer_addr < buf_start || buffer_addr.saturating_add(len_bytes) > buf_end {
return None;
}
let wide: &[u16] = unsafe {
std::slice::from_raw_parts(us.buffer as *const u16, len_bytes / 2)
};
Some(String::from_utf16_lossy(wide))
}
struct ProcHandle(HANDLE);
impl Drop for ProcHandle {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { CloseHandle(self.0) };
}
}
}
}
#[cfg(target_os = "linux")]
mod linux_impl {
pub(super) fn read_process_file_handles(pid: u32) -> std::io::Result<Vec<String>> {
if pid == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"pid 0 is the kernel scheduler — not queryable",
));
}
let dir = format!("/proc/{pid}/fd");
let entries = std::fs::read_dir(&dir)?;
let mut handles = Vec::new();
for entry in entries {
let Ok(entry) = entry else { continue };
let Ok(target) = std::fs::read_link(entry.path()) else {
continue;
};
handles.push(target.to_string_lossy().into_owned());
}
Ok(handles)
}
}
#[cfg(target_os = "macos")]
mod macos_impl {
const PROC_PIDLISTFDS: libc::c_int = 1;
const PROC_PIDFDVNODEPATHINFO: libc::c_int = 2;
const PROX_FDTYPE_VNODE: u32 = 1;
const MAXPATHLEN: usize = 1024;
#[repr(C)]
#[derive(Copy, Clone)]
struct ProcFdInfo {
proc_fd: i32,
proc_fdtype: u32,
}
const VNODE_FDINFOWITHPATH_SIZE: usize = 1200;
const VIP_PATH_OFFSET: usize = 176;
#[repr(C)]
struct VnodeFdInfoWithPath {
_opaque: [u8; VNODE_FDINFOWITHPATH_SIZE],
}
pub(super) fn read_process_file_handles(pid: u32) -> std::io::Result<Vec<String>> {
if pid == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"pid 0 is the kernel scheduler — not queryable",
));
}
let size = unsafe {
libc::proc_pidinfo(
pid as libc::c_int,
PROC_PIDLISTFDS,
0,
std::ptr::null_mut(),
0,
)
};
if size <= 0 {
return Err(std::io::Error::last_os_error());
}
let entry_size = std::mem::size_of::<ProcFdInfo>();
let count = (size as usize) / entry_size;
let mut fds: Vec<ProcFdInfo> = vec![ProcFdInfo { proc_fd: 0, proc_fdtype: 0 }; count];
let written = unsafe {
libc::proc_pidinfo(
pid as libc::c_int,
PROC_PIDLISTFDS,
0,
fds.as_mut_ptr() as *mut libc::c_void,
(count * entry_size) as libc::c_int,
)
};
if written <= 0 {
return Err(std::io::Error::last_os_error());
}
let written_count = (written as usize) / entry_size;
fds.truncate(written_count);
let mut handles = Vec::new();
for fd in &fds {
if fd.proc_fdtype != PROX_FDTYPE_VNODE {
continue;
}
let mut info: VnodeFdInfoWithPath = unsafe { std::mem::zeroed() };
let n = unsafe {
libc::proc_pidfdinfo(
pid as libc::c_int,
fd.proc_fd,
PROC_PIDFDVNODEPATHINFO,
&mut info as *mut VnodeFdInfoWithPath as *mut libc::c_void,
std::mem::size_of::<VnodeFdInfoWithPath>() as libc::c_int,
)
};
if n <= 0 {
continue;
}
let path_bytes = &info._opaque[VIP_PATH_OFFSET..VIP_PATH_OFFSET + MAXPATHLEN];
let nul = path_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(path_bytes.len());
if nul == 0 {
continue;
}
let path = String::from_utf8_lossy(&path_bytes[..nul]).into_owned();
handles.push(path);
}
Ok(handles)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[test]
fn read_handles_for_pid_zero_returns_invalid_input() {
let err = read_process_file_handles(0).expect_err("pid 0 should be rejected");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
#[test]
fn self_snapshot_includes_a_temp_file_we_just_opened() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let path = tmp.path().to_path_buf();
let canonical = std::fs::canonicalize(&path).unwrap_or(path.clone());
let filename = path
.file_name()
.and_then(|s| s.to_str())
.map(str::to_owned)
.unwrap_or_default();
let handles =
read_process_file_handles(std::process::id()).expect("read self handles");
let canonical_str = canonical.to_string_lossy();
let raw_str = path.to_string_lossy();
let found = handles.iter().any(|h| {
h == canonical_str.as_ref()
|| h == raw_str.as_ref()
|| (!filename.is_empty() && h.ends_with(&filename))
});
assert!(
found,
"expected temp file (filename={filename}, canonical={canonical_str}, raw={raw_str}) in handles, got {handles:?}",
);
drop(tmp);
}
}