From 9537be93bdc73978a62327958530760832a47ccc Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:35:17 +0530 Subject: [PATCH] pidof: match processes by executable and by rewritten titles pidof took argv[0] as the first space-separated word of the joined command line. Servers that rewrite their title, like "sshd: /usr/sbin/sshd -D [listener]", came out as "sshd:", so "pidof sshd" found nothing, while "pidof sshd:" matched. A process started with another argv[0] (exec -a) was not found by its program name either. Match like procps-ng: the base name of the whole argv[0], the base name of /proc/PID/exe (also cut to the 15 bytes the kernel keeps), or, when argv[0] has a space in it, the process name. --- src/uu/pidof/src/pidof.rs | 34 +++++++++++++++++++++++++---- tests/by-util/test_pidof.rs | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/uu/pidof/src/pidof.rs b/src/uu/pidof/src/pidof.rs index 00069481..6b005101 100644 --- a/src/uu/pidof/src/pidof.rs +++ b/src/uu/pidof/src/pidof.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::fs; use std::path::PathBuf; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; @@ -50,8 +51,16 @@ fn match_process_name( with_workers: bool, match_scripts: bool, ) -> bool { - let binding = process.cmdline.split(' ').collect::>(); - let path = binding.first().unwrap().to_string(); + // `cmdline` has the arguments joined with spaces, so read them again to + // get argv[0] as it is. + let Ok(raw) = fs::read(format!("/proc/{}/cmdline", process.pid)) else { + return false; + }; + let binding = raw + .split(|&b| b == 0) + .map(String::from_utf8_lossy) + .collect::>(); + let path = binding.first().map(|it| it.to_string()).unwrap_or_default(); if path.is_empty() { if !with_workers { @@ -60,7 +69,24 @@ fn match_process_name( return process.name().unwrap() == name_to_match; }; - if PathBuf::from(path).file_name().unwrap().to_str().unwrap() == name_to_match { + if PathBuf::from(&path).file_name().and_then(|f| f.to_str()) == Some(name_to_match) { + return true; + } + + // Like procps-ng, also match the executable, so a process started with + // another argv[0] (`exec -a`) is still found, also by the 15-byte name + // the kernel cuts it to. + if let Ok(exe) = fs::read_link(format!("/proc/{}/exe", process.pid)) { + if let Some(exe) = exe.file_name().and_then(|f| f.to_str()) { + if exe == name_to_match || exe.as_bytes().get(..15) == Some(name_to_match.as_bytes()) { + return true; + } + } + } + + // A process that rewrites its title, like "sshd: /usr/sbin/sshd -D", is + // matched by its process name too. + if path.contains(' ') && process.name().unwrap() == name_to_match { return true; } @@ -68,7 +94,7 @@ fn match_process_name( // its cmdline will look like `/bin/sh ./script.sh` but its .name() will be `script.sh`. // As name() gets truncated to 15 characters, the original pidof seems to always do a prefix match. if match_scripts && binding.len() > 1 { - return PathBuf::from(binding[1]) + return PathBuf::from(binding[1].as_ref()) .file_name() .map(|f| f.to_str().unwrap()) .is_some_and(|f| f == name_to_match && f.starts_with(&process.name().unwrap())); diff --git a/tests/by-util/test_pidof.rs b/tests/by-util/test_pidof.rs index 3dc7059d..07622237 100644 --- a/tests/by-util/test_pidof.rs +++ b/tests/by-util/test_pidof.rs @@ -158,3 +158,46 @@ fn test_script() { executed_via_sh_child.kill().unwrap(); executed_via_sh_child.wait().unwrap(); } + +#[test] +#[cfg(target_os = "linux")] +fn test_process_with_another_argv0() { + use std::os::unix::process::CommandExt; + + // A copy of the shell under its own name, so the executable and the + // process name are known. + let temp_dir = tempfile::tempdir().unwrap(); + let program = temp_dir.path().join("pidoftitletest"); + std::fs::copy("/bin/sh", &program).unwrap(); + + for argv0 in ["pidoftitletest: worker process", "another-argv0"] { + // "; :" keeps the shell from replacing itself with sleep. Other tests + // forking while the copy was open for writing make exec fail with + // ETXTBSY for a moment, so retry that. + let mut child = (0..100) + .find_map(|_| { + match std::process::Command::new(&program) + .arg0(argv0) + .args(["-c", "sleep 5; :"]) + .spawn() + { + Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => { + std::thread::sleep(std::time::Duration::from_millis(10)); + None + } + result => Some(result.unwrap()), + } + }) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(200)); + + new_ucmd!() + .arg("pidoftitletest") + .succeeds() + .stdout_contains(child.id().to_string()); + new_ucmd!().arg("pidoftitletest:").fails(); + + child.kill().unwrap(); + child.wait().unwrap(); + } +}