fix(pi-shell): scan /proc for children when CONFIG_PROC_CHILDREN is absent

Some Kata/microVM guest kernels (e.g. the CI runner's 6.18.x) are built
without CONFIG_PROC_CHILDREN, so /proc/<pid>/task/<tid>/children does not
exist. The Linux children() relied on it with no fallback, making
children()/live_descendants() return empty and silently turning shell
cancellation cleanup into a no-op inside such containers.

Fall back to scanning /proc and grouping by parent pid (the same primitive
the macOS path already uses) when no children file is readable; kernels
with the file keep the cheap per-task fast path. Also fixes
process::tests::descendants_includes_freshly_spawned_child under Kata CI.
This commit is contained in:
can1357
2026-06-14 23:24:03 +02:00
parent 72f76280c9
commit f41b1e35f3
+42 -9
View File
@@ -65,6 +65,7 @@ mod platform {
let mut seen: HashSet<i32> = HashSet::new();
let mut out = Vec::new();
let mut children_file_available = false;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(tid_str) = name.to_str() else {
@@ -77,26 +78,58 @@ mod platform {
let Ok(content) = fs::read_to_string(&children_path) else {
continue;
};
// The file is readable -> this kernel has CONFIG_PROC_CHILDREN.
children_file_available = true;
for part in content.split_whitespace() {
let Ok(child_pid) = part.parse::<i32>() else {
continue;
};
if !seen.insert(child_pid) {
continue;
}
let Some(child) = Self::from_pid(child_pid) else {
self.push_validated_child(child_pid, &mut seen, &mut out);
}
}
// Some Kata / microVM guest kernels are built without CONFIG_PROC_CHILDREN,
// so no `.../children` file exists and the walk above finds nothing — which
// would silently turn descendant signaling (cancellation cleanup) into a
// no-op inside such containers. Fall back to scanning `/proc` and grouping
// by parent pid, the same primitive the macOS path uses. Only taken when no
// `children` file was readable, so kernels that support it keep the cheap
// per-task fast path.
if !children_file_available
&& let Ok(proc_entries) = fs::read_dir("/proc")
{
for entry in proc_entries.flatten() {
let name = entry.file_name();
let Some(pid_str) = name.to_str() else {
continue;
};
if child.status() == ProcessStatus::Running
&& current_parent_pid(child.pid) == Some(self.pid)
{
out.push(child);
}
let Ok(child_pid) = pid_str.parse::<i32>() else {
continue;
};
self.push_validated_child(child_pid, &mut seen, &mut out);
}
}
out
}
/// Validate a candidate child pid — dedup, still running, and currently
/// parented to `self` — then push it onto `out`. Shared by the
/// `/proc/<pid>/task/<tid>/children` fast path and the `/proc`-scan fallback
/// for kernels without `CONFIG_PROC_CHILDREN`.
fn push_validated_child(&self, child_pid: i32, seen: &mut HashSet<i32>, out: &mut Vec<Self>) {
if child_pid == self.pid || !seen.insert(child_pid) {
return;
}
let Some(child) = Self::from_pid(child_pid) else {
return;
};
if child.status() == ProcessStatus::Running
&& current_parent_pid(child.pid) == Some(self.pid)
{
out.push(child);
}
}
pub fn parent_pid(&self) -> Option<i32> {
if self.status() == ProcessStatus::Running {
current_parent_pid(self.pid)