From e4a7095c9339fcf14d286936b7eeb16ad4106cc9 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 7 Jul 2026 21:20:01 +0300 Subject: fix: guard windows process tree cycles refs #1083 --- src/platform/windows.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) (limited to 'src/platform') diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6ac05767..49a68111 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, ffi::c_void, mem::{size_of, MaybeUninit}, path::PathBuf, @@ -223,11 +223,15 @@ fn process_is_ancestor( parent_by_pid: &HashMap, ) -> bool { let mut current = descendant_pid; - while let Some(parent) = parent_by_pid.get(¤t).copied() { + let mut visited = HashSet::new(); + while visited.insert(current) { + let Some(parent) = parent_by_pid.get(¤t).copied() else { + return false; + }; if parent == ancestor_pid { return true; } - if parent == 0 || parent == current { + if parent == 0 { return false; } current = parent; @@ -244,13 +248,23 @@ fn descendant_entries(root_pid: u32, entries: &[WindowsProcessEntry]) -> Vec<&Wi let mut output = Vec::new(); let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + visited.insert(root_pid); if let Some(root_children) = children.get(&root_pid) { - queue.extend(root_children.iter().copied()); + for entry in root_children.iter().copied() { + if visited.insert(entry.pid) { + queue.push_back(entry); + } + } } while let Some(entry) = queue.pop_front() { output.push(entry); if let Some(next) = children.get(&entry.pid) { - queue.extend(next.iter().copied()); + for child in next.iter().copied() { + if visited.insert(child.pid) { + queue.push_back(child); + } + } } } output @@ -896,6 +910,40 @@ mod tests { assert_eq!(pids, vec![10, 20, 30]); } + #[test] + fn windows_process_tree_ignores_pid_reuse_cycles() { + let entries = vec![ + test_entry(10, 30, "powershell.exe", &["powershell.exe"]), + test_entry(20, 10, "codex.exe", &["codex.exe"]), + test_entry(30, 20, "node.exe", &["node.exe"]), + ]; + + let descendants = super::descendant_entries(10, &entries); + + assert_eq!( + descendants + .iter() + .map(|entry| entry.pid) + .collect::>(), + vec![20, 30] + ); + } + + #[test] + fn windows_process_tree_returns_shell_when_candidate_parent_chain_cycles() { + let entries = vec![ + test_entry(10, 40, "powershell.exe", &["powershell.exe"]), + test_entry(20, 10, "codex.exe", &["codex.exe"]), + test_entry(30, 10, "codex.exe", &["codex.exe"]), + test_entry(40, 10, "node.exe", &["node.exe"]), + ]; + + let job = super::select_pane_foreground_job(10, &entries).unwrap(); + + assert_eq!(job.process_group_id, 10); + assert_eq!(job.processes[0].name, "powershell.exe"); + } + #[test] fn scrollback_editor_argv_uses_editor_env_and_appends_path() { let path = std::path::Path::new(r"C:\Users\User\AppData\Local\Temp\herdr scrollback.txt"); -- cgit v1.2.3