Skip to content

Treat a process that exits during Identify as gone, not unreadable - #2061

Merged
ppXD merged 1 commit into
mainfrom
fix/treat-identify-exit-after-stat-as-gone
Sep 30, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/treat-identify-exit-after-stat-as-gone

Conversation

@ppXD

@ppXD ppXD commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • NativeProcess.Identify still escaped raw exceptions for a process that exits after its /proc/<pid>/stat read: Process.GetProcessById throws ArgumentException ("not running") and Process.StartTime throws Win32Exception ("may have exited or may be privileged"). Treat a process that vanishes mid-read as gone, not unreadable #2053 mapped the IOException from the stat read only. When kill(pid, 0) answers ESRCH, both now become the same IOException("Cannot identify a terminated native process.", inner) the stat read, the Z/X states and the macOS branch already give; otherwise they propagate, so unknowable stays unknowable.
  • TreatsAsGone (internal) accepts ArgumentException and Win32Exception alongside IOException, on the same evidence and never the exception's type or message.
  • Existing callers are unchanged. IsRunning, IsAlive and KillSession reach TreatsAsGone only from catch (IOException) clauses, so the filter is never handed the two new types there; each already swallows ArgumentException unconditionally and guards Win32Exception with its own IsAbsent probe. Only the new catch (Exception) in Identify sees them.

Test plan

  • Unit: NativeProcessGoneTests 10 -> 17 tests, all green (macOS). New: ArgumentException and Win32Exception with an absent pid read as gone; all of IOException, ArgumentException and Win32Exception with a live pid do not; InvalidDataException and UnauthorizedAccessException still do not. Two more feed the runtime's own exceptions rather than built ones: the ArgumentException a real GetProcessById of a reaped pid raises, and (Linux only) the Win32Exception a real StartTime raises for a process that exited after it was opened.
  • Mutation: reverting the widening (failure is IOException && IsAbsent(pid)) turns 3 tests red on macOS (the two new rows and the real GetProcessById exception) and 4 on Linux (plus the real StartTime exception); restored by copy and cmp-verified.
  • Linux (aarch64 container, no network): the same 17 tests pass, including the Linux-only one. A race harness calling Identify from 4-16 threads while the target process exits and is reaped saw 649-1258 escapes per run over six runs on main (only ArgumentException and Win32Exception) and none over five runs with this change (8x800 three times, 16x400, 4x1500; 200k-490k identities and 200k-440k typed refusals per run).
  • Neighbours: LocalProcessDurableRunnerTests 154/154, NativeLaunchRegistryTests 91/91, ProcessLivenessDriftTests 36/36 (integration project, it regex-parses NativeProcess.cs).

Identify reads /proc/<pid>/stat and then asks the runtime for the
process. A process that exits between the two is refused by the runtime
rather than the kernel: Process.GetProcessById throws ArgumentException
("not running") and Process.StartTime throws Win32Exception ("may have
exited or may be privileged"). The previous change mapped the
IOException from the stat read to the "Cannot identify a terminated
native process." refusal but left these two to escape as raw
exceptions, so a plainly gone process still read as one that could not
be identified. A Linux harness calling Identify from 4-16 threads
against a process exiting underneath them saw 649-1258 of them per run.

TreatsAsGone now accepts both alongside IOException, on the same
evidence: kill(pid, 0) answering ESRCH. The Win32Exception message names
the ambiguity itself (privileged), so the same exception about a pid
that still exists keeps propagating; unknowable stays unknowable.
Identify wraps the GetProcessById/StartTime pair in that decision and
throws the refusal the Z/X state, the stat read and the macOS branch
already give.

IsRunning, IsAlive and KillSession are unchanged: they reach
TreatsAsGone only from catch (IOException) clauses, which never hand it
the two new types, and each already swallows ArgumentException and
guards Win32Exception with its own absence probe.
@ppXD
ppXD merged commit f48995d into main Sep 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant