From 0fb279d31c054386ac9a30e3ca8390ba1e3cc6e3 Mon Sep 17 00:00:00 2001 From: wandalen Date: Fri, 16 Aug 2024 21:07:10 +0300 Subject: [PATCH 1/3] [fuser] basic implementation --- process/Cargo.toml | 7 + process/fuser.rs | 1366 ++++++++++++++++++++++++++++++++ process/tests/fuser/mod.rs | 346 ++++++++ process/tests/process-tests.rs | 1 + 4 files changed, 1720 insertions(+) create mode 100644 process/fuser.rs create mode 100644 process/tests/fuser/mod.rs diff --git a/process/Cargo.toml b/process/Cargo.toml index b76a93c9..3c90f943 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -15,6 +15,13 @@ atty.workspace = true errno = "0.3" dirs = "5.0" +[dev-dependencies] +tokio = { version = "1.39", features = ["net", "macros", "rt"]} + +[[bin]] +name = "fuser" +path = "./fuser.rs" + [[bin]] name = "env" path = "./env.rs" diff --git a/process/fuser.rs b/process/fuser.rs new file mode 100644 index 00000000..068db5d6 --- /dev/null +++ b/process/fuser.rs @@ -0,0 +1,1366 @@ +extern crate clap; +extern crate libc; +extern crate plib; + +use clap::Parser; +use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; +use libc::fstat; +use plib::PROJECT_NAME; +use std::{ + collections::BTreeMap, + env, + ffi::{CStr, CString}, + fmt, + fs::{self, File}, + io::{self, BufRead, Error, ErrorKind, Write}, + net::{IpAddr, Ipv4Addr, UdpSocket}, + os::unix::io::AsRawFd, + path::{Component, Path, PathBuf}, + sync::mpsc, + thread, + time::Duration, +}; + +const PROC_PATH: &'static str = "/proc"; +const PROC_MOUNTS: &'static str = "/proc/mounts"; +const NAME_FIELD: usize = 20; + +#[derive(Clone, Default, PartialEq)] +enum ProcType { + #[default] + Normal = 0, + Mount = 1, + Knfsd = 2, + Swap = 3, +} + +#[derive(Clone, Default, PartialEq)] +enum NameSpace { + #[default] + File = 0, + Tcp = 1, + Udp = 2, +} + +#[derive(Clone, Default, PartialEq)] +enum Access { + Cwd = 1, + Exe = 2, + #[default] + File = 4, + Root = 8, + Mmap = 16, + Filewr = 32, +} + +#[derive(Clone)] +struct IpConnections { + names: Names, + lcl_port: u64, + rmt_port: u64, + rmt_addr: IpAddr, + next: Option>, +} + +impl Default for IpConnections { + fn default() -> Self { + IpConnections { + names: Names::default(), + lcl_port: 0, + rmt_port: 0, + rmt_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + next: None, + } + } +} + +impl IpConnections { + fn new(names: Names, lcl_port: u64, rmt_port: u64, rmt_addr: IpAddr) -> Self { + IpConnections { + names, + lcl_port, + rmt_port, + rmt_addr, + next: None, + } + } + + fn iter(&self) -> IpConnectionsIterator { + IpConnectionsIterator { + current: Some(self), + } + } +} + +struct IpConnectionsIterator<'a> { + current: Option<&'a IpConnections>, +} + +impl<'a> Iterator for IpConnectionsIterator<'a> { + type Item = &'a IpConnections; + + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } +} + +#[derive(Clone, Default)] +struct Procs { + pid: i32, + uid: u32, + access: Access, + proc_type: ProcType, + username: Option, + command: String, +} + +impl Procs { + fn new(pid: i32, uid: u32, access: Access, proc_type: ProcType, command: String) -> Self { + Self { + pid, + uid, + access, + proc_type, + username: None, + command, + } + } +} + +#[derive(Default, Clone)] +struct UnixSocketList { + name: String, + device_id: u64, + inode: u64, + net_inode: u64, + next: Option>, +} + +impl UnixSocketList { + fn new(name: String, device_id: u64, inode: u64, net_inode: u64) -> Self { + UnixSocketList { + name, + device_id, + inode, + net_inode, + next: None, + } + } + + fn add_socket(&mut self, name: String, device_id: u64, inode: u64, net_inode: u64) { + let new_node = Box::new(UnixSocketList { + name, + device_id, + net_inode, + inode, + next: self.next.take(), + }); + + self.next = Some(new_node); + } + + fn iter(&self) -> UnixSocketListIterator { + UnixSocketListIterator { + current: Some(self), + } + } +} + +struct UnixSocketListIterator<'a> { + current: Option<&'a UnixSocketList>, +} + +impl<'a> Iterator for UnixSocketListIterator<'a> { + type Item = &'a UnixSocketList; + + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } +} + +#[derive(Default)] +struct InodeList { + name: Names, + device_id: u64, + inode: u64, + next: Option>, +} + +impl InodeList { + fn new(name: Names, device_id: u64, inode: u64) -> Self { + InodeList { + name, + device_id, + inode, + next: None, + } + } + + fn iter(&self) -> InodeListIterator { + InodeListIterator { + current: Some(self), + } + } +} + +struct InodeListIterator<'a> { + current: Option<&'a InodeList>, +} + +impl<'a> Iterator for InodeListIterator<'a> { + type Item = &'a InodeList; + + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } +} + +#[derive(Default, Clone)] +struct MountList { + mountpoints: Vec, +} + +struct LibcStat { + inner: libc::stat, +} + +impl Default for LibcStat { + fn default() -> Self { + LibcStat { + inner: unsafe { std::mem::zeroed() }, + } + } +} + +impl Clone for LibcStat { + fn clone(&self) -> Self { + LibcStat { inner: self.inner } + } +} + +#[derive(Clone, Default)] +struct Names { + filename: PathBuf, + name_space: NameSpace, + matched_procs: Vec, + st: LibcStat, +} + +impl Names { + fn new( + filename: PathBuf, + name_space: NameSpace, + st: LibcStat, + matched_procs: Vec, + ) -> Self { + Names { + filename, + name_space, + st, + matched_procs, + } + } + + fn add_procs(&mut self, proc: Procs) { + let exists = self + .matched_procs + .iter() + .any(|p| p.access == proc.access && p.pid == proc.pid); + + if !exists { + self.matched_procs.push(proc); + } + } +} + +#[derive(Default)] +struct DeviceList { + name: Names, + device_id: u64, + next: Option>, +} + +impl DeviceList { + fn new(name: Names, device_id: u64) -> Self { + DeviceList { + name, + device_id, + next: None, + } + } + fn iter(&self) -> DeviceListIterator { + DeviceListIterator { + current: Some(self), + } + } +} + +struct DeviceListIterator<'a> { + current: Option<&'a DeviceList>, +} + +impl<'a> Iterator for DeviceListIterator<'a> { + type Item = &'a DeviceList; + + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } +} + +/// fuser - list process IDs of all processes that have one or more files open +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// The file is treated as a mount point and the utility shall report on any files open in the file system. + #[arg(short = 'c')] + mount: bool, + /// The report shall be only for the named files. + #[arg(short = 'f')] + named_files: bool, + /// The user name, in parentheses, associated with each process ID written to standard output shall be written to standard error. + #[arg(short = 'u')] + user: bool, + + #[arg(required = true, name = "FILE", num_args(0..))] + /// A pathname on which the file or file system is to be reported. + file: Vec, +} +use clap::CommandFactory; +fn main() -> Result<(), Box> { + setlocale(LocaleCategory::LcAll, ""); + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + let Args { + mount, user, file, .. + } = Args::try_parse().unwrap_or_else(|err| match err.kind() { + clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { + print!("{err}"); + std::process::exit(1); + } + _ => { + let mut stdout = std::io::stdout(); + let mut cmd = Args::command(); + eprintln!("No process specification given"); + cmd.write_help(&mut stdout).unwrap(); + std::process::exit(1); + } + }); + + let ( + mut names, + mut unix_socket_list, + mut mount_list, + mut device_list, + mut inode_list, + mut need_check_map, + ) = init_defaults(file); + + fill_unix_cache(&mut unix_socket_list)?; + + let net_dev = find_net_dev()?; + + for name in names.iter_mut() { + name.name_space = determine_namespace(&name.filename); + + match name.name_space { + NameSpace::File => handle_file_namespace( + name, + mount, + &mut mount_list, + &mut inode_list, + &mut device_list, + &mut need_check_map, + )?, + NameSpace::Tcp => { + if !mount { + handle_tcp_namespace(name, &mut inode_list, net_dev)? + } + } + NameSpace::Udp => { + if !mount { + handle_udp_namespace(name, &mut inode_list, net_dev)? + } + } + } + + if (scan_procs( + need_check_map, + name, + &inode_list, + &device_list, + &unix_socket_list, + net_dev, + )) + .is_err() + { + std::process::exit(1); + } + + print_matches(name, user)?; + } + + std::process::exit(0) +} + +/// Initializes and returns default values. +/// +/// # Arguments +/// +/// * `file` - A vector of `PathBuf` representing the file paths used to initialize `Names` objects. +/// +/// # Returns +/// +/// Returns a tuple containing: +/// +/// * Default-initialized `Vec`, UnixSocketList`, `MountList`, `DeviceList`, `InodeList`, `IpConnections` (TCP), and `IpConnections` (UDP). +/// * A boolean value set to `false`, indicating the initial state. +fn init_defaults( + files: Vec, +) -> ( + Vec, + UnixSocketList, + MountList, + DeviceList, + InodeList, + bool, +) { + let names_vec = files + .iter() + .map(|path| { + Names::new( + path.clone(), + NameSpace::default(), + LibcStat::default(), + vec![], + ) + }) + .collect(); + + ( + names_vec, + UnixSocketList::default(), + MountList::default(), + DeviceList::default(), + InodeList::default(), + false, + ) +} + +/// Determines the `NameSpace` based on the presence of "tcp" or "udp" in the path string. +/// +/// # Arguments +/// +/// * `filename` -`PathBuf`. +/// +/// # Returns +/// +/// Namespace type +fn determine_namespace(filename: &Path) -> NameSpace { + filename + .to_str() + .and_then(|name_str| { + let parts: Vec<&str> = name_str.split('/').collect(); + if parts.len() == 2 && parts[0].parse::().is_ok() { + match parts[1] { + "tcp" => Some(NameSpace::Tcp), + "udp" => Some(NameSpace::Udp), + _ => None, + } + } else { + None + } + }) + .unwrap_or_else(NameSpace::default) +} + +/// Processes file namespaces by expanding paths and updating lists based on the mount flag. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object containing the file path. +/// * `mount` - A boolean indicating whether to handle mount information or not. +/// * `mount_list` - A mutable reference to a `MountList` for reading mount points. +/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. +/// * `device_list` - A mutable reference to a `DeviceList` for updating device information. +/// +/// # Errors +/// +/// Returns an error if path expansion, file status retrieval, or `/proc/mounts` reading fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn handle_file_namespace( + names: &mut Names, + mount: bool, + mount_list: &mut MountList, + inode_list: &mut InodeList, + device_list: &mut DeviceList, + need_check_map: &mut bool, +) -> Result<(), std::io::Error> { + names.filename = expand_path(&names.filename)?; + let st = timeout(&names.filename.to_string_lossy(), 5)?; + read_proc_mounts(mount_list)?; + + if mount { + *device_list = DeviceList::new(names.clone(), st.st_dev); + *need_check_map = true; + } else { + let st = stat(&names.filename.to_string_lossy())?; + *inode_list = InodeList::new(names.clone(), st.st_dev, st.st_ino); + } + Ok(()) +} + +/// Handles TCP namespace processing by updating connection and inode lists. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object containing the file path. +/// * `tcp_connection_list` - A mutable reference to an `IpConnections` object for TCP connections. +/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. +/// * `net_dev` - A `u64` representing the network device identifier. +/// +/// # Errors +/// +/// Returns an error if parsing TCP connections or finding network sockets fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn handle_tcp_namespace( + names: &mut Names, + inode_list: &mut InodeList, + net_dev: u64, +) -> Result<(), std::io::Error> { + let tcp_connection_list = parse_inet(names)?; + *inode_list = find_net_sockets(&tcp_connection_list, "tcp", net_dev)?; + Ok(()) +} + +/// Handles UDP namespace processing by updating connection and inode lists. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object containing the file path. +/// * `udp_connection_list` - A mutable reference to an `IpConnections` object for UDP connections. +/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. +/// * `net_dev` - A `u64` representing the network device identifier. +/// +/// # Errors +/// +/// Returns an error if parsing UDP connections or finding network sockets fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn handle_udp_namespace( + names: &mut Names, + inode_list: &mut InodeList, + net_dev: u64, +) -> Result<(), std::io::Error> { + let udp_connection_list = parse_inet(names)?; + *inode_list = find_net_sockets(&udp_connection_list, "udp", net_dev)?; + Ok(()) +} + +/// Prints process matches for a given `Names` object to `stderr` and `stdout`. +/// +/// # Arguments +/// +/// * `name` - A mutable reference to a `Names` object containing matched processes. +/// * `user` - A boolean indicating whether to display the process owner name. +/// +/// # Errors +/// +/// Returns an error if flushing output to `stderr` or `stdout` fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn print_matches(name: &mut Names, user: bool) -> Result<(), io::Error> { + let mut proc_map: BTreeMap = BTreeMap::new(); + let mut name_has_procs = false; + let mut len = name.filename.to_string_lossy().len() + 1; + + eprint!("{}:", name.filename.display()); + while len < NAME_FIELD { + len += 1; + eprint!(" "); + } + io::stderr().flush()?; + + for procs in name.matched_procs.iter() { + if procs.proc_type == ProcType::Normal { + name_has_procs = true; + } + + let entry = proc_map + .entry(procs.pid) + .or_insert((String::new(), procs.uid)); + + match procs.access { + Access::Root => entry.0.push('r'), + Access::Cwd => entry.0.push('c'), + Access::Exe => entry.0.push('e'), + Access::Mmap => entry.0.push('m'), + _ => (), + } + } + + if !name_has_procs { + // exit if no processes matched + return Ok(()); + } + + for (pid, (access, uid)) in proc_map { + let width = if pid.to_string().len() > 4 { " " } else { " " }; + + print!("{}{}", width, pid); + io::stdout().flush()?; + + eprint!("{}", access); + if user { + let owner = unsafe { + let pw_entry = libc::getpwuid(uid); + if pw_entry.is_null() { + "unknownr" + } else { + CStr::from_ptr((*pw_entry).pw_name) + .to_str() + .unwrap_or("invalid_string") + } + }; + eprint!("({})", owner); + } + io::stderr().flush()?; + } + + eprintln!(); + Ok(()) +} + +/// Scans the `/proc` directory for process information and checks various access types. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for storing matched processes. +/// * `inode_list` - A reference to an `InodeList` for updating inode information. +/// * `device_list` - A reference to a `DeviceList` for updating device information. +/// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. +/// * `net_dev` - A `u64` representing the network device identifier. +/// +/// # Errors +/// +/// Returns an error if reading directory entries, accessing process stats, or checking access types fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn scan_procs( + need_check_map: bool, + names: &mut Names, + inode_list: &InodeList, + device_list: &DeviceList, + unix_socket_list: &UnixSocketList, + net_dev: u64, +) -> Result<(), io::Error> { + let my_pid = std::process::id() as i32; + let dir_entries = fs::read_dir(PROC_PATH)?; + + for entry in dir_entries { + let entry = entry?; + let filename = entry + .file_name() + .into_string() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid file name"))?; + + if let Ok(pid) = filename.parse::() { + // Skip the current process + if pid == my_pid { + continue; + } + + let cwd_stat = match get_pid_stat(pid, "/cwd") { + Ok(stat) => stat, + Err(_) => continue, + }; + + let exe_stat = match get_pid_stat(pid, "/exe") { + Ok(stat) => stat, + Err(_) => continue, + }; + let root_stat = match get_pid_stat(pid, "/root") { + Ok(stat) => stat, + Err(_) => continue, + }; + + let st = timeout(&entry.path().to_string_lossy(), 5)?; + let uid = st.st_uid; + + check_root_access(names, pid, uid, &root_stat, device_list, inode_list)?; + check_cwd_access(names, pid, uid, &cwd_stat, device_list, inode_list)?; + check_exe_access(names, pid, uid, &exe_stat, device_list, inode_list)?; + + if need_check_map { + check_map(names, pid, "maps", device_list, uid, Access::Mmap)?; + } + + check_dir( + names, + pid, + "fd", + device_list, + inode_list, + uid, + Access::File, + unix_socket_list, + net_dev, + )?; + } + } + + Ok(()) +} + +/// Checks if a process has access to the root directory and updates the `Names` object if it does. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for adding process information. +/// * `pid` - The process ID to check. +/// * `uid` - The user ID of the process. +/// * `root_stat` - A reference to a `libc::stat` structure containing root directory information. +/// * `device_list` - A reference to a `DeviceList` for checking device IDs. +/// * `inode_list` - A reference to an `InodeList` for checking inode information. +/// +/// # Errors +/// +/// Returns an error if adding process information fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn check_root_access( + names: &mut Names, + pid: i32, + uid: u32, + root_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, +) -> Result<(), io::Error> { + if device_list + .iter() + .any(|device| device.device_id == root_stat.st_dev) + { + add_process(names, pid, uid, Access::Root, ProcType::Normal, None); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == root_stat.st_dev && inode.inode == root_stat.st_ino) + { + add_process(names, pid, uid, Access::Root, ProcType::Normal, None); + return Ok(()); + } + + Ok(()) +} + +/// Checks if a process has access to the current working directory and updates the `Names` object if it does. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for adding process information. +/// * `pid` - The process ID to check. +/// * `uid` - The user ID of the process. +/// * `cwd_stat` - A reference to a `libc::stat` structure containing current working directory information. +/// * `device_list` - A reference to a `DeviceList` for checking device IDs. +/// * `inode_list` - A reference to an `InodeList` for checking inode information. +/// +/// # Errors +/// +/// Returns an error if adding process information fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn check_cwd_access( + names: &mut Names, + pid: i32, + uid: u32, + cwd_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, +) -> Result<(), std::io::Error> { + if device_list + .iter() + .any(|device| device.device_id == cwd_stat.st_dev) + { + add_process(names, pid, uid, Access::Cwd, ProcType::Normal, None); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == cwd_stat.st_dev && inode.inode == cwd_stat.st_ino) + { + add_process(names, pid, uid, Access::Cwd, ProcType::Normal, None); + return Ok(()); + } + + Ok(()) +} + +/// Checks if a process has access to the executable file and updates the `Names` object if it does. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for adding process information. +/// * `pid` - The process ID to check. +/// * `uid` - The user ID of the process. +/// * `exe_stat` - A reference to a `libc::stat` structure containing executable file information. +/// * `device_list` - A reference to a `DeviceList` for checking device IDs. +/// * `inode_list` - A reference to an `InodeList` for checking inode information. +/// +/// # Errors +/// +/// Returns an error if adding process information fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn check_exe_access( + names: &mut Names, + pid: i32, + uid: u32, + exe_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, +) -> Result<(), io::Error> { + if device_list + .iter() + .any(|device| device.device_id == exe_stat.st_dev) + { + add_process(names, pid, uid, Access::Exe, ProcType::Normal, None); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == exe_stat.st_dev && inode.inode == exe_stat.st_ino) + { + add_process(names, pid, uid, Access::Exe, ProcType::Normal, None); + return Ok(()); + } + + Ok(()) +} + +/// Adds a new process to the `Names` object with specified access and process type. +fn add_process( + names: &mut Names, + pid: i32, + uid: u32, + access: Access, + proc_type: ProcType, + command: Option, +) { + let proc = Procs::new(pid, uid, access, proc_type, command.unwrap_or_default()); + names.add_procs(proc); +} + +/// Checks a directory within a process's `/proc` entry for matching devices and inodes, +/// and updates the `Names` object with relevant process information. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for adding process information. +/// * `pid` - The process ID whose directory is being checked. +/// * `dirname` - The name of the directory to check (e.g., "fd"). +/// * `device_list` - A reference to a `DeviceList` for checking device IDs. +/// * `inode_list` - A reference to an `InodeList` for checking inode information. +/// * `uid` - The user ID of the process. +/// * `access` - The type of access to assign (e.g., File, Filewr). +/// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. +/// * `net_dev` - A `u64` representing the network device identifier. +/// +/// # Errors +/// +/// Returns an error if reading directory entries or accessing file stats fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn check_dir( + names: &mut Names, + pid: i32, + dirname: &str, + device_list: &DeviceList, + inode_list: &InodeList, + uid: u32, + access: Access, + unix_socket_list: &UnixSocketList, + net_dev: u64, +) -> Result<(), io::Error> { + let dir_path = format!("/proc/{}/{}", pid, dirname); + let dir_entries = fs::read_dir(&dir_path)?; + for entry in dir_entries { + let entry = entry?; + let path = entry.path(); + let path_str = path.to_string_lossy(); + + let mut stat = match timeout(&path_str, 5) { + Ok(stat) => stat, + Err(_) => continue, + }; + + if stat.st_dev == net_dev { + if let Some(unix_socket) = unix_socket_list + .iter() + .find(|sock| sock.net_inode == stat.st_ino) + { + stat.st_dev = unix_socket.device_id; + stat.st_ino = unix_socket.inode; + } + } + + let new_access = match access { + Access::File => Access::Filewr, + _ => access.clone(), + }; + if device_list + .iter() + .any(|dev| dev.name.filename != PathBuf::from("") && stat.st_dev == dev.device_id) + || inode_list.iter().any(|inode| inode.inode == stat.st_ino) + { + add_process(names, pid, uid, new_access, ProcType::Normal, None); + } + } + + Ok(()) +} + +/// Checks the memory map of a process for matching devices and updates the `Names` object. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` object for adding process information. +/// * `pid` - The process ID whose memory map is being checked. +/// * `filename` - The name of the file containing the memory map (e.g., "maps"). +/// * `device_list` - A reference to a `DeviceList` for checking device IDs. +/// * `uid` - The user ID of the process. +/// * `access` - The type of access to assign (e.g., Mmap). +/// +/// # Errors +/// +/// Returns an error if opening the file or reading lines fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn check_map( + names: &mut Names, + pid: i32, + filename: &str, + device_list: &DeviceList, + uid: u32, + access: Access, +) -> Result<(), io::Error> { + let already_exists = names.matched_procs.iter().any(|p| p.pid == pid); + + if already_exists { + return Ok(()); + } + + let pathname = format!("/proc/{}/{}", pid, filename); + let file = File::open(&pathname)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 5 { + let dev_info: Vec<&str> = parts[3].split(':').collect(); + if dev_info.len() == 2 { + let tmp_maj = match u32::from_str_radix(dev_info[0], 16) { + Ok(value) => value, + Err(_) => continue, + }; + + let tmp_min = match u32::from_str_radix(dev_info[1], 16) { + Ok(value) => value, + Err(_) => continue, + }; + + let device = tmp_maj * 256 + tmp_min; + let device_u64 = device as u64; + + if device_list + .iter() + .any(|device| device.device_id == device_u64) + { + add_process(names, pid, uid, access.clone(), ProcType::Normal, None); + } + } + } + } + Ok(()) +} + +/// get stat of current /proc/{pid}/{filename} +fn get_pid_stat(pid: i32, filename: &str) -> Result { + let path = format!("{}/{}{}", PROC_PATH, pid, filename); + timeout(&path, 5) +} + +/// Fills the `unix_socket_list` with information from the `/proc/net/unix` file. +/// +/// # Arguments +/// +/// * `unix_socket_list` - A mutable reference to a `UnixSocketList` to be populated with socket information. +/// +/// # Errors +/// +/// Returns an error if opening the file or reading lines fails. +/// +/// # Returns +/// +/// Returns `Ok(())` on success. +fn fill_unix_cache(unix_socket_list: &mut UnixSocketList) -> Result<(), io::Error> { + let file = File::open("/proc/net/unix")?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + + if let (Some(net_inode_str), Some(scanned_path)) = (parts.get(6), parts.get(7)) { + let net_inode = net_inode_str.parse().unwrap_or(0); + let path = normalize_path(scanned_path); + + match timeout(&path, 5) { + Ok(stat) => UnixSocketList::add_socket( + unix_socket_list, + scanned_path.to_string(), + stat.st_dev, + stat.st_ino, + net_inode, + ), + Err(_) => continue, + } + } + } + Ok(()) +} + +/// Reads the `/proc/mounts` file and updates the `mount_list` with mount points. +/// +/// # Arguments +/// +/// * `mount_list` - A mutable reference to a `MountList` for storing the mount points. +/// +/// # Errors +/// +/// Returns an error if opening the file or reading lines fails. +/// +/// # Returns +/// +/// Returns `Ok(mount_list)` on success, with the `mount_list` updated. +fn read_proc_mounts(mount_list: &mut MountList) -> io::Result<&mut MountList> { + let file = File::open(PROC_MOUNTS)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + let mountpoint = PathBuf::from(parts[1].trim()); + mount_list.mountpoints.push(mountpoint); + } + + Ok(mount_list) +} + +/// Normalizes a file path by removing the leading '@' character if present. +fn normalize_path(scanned_path: &str) -> String { + if let Some(path) = scanned_path.strip_prefix('@') { + path.to_string() + } else { + scanned_path.to_string() + } +} + +/// Parses network socket information from the `filename` field of the `Names` struct +/// and returns an `IpConnections` instance. +/// +/// # Arguments +/// +/// * `names` - A mutable reference to a `Names` struct containing the filename to parse. +/// * `ip_list` - A mutable reference to an `IpConnections` instance to be populated. +/// +/// # Errors +/// +/// Returns an error if the filename format is invalid or if parsing fails. +/// +/// # Returns +/// +/// Returns an `IpConnections` instance populated with parsed network information. +fn parse_inet(names: &mut Names) -> Result { + let filename_str = names.filename.to_string_lossy(); + let parts: Vec<&str> = filename_str.split('/').collect(); + + if parts.len() < 2 { + return Err(Error::new( + ErrorKind::InvalidInput, + "Invalid filename format", + )); + } + + let hostspec = parts[0]; + let host_parts: Vec<&str> = hostspec.split(',').collect(); + + let lcl_port_str = host_parts + .first() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "Local port is missing"))?; + let rmt_addr_str = host_parts.get(1).cloned(); + let rmt_port_str = host_parts.get(2).cloned(); + + let lcl_port = lcl_port_str + .parse::() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid local port format"))?; + + let rmt_port = rmt_port_str + .as_ref() + .map(|s| { + s.parse::() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid remote port format")) + }) + .transpose()? + .unwrap_or(0); + + let rmt_addr = match rmt_addr_str { + Some(addr_str) => match addr_str.parse::() { + Ok(addr) => addr, + Err(_) => { + eprintln!("Warning: Invalid remote address {}", addr_str); + IpAddr::V4(Ipv4Addr::UNSPECIFIED) // Default value if address parsing fails + } + }, + None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), // Default value if address is not provided + }; + + Ok(IpConnections::new( + names.clone(), + lcl_port, + rmt_port, + rmt_addr, + )) +} +/// Retrieves the device identifier of the network interface associated with a UDP socket. +/// +/// # Errors +/// +/// Returns an error if binding the socket or retrieving the device identifier fails. +/// +/// # Returns +/// +/// Returns the device identifier (`u64`) of the network interface. +fn find_net_dev() -> Result { + let socket = UdpSocket::bind("0.0.0.0:0")?; + let fd = socket.as_raw_fd(); + let mut stat_buf = unsafe { std::mem::zeroed() }; + + unsafe { + if fstat(fd, &mut stat_buf) != 0 { + return Err(io::Error::last_os_error()); + } + } + + Ok(stat_buf.st_dev as u64) +} + +/// Finds network sockets based on the given protocol and updates the `InodeList` +/// with the relevant inode information if a matching connection is found. +/// +/// # Arguments +/// +/// * `inode_list` - A mutable reference to the `InodeList` that will be updated. +/// * `connections_list` - A reference to the `IpConnections` that will be used to match connections. +/// * `protocol` - A `&str` representing the protocol (e.g., "tcp", "udp") to look for in `/proc/net`. +/// * `net_dev` - A `u64` representing the network device identifier. +/// +/// # Errors +/// +/// Returns an `io::Error` if there is an issue opening or reading the file at `/proc/net/{protocol}`, or +/// if parsing the net sockets fails. +/// +/// # Returns +/// +/// Returns an `InodeList` containing the updated information if a matching connection is found. +/// Returns an `io::Error` with `ErrorKind::ConnectionRefused` if can't parse sockets. + +fn find_net_sockets( + connections_list: &IpConnections, + protocol: &str, + net_dev: u64, +) -> Result { + let pathname = format!("/proc/net/{}", protocol); + + let file = File::open(&pathname)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + + let parts: Vec<&str> = line.split_whitespace().collect(); + let parse_hex_port = |port_str: &str| -> Option { + port_str + .split(':') + .nth(1) + .and_then(|s| u64::from_str_radix(s, 16).ok()) + }; + + let loc_port = parts.get(1).and_then(|&s| parse_hex_port(s)); + let rmt_port = parts.get(2).and_then(|&s| parse_hex_port(s)); + let scanned_inode = parts.get(9).and_then(|&s| s.parse::().ok()); + + if let Some(scanned_inode) = scanned_inode { + for connection in connections_list.iter() { + let loc_port = loc_port.unwrap_or(0); + let rmt_port = rmt_port.unwrap_or(0); + let rmt_addr = parse_ipv4_addr(parts[2].split(':').next().unwrap_or("")) + .unwrap_or(Ipv4Addr::UNSPECIFIED); + + if (connection.lcl_port == 0 || connection.lcl_port == loc_port) + && (connection.rmt_port == 0 || connection.rmt_port == rmt_port) + && (connection.rmt_addr == Ipv4Addr::UNSPECIFIED + || connection.rmt_addr == rmt_addr) + { + return Ok(InodeList::new( + connection.names.clone(), + net_dev, + scanned_inode, + )); + } + } + } + } + + Err(Error::new( + ErrorKind::ConnectionRefused, + "Cannot parse net sockets", + )) +} + +/// Parses a hexadecimal string representation of an IPv4 address. +fn parse_ipv4_addr(addr: &str) -> Option { + if addr.len() == 8 { + let octets = [ + u8::from_str_radix(&addr[0..2], 16).ok()?, + u8::from_str_radix(&addr[2..4], 16).ok()?, + u8::from_str_radix(&addr[4..6], 16).ok()?, + u8::from_str_radix(&addr[6..8], 16).ok()?, + ]; + Some(Ipv4Addr::from(octets)) + } else { + None + } +} + +/// Retrieves the status of a file given its filename. +fn stat(filename_str: &str) -> io::Result { + let filename = CString::new(filename_str)?; + + unsafe { + let mut st: libc::stat = std::mem::zeroed(); + let rc = libc::stat(filename.as_ptr(), &mut st); + if rc == 0 { + Ok(st) + } else { + Err(io::Error::last_os_error()) + } + } +} + +/// Execute stat() system call with timeout to avoid deadlock +/// on network based file systems. +fn timeout(path: &str, seconds: u32) -> Result { + let (tx, rx) = mpsc::channel(); + + thread::scope(|s| { + s.spawn(|| { + if let Err(e) = tx.send(stat(path)) { + eprintln!("Failed to send result through channel: {}", e); + } + }); + }); + + match rx.recv_timeout(Duration::from_secs(seconds.into())) { + Ok(stat) => stat, + Err(mpsc::RecvTimeoutError::Timeout) => Err(io::Error::new( + io::ErrorKind::TimedOut, + "Operation timed out", + )), + Err(mpsc::RecvTimeoutError::Disconnected) => { + Err(io::Error::new(io::ErrorKind::Other, "Channel disconnected")) + } + } +} + +/// This function handles relative paths by resolving them against the current working directory to absolute path +/// +/// # Arguments +/// +/// * `path` - [str](std::str) that represents the file path. +/// +/// # Errors +/// +/// Returns an error if passed invalid input. +/// +/// # Returns +/// +/// Returns PathBuf real_path. +pub fn expand_path(path: &PathBuf) -> Result { + let mut real_path = if path.starts_with(Path::new("/")) { + PathBuf::from("/") + } else { + env::current_dir()? + }; + + for component in Path::new(path).components() { + match component { + Component::CurDir => { + // Ignore '.' + } + Component::ParentDir => { + // Handle '..' by moving up one directory level if possible + real_path.pop(); + } + Component::Normal(name) => { + // Append directory or file name + real_path.push(name); + } + Component::RootDir | Component::Prefix(_) => { + // Handle root directory or prefix + real_path = PathBuf::from(component.as_os_str()); + } + } + } + + if real_path.as_os_str() != "/" && real_path.as_os_str().to_string_lossy().ends_with('/') { + real_path.pop(); + } + + Ok(real_path) +} diff --git a/process/tests/fuser/mod.rs b/process/tests/fuser/mod.rs new file mode 100644 index 00000000..f71cfd11 --- /dev/null +++ b/process/tests/fuser/mod.rs @@ -0,0 +1,346 @@ +use libc::uid_t; +use plib::{run_test_with_checker, TestPlan}; +use std::{ + ffi::CStr, + fs::{self, File}, + io::{self, Read}, + path::{Path, PathBuf}, + process::{Command, Output}, + str, +}; +use tokio::net::{TcpListener, UdpSocket, UnixListener}; + +fn fuser_test( + args: Vec, + expected_err: &str, + expected_exit_code: i32, + checker: impl FnMut(&TestPlan, &Output), +) { + run_test_with_checker( + TestPlan { + cmd: "fuser".to_string(), + args, + stdin_data: String::new(), + expected_out: String::new(), + expected_err: expected_err.to_string(), + expected_exit_code, + }, + checker, + ); +} + +/// Tests the basic functionality of `fuser` by ensuring it can find the PID of a process. +/// +/// **Setup:** +/// - Starts a process running `sleep 1`. +/// +/// **Assertions:** +/// - Verifies that the PID of the process is included in the output of `fuser`. +#[tokio::test] +async fn test_fuser_basic() { + let process = Command::new("sleep") + .arg("1") + .spawn() + .expect("Failed to start process"); + + let pid = process.id(); + + fuser_test(vec!["/".to_string()], "", 0, |_, output| { + let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); + let pid_str = pid.to_string(); + assert!( + stdout_str.contains(&pid_str), + "PID {} not found in the output.", + pid_str + ); + }); +} + +/// Retrieves the username associated with the given process ID. +/// +/// # Arguments +/// +/// * `pid` - The process ID for which to retrieve the user information. +/// +/// # Returns +/// +/// Returns a `Result` where: +/// - `Ok(String)` contains the username if the process and user information are successfully retrieved. +/// - `Err(io::Error)` contains an error if the process status file cannot be read, if the UID cannot be parsed, or if other issues occur. +fn get_process_user(pid: u32) -> io::Result { + let status_path = format!("/proc/{}/status", pid); + let mut file = File::open(&status_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let uid_line = contents + .lines() + .find(|line| line.starts_with("Uid:")) + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Uid line not found"))?; + + let uid_str = uid_line + .split_whitespace() + .nth(1) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "UID not found"))?; + let uid: uid_t = uid_str + .parse() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UID"))?; + + let pwd = unsafe { libc::getpwuid(uid) }; + + unsafe { + let user_name = CStr::from_ptr((*pwd).pw_name) + .to_string_lossy() + .into_owned(); + Ok(user_name) + } +} + +/// Tests `fuser` with the `-u` flag to ensure it outputs the process owner. +/// +/// **Setup:** +/// - Starts a process running `sleep 1`. +/// +/// **Assertions:** +/// - Verifies that the owner printed in stderr. +#[test] +fn test_fuser_with_user() { + let process = Command::new("sleep") + .arg("1") + .spawn() + .expect("Failed to start process"); + + let pid = process.id(); + + fuser_test( + vec!["/".to_string(), "-u".to_string()], + "", + 0, + |_, output| { + let owner = get_process_user(pid).expect("Failed to get owner of process"); + let stderr_str = str::from_utf8(&output.stderr).expect("Invalid UTF-8 in stderr"); + assert!( + stderr_str.contains(&owner), + "owner {} not found in the output.", + owner + ); + }, + ); +} + +/// Tests `fuser` with the `-c` flag to check if it identifies processes using files on a mount point. +/// +/// **Setup:** +/// - Starts a process running `sleep 1`. +/// +/// **Assertions:** +/// - Verifies that the PID of the process is included in the `fuser` output for the mount. +#[test] +fn test_fuser_with_mount() { + let process = Command::new("sleep") + .arg("1") + .spawn() + .expect("Failed to start process"); + + let pid = process.id(); + + fuser_test( + vec!["/".to_string(), "-c".to_string()], + "", + 0, + |_, output| { + let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); + let pid_str = pid.to_string(); + assert!( + stdout_str.contains(&pid_str), + "PID {} not found in the output.", + pid_str + ); + }, + ); +} + +/// Tests `fuser` with multiple file paths. +/// +/// **Setup:** +/// - Starts two processes running `sleep 1` in different directories. +/// +/// **Assertions:** +/// - Verifies that the PIDs of both processes are included in the stdout. +#[test] +fn test_fuser_with_many_files() { + let process1 = Command::new("sleep") + .current_dir("../") + .arg("1") + .spawn() + .expect("Failed to start process"); + + let process2 = Command::new("sleep") + .current_dir("/") + .arg("1") + .spawn() + .expect("Failed to start process"); + + let pid1 = process1.id(); + let pid2 = process2.id(); + + fuser_test( + vec!["/".to_string(), "../".to_string()], + "", + 0, + |_, output| { + let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); + let pid_str1 = pid1.to_string(); + let pid_str2 = pid2.to_string(); + assert!( + stdout_str.contains(&pid_str1), + "PID {} not found in the output.", + pid_str1 + ); + assert!( + stdout_str.contains(&pid_str2), + "PID {} not found in the output.", + pid_str2 + ); + }, + ); +} + +/// Starts a TCP server on port 8080. +async fn start_tcp_server() -> TcpListener { + TcpListener::bind(("127.0.0.1", 8080)) + .await + .expect("Failed to bind TCP server") +} + +/// Tests `fuser` with TCP socket. +/// +/// **Setup:** +/// - Starts a TCP server on port 8080. +/// +/// **Assertions:** +/// - Verifies that the output of `fuser` matches the manual execution for TCP sockets. +#[tokio::test] +async fn test_fuser_tcp() { + let _server = start_tcp_server().await; + fuser_test(vec!["8080/tcp".to_string()], "", 0, |_, output| { + let manual_output = Command::new("fuser").arg("8080/tcp").output().unwrap(); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, manual_output.stdout); + assert_eq!(output.stderr, manual_output.stderr); + }); +} + +/// Starts a UDP server on port 8081. +async fn start_udp_server() -> UdpSocket { + UdpSocket::bind(("127.0.0.1", 8081)) + .await + .expect("Failed to bind UDP server") +} + +/// Tests `fuser` with UDP socket. +/// +/// **Setup:** +/// - Starts a UDP server on port 8081. +/// +/// **Assertions:** +/// - Verifies that the output of `fuser` matches the manual execution for UDP sockets. +#[tokio::test] +async fn test_fuser_udp() { + let _server = start_udp_server().await; + fuser_test(vec!["8081/udp".to_string()], "", 0, |_, output| { + let manual_output = Command::new("fuser").arg("8081/udp").output().unwrap(); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, manual_output.stdout); + assert_eq!(output.stderr, manual_output.stderr); + }); +} +/// Starts a Unix socket server at the specified path. +async fn start_unix_socket(socket_path: &str) -> UnixListener { + if fs::metadata(socket_path).is_ok() { + println!("A socket is already present. Deleting..."); + fs::remove_file(socket_path).expect("Failed to delete existing socket"); + } + + UnixListener::bind(socket_path).expect("Failed to bind Unix socket") +} + +/// Tests `fuser` with Unix socket. +/// +/// **Setup:** +/// - Starts a Unix socket server at the specified path (`/tmp/test.sock`). +/// +/// **Assertions:** +/// - Verifies that the output of `fuser` matches the manual execution for the Unix socket at `/tmp/test.sock`. +/// +/// **Note:** +/// - Before binding to the socket, the function checks if a socket file already exists at the path and deletes it if present. +/// - This ensures that the test environment is clean and prevents issues with existing sockets. +#[tokio::test] +async fn test_fuser_unixsocket() { + let socket_path = "/tmp/test.sock"; + let _unix_socket = start_unix_socket(socket_path).await; + fuser_test(vec![socket_path.to_string()], "", 0, |_, output| { + let manual_output = Command::new("fuser").arg(socket_path).output().unwrap(); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, manual_output.stdout); + assert_eq!(output.stderr, manual_output.stderr); + }); +} + +/// Creates a directory and populates it with a specified number of test files. +fn create_large_directory(dir_path: &Path, num_files: usize) -> std::io::Result<()> { + fs::create_dir_all(dir_path)?; + for i in 0..num_files { + let file_path = dir_path.join(format!("file_{:04}", i)); + fs::write(file_path, "This is a test file.")?; + } + Ok(()) +} + +/// Deletes a directory and all of its contents. +fn delete_directory(dir_path: &Path) -> io::Result<()> { + if dir_path.exists() { + fs::remove_dir_all(dir_path) + } else { + Ok(()) + } +} + +/// Tests `fuser` with a very large directory to ensure it can handle large numbers of files. +/// +/// **Setup:** +/// - Creates a directory with a large number of files at a fixed path. +/// +/// **Assertions:** +/// - Verifies that the `fuser` command completes successfully and the output is as expected. +/// - Executes an additional command to ensure it works after `fuser` and checks its output. +/// - Ensures that the test does not block indefinitely and completes within a reasonable time frame. +#[tokio::test] +async fn test_fuser_large_directory() { + let test_dir_path = PathBuf::from("large_test_dir"); + + let num_files = 10_000; + + create_large_directory(&test_dir_path, num_files).expect("Failed to create large directory"); + + fuser_test( + vec![test_dir_path.to_str().unwrap().to_string()], + "", + 0, + |_, output| { + let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); + assert!(stdout_str.contains("")); + }, + ); + + let additional_command = Command::new("ls") + .args(&[test_dir_path.to_str().unwrap()]) + .output() + .expect("Failed to execute command"); + + assert_eq!(additional_command.status.code(), Some(0)); + + // Clean up the directory after the test + delete_directory(&test_dir_path).expect("Failed to delete large directory"); +} diff --git a/process/tests/process-tests.rs b/process/tests/process-tests.rs index 2b28a36c..ef54df04 100644 --- a/process/tests/process-tests.rs +++ b/process/tests/process-tests.rs @@ -1 +1,2 @@ mod xargs; +mod fuser; From 629ea80f0afda01462a21cc4f7ce9c6a7be589a2 Mon Sep 17 00:00:00 2001 From: wandalen Date: Wed, 4 Sep 2024 10:39:09 +0300 Subject: [PATCH 2/3] [fuser] full implementation --- Cargo.lock | 194 +++++++++++++++++++++++++++++- process/Cargo.toml | 4 + process/build.rs | 27 +++++ process/fuser.rs | 234 +++++++++++++++++++++++++++++++------ process/tests/fuser/mod.rs | 76 ++++-------- 5 files changed, 447 insertions(+), 88 deletions(-) create mode 100644 process/build.rs diff --git a/Cargo.lock b/Cargo.lock index 41a4c724..41628fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -105,7 +114,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -116,6 +125,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.36.3", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.21.7" @@ -135,6 +159,26 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bindgen" +version = "0.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0127a1da21afb5adaae26910922c3f7afd3d329ba1a1b98a0884cab4907a251" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -208,6 +252,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -232,6 +285,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.15" @@ -439,6 +503,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -576,6 +646,18 @@ dependencies = [ name = "gettext-rs" version = "0.1.0" +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.14.5" @@ -600,6 +682,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hostname" version = "0.3.1" @@ -676,6 +764,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -723,6 +820,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" @@ -817,6 +924,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nix" version = "0.28.0" @@ -853,7 +972,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -929,6 +1048,15 @@ dependencies = [ "ruzstd", ] +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1094,7 +1222,7 @@ dependencies = [ "chrono", "clap", "gettext-rs", - "object", + "object 0.35.0", "plib", ] @@ -1186,12 +1314,14 @@ name = "posixutils-process" version = "0.2.0" dependencies = [ "atty", + "bindgen", "clap", "dirs 5.0.1", "errno", "gettext-rs", "libc", "plib", + "tokio", ] [[package]] @@ -1291,6 +1421,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1394,6 +1534,18 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.34" @@ -1530,6 +1682,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1680,6 +1842,32 @@ dependencies = [ "time-core", ] +[[package]] +name = "tokio" +version = "1.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +dependencies = [ + "backtrace", + "libc", + "mio 1.0.2", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "topological-sort" version = "0.2.2" diff --git a/process/Cargo.toml b/process/Cargo.toml index 3c90f943..e42eb5ea 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -18,6 +18,9 @@ dirs = "5.0" [dev-dependencies] tokio = { version = "1.39", features = ["net", "macros", "rt"]} +[build-dependencies] +bindgen = { version = "0.70.0", features = ["runtime"] } + [[bin]] name = "fuser" path = "./fuser.rs" @@ -46,3 +49,4 @@ path = "./renice.rs" name = "xargs" path = "./xargs.rs" + diff --git a/process/build.rs b/process/build.rs new file mode 100644 index 00000000..8d1d38ca --- /dev/null +++ b/process/build.rs @@ -0,0 +1,27 @@ +#[cfg(target_os = "macos")] +fn main() { + use std::env; + use std::path::Path; + + let bindings = bindgen::builder() + .header_contents("libproc_rs.h", "#include ") + .layout_tests(false) + .clang_args(&[ + "-x", + "c++", + "-I", + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/", + ]) + .generate() + .expect("Failed to build libproc bindings"); + + let output_path = Path::new(&env::var("OUT_DIR").expect("OUT_DIR env var was not defined")) + .join("osx_libproc_bindings.rs"); + + bindings + .write_to_file(output_path) + .expect("Failed to write libproc bindings"); +} + +#[cfg(target_os = "linux")] +fn main() {} diff --git a/process/fuser.rs b/process/fuser.rs index 068db5d6..e6ed1b60 100644 --- a/process/fuser.rs +++ b/process/fuser.rs @@ -2,15 +2,22 @@ extern crate clap; extern crate libc; extern crate plib; -use clap::Parser; +#[cfg(target_os = "macos")] +#[allow(warnings, missing_docs)] +pub mod osx_libproc_bindings { + include!(concat!(env!("OUT_DIR"), "/osx_libproc_bindings.rs")); +} + +use clap::{CommandFactory, Parser}; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; use libc::fstat; +#[cfg(target_os = "macos")] +use libc::{c_char, c_int, c_void}; use plib::PROJECT_NAME; use std::{ collections::BTreeMap, env, ffi::{CStr, CString}, - fmt, fs::{self, File}, io::{self, BufRead, Error, ErrorKind, Write}, net::{IpAddr, Ipv4Addr, UdpSocket}, @@ -20,11 +27,113 @@ use std::{ thread, time::Duration, }; +#[cfg(target_os = "macos")] +use std::{os::unix::ffi::OsStrExt, ptr}; const PROC_PATH: &'static str = "/proc"; const PROC_MOUNTS: &'static str = "/proc/mounts"; const NAME_FIELD: usize = 20; +// similar to list_pids_ret() below, there are two cases when 0 is returned, one when there are +// no pids, and the other when there is an error +// when `errno` is set to indicate an error in the input type, the return value is 0 +#[cfg(target_os = "macos")] +fn check_listpid_ret(ret: c_int) -> io::Result> { + if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { + return Err(io::Error::last_os_error()); + } + + // `ret` cannot be negative here - so no possible loss of sign + #[allow(clippy::cast_sign_loss)] + let capacity = ret as usize / std::mem::size_of::(); + Ok(Vec::with_capacity(capacity)) +} + +// Common code for handling the special case of listpids return, where 0 is a valid return +// but is also used in the error case - so we need to look at errno to distringish between a valid +// 0 return and an error return +// when `errno` is set to indicate an error in the input type, the return value is 0 +#[cfg(target_os = "macos")] +fn list_pids_ret(ret: c_int, mut pids: Vec) -> io::Result> { + if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { + Err(io::Error::last_os_error()) + } else { + // `ret` cannot be negative here, so no possible loss of sign + #[allow(clippy::cast_sign_loss)] + let items_count = ret as usize / std::mem::size_of::(); + unsafe { + pids.set_len(items_count); + } + Ok(pids) + } +} + +#[cfg(target_os = "macos")] +pub(crate) fn listpids(proc_type: u32) -> io::Result> { + let buffer_size = + unsafe { osx_libproc_bindings::proc_listpids(proc_type, proc_type, ptr::null_mut(), 0) }; + + let mut pids = check_listpid_ret(buffer_size)?; + let buffer_ptr = pids.as_mut_ptr().cast::(); + + let ret = unsafe { + osx_libproc_bindings::proc_listpids(proc_type, proc_type, buffer_ptr, buffer_size) + }; + + list_pids_ret(ret, pids) +} + +#[cfg(target_os = "macos")] +pub(crate) fn listpidspath( + proc_type: u32, + path: &Path, + is_volume: bool, + exclude_event_only: bool, +) -> io::Result> { + let path_bytes = path.as_os_str().as_bytes(); + let c_path = CString::new(path_bytes) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "CString::new failed"))?; + let mut pathflags: u32 = 0; + if is_volume { + pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_PATH_IS_VOLUME; + } + if exclude_event_only { + pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_EXCLUDE_EVTONLY; + } + + let buffer_size = unsafe { + osx_libproc_bindings::proc_listpidspath( + proc_type, + proc_type, + c_path.as_ptr().cast::(), + pathflags, + ptr::null_mut(), + 0, + ) + }; + let mut pids = check_listpid_ret(buffer_size)?; + let buffer_ptr = pids.as_mut_ptr().cast::(); + + let ret = unsafe { + osx_libproc_bindings::proc_listpidspath( + proc_type, + proc_type, + c_path.as_ptr().cast::(), + 0, + buffer_ptr, + buffer_size, + ) + }; + + list_pids_ret(ret, pids) +} + +#[cfg(target_os = "macos")] +type DeviceId = i32; + +#[cfg(target_os = "linux")] +type DeviceId = u64; + #[derive(Clone, Default, PartialEq)] enum ProcType { #[default] @@ -130,17 +239,17 @@ impl Procs { } } -#[derive(Default, Clone)] +#[derive(Clone, Default)] struct UnixSocketList { name: String, - device_id: u64, + device_id: DeviceId, inode: u64, net_inode: u64, next: Option>, } impl UnixSocketList { - fn new(name: String, device_id: u64, inode: u64, net_inode: u64) -> Self { + fn new(name: String, device_id: DeviceId, inode: u64, net_inode: u64) -> Self { UnixSocketList { name, device_id, @@ -150,7 +259,7 @@ impl UnixSocketList { } } - fn add_socket(&mut self, name: String, device_id: u64, inode: u64, net_inode: u64) { + fn add_socket(&mut self, name: String, device_id: DeviceId, inode: u64, net_inode: u64) { let new_node = Box::new(UnixSocketList { name, device_id, @@ -187,13 +296,13 @@ impl<'a> Iterator for UnixSocketListIterator<'a> { #[derive(Default)] struct InodeList { name: Names, - device_id: u64, + device_id: DeviceId, inode: u64, next: Option>, } impl InodeList { - fn new(name: Names, device_id: u64, inode: u64) -> Self { + fn new(name: Names, device_id: DeviceId, inode: u64) -> Self { InodeList { name, device_id, @@ -285,12 +394,12 @@ impl Names { #[derive(Default)] struct DeviceList { name: Names, - device_id: u64, + device_id: DeviceId, next: Option>, } impl DeviceList { - fn new(name: Names, device_id: u64) -> Self { + fn new(name: Names, device_id: DeviceId) -> Self { DeviceList { name, device_id, @@ -337,7 +446,6 @@ struct Args { /// A pathname on which the file or file system is to be reported. file: Vec, } -use clap::CommandFactory; fn main() -> Result<(), Box> { setlocale(LocaleCategory::LcAll, ""); textdomain(PROJECT_NAME)?; @@ -368,51 +476,109 @@ fn main() -> Result<(), Box> { mut need_check_map, ) = init_defaults(file); - fill_unix_cache(&mut unix_socket_list)?; + #[cfg(target_os = "linux")] + get_matched_procs_linux( + &mut names, + &mut unix_socket_list, + &mut mount_list, + &mut inode_list, + &mut device_list, + &mut need_check_map, + mount, + )?; + + #[cfg(target_os = "macos")] + get_matched_procs_macos(&mut names, mount)?; + + for name in names.iter_mut() { + print_matches(name, user)?; + } + + std::process::exit(0); +} + +#[cfg(target_os = "linux")] +fn get_matched_procs_linux( + names: &mut Vec, + unix_socket_list: &mut UnixSocketList, + mount_list: &mut MountList, + inode_list: &mut InodeList, + device_list: &mut DeviceList, + need_check_map: &mut bool, + mount: bool, +) -> Result<(), io::Error> { + fill_unix_cache(unix_socket_list)?; let net_dev = find_net_dev()?; for name in names.iter_mut() { name.name_space = determine_namespace(&name.filename); - match name.name_space { NameSpace::File => handle_file_namespace( name, mount, - &mut mount_list, - &mut inode_list, - &mut device_list, - &mut need_check_map, + mount_list, + inode_list, + device_list, + need_check_map, )?, NameSpace::Tcp => { if !mount { - handle_tcp_namespace(name, &mut inode_list, net_dev)? + handle_tcp_namespace(name, inode_list, net_dev)?; } } NameSpace::Udp => { if !mount { - handle_udp_namespace(name, &mut inode_list, net_dev)? + handle_udp_namespace(name, inode_list, net_dev)?; } } } - if (scan_procs( - need_check_map, + if scan_procs( + *need_check_map, name, &inode_list, &device_list, &unix_socket_list, net_dev, - )) + ) .is_err() { std::process::exit(1); } - - print_matches(name, user)?; } - std::process::exit(0) + Ok(()) +} + +#[cfg(target_os = "macos")] +fn get_matched_procs_macos( + names: &mut Vec, + mount: bool, +) -> Result<(), Box> { + for name in names.iter_mut() { + let st = timeout(&name.filename.to_string_lossy(), 5)?; + let uid = st.st_uid; + + let pids = listpidspath( + osx_libproc_bindings::PROC_ALL_PIDS, + Path::new(&name.filename), + mount, + false, + )?; + + for pid in pids { + add_process( + name, + pid.try_into().unwrap(), + uid, + Access::Cwd, + ProcType::Normal, + None, + ); + } + } + Ok(()) } /// Initializes and returns default values. @@ -544,7 +710,7 @@ fn handle_file_namespace( fn handle_tcp_namespace( names: &mut Names, inode_list: &mut InodeList, - net_dev: u64, + net_dev: DeviceId, ) -> Result<(), std::io::Error> { let tcp_connection_list = parse_inet(names)?; *inode_list = find_net_sockets(&tcp_connection_list, "tcp", net_dev)?; @@ -570,7 +736,7 @@ fn handle_tcp_namespace( fn handle_udp_namespace( names: &mut Names, inode_list: &mut InodeList, - net_dev: u64, + net_dev: DeviceId, ) -> Result<(), std::io::Error> { let udp_connection_list = parse_inet(names)?; *inode_list = find_net_sockets(&udp_connection_list, "udp", net_dev)?; @@ -676,7 +842,7 @@ fn scan_procs( inode_list: &InodeList, device_list: &DeviceList, unix_socket_list: &UnixSocketList, - net_dev: u64, + net_dev: DeviceId, ) -> Result<(), io::Error> { let my_pid = std::process::id() as i32; let dir_entries = fs::read_dir(PROC_PATH)?; @@ -912,7 +1078,7 @@ fn check_dir( uid: u32, access: Access, unix_socket_list: &UnixSocketList, - net_dev: u64, + net_dev: DeviceId, ) -> Result<(), io::Error> { let dir_path = format!("/proc/{}/{}", pid, dirname); let dir_entries = fs::read_dir(&dir_path)?; @@ -1005,11 +1171,11 @@ fn check_map( }; let device = tmp_maj * 256 + tmp_min; - let device_u64 = device as u64; + let device_int = device as DeviceId; if device_list .iter() - .any(|device| device.device_id == device_u64) + .any(|device| device.device_id == device_int) { add_process(names, pid, uid, access.clone(), ProcType::Normal, None); } @@ -1176,7 +1342,7 @@ fn parse_inet(names: &mut Names) -> Result { /// # Returns /// /// Returns the device identifier (`u64`) of the network interface. -fn find_net_dev() -> Result { +fn find_net_dev() -> Result { let socket = UdpSocket::bind("0.0.0.0:0")?; let fd = socket.as_raw_fd(); let mut stat_buf = unsafe { std::mem::zeroed() }; @@ -1187,7 +1353,7 @@ fn find_net_dev() -> Result { } } - Ok(stat_buf.st_dev as u64) + Ok(stat_buf.st_dev) } /// Finds network sockets based on the given protocol and updates the `InodeList` @@ -1213,7 +1379,7 @@ fn find_net_dev() -> Result { fn find_net_sockets( connections_list: &IpConnections, protocol: &str, - net_dev: u64, + net_dev: DeviceId, ) -> Result { let pathname = format!("/proc/net/{}", protocol); diff --git a/process/tests/fuser/mod.rs b/process/tests/fuser/mod.rs index f71cfd11..3a31dcb7 100644 --- a/process/tests/fuser/mod.rs +++ b/process/tests/fuser/mod.rs @@ -45,7 +45,7 @@ async fn test_fuser_basic() { let pid = process.id(); - fuser_test(vec!["/".to_string()], "", 0, |_, output| { + fuser_test(vec!["./".to_string()], "", 0, |_, output| { let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); let pid_str = pid.to_string(); assert!( @@ -56,17 +56,7 @@ async fn test_fuser_basic() { }); } -/// Retrieves the username associated with the given process ID. -/// -/// # Arguments -/// -/// * `pid` - The process ID for which to retrieve the user information. -/// -/// # Returns -/// -/// Returns a `Result` where: -/// - `Ok(String)` contains the username if the process and user information are successfully retrieved. -/// - `Err(io::Error)` contains an error if the process status file cannot be read, if the UID cannot be parsed, or if other issues occur. +#[cfg(target_os = "linux")] fn get_process_user(pid: u32) -> io::Result { let status_path = format!("/proc/{}/status", pid); let mut file = File::open(&status_path)?; @@ -86,16 +76,29 @@ fn get_process_user(pid: u32) -> io::Result { .parse() .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UID"))?; + get_username_by_uid(uid) +} + +#[cfg(target_os = "macos")] +fn get_process_user(pid: u32) -> io::Result { + let uid = unsafe{ libc::getuid() }; + get_username_by_uid(uid) +} + +fn get_username_by_uid(uid: uid_t) -> io::Result { let pwd = unsafe { libc::getpwuid(uid) }; + if pwd.is_null() { + return Err(io::Error::new(io::ErrorKind::NotFound, "User not found")); + } - unsafe { - let user_name = CStr::from_ptr((*pwd).pw_name) + let user_name = unsafe { + CStr::from_ptr((*pwd).pw_name) .to_string_lossy() - .into_owned(); - Ok(user_name) - } -} + .into_owned() + }; + Ok(user_name) +} /// Tests `fuser` with the `-u` flag to ensure it outputs the process owner. /// /// **Setup:** @@ -113,7 +116,7 @@ fn test_fuser_with_user() { let pid = process.id(); fuser_test( - vec!["/".to_string(), "-u".to_string()], + vec!["./".to_string(), "-u".to_string()], "", 0, |_, output| { @@ -128,38 +131,6 @@ fn test_fuser_with_user() { ); } -/// Tests `fuser` with the `-c` flag to check if it identifies processes using files on a mount point. -/// -/// **Setup:** -/// - Starts a process running `sleep 1`. -/// -/// **Assertions:** -/// - Verifies that the PID of the process is included in the `fuser` output for the mount. -#[test] -fn test_fuser_with_mount() { - let process = Command::new("sleep") - .arg("1") - .spawn() - .expect("Failed to start process"); - - let pid = process.id(); - - fuser_test( - vec!["/".to_string(), "-c".to_string()], - "", - 0, - |_, output| { - let stdout_str = str::from_utf8(&output.stdout).expect("Invalid UTF-8 in stdout"); - let pid_str = pid.to_string(); - assert!( - stdout_str.contains(&pid_str), - "PID {} not found in the output.", - pid_str - ); - }, - ); -} - /// Tests `fuser` with multiple file paths. /// /// **Setup:** @@ -221,6 +192,7 @@ async fn start_tcp_server() -> TcpListener { /// **Assertions:** /// - Verifies that the output of `fuser` matches the manual execution for TCP sockets. #[tokio::test] +#[cfg(target_os = "linux")] async fn test_fuser_tcp() { let _server = start_tcp_server().await; fuser_test(vec!["8080/tcp".to_string()], "", 0, |_, output| { @@ -246,6 +218,7 @@ async fn start_udp_server() -> UdpSocket { /// **Assertions:** /// - Verifies that the output of `fuser` matches the manual execution for UDP sockets. #[tokio::test] +#[cfg(target_os = "linux")] async fn test_fuser_udp() { let _server = start_udp_server().await; fuser_test(vec!["8081/udp".to_string()], "", 0, |_, output| { @@ -277,6 +250,7 @@ async fn start_unix_socket(socket_path: &str) -> UnixListener { /// - Before binding to the socket, the function checks if a socket file already exists at the path and deletes it if present. /// - This ensures that the test environment is clean and prevents issues with existing sockets. #[tokio::test] +#[cfg(target_os = "linux")] async fn test_fuser_unixsocket() { let socket_path = "/tmp/test.sock"; let _unix_socket = start_unix_socket(socket_path).await; From 02489e19be54567665fa5a1fcbd974e0d00dcf91 Mon Sep 17 00:00:00 2001 From: wandalen Date: Fri, 13 Sep 2024 23:30:57 +0300 Subject: [PATCH 3/3] [fuser] fix warning, small improvements --- Cargo.lock | 48 +- process/Cargo.toml | 2 - process/fuser.rs | 2516 ++++++++++++++++++------------------ process/tests/fuser/mod.rs | 13 +- 4 files changed, 1267 insertions(+), 1312 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c88a9574..d4526612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] @@ -127,17 +127,17 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.36.3", + "object 0.36.4", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.70.0" +version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0127a1da21afb5adaae26910922c3f7afd3d329ba1a1b98a0884cab4907a251" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags 2.6.0", "cexpr", @@ -657,9 +657,9 @@ version = "0.1.0" [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -1132,9 +1132,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -1521,22 +1521,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -<<<<<<< HEAD -name = "prettyplease" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" -dependencies = [ - "proc-macro2", - "syn", -======= name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ->>>>>>> main +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] @@ -1967,9 +1967,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "libc", diff --git a/process/Cargo.toml b/process/Cargo.toml index e42eb5ea..a8748273 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -48,5 +48,3 @@ path = "./renice.rs" [[bin]] name = "xargs" path = "./xargs.rs" - - diff --git a/process/fuser.rs b/process/fuser.rs index e6ed1b60..9fab94df 100644 --- a/process/fuser.rs +++ b/process/fuser.rs @@ -2,155 +2,38 @@ extern crate clap; extern crate libc; extern crate plib; -#[cfg(target_os = "macos")] -#[allow(warnings, missing_docs)] -pub mod osx_libproc_bindings { - include!(concat!(env!("OUT_DIR"), "/osx_libproc_bindings.rs")); -} - use clap::{CommandFactory, Parser}; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; -use libc::fstat; -#[cfg(target_os = "macos")] -use libc::{c_char, c_int, c_void}; use plib::PROJECT_NAME; +use std::io::{self, Write}; use std::{ collections::BTreeMap, - env, ffi::{CStr, CString}, - fs::{self, File}, - io::{self, BufRead, Error, ErrorKind, Write}, - net::{IpAddr, Ipv4Addr, UdpSocket}, - os::unix::io::AsRawFd, - path::{Component, Path, PathBuf}, + path::{Path, PathBuf}, sync::mpsc, thread, time::Duration, }; -#[cfg(target_os = "macos")] -use std::{os::unix::ffi::OsStrExt, ptr}; -const PROC_PATH: &'static str = "/proc"; -const PROC_MOUNTS: &'static str = "/proc/mounts"; const NAME_FIELD: usize = 20; -// similar to list_pids_ret() below, there are two cases when 0 is returned, one when there are -// no pids, and the other when there is an error -// when `errno` is set to indicate an error in the input type, the return value is 0 -#[cfg(target_os = "macos")] -fn check_listpid_ret(ret: c_int) -> io::Result> { - if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { - return Err(io::Error::last_os_error()); - } - - // `ret` cannot be negative here - so no possible loss of sign - #[allow(clippy::cast_sign_loss)] - let capacity = ret as usize / std::mem::size_of::(); - Ok(Vec::with_capacity(capacity)) -} - -// Common code for handling the special case of listpids return, where 0 is a valid return -// but is also used in the error case - so we need to look at errno to distringish between a valid -// 0 return and an error return -// when `errno` is set to indicate an error in the input type, the return value is 0 -#[cfg(target_os = "macos")] -fn list_pids_ret(ret: c_int, mut pids: Vec) -> io::Result> { - if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { - Err(io::Error::last_os_error()) - } else { - // `ret` cannot be negative here, so no possible loss of sign - #[allow(clippy::cast_sign_loss)] - let items_count = ret as usize / std::mem::size_of::(); - unsafe { - pids.set_len(items_count); - } - Ok(pids) - } -} - -#[cfg(target_os = "macos")] -pub(crate) fn listpids(proc_type: u32) -> io::Result> { - let buffer_size = - unsafe { osx_libproc_bindings::proc_listpids(proc_type, proc_type, ptr::null_mut(), 0) }; - - let mut pids = check_listpid_ret(buffer_size)?; - let buffer_ptr = pids.as_mut_ptr().cast::(); - - let ret = unsafe { - osx_libproc_bindings::proc_listpids(proc_type, proc_type, buffer_ptr, buffer_size) - }; - - list_pids_ret(ret, pids) -} - -#[cfg(target_os = "macos")] -pub(crate) fn listpidspath( - proc_type: u32, - path: &Path, - is_volume: bool, - exclude_event_only: bool, -) -> io::Result> { - let path_bytes = path.as_os_str().as_bytes(); - let c_path = CString::new(path_bytes) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "CString::new failed"))?; - let mut pathflags: u32 = 0; - if is_volume { - pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_PATH_IS_VOLUME; - } - if exclude_event_only { - pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_EXCLUDE_EVTONLY; - } - - let buffer_size = unsafe { - osx_libproc_bindings::proc_listpidspath( - proc_type, - proc_type, - c_path.as_ptr().cast::(), - pathflags, - ptr::null_mut(), - 0, - ) - }; - let mut pids = check_listpid_ret(buffer_size)?; - let buffer_ptr = pids.as_mut_ptr().cast::(); - - let ret = unsafe { - osx_libproc_bindings::proc_listpidspath( - proc_type, - proc_type, - c_path.as_ptr().cast::(), - 0, - buffer_ptr, - buffer_size, - ) - }; - - list_pids_ret(ret, pids) -} - -#[cfg(target_os = "macos")] -type DeviceId = i32; - #[cfg(target_os = "linux")] -type DeviceId = u64; - #[derive(Clone, Default, PartialEq)] enum ProcType { #[default] - Normal = 0, - Mount = 1, - Knfsd = 2, - Swap = 3, + Normal, } +#[cfg(target_os = "linux")] #[derive(Clone, Default, PartialEq)] enum NameSpace { #[default] - File = 0, - Tcp = 1, - Udp = 2, + File, + Tcp, + Udp, } +#[cfg(target_os = "linux")] #[derive(Clone, Default, PartialEq)] enum Access { Cwd = 1, @@ -162,58 +45,19 @@ enum Access { Filewr = 32, } -#[derive(Clone)] -struct IpConnections { - names: Names, - lcl_port: u64, - rmt_port: u64, - rmt_addr: IpAddr, - next: Option>, -} - -impl Default for IpConnections { - fn default() -> Self { - IpConnections { - names: Names::default(), - lcl_port: 0, - rmt_port: 0, - rmt_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - next: None, - } - } -} - -impl IpConnections { - fn new(names: Names, lcl_port: u64, rmt_port: u64, rmt_addr: IpAddr) -> Self { - IpConnections { - names, - lcl_port, - rmt_port, - rmt_addr, - next: None, - } - } - - fn iter(&self) -> IpConnectionsIterator { - IpConnectionsIterator { - current: Some(self), - } - } -} - -struct IpConnectionsIterator<'a> { - current: Option<&'a IpConnections>, +#[cfg(target_os = "macos")] +#[derive(Clone, Default, PartialEq)] +enum ProcType { + #[default] + Normal, } -impl<'a> Iterator for IpConnectionsIterator<'a> { - type Item = &'a IpConnections; - - fn next(&mut self) -> Option { - self.current.map(|node| { - self.current = node.next.as_deref(); - node - }) - } +#[cfg(target_os = "macos")] +#[derive(Clone, Default, PartialEq)] +enum Access { + Cwd = 1, + #[default] + File = 4, } #[derive(Clone, Default)] @@ -222,525 +66,1292 @@ struct Procs { uid: u32, access: Access, proc_type: ProcType, - username: Option, - command: String, } impl Procs { - fn new(pid: i32, uid: u32, access: Access, proc_type: ProcType, command: String) -> Self { + fn new(pid: i32, uid: u32, access: Access, proc_type: ProcType) -> Self { Self { pid, uid, access, proc_type, - username: None, - command, } } } +#[cfg(target_os = "linux")] #[derive(Clone, Default)] -struct UnixSocketList { - name: String, - device_id: DeviceId, - inode: u64, - net_inode: u64, - next: Option>, +struct Names { + filename: PathBuf, + name_space: NameSpace, + matched_procs: Vec, } -impl UnixSocketList { - fn new(name: String, device_id: DeviceId, inode: u64, net_inode: u64) -> Self { - UnixSocketList { - name, - device_id, - inode, - net_inode, - next: None, +#[cfg(target_os = "linux")] +impl Names { + fn new(filename: PathBuf, name_space: NameSpace, matched_procs: Vec) -> Self { + Names { + filename, + name_space, + matched_procs, } } - fn add_socket(&mut self, name: String, device_id: DeviceId, inode: u64, net_inode: u64) { - let new_node = Box::new(UnixSocketList { - name, - device_id, - net_inode, - inode, - next: self.next.take(), - }); - - self.next = Some(new_node); - } + fn add_procs(&mut self, proc: Procs) { + let exists = self + .matched_procs + .iter() + .any(|p| p.access == proc.access && p.pid == proc.pid); - fn iter(&self) -> UnixSocketListIterator { - UnixSocketListIterator { - current: Some(self), + if !exists { + self.matched_procs.push(proc); } } } -struct UnixSocketListIterator<'a> { - current: Option<&'a UnixSocketList>, +#[cfg(target_os = "macos")] +#[derive(Clone, Default)] +struct Names { + filename: PathBuf, + matched_procs: Vec, } -impl<'a> Iterator for UnixSocketListIterator<'a> { - type Item = &'a UnixSocketList; - - fn next(&mut self) -> Option { - self.current.map(|node| { - self.current = node.next.as_deref(); - node - }) +#[cfg(target_os = "macos")] +impl Names { + fn new(filename: PathBuf, matched_procs: Vec) -> Self { + Names { + filename, + matched_procs, + } } -} -#[derive(Default)] -struct InodeList { - name: Names, - device_id: DeviceId, - inode: u64, - next: Option>, -} + fn add_procs(&mut self, proc: Procs) { + let exists = self.matched_procs.iter().any(|p| p.pid == proc.pid); -impl InodeList { - fn new(name: Names, device_id: DeviceId, inode: u64) -> Self { - InodeList { - name, - device_id, - inode, - next: None, + if !exists { + self.matched_procs.push(proc); } } +} - fn iter(&self) -> InodeListIterator { - InodeListIterator { - current: Some(self), - } +#[cfg(target_os = "linux")] +mod linux { + use super::*; + + use libc::fstat; + use std::{ + env, + fs::{self, File}, + io::{BufRead, Error, ErrorKind}, + net::{IpAddr, Ipv4Addr, UdpSocket}, + os::unix::io::AsRawFd, + path::Component, + }; + const PROC_PATH: &str = "/proc"; + const PROC_MOUNTS: &str = "/proc/mounts"; + + #[derive(Clone)] + struct IpConnections { + lcl_port: u64, + rmt_port: u64, + rmt_addr: IpAddr, + next: Option>, } -} -struct InodeListIterator<'a> { - current: Option<&'a InodeList>, -} + impl Default for IpConnections { + fn default() -> Self { + IpConnections { + lcl_port: 0, + rmt_port: 0, + rmt_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + next: None, + } + } + } -impl<'a> Iterator for InodeListIterator<'a> { - type Item = &'a InodeList; + impl IpConnections { + fn new(lcl_port: u64, rmt_port: u64, rmt_addr: IpAddr) -> Self { + IpConnections { + lcl_port, + rmt_port, + rmt_addr, + next: None, + } + } - fn next(&mut self) -> Option { - self.current.map(|node| { - self.current = node.next.as_deref(); - node - }) + fn iter(&self) -> IpConnectionsIterator { + IpConnectionsIterator { + current: Some(self), + } + } } -} -#[derive(Default, Clone)] -struct MountList { - mountpoints: Vec, -} + struct IpConnectionsIterator<'a> { + current: Option<&'a IpConnections>, + } -struct LibcStat { - inner: libc::stat, -} + impl<'a> Iterator for IpConnectionsIterator<'a> { + type Item = &'a IpConnections; -impl Default for LibcStat { - fn default() -> Self { - LibcStat { - inner: unsafe { std::mem::zeroed() }, + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) } } -} -impl Clone for LibcStat { - fn clone(&self) -> Self { - LibcStat { inner: self.inner } + #[derive(Clone, Default)] + struct UnixSocketList { + device_id: u64, + inode: u64, + net_inode: u64, + next: Option>, } -} -#[derive(Clone, Default)] -struct Names { - filename: PathBuf, - name_space: NameSpace, - matched_procs: Vec, - st: LibcStat, -} + impl UnixSocketList { + fn add_socket(&mut self, device_id: u64, inode: u64, net_inode: u64) { + let new_node = Box::new(UnixSocketList { + device_id, + net_inode, + inode, + next: self.next.take(), + }); -impl Names { - fn new( - filename: PathBuf, - name_space: NameSpace, - st: LibcStat, - matched_procs: Vec, - ) -> Self { - Names { - filename, - name_space, - st, - matched_procs, + self.next = Some(new_node); + } + + fn iter(&self) -> UnixSocketListIterator { + UnixSocketListIterator { + current: Some(self), + } } } - fn add_procs(&mut self, proc: Procs) { - let exists = self - .matched_procs - .iter() - .any(|p| p.access == proc.access && p.pid == proc.pid); + struct UnixSocketListIterator<'a> { + current: Option<&'a UnixSocketList>, + } - if !exists { - self.matched_procs.push(proc); + impl<'a> Iterator for UnixSocketListIterator<'a> { + type Item = &'a UnixSocketList; + + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) } } -} -#[derive(Default)] -struct DeviceList { - name: Names, - device_id: DeviceId, - next: Option>, -} + #[derive(Default)] + struct InodeList { + device_id: u64, + inode: u64, + next: Option>, + } -impl DeviceList { - fn new(name: Names, device_id: DeviceId) -> Self { - DeviceList { - name, - device_id, - next: None, + impl InodeList { + fn new(device_id: u64, inode: u64) -> Self { + InodeList { + device_id, + inode, + next: None, + } } - } - fn iter(&self) -> DeviceListIterator { - DeviceListIterator { - current: Some(self), + + fn iter(&self) -> InodeListIterator { + InodeListIterator { + current: Some(self), + } } } -} -struct DeviceListIterator<'a> { - current: Option<&'a DeviceList>, -} + struct InodeListIterator<'a> { + current: Option<&'a InodeList>, + } -impl<'a> Iterator for DeviceListIterator<'a> { - type Item = &'a DeviceList; + impl<'a> Iterator for InodeListIterator<'a> { + type Item = &'a InodeList; - fn next(&mut self) -> Option { - self.current.map(|node| { - self.current = node.next.as_deref(); - node - }) + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } } -} -/// fuser - list process IDs of all processes that have one or more files open -#[derive(Parser, Debug)] -#[command(author, version, about, long_about)] -struct Args { - /// The file is treated as a mount point and the utility shall report on any files open in the file system. - #[arg(short = 'c')] - mount: bool, - /// The report shall be only for the named files. - #[arg(short = 'f')] - named_files: bool, - /// The user name, in parentheses, associated with each process ID written to standard output shall be written to standard error. - #[arg(short = 'u')] - user: bool, + #[derive(Default, Clone)] + struct MountList { + mountpoints: Vec, + } - #[arg(required = true, name = "FILE", num_args(0..))] - /// A pathname on which the file or file system is to be reported. - file: Vec, -} -fn main() -> Result<(), Box> { - setlocale(LocaleCategory::LcAll, ""); - textdomain(PROJECT_NAME)?; - bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + #[derive(Default)] + struct DeviceList { + name: Names, + device_id: u64, + next: Option>, + } - let Args { - mount, user, file, .. - } = Args::try_parse().unwrap_or_else(|err| match err.kind() { - clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { - print!("{err}"); - std::process::exit(1); + impl DeviceList { + fn new(name: Names, device_id: u64) -> Self { + DeviceList { + name, + device_id, + next: None, + } } - _ => { - let mut stdout = std::io::stdout(); - let mut cmd = Args::command(); - eprintln!("No process specification given"); - cmd.write_help(&mut stdout).unwrap(); - std::process::exit(1); + fn iter(&self) -> DeviceListIterator { + DeviceListIterator { + current: Some(self), + } } - }); - - let ( - mut names, - mut unix_socket_list, - mut mount_list, - mut device_list, - mut inode_list, - mut need_check_map, - ) = init_defaults(file); - - #[cfg(target_os = "linux")] - get_matched_procs_linux( - &mut names, - &mut unix_socket_list, - &mut mount_list, - &mut inode_list, - &mut device_list, - &mut need_check_map, - mount, - )?; - - #[cfg(target_os = "macos")] - get_matched_procs_macos(&mut names, mount)?; - - for name in names.iter_mut() { - print_matches(name, user)?; } - std::process::exit(0); -} + struct DeviceListIterator<'a> { + current: Option<&'a DeviceList>, + } -#[cfg(target_os = "linux")] -fn get_matched_procs_linux( - names: &mut Vec, - unix_socket_list: &mut UnixSocketList, - mount_list: &mut MountList, - inode_list: &mut InodeList, - device_list: &mut DeviceList, - need_check_map: &mut bool, - mount: bool, -) -> Result<(), io::Error> { - fill_unix_cache(unix_socket_list)?; + impl<'a> Iterator for DeviceListIterator<'a> { + type Item = &'a DeviceList; - let net_dev = find_net_dev()?; + fn next(&mut self) -> Option { + self.current.map(|node| { + self.current = node.next.as_deref(); + node + }) + } + } - for name in names.iter_mut() { - name.name_space = determine_namespace(&name.filename); - match name.name_space { - NameSpace::File => handle_file_namespace( - name, - mount, - mount_list, - inode_list, - device_list, - need_check_map, - )?, - NameSpace::Tcp => { - if !mount { - handle_tcp_namespace(name, inode_list, net_dev)?; + pub fn get_matched_procs(file: Vec, mount: bool) -> Result, io::Error> { + let ( + mut names, + mut unix_socket_list, + mut mount_list, + mut device_list, + mut inode_list, + mut need_check_map, + ) = init_defaults(file); + fill_unix_cache(&mut unix_socket_list)?; + + let net_dev = find_net_dev()?; + + for name in names.iter_mut() { + name.name_space = determine_namespace(&name.filename); + match name.name_space { + NameSpace::File => handle_file_namespace( + name, + mount, + &mut mount_list, + &mut inode_list, + &mut device_list, + &mut need_check_map, + )?, + NameSpace::Tcp => { + if !mount { + handle_tcp_namespace(name, &mut inode_list, net_dev)?; + } } - } - NameSpace::Udp => { - if !mount { - handle_udp_namespace(name, inode_list, net_dev)?; + NameSpace::Udp => { + if !mount { + handle_udp_namespace(name, &mut inode_list, net_dev)?; + } } } - } - if scan_procs( - *need_check_map, - name, - &inode_list, - &device_list, - &unix_socket_list, - net_dev, - ) - .is_err() - { - std::process::exit(1); + if scan_procs( + need_check_map, + name, + &inode_list, + &device_list, + &unix_socket_list, + net_dev, + ) + .is_err() + { + std::process::exit(1); + } } + + Ok(names) } - Ok(()) -} + /// Scans the `/proc` directory for process information and checks various access types. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for storing matched processes. + /// * `inode_list` - A reference to an `InodeList` for updating inode information. + /// * `device_list` - A reference to a `DeviceList` for updating device information. + /// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. + /// * `net_dev` - A `u64` representing the network device identifier. + /// + /// # Errors + /// + /// Returns an error if reading directory entries, accessing process stats, or checking access types fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn scan_procs( + need_check_map: bool, + names: &mut Names, + inode_list: &InodeList, + device_list: &DeviceList, + unix_socket_list: &UnixSocketList, + net_dev: u64, + ) -> Result<(), io::Error> { + let my_pid = std::process::id() as i32; + let dir_entries = fs::read_dir(PROC_PATH)?; + + for entry in dir_entries { + let entry = entry?; + let filename = entry + .file_name() + .into_string() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid file name"))?; + + if let Ok(pid) = filename.parse::() { + // Skip the current process + if pid == my_pid { + continue; + } -#[cfg(target_os = "macos")] -fn get_matched_procs_macos( - names: &mut Vec, - mount: bool, -) -> Result<(), Box> { - for name in names.iter_mut() { - let st = timeout(&name.filename.to_string_lossy(), 5)?; - let uid = st.st_uid; + let cwd_stat = match get_pid_stat(pid, "/cwd") { + Ok(stat) => stat, + Err(_) => continue, + }; - let pids = listpidspath( - osx_libproc_bindings::PROC_ALL_PIDS, - Path::new(&name.filename), - mount, - false, - )?; + let exe_stat = match get_pid_stat(pid, "/exe") { + Ok(stat) => stat, + Err(_) => continue, + }; + let root_stat = match get_pid_stat(pid, "/root") { + Ok(stat) => stat, + Err(_) => continue, + }; - for pid in pids { - add_process( - name, - pid.try_into().unwrap(), - uid, - Access::Cwd, - ProcType::Normal, - None, - ); + let st = timeout(&entry.path().to_string_lossy(), 5)?; + let uid = st.st_uid; + + check_root_access(names, pid, uid, &root_stat, device_list, inode_list)?; + check_cwd_access(names, pid, uid, &cwd_stat, device_list, inode_list)?; + check_exe_access(names, pid, uid, &exe_stat, device_list, inode_list)?; + + if need_check_map { + check_map(names, pid, "maps", device_list, uid, Access::Mmap)?; + } + + check_dir( + names, + pid, + "fd", + device_list, + inode_list, + uid, + Access::File, + unix_socket_list, + net_dev, + )?; + } } + + Ok(()) } - Ok(()) -} -/// Initializes and returns default values. -/// -/// # Arguments -/// -/// * `file` - A vector of `PathBuf` representing the file paths used to initialize `Names` objects. -/// -/// # Returns -/// -/// Returns a tuple containing: -/// -/// * Default-initialized `Vec`, UnixSocketList`, `MountList`, `DeviceList`, `InodeList`, `IpConnections` (TCP), and `IpConnections` (UDP). -/// * A boolean value set to `false`, indicating the initial state. -fn init_defaults( - files: Vec, -) -> ( - Vec, - UnixSocketList, - MountList, - DeviceList, - InodeList, - bool, -) { - let names_vec = files - .iter() - .map(|path| { - Names::new( - path.clone(), - NameSpace::default(), - LibcStat::default(), - vec![], - ) - }) - .collect(); - - ( - names_vec, - UnixSocketList::default(), - MountList::default(), - DeviceList::default(), - InodeList::default(), - false, - ) -} + /// Checks if a process has access to the root directory and updates the `Names` object if it does. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for adding process information. + /// * `pid` - The process ID to check. + /// * `uid` - The user ID of the process. + /// * `root_stat` - A reference to a `libc::stat` structure containing root directory information. + /// * `device_list` - A reference to a `DeviceList` for checking device IDs. + /// * `inode_list` - A reference to an `InodeList` for checking inode information. + /// + /// # Errors + /// + /// Returns an error if adding process information fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn check_root_access( + names: &mut Names, + pid: i32, + uid: u32, + root_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, + ) -> Result<(), io::Error> { + if device_list + .iter() + .any(|device| device.device_id == root_stat.st_dev) + { + add_process(names, pid, uid, Access::Root, ProcType::Normal); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == root_stat.st_dev && inode.inode == root_stat.st_ino) + { + add_process(names, pid, uid, Access::Root, ProcType::Normal); + return Ok(()); + } -/// Determines the `NameSpace` based on the presence of "tcp" or "udp" in the path string. -/// -/// # Arguments -/// -/// * `filename` -`PathBuf`. -/// -/// # Returns -/// -/// Namespace type -fn determine_namespace(filename: &Path) -> NameSpace { - filename - .to_str() - .and_then(|name_str| { - let parts: Vec<&str> = name_str.split('/').collect(); - if parts.len() == 2 && parts[0].parse::().is_ok() { - match parts[1] { - "tcp" => Some(NameSpace::Tcp), - "udp" => Some(NameSpace::Udp), - _ => None, + Ok(()) + } + + /// Checks if a process has access to the current working directory and updates the `Names` object if it does. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for adding process information. + /// * `pid` - The process ID to check. + /// * `uid` - The user ID of the process. + /// * `cwd_stat` - A reference to a `libc::stat` structure containing current working directory information. + /// * `device_list` - A reference to a `DeviceList` for checking device IDs. + /// * `inode_list` - A reference to an `InodeList` for checking inode information. + /// + /// # Errors + /// + /// Returns an error if adding process information fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn check_cwd_access( + names: &mut Names, + pid: i32, + uid: u32, + cwd_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, + ) -> Result<(), std::io::Error> { + if device_list + .iter() + .any(|device| device.device_id == cwd_stat.st_dev) + { + add_process(names, pid, uid, Access::Cwd, ProcType::Normal); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == cwd_stat.st_dev && inode.inode == cwd_stat.st_ino) + { + add_process(names, pid, uid, Access::Cwd, ProcType::Normal); + return Ok(()); + } + + Ok(()) + } + + /// Checks if a process has access to the executable file and updates the `Names` object if it does. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for adding process information. + /// * `pid` - The process ID to check. + /// * `uid` - The user ID of the process. + /// * `exe_stat` - A reference to a `libc::stat` structure containing executable file information. + /// * `device_list` - A reference to a `DeviceList` for checking device IDs. + /// * `inode_list` - A reference to an `InodeList` for checking inode information. + /// + /// # Errors + /// + /// Returns an error if adding process information fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn check_exe_access( + names: &mut Names, + pid: i32, + uid: u32, + exe_stat: &libc::stat, + device_list: &DeviceList, + inode_list: &InodeList, + ) -> Result<(), io::Error> { + if device_list + .iter() + .any(|device| device.device_id == exe_stat.st_dev) + { + add_process(names, pid, uid, Access::Exe, ProcType::Normal); + return Ok(()); + } + if inode_list + .iter() + .any(|inode| inode.device_id == exe_stat.st_dev && inode.inode == exe_stat.st_ino) + { + add_process(names, pid, uid, Access::Exe, ProcType::Normal); + return Ok(()); + } + + Ok(()) + } + + /// Checks a directory within a process's `/proc` entry for matching devices and inodes, + /// and updates the `Names` object with relevant process information. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for adding process information. + /// * `pid` - The process ID whose directory is being checked. + /// * `dirname` - The name of the directory to check (e.g., "fd"). + /// * `device_list` - A reference to a `DeviceList` for checking device IDs. + /// * `inode_list` - A reference to an `InodeList` for checking inode information. + /// * `uid` - The user ID of the process. + /// * `access` - The type of access to assign (e.g., File, Filewr). + /// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. + /// * `net_dev` - A `u64` representing the network device identifier. + /// + /// # Errors + /// + /// Returns an error if reading directory entries or accessing file stats fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn check_dir( + names: &mut Names, + pid: i32, + dirname: &str, + device_list: &DeviceList, + inode_list: &InodeList, + uid: u32, + access: Access, + unix_socket_list: &UnixSocketList, + net_dev: u64, + ) -> Result<(), io::Error> { + let dir_path = format!("/proc/{}/{}", pid, dirname); + let dir_entries = fs::read_dir(&dir_path)?; + for entry in dir_entries { + let entry = entry?; + let path = entry.path(); + let path_str = path.to_string_lossy(); + + let mut stat = match timeout(&path_str, 5) { + Ok(stat) => stat, + Err(_) => continue, + }; + + if stat.st_dev == net_dev { + if let Some(unix_socket) = unix_socket_list + .iter() + .find(|sock| sock.net_inode == stat.st_ino) + { + stat.st_dev = unix_socket.device_id; + stat.st_ino = unix_socket.inode; + } + } + + let new_access = match access { + Access::File => Access::Filewr, + _ => access.clone(), + }; + if device_list + .iter() + .any(|dev| dev.name.filename != PathBuf::from("") && stat.st_dev == dev.device_id) + || inode_list.iter().any(|inode| inode.inode == stat.st_ino) + { + add_process(names, pid, uid, new_access, ProcType::Normal); + } + } + + Ok(()) + } + + /// Checks the memory map of a process for matching devices and updates the `Names` object. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object for adding process information. + /// * `pid` - The process ID whose memory map is being checked. + /// * `filename` - The name of the file containing the memory map (e.g., "maps"). + /// * `device_list` - A reference to a `DeviceList` for checking device IDs. + /// * `uid` - The user ID of the process. + /// * `access` - The type of access to assign (e.g., Mmap). + /// + /// # Errors + /// + /// Returns an error if opening the file or reading lines fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn check_map( + names: &mut Names, + pid: i32, + filename: &str, + device_list: &DeviceList, + uid: u32, + access: Access, + ) -> Result<(), io::Error> { + let already_exists = names.matched_procs.iter().any(|p| p.pid == pid); + + if already_exists { + return Ok(()); + } + + let pathname = format!("/proc/{}/{}", pid, filename); + let file = File::open(&pathname)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 5 { + let dev_info: Vec<&str> = parts[3].split(':').collect(); + if dev_info.len() == 2 { + let tmp_maj = match u32::from_str_radix(dev_info[0], 16) { + Ok(value) => value, + Err(_) => continue, + }; + + let tmp_min = match u32::from_str_radix(dev_info[1], 16) { + Ok(value) => value, + Err(_) => continue, + }; + + let device = tmp_maj * 256 + tmp_min; + let device_int = device as u64; + + if device_list + .iter() + .any(|device| device.device_id == device_int) + { + add_process(names, pid, uid, access.clone(), ProcType::Normal); + } + } + } + } + Ok(()) + } + + /// get stat of current /proc/{pid}/{filename} + fn get_pid_stat(pid: i32, filename: &str) -> Result { + let path = format!("{}/{}{}", PROC_PATH, pid, filename); + timeout(&path, 5) + } + + /// Fills the `unix_socket_list` with information from the `/proc/net/unix` file. + /// + /// # Arguments + /// + /// * `unix_socket_list` - A mutable reference to a `UnixSocketList` to be populated with socket information. + /// + /// # Errors + /// + /// Returns an error if opening the file or reading lines fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn fill_unix_cache(unix_socket_list: &mut UnixSocketList) -> Result<(), io::Error> { + let file = File::open("/proc/net/unix")?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + + if let (Some(net_inode_str), Some(scanned_path)) = (parts.get(6), parts.get(7)) { + let net_inode = net_inode_str.parse().unwrap_or(0); + let path = normalize_path(scanned_path); + + match timeout(&path, 5) { + Ok(stat) => UnixSocketList::add_socket( + unix_socket_list, + stat.st_dev, + stat.st_ino, + net_inode, + ), + Err(_) => continue, + } + } + } + Ok(()) + } + + /// Reads the `/proc/mounts` file and updates the `mount_list` with mount points. + /// + /// # Arguments + /// + /// * `mount_list` - A mutable reference to a `MountList` for storing the mount points. + /// + /// # Errors + /// + /// Returns an error if opening the file or reading lines fails. + /// + /// # Returns + /// + /// Returns `Ok(mount_list)` on success, with the `mount_list` updated. + fn read_proc_mounts(mount_list: &mut MountList) -> io::Result<&mut MountList> { + let file = File::open(PROC_MOUNTS)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + let mountpoint = PathBuf::from(parts[1].trim()); + mount_list.mountpoints.push(mountpoint); + } + + Ok(mount_list) + } + + /// Normalizes a file path by removing the leading '@' character if present. + fn normalize_path(scanned_path: &str) -> String { + if let Some(path) = scanned_path.strip_prefix('@') { + path.to_string() + } else { + scanned_path.to_string() + } + } + + /// Parses network socket information from the `filename` field of the `Names` struct + /// and returns an `IpConnections` instance. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` struct containing the filename to parse. + /// * `ip_list` - A mutable reference to an `IpConnections` instance to be populated. + /// + /// # Errors + /// + /// Returns an error if the filename format is invalid or if parsing fails. + /// + /// # Returns + /// + /// Returns an `IpConnections` instance populated with parsed network information. + fn parse_inet(names: &mut Names) -> Result { + let filename_str = names.filename.to_string_lossy(); + let parts: Vec<&str> = filename_str.split('/').collect(); + + if parts.len() < 2 { + return Err(Error::new( + ErrorKind::InvalidInput, + "Invalid filename format", + )); + } + + let hostspec = parts[0]; + let host_parts: Vec<&str> = hostspec.split(',').collect(); + + let lcl_port_str = host_parts + .first() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "Local port is missing"))?; + let rmt_addr_str = host_parts.get(1).cloned(); + let rmt_port_str = host_parts.get(2).cloned(); + + let lcl_port = lcl_port_str + .parse::() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid local port format"))?; + + let rmt_port = rmt_port_str + .as_ref() + .map(|s| { + s.parse::() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid remote port format")) + }) + .transpose()? + .unwrap_or(0); + + let rmt_addr = match rmt_addr_str { + Some(addr_str) => match addr_str.parse::() { + Ok(addr) => addr, + Err(_) => { + eprintln!("Warning: Invalid remote address {}", addr_str); + IpAddr::V4(Ipv4Addr::UNSPECIFIED) // Default value if address parsing fails + } + }, + None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), // Default value if address is not provided + }; + + Ok(IpConnections::new(lcl_port, rmt_port, rmt_addr)) + } + /// Retrieves the device identifier of the network interface associated with a UDP socket. + /// + /// # Errors + /// + /// Returns an error if binding the socket or retrieving the device identifier fails. + /// + /// # Returns + /// + /// Returns the device identifier (`u64`) of the network interface. + fn find_net_dev() -> Result { + let socket = UdpSocket::bind("0.0.0.0:0")?; + let fd = socket.as_raw_fd(); + let mut stat_buf = unsafe { std::mem::zeroed() }; + + unsafe { + if fstat(fd, &mut stat_buf) != 0 { + return Err(io::Error::last_os_error()); + } + } + + Ok(stat_buf.st_dev) + } + + /// Finds network sockets based on the given protocol and updates the `InodeList` + /// with the relevant inode information if a matching connection is found. + /// + /// # Arguments + /// + /// * `inode_list` - A mutable reference to the `InodeList` that will be updated. + /// * `connections_list` - A reference to the `IpConnections` that will be used to match connections. + /// * `protocol` - A `&str` representing the protocol (e.g., "tcp", "udp") to look for in `/proc/net`. + /// * `net_dev` - A `u64` representing the network device identifier. + /// + /// # Errors + /// + /// Returns an `io::Error` if there is an issue opening or reading the file at `/proc/net/{protocol}`, or + /// if parsing the net sockets fails. + /// + /// # Returns + /// + /// Returns an `InodeList` containing the updated information if a matching connection is found. + /// Returns an `io::Error` with `ErrorKind::ConnectionRefused` if can't parse sockets. + fn find_net_sockets( + connections_list: &IpConnections, + protocol: &str, + net_dev: u64, + ) -> Result { + let pathname = format!("/proc/net/{}", protocol); + + let file = File::open(&pathname)?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + + let parts: Vec<&str> = line.split_whitespace().collect(); + let parse_hex_port = |port_str: &str| -> Option { + port_str + .split(':') + .nth(1) + .and_then(|s| u64::from_str_radix(s, 16).ok()) + }; + + let loc_port = parts.get(1).and_then(|&s| parse_hex_port(s)); + let rmt_port = parts.get(2).and_then(|&s| parse_hex_port(s)); + let scanned_inode = parts.get(9).and_then(|&s| s.parse::().ok()); + + if let Some(scanned_inode) = scanned_inode { + for connection in connections_list.iter() { + let loc_port = loc_port.unwrap_or(0); + let rmt_port = rmt_port.unwrap_or(0); + let rmt_addr = parse_ipv4_addr(parts[2].split(':').next().unwrap_or("")) + .unwrap_or(Ipv4Addr::UNSPECIFIED); + + if (connection.lcl_port == 0 || connection.lcl_port == loc_port) + && (connection.rmt_port == 0 || connection.rmt_port == rmt_port) + && (connection.rmt_addr == Ipv4Addr::UNSPECIFIED + || connection.rmt_addr == rmt_addr) + { + return Ok(InodeList::new(net_dev, scanned_inode)); + } + } + } + } + + Err(Error::new( + ErrorKind::ConnectionRefused, + "Cannot parse net sockets", + )) + } + + /// Parses a hexadecimal string representation of an IPv4 address. + fn parse_ipv4_addr(addr: &str) -> Option { + if addr.len() == 8 { + let octets = [ + u8::from_str_radix(&addr[0..2], 16).ok()?, + u8::from_str_radix(&addr[2..4], 16).ok()?, + u8::from_str_radix(&addr[4..6], 16).ok()?, + u8::from_str_radix(&addr[6..8], 16).ok()?, + ]; + Some(Ipv4Addr::from(octets)) + } else { + None + } + } + + /// This function handles relative paths by resolving them against the current working directory to absolute path + /// + /// # Arguments + /// + /// * `path` - [str](std::str) that represents the file path. + /// + /// # Errors + /// + /// Returns an error if passed invalid input. + /// + /// # Returns + /// + /// Returns PathBuf real_path. + pub fn expand_path(path: &PathBuf) -> Result { + let mut real_path = if path.starts_with(Path::new("/")) { + PathBuf::from("/") + } else { + env::current_dir()? + }; + + for component in Path::new(path).components() { + match component { + Component::CurDir => { + // Ignore '.' + } + Component::ParentDir => { + // Handle '..' by moving up one directory level if possible + real_path.pop(); + } + Component::Normal(name) => { + // Append directory or file name + real_path.push(name); + } + Component::RootDir | Component::Prefix(_) => { + // Handle root directory or prefix + real_path = PathBuf::from(component.as_os_str()); } - } else { - None } - }) - .unwrap_or_else(NameSpace::default) + } + + if real_path.as_os_str() != "/" && real_path.as_os_str().to_string_lossy().ends_with('/') { + real_path.pop(); + } + + Ok(real_path) + } + + /// Determines the `NameSpace` based on the presence of "tcp" or "udp" in the path string. + /// + /// # Arguments + /// + /// * `filename` -`PathBuf`. + /// + /// # Returns + /// + /// Namespace type + fn determine_namespace(filename: &Path) -> NameSpace { + filename + .to_str() + .and_then(|name_str| { + let parts: Vec<&str> = name_str.split('/').collect(); + if parts.len() == 2 && parts[0].parse::().is_ok() { + match parts[1] { + "tcp" => Some(NameSpace::Tcp), + "udp" => Some(NameSpace::Udp), + _ => None, + } + } else { + None + } + }) + .unwrap_or_else(NameSpace::default) + } + + /// Processes file namespaces by expanding paths and updating lists based on the mount flag. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object containing the file path. + /// * `mount` - A boolean indicating whether to handle mount information or not. + /// * `mount_list` - A mutable reference to a `MountList` for reading mount points. + /// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. + /// * `device_list` - A mutable reference to a `DeviceList` for updating device information. + /// + /// # Errors + /// + /// Returns an error if path expansion, file status retrieval, or `/proc/mounts` reading fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn handle_file_namespace( + names: &mut Names, + mount: bool, + mount_list: &mut MountList, + inode_list: &mut InodeList, + device_list: &mut DeviceList, + need_check_map: &mut bool, + ) -> Result<(), std::io::Error> { + names.filename = expand_path(&names.filename)?; + let st = timeout(&names.filename.to_string_lossy(), 5)?; + read_proc_mounts(mount_list)?; + + if mount { + *device_list = DeviceList::new(names.clone(), st.st_dev); + *need_check_map = true; + } else { + let st = stat(&names.filename.to_string_lossy())?; + *inode_list = InodeList::new(st.st_dev, st.st_ino); + } + Ok(()) + } + + /// Handles TCP namespace processing by updating connection and inode lists. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object containing the file path. + /// * `tcp_connection_list` - A mutable reference to an `IpConnections` object for TCP connections. + /// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. + /// * `net_dev` - A `u64` representing the network device identifier. + /// + /// # Errors + /// + /// Returns an error if parsing TCP connections or finding network sockets fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn handle_tcp_namespace( + names: &mut Names, + inode_list: &mut InodeList, + net_dev: u64, + ) -> Result<(), std::io::Error> { + let tcp_connection_list = parse_inet(names)?; + *inode_list = find_net_sockets(&tcp_connection_list, "tcp", net_dev)?; + Ok(()) + } + + /// Handles UDP namespace processing by updating connection and inode lists. + /// + /// # Arguments + /// + /// * `names` - A mutable reference to a `Names` object containing the file path. + /// * `udp_connection_list` - A mutable reference to an `IpConnections` object for UDP connections. + /// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. + /// * `net_dev` - A `u64` representing the network device identifier. + /// + /// # Errors + /// + /// Returns an error if parsing UDP connections or finding network sockets fails. + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + fn handle_udp_namespace( + names: &mut Names, + inode_list: &mut InodeList, + net_dev: u64, + ) -> Result<(), std::io::Error> { + let udp_connection_list = parse_inet(names)?; + *inode_list = find_net_sockets(&udp_connection_list, "udp", net_dev)?; + Ok(()) + } + + /// Initializes and returns default values. + /// + /// # Arguments + /// + /// * `file` - A vector of `PathBuf` representing the file paths used to initialize `Names` objects. + /// + /// # Returns + /// + /// Returns a tuple containing: + /// + /// * Default-initialized `Vec`, UnixSocketList`, `MountList`, `DeviceList`, `InodeList`, `IpConnections` (TCP), and `IpConnections` (UDP). + /// * A boolean value set to `false`, indicating the initial state. + fn init_defaults( + files: Vec, + ) -> ( + Vec, + UnixSocketList, + MountList, + DeviceList, + InodeList, + bool, + ) { + let names_vec = files + .iter() + .map(|path| Names::new(path.clone(), NameSpace::default(), vec![])) + .collect(); + + ( + names_vec, + UnixSocketList::default(), + MountList::default(), + DeviceList::default(), + InodeList::default(), + false, + ) + } } -/// Processes file namespaces by expanding paths and updating lists based on the mount flag. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object containing the file path. -/// * `mount` - A boolean indicating whether to handle mount information or not. -/// * `mount_list` - A mutable reference to a `MountList` for reading mount points. -/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. -/// * `device_list` - A mutable reference to a `DeviceList` for updating device information. -/// -/// # Errors -/// -/// Returns an error if path expansion, file status retrieval, or `/proc/mounts` reading fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn handle_file_namespace( - names: &mut Names, - mount: bool, - mount_list: &mut MountList, - inode_list: &mut InodeList, - device_list: &mut DeviceList, - need_check_map: &mut bool, -) -> Result<(), std::io::Error> { - names.filename = expand_path(&names.filename)?; - let st = timeout(&names.filename.to_string_lossy(), 5)?; - read_proc_mounts(mount_list)?; - - if mount { - *device_list = DeviceList::new(names.clone(), st.st_dev); - *need_check_map = true; - } else { - let st = stat(&names.filename.to_string_lossy())?; - *inode_list = InodeList::new(names.clone(), st.st_dev, st.st_ino); +#[cfg(target_os = "macos")] +mod macos { + use super::*; + + #[allow(warnings, missing_docs)] + pub mod osx_libproc_bindings { + include!(concat!(env!("OUT_DIR"), "/osx_libproc_bindings.rs")); + } + use libc::{c_char, c_int, c_void}; + use std::{os::unix::ffi::OsStrExt, ptr}; + + // similar to list_pids_ret() below, there are two cases when 0 is returned, one when there are + // no pids, and the other when there is an error + // when `errno` is set to indicate an error in the input type, the return value is 0 + fn check_listpid_ret(ret: c_int) -> io::Result> { + if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { + return Err(io::Error::last_os_error()); + } + + // `ret` cannot be negative here - so no possible loss of sign + #[allow(clippy::cast_sign_loss)] + let capacity = ret as usize / std::mem::size_of::(); + Ok(Vec::with_capacity(capacity)) + } + + // Common code for handling the special case of listpids return, where 0 is a valid return + // but is also used in the error case - so we need to look at errno to distringish between a valid + // 0 return and an error return + // when `errno` is set to indicate an error in the input type, the return value is 0 + fn list_pids_ret(ret: c_int, mut pids: Vec) -> io::Result> { + if ret < 0 || (ret == 0 && io::Error::last_os_error().raw_os_error().unwrap_or(0) != 0) { + Err(io::Error::last_os_error()) + } else { + // `ret` cannot be negative here, so no possible loss of sign + #[allow(clippy::cast_sign_loss)] + let items_count = ret as usize / std::mem::size_of::(); + unsafe { + pids.set_len(items_count); + } + Ok(pids) + } + } + + pub(crate) fn listpidspath( + proc_type: u32, + path: &Path, + is_volume: bool, + exclude_event_only: bool, + ) -> io::Result> { + let path_bytes = path.as_os_str().as_bytes(); + let c_path = CString::new(path_bytes) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "CString::new failed"))?; + let mut pathflags: u32 = 0; + if is_volume { + pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_PATH_IS_VOLUME; + } + if exclude_event_only { + pathflags |= osx_libproc_bindings::PROC_LISTPIDSPATH_EXCLUDE_EVTONLY; + } + + let buffer_size = unsafe { + osx_libproc_bindings::proc_listpidspath( + proc_type, + proc_type, + c_path.as_ptr().cast::(), + pathflags, + ptr::null_mut(), + 0, + ) + }; + let mut pids = check_listpid_ret(buffer_size)?; + let buffer_ptr = pids.as_mut_ptr().cast::(); + + let ret = unsafe { + osx_libproc_bindings::proc_listpidspath( + proc_type, + proc_type, + c_path.as_ptr().cast::(), + 0, + buffer_ptr, + buffer_size, + ) + }; + + list_pids_ret(ret, pids) + } + + pub fn get_matched_procs( + file: Vec, + mount: bool, + ) -> Result, Box> { + let mut names: Vec = file + .iter() + .map(|path| Names::new(path.clone(), vec![])) + .collect(); + + for name in names.iter_mut() { + let st = timeout(&name.filename.to_string_lossy(), 5)?; + let uid = st.st_uid; + + let pids = listpidspath( + osx_libproc_bindings::PROC_ALL_PIDS, + Path::new(&name.filename), + mount, + false, + )?; + + for pid in pids { + add_process( + name, + pid.try_into().unwrap(), + uid, + Access::Cwd, + ProcType::Normal, + ); + } + } + + Ok(names) } - Ok(()) } -/// Handles TCP namespace processing by updating connection and inode lists. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object containing the file path. -/// * `tcp_connection_list` - A mutable reference to an `IpConnections` object for TCP connections. -/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. -/// * `net_dev` - A `u64` representing the network device identifier. -/// -/// # Errors -/// -/// Returns an error if parsing TCP connections or finding network sockets fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn handle_tcp_namespace( - names: &mut Names, - inode_list: &mut InodeList, - net_dev: DeviceId, -) -> Result<(), std::io::Error> { - let tcp_connection_list = parse_inet(names)?; - *inode_list = find_net_sockets(&tcp_connection_list, "tcp", net_dev)?; - Ok(()) +/// fuser - list process IDs of all processes that have one or more files open +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// The file is treated as a mount point and the utility shall report on any files open in the file system. + #[arg(short = 'c')] + mount: bool, + /// The report shall be only for the named files. + #[arg(short = 'f')] + named_files: bool, + /// The user name, in parentheses, associated with each process ID written to standard output shall be written to standard error. + #[arg(short = 'u')] + user: bool, + + #[arg(required = true, name = "FILE", num_args(0..))] + /// A pathname on which the file or file system is to be reported. + file: Vec, } +fn main() -> Result<(), Box> { + setlocale(LocaleCategory::LcAll, ""); + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; -/// Handles UDP namespace processing by updating connection and inode lists. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object containing the file path. -/// * `udp_connection_list` - A mutable reference to an `IpConnections` object for UDP connections. -/// * `inode_list` - A mutable reference to an `InodeList` for updating inode information. -/// * `net_dev` - A `u64` representing the network device identifier. -/// -/// # Errors -/// -/// Returns an error if parsing UDP connections or finding network sockets fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn handle_udp_namespace( - names: &mut Names, - inode_list: &mut InodeList, - net_dev: DeviceId, -) -> Result<(), std::io::Error> { - let udp_connection_list = parse_inet(names)?; - *inode_list = find_net_sockets(&udp_connection_list, "udp", net_dev)?; - Ok(()) + let Args { + mount, user, file, .. + } = Args::try_parse().unwrap_or_else(|err| match err.kind() { + clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { + print!("{err}"); + std::process::exit(1); + } + _ => { + let mut stdout = std::io::stdout(); + let mut cmd = Args::command(); + eprintln!("No process specification given"); + cmd.write_help(&mut stdout).unwrap(); + std::process::exit(1); + } + }); + + #[cfg(target_os = "linux")] + let mut names = linux::get_matched_procs(file, mount)?; + + #[cfg(target_os = "macos")] + let mut names = macos::get_matched_procs(file, mount)?; + + for name in names.iter_mut() { + print_matches(name, user)?; + } + + std::process::exit(0); } /// Prints process matches for a given `Names` object to `stderr` and `stdout`. @@ -778,6 +1389,7 @@ fn print_matches(name: &mut Names, user: bool) -> Result<(), io::Error> { .entry(procs.pid) .or_insert((String::new(), procs.uid)); + #[cfg(target_os = "linux")] match procs.access { Access::Root => entry.0.push('r'), Access::Cwd => entry.0.push('c'), @@ -785,6 +1397,12 @@ fn print_matches(name: &mut Names, user: bool) -> Result<(), io::Error> { Access::Mmap => entry.0.push('m'), _ => (), } + + #[cfg(target_os = "macos")] + match procs.access { + Access::Cwd => entry.0.push('c'), + _ => (), + } } if !name_has_procs { @@ -819,646 +1437,6 @@ fn print_matches(name: &mut Names, user: bool) -> Result<(), io::Error> { Ok(()) } -/// Scans the `/proc` directory for process information and checks various access types. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for storing matched processes. -/// * `inode_list` - A reference to an `InodeList` for updating inode information. -/// * `device_list` - A reference to a `DeviceList` for updating device information. -/// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. -/// * `net_dev` - A `u64` representing the network device identifier. -/// -/// # Errors -/// -/// Returns an error if reading directory entries, accessing process stats, or checking access types fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn scan_procs( - need_check_map: bool, - names: &mut Names, - inode_list: &InodeList, - device_list: &DeviceList, - unix_socket_list: &UnixSocketList, - net_dev: DeviceId, -) -> Result<(), io::Error> { - let my_pid = std::process::id() as i32; - let dir_entries = fs::read_dir(PROC_PATH)?; - - for entry in dir_entries { - let entry = entry?; - let filename = entry - .file_name() - .into_string() - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid file name"))?; - - if let Ok(pid) = filename.parse::() { - // Skip the current process - if pid == my_pid { - continue; - } - - let cwd_stat = match get_pid_stat(pid, "/cwd") { - Ok(stat) => stat, - Err(_) => continue, - }; - - let exe_stat = match get_pid_stat(pid, "/exe") { - Ok(stat) => stat, - Err(_) => continue, - }; - let root_stat = match get_pid_stat(pid, "/root") { - Ok(stat) => stat, - Err(_) => continue, - }; - - let st = timeout(&entry.path().to_string_lossy(), 5)?; - let uid = st.st_uid; - - check_root_access(names, pid, uid, &root_stat, device_list, inode_list)?; - check_cwd_access(names, pid, uid, &cwd_stat, device_list, inode_list)?; - check_exe_access(names, pid, uid, &exe_stat, device_list, inode_list)?; - - if need_check_map { - check_map(names, pid, "maps", device_list, uid, Access::Mmap)?; - } - - check_dir( - names, - pid, - "fd", - device_list, - inode_list, - uid, - Access::File, - unix_socket_list, - net_dev, - )?; - } - } - - Ok(()) -} - -/// Checks if a process has access to the root directory and updates the `Names` object if it does. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for adding process information. -/// * `pid` - The process ID to check. -/// * `uid` - The user ID of the process. -/// * `root_stat` - A reference to a `libc::stat` structure containing root directory information. -/// * `device_list` - A reference to a `DeviceList` for checking device IDs. -/// * `inode_list` - A reference to an `InodeList` for checking inode information. -/// -/// # Errors -/// -/// Returns an error if adding process information fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn check_root_access( - names: &mut Names, - pid: i32, - uid: u32, - root_stat: &libc::stat, - device_list: &DeviceList, - inode_list: &InodeList, -) -> Result<(), io::Error> { - if device_list - .iter() - .any(|device| device.device_id == root_stat.st_dev) - { - add_process(names, pid, uid, Access::Root, ProcType::Normal, None); - return Ok(()); - } - if inode_list - .iter() - .any(|inode| inode.device_id == root_stat.st_dev && inode.inode == root_stat.st_ino) - { - add_process(names, pid, uid, Access::Root, ProcType::Normal, None); - return Ok(()); - } - - Ok(()) -} - -/// Checks if a process has access to the current working directory and updates the `Names` object if it does. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for adding process information. -/// * `pid` - The process ID to check. -/// * `uid` - The user ID of the process. -/// * `cwd_stat` - A reference to a `libc::stat` structure containing current working directory information. -/// * `device_list` - A reference to a `DeviceList` for checking device IDs. -/// * `inode_list` - A reference to an `InodeList` for checking inode information. -/// -/// # Errors -/// -/// Returns an error if adding process information fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn check_cwd_access( - names: &mut Names, - pid: i32, - uid: u32, - cwd_stat: &libc::stat, - device_list: &DeviceList, - inode_list: &InodeList, -) -> Result<(), std::io::Error> { - if device_list - .iter() - .any(|device| device.device_id == cwd_stat.st_dev) - { - add_process(names, pid, uid, Access::Cwd, ProcType::Normal, None); - return Ok(()); - } - if inode_list - .iter() - .any(|inode| inode.device_id == cwd_stat.st_dev && inode.inode == cwd_stat.st_ino) - { - add_process(names, pid, uid, Access::Cwd, ProcType::Normal, None); - return Ok(()); - } - - Ok(()) -} - -/// Checks if a process has access to the executable file and updates the `Names` object if it does. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for adding process information. -/// * `pid` - The process ID to check. -/// * `uid` - The user ID of the process. -/// * `exe_stat` - A reference to a `libc::stat` structure containing executable file information. -/// * `device_list` - A reference to a `DeviceList` for checking device IDs. -/// * `inode_list` - A reference to an `InodeList` for checking inode information. -/// -/// # Errors -/// -/// Returns an error if adding process information fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn check_exe_access( - names: &mut Names, - pid: i32, - uid: u32, - exe_stat: &libc::stat, - device_list: &DeviceList, - inode_list: &InodeList, -) -> Result<(), io::Error> { - if device_list - .iter() - .any(|device| device.device_id == exe_stat.st_dev) - { - add_process(names, pid, uid, Access::Exe, ProcType::Normal, None); - return Ok(()); - } - if inode_list - .iter() - .any(|inode| inode.device_id == exe_stat.st_dev && inode.inode == exe_stat.st_ino) - { - add_process(names, pid, uid, Access::Exe, ProcType::Normal, None); - return Ok(()); - } - - Ok(()) -} - -/// Adds a new process to the `Names` object with specified access and process type. -fn add_process( - names: &mut Names, - pid: i32, - uid: u32, - access: Access, - proc_type: ProcType, - command: Option, -) { - let proc = Procs::new(pid, uid, access, proc_type, command.unwrap_or_default()); - names.add_procs(proc); -} - -/// Checks a directory within a process's `/proc` entry for matching devices and inodes, -/// and updates the `Names` object with relevant process information. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for adding process information. -/// * `pid` - The process ID whose directory is being checked. -/// * `dirname` - The name of the directory to check (e.g., "fd"). -/// * `device_list` - A reference to a `DeviceList` for checking device IDs. -/// * `inode_list` - A reference to an `InodeList` for checking inode information. -/// * `uid` - The user ID of the process. -/// * `access` - The type of access to assign (e.g., File, Filewr). -/// * `unix_socket_list` - A reference to a `UnixSocketList` for checking Unix sockets. -/// * `net_dev` - A `u64` representing the network device identifier. -/// -/// # Errors -/// -/// Returns an error if reading directory entries or accessing file stats fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn check_dir( - names: &mut Names, - pid: i32, - dirname: &str, - device_list: &DeviceList, - inode_list: &InodeList, - uid: u32, - access: Access, - unix_socket_list: &UnixSocketList, - net_dev: DeviceId, -) -> Result<(), io::Error> { - let dir_path = format!("/proc/{}/{}", pid, dirname); - let dir_entries = fs::read_dir(&dir_path)?; - for entry in dir_entries { - let entry = entry?; - let path = entry.path(); - let path_str = path.to_string_lossy(); - - let mut stat = match timeout(&path_str, 5) { - Ok(stat) => stat, - Err(_) => continue, - }; - - if stat.st_dev == net_dev { - if let Some(unix_socket) = unix_socket_list - .iter() - .find(|sock| sock.net_inode == stat.st_ino) - { - stat.st_dev = unix_socket.device_id; - stat.st_ino = unix_socket.inode; - } - } - - let new_access = match access { - Access::File => Access::Filewr, - _ => access.clone(), - }; - if device_list - .iter() - .any(|dev| dev.name.filename != PathBuf::from("") && stat.st_dev == dev.device_id) - || inode_list.iter().any(|inode| inode.inode == stat.st_ino) - { - add_process(names, pid, uid, new_access, ProcType::Normal, None); - } - } - - Ok(()) -} - -/// Checks the memory map of a process for matching devices and updates the `Names` object. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` object for adding process information. -/// * `pid` - The process ID whose memory map is being checked. -/// * `filename` - The name of the file containing the memory map (e.g., "maps"). -/// * `device_list` - A reference to a `DeviceList` for checking device IDs. -/// * `uid` - The user ID of the process. -/// * `access` - The type of access to assign (e.g., Mmap). -/// -/// # Errors -/// -/// Returns an error if opening the file or reading lines fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn check_map( - names: &mut Names, - pid: i32, - filename: &str, - device_list: &DeviceList, - uid: u32, - access: Access, -) -> Result<(), io::Error> { - let already_exists = names.matched_procs.iter().any(|p| p.pid == pid); - - if already_exists { - return Ok(()); - } - - let pathname = format!("/proc/{}/{}", pid, filename); - let file = File::open(&pathname)?; - let reader = io::BufReader::new(file); - - for line in reader.lines() { - let line = line?; - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 5 { - let dev_info: Vec<&str> = parts[3].split(':').collect(); - if dev_info.len() == 2 { - let tmp_maj = match u32::from_str_radix(dev_info[0], 16) { - Ok(value) => value, - Err(_) => continue, - }; - - let tmp_min = match u32::from_str_radix(dev_info[1], 16) { - Ok(value) => value, - Err(_) => continue, - }; - - let device = tmp_maj * 256 + tmp_min; - let device_int = device as DeviceId; - - if device_list - .iter() - .any(|device| device.device_id == device_int) - { - add_process(names, pid, uid, access.clone(), ProcType::Normal, None); - } - } - } - } - Ok(()) -} - -/// get stat of current /proc/{pid}/{filename} -fn get_pid_stat(pid: i32, filename: &str) -> Result { - let path = format!("{}/{}{}", PROC_PATH, pid, filename); - timeout(&path, 5) -} - -/// Fills the `unix_socket_list` with information from the `/proc/net/unix` file. -/// -/// # Arguments -/// -/// * `unix_socket_list` - A mutable reference to a `UnixSocketList` to be populated with socket information. -/// -/// # Errors -/// -/// Returns an error if opening the file or reading lines fails. -/// -/// # Returns -/// -/// Returns `Ok(())` on success. -fn fill_unix_cache(unix_socket_list: &mut UnixSocketList) -> Result<(), io::Error> { - let file = File::open("/proc/net/unix")?; - let reader = io::BufReader::new(file); - - for line in reader.lines() { - let line = line?; - let parts: Vec<&str> = line.split_whitespace().collect(); - - if let (Some(net_inode_str), Some(scanned_path)) = (parts.get(6), parts.get(7)) { - let net_inode = net_inode_str.parse().unwrap_or(0); - let path = normalize_path(scanned_path); - - match timeout(&path, 5) { - Ok(stat) => UnixSocketList::add_socket( - unix_socket_list, - scanned_path.to_string(), - stat.st_dev, - stat.st_ino, - net_inode, - ), - Err(_) => continue, - } - } - } - Ok(()) -} - -/// Reads the `/proc/mounts` file and updates the `mount_list` with mount points. -/// -/// # Arguments -/// -/// * `mount_list` - A mutable reference to a `MountList` for storing the mount points. -/// -/// # Errors -/// -/// Returns an error if opening the file or reading lines fails. -/// -/// # Returns -/// -/// Returns `Ok(mount_list)` on success, with the `mount_list` updated. -fn read_proc_mounts(mount_list: &mut MountList) -> io::Result<&mut MountList> { - let file = File::open(PROC_MOUNTS)?; - let reader = io::BufReader::new(file); - - for line in reader.lines() { - let line = line?; - let parts: Vec<&str> = line.split_whitespace().collect(); - let mountpoint = PathBuf::from(parts[1].trim()); - mount_list.mountpoints.push(mountpoint); - } - - Ok(mount_list) -} - -/// Normalizes a file path by removing the leading '@' character if present. -fn normalize_path(scanned_path: &str) -> String { - if let Some(path) = scanned_path.strip_prefix('@') { - path.to_string() - } else { - scanned_path.to_string() - } -} - -/// Parses network socket information from the `filename` field of the `Names` struct -/// and returns an `IpConnections` instance. -/// -/// # Arguments -/// -/// * `names` - A mutable reference to a `Names` struct containing the filename to parse. -/// * `ip_list` - A mutable reference to an `IpConnections` instance to be populated. -/// -/// # Errors -/// -/// Returns an error if the filename format is invalid or if parsing fails. -/// -/// # Returns -/// -/// Returns an `IpConnections` instance populated with parsed network information. -fn parse_inet(names: &mut Names) -> Result { - let filename_str = names.filename.to_string_lossy(); - let parts: Vec<&str> = filename_str.split('/').collect(); - - if parts.len() < 2 { - return Err(Error::new( - ErrorKind::InvalidInput, - "Invalid filename format", - )); - } - - let hostspec = parts[0]; - let host_parts: Vec<&str> = hostspec.split(',').collect(); - - let lcl_port_str = host_parts - .first() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "Local port is missing"))?; - let rmt_addr_str = host_parts.get(1).cloned(); - let rmt_port_str = host_parts.get(2).cloned(); - - let lcl_port = lcl_port_str - .parse::() - .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid local port format"))?; - - let rmt_port = rmt_port_str - .as_ref() - .map(|s| { - s.parse::() - .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid remote port format")) - }) - .transpose()? - .unwrap_or(0); - - let rmt_addr = match rmt_addr_str { - Some(addr_str) => match addr_str.parse::() { - Ok(addr) => addr, - Err(_) => { - eprintln!("Warning: Invalid remote address {}", addr_str); - IpAddr::V4(Ipv4Addr::UNSPECIFIED) // Default value if address parsing fails - } - }, - None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), // Default value if address is not provided - }; - - Ok(IpConnections::new( - names.clone(), - lcl_port, - rmt_port, - rmt_addr, - )) -} -/// Retrieves the device identifier of the network interface associated with a UDP socket. -/// -/// # Errors -/// -/// Returns an error if binding the socket or retrieving the device identifier fails. -/// -/// # Returns -/// -/// Returns the device identifier (`u64`) of the network interface. -fn find_net_dev() -> Result { - let socket = UdpSocket::bind("0.0.0.0:0")?; - let fd = socket.as_raw_fd(); - let mut stat_buf = unsafe { std::mem::zeroed() }; - - unsafe { - if fstat(fd, &mut stat_buf) != 0 { - return Err(io::Error::last_os_error()); - } - } - - Ok(stat_buf.st_dev) -} - -/// Finds network sockets based on the given protocol and updates the `InodeList` -/// with the relevant inode information if a matching connection is found. -/// -/// # Arguments -/// -/// * `inode_list` - A mutable reference to the `InodeList` that will be updated. -/// * `connections_list` - A reference to the `IpConnections` that will be used to match connections. -/// * `protocol` - A `&str` representing the protocol (e.g., "tcp", "udp") to look for in `/proc/net`. -/// * `net_dev` - A `u64` representing the network device identifier. -/// -/// # Errors -/// -/// Returns an `io::Error` if there is an issue opening or reading the file at `/proc/net/{protocol}`, or -/// if parsing the net sockets fails. -/// -/// # Returns -/// -/// Returns an `InodeList` containing the updated information if a matching connection is found. -/// Returns an `io::Error` with `ErrorKind::ConnectionRefused` if can't parse sockets. - -fn find_net_sockets( - connections_list: &IpConnections, - protocol: &str, - net_dev: DeviceId, -) -> Result { - let pathname = format!("/proc/net/{}", protocol); - - let file = File::open(&pathname)?; - let reader = io::BufReader::new(file); - - for line in reader.lines() { - let line = line?; - - let parts: Vec<&str> = line.split_whitespace().collect(); - let parse_hex_port = |port_str: &str| -> Option { - port_str - .split(':') - .nth(1) - .and_then(|s| u64::from_str_radix(s, 16).ok()) - }; - - let loc_port = parts.get(1).and_then(|&s| parse_hex_port(s)); - let rmt_port = parts.get(2).and_then(|&s| parse_hex_port(s)); - let scanned_inode = parts.get(9).and_then(|&s| s.parse::().ok()); - - if let Some(scanned_inode) = scanned_inode { - for connection in connections_list.iter() { - let loc_port = loc_port.unwrap_or(0); - let rmt_port = rmt_port.unwrap_or(0); - let rmt_addr = parse_ipv4_addr(parts[2].split(':').next().unwrap_or("")) - .unwrap_or(Ipv4Addr::UNSPECIFIED); - - if (connection.lcl_port == 0 || connection.lcl_port == loc_port) - && (connection.rmt_port == 0 || connection.rmt_port == rmt_port) - && (connection.rmt_addr == Ipv4Addr::UNSPECIFIED - || connection.rmt_addr == rmt_addr) - { - return Ok(InodeList::new( - connection.names.clone(), - net_dev, - scanned_inode, - )); - } - } - } - } - - Err(Error::new( - ErrorKind::ConnectionRefused, - "Cannot parse net sockets", - )) -} - -/// Parses a hexadecimal string representation of an IPv4 address. -fn parse_ipv4_addr(addr: &str) -> Option { - if addr.len() == 8 { - let octets = [ - u8::from_str_radix(&addr[0..2], 16).ok()?, - u8::from_str_radix(&addr[2..4], 16).ok()?, - u8::from_str_radix(&addr[4..6], 16).ok()?, - u8::from_str_radix(&addr[6..8], 16).ok()?, - ]; - Some(Ipv4Addr::from(octets)) - } else { - None - } -} - -/// Retrieves the status of a file given its filename. -fn stat(filename_str: &str) -> io::Result { - let filename = CString::new(filename_str)?; - - unsafe { - let mut st: libc::stat = std::mem::zeroed(); - let rc = libc::stat(filename.as_ptr(), &mut st); - if rc == 0 { - Ok(st) - } else { - Err(io::Error::last_os_error()) - } - } -} - /// Execute stat() system call with timeout to avoid deadlock /// on network based file systems. fn timeout(path: &str, seconds: u32) -> Result { @@ -1484,49 +1462,23 @@ fn timeout(path: &str, seconds: u32) -> Result { } } -/// This function handles relative paths by resolving them against the current working directory to absolute path -/// -/// # Arguments -/// -/// * `path` - [str](std::str) that represents the file path. -/// -/// # Errors -/// -/// Returns an error if passed invalid input. -/// -/// # Returns -/// -/// Returns PathBuf real_path. -pub fn expand_path(path: &PathBuf) -> Result { - let mut real_path = if path.starts_with(Path::new("/")) { - PathBuf::from("/") - } else { - env::current_dir()? - }; +/// Retrieves the status of a file given its filename. +fn stat(filename_str: &str) -> io::Result { + let filename = CString::new(filename_str)?; - for component in Path::new(path).components() { - match component { - Component::CurDir => { - // Ignore '.' - } - Component::ParentDir => { - // Handle '..' by moving up one directory level if possible - real_path.pop(); - } - Component::Normal(name) => { - // Append directory or file name - real_path.push(name); - } - Component::RootDir | Component::Prefix(_) => { - // Handle root directory or prefix - real_path = PathBuf::from(component.as_os_str()); - } + unsafe { + let mut st: libc::stat = std::mem::zeroed(); + let rc = libc::stat(filename.as_ptr(), &mut st); + if rc == 0 { + Ok(st) + } else { + Err(io::Error::last_os_error()) } } +} - if real_path.as_os_str() != "/" && real_path.as_os_str().to_string_lossy().ends_with('/') { - real_path.pop(); - } - - Ok(real_path) +/// Adds a new process to the `Names` object with specified access and process type. +fn add_process(names: &mut Names, pid: i32, uid: u32, access: Access, proc_type: ProcType) { + let proc = Procs::new(pid, uid, access, proc_type); + names.add_procs(proc); } diff --git a/process/tests/fuser/mod.rs b/process/tests/fuser/mod.rs index 3a31dcb7..33420121 100644 --- a/process/tests/fuser/mod.rs +++ b/process/tests/fuser/mod.rs @@ -2,12 +2,14 @@ use libc::uid_t; use plib::{run_test_with_checker, TestPlan}; use std::{ ffi::CStr, - fs::{self, File}, - io::{self, Read}, + fs, io, path::{Path, PathBuf}, process::{Command, Output}, str, }; + +#[cfg(target_os = "linux")] +use std::{fs::File, io::Read}; use tokio::net::{TcpListener, UdpSocket, UnixListener}; fn fuser_test( @@ -80,8 +82,8 @@ fn get_process_user(pid: u32) -> io::Result { } #[cfg(target_os = "macos")] -fn get_process_user(pid: u32) -> io::Result { - let uid = unsafe{ libc::getuid() }; +fn get_process_user(_pid: u32) -> io::Result { + let uid = unsafe { libc::getuid() }; get_username_by_uid(uid) } @@ -178,6 +180,7 @@ fn test_fuser_with_many_files() { } /// Starts a TCP server on port 8080. +#[cfg(target_os = "linux")] async fn start_tcp_server() -> TcpListener { TcpListener::bind(("127.0.0.1", 8080)) .await @@ -204,6 +207,7 @@ async fn test_fuser_tcp() { } /// Starts a UDP server on port 8081. +#[cfg(target_os = "linux")] async fn start_udp_server() -> UdpSocket { UdpSocket::bind(("127.0.0.1", 8081)) .await @@ -229,6 +233,7 @@ async fn test_fuser_udp() { }); } /// Starts a Unix socket server at the specified path. +#[cfg(target_os = "linux")] async fn start_unix_socket(socket_path: &str) -> UnixListener { if fs::metadata(socket_path).is_ok() { println!("A socket is already present. Deleting...");