Phase 5's correctness half. The previous implementation could not ship:
its two main checks were structurally wrong rather than badly tuned.
hidden processes was "any /proc/<pid> whose comm we cannot read",
which fires on every process that exits between the
listing and the read. A race, not a signal.
setuid anomalies compared against a hardcoded allowlist of binary
names, written on one distribution.
Replaced with questions that have factual answers:
A process is hidden when the kernel agrees it exists and /proc does
not list it. kill(pid, 0) answers the first half for the whole PID
space — ESRCH means gone, EPERM means it exists and belongs to
somebody else, which is the case that matters since a rootkit's
process will not be ours. The sweep is bracketed by two listings and
candidates are re-verified, so a process that merely started or
exited during the scan cannot be mistaken for a hidden one.
A setuid binary is suspicious when no installed package claims it.
The package manager already knows what belongs on the system.
Two bugs found by testing against this machine rather than reasoning
about it, both of which would have made the feature useless in the
field:
* /proc lists thread-group leaders; kill() accepts any THREAD id. A
process with twenty threads therefore has nineteen ids that answer
kill and appear in no /proc listing. Comparing against the pid set
alone reported dozens of criticals on a completely healthy laptop.
The honest set is the union of leaders and their /proc/<tgid>/task
entries.
* Merged-/usr breaks package ownership in BOTH directions. /bin is a
symlink to usr/bin, so every binary has two names, and dpkg's own
index is inconsistent about which it records: sudo.list says
/usr/bin/sudo while fuse3.list says /bin/fusermount3 and cifs-utils
says /sbin/mount.cifs. String comparison reported the entire setuid
set as unowned. Both spellings now go into the index, candidates are
deduplicated by resolved path, and lookups try both.
Also: ld.so.preload is now checked (it is empty on a healthy system and
is the classic userland rootkit), the writable-directory check no
longer counts sticky-bit directories, and the hidden-file check uses
symlink_metadata so an ordinary dangling symlink is not an incident.
Verified on this machine, privileged and not: clean, 0 findings, 2.8s
including the full 4.2-million-pid sweep. The exit criterion asks for
five machines across three distributions and only one was available
here, so treat the cross-distro half as unmet.
The regression tests are the point: a scan run while processes churn
continuously must produce no criticals, and a process with eight live
threads must not produce eight findings.
200 tests pass across the workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>