Skip to content

perf(platform): read macOS sockets through proc_info instead of lsof - #88

Merged
vishr merged 2 commits into
mainfrom
feat/darwin-libproc-sockets
Oct 31, 2026
Merged

vishr merged 2 commits into
mainfrom
feat/darwin-libproc-sockets

Conversation

@vishr

@vishr vishr commented Oct 31, 2026

Copy link
Copy Markdown
Member

What changed

On macOS, Driga's socket check now reads sockets through the kernel's proc_info system call, the interface lsof itself uses, instead of starting lsof and parsing its text output. It decodes each socket record at the SDK offsets and feeds the result to the existing classifier unchanged. If the kernel's record layout ever changes, Driga logs socket_layout_fallback and goes back to lsof.

Why

lsof caused both macOS socket bugs fixed in #87:

  • The socket address is in field d, not D.
  • lsof shows a connection a server accepted only by its bound path, so finding a listener's clients needed a scan of every process.

lsof also timed out on loaded machines. The kernel record names an accepted connection's client directly, through unsi_conn_pcb. The all-process scan now takes about 7 ms instead of 126 ms.

Rejected alternatives:

  • purego bindings to libproc.dylib. footprint_darwin.go already makes the raw SYS_PROC_INFO call, and a raw call keeps release builds free of cgo.
  • Dropping lsof entirely. I kept it as the fallback for one release, since the layout has only been checked on macOS 27 arm64 at runtime.

Risk and rollout

This can affect when Driga kills a process.

  • The classification rules are unchanged. Only the input switches from lsof text to kernel records, and a test checks that both agree on real sockets.
  • Errors fail closed:
    • A tree process that exited is skipped.
    • A tree process that can't be read counts as connected.
    • In the full scan, any unreadable process of this user, or running out of time, makes every tree process count as connected.
    • Other users' processes are skipped using one kern.proc.all snapshot.
  • A socket record of the wrong size, or ENOMEM, means the layout changed, so Driga falls back to lsof.
  • No migration, no change to agent settings, and no extra text captured.

Contract changes

None. ExternalPeers and ResolvedPeers keep their signatures and semantics. One new log event, socket_layout_fallback.

Verified

Run on macOS 27.0.1, arm64.

  • go test ./... and go test -race ./internal/platform/: all pass except TestPermissionHookBuiltBinary (TempDir cleanup), which also fails on main.
  • GOOS=linux and GOOS=windows builds and vet; just _fmt-check tidy-check vet generate-check docs-check.
  • New tests:
    • Decoding hand-built socket records.
    • A live Unix listener with a client in a child process: the accepted connection names the client, the client outside the tree reads as connected, and inside the tree as internal.
    • libproc matching real lsof for this process.
    • Skipping exited processes.
    • A full scan that stays complete and never reads other users' processes.
    • An unreadable tree process counting as connected.
    • Scan deadlines.
  • Live test against a real Codex plugin broker: with a client connected, driga orphans kill --yes refused and named the broker itself, which lsof couldn't see. With the client gone, it killed the tree. No fallback was logged.
  • /code-review high: all 10 findings fixed in the second commit.
  • Not verified: Intel Macs at runtime (the layout matches at compile time), and other macOS versions.
  • I have accepted the CLA, or will when the bot asks on my first pull request.

vishr added 2 commits October 30, 2026 23:40
The macOS socket check started lsof once per socket class and 200-pid
batch, plus one unrestricted lsof pass to resolve a listener's clients,
and parsed its text output. That text caused two bugs this month (the
socket address in field d, not D; accepted connections shown only by
their bound path) and timed out on loaded machines.

The check now calls the proc_info syscall that libproc and lsof use:
PROC_PIDLISTFDS lists a process's descriptors and PROC_PIDFDSOCKETINFO
returns struct socket_fdinfo, decoded at the SDK's offsets (identical
for arm64 and x86_64). soi_pcb is a Unix socket's own address and
unsi_conn_pcb its peer, so an accepted connection now names its client
directly. Records feed the existing classifier unchanged.

- The all-process scan of resolved checks takes about 7 ms instead of
  126 ms for lsof.
- A process that exited is skipped; another user's process (EPERM with
  a different owner) is expected and makes its peers system services;
  any other unreadable process of this user fails the check closed.
- A socket record of unexpected size means the kernel layout changed:
  the check logs socket_layout_fallback once and uses lsof.
Review fixes for the proc_info socket check:

- A tree pid that cannot be read (for example a setuid child) now marks
  only that pid connected instead of failing the check for every tree.
- ENOTSOCK and EOPNOTSUPP, like EBADF, mean a descriptor changed between
  listing and reading, so it is skipped.
- ENOMEM from PROC_PIDFDSOCKETINFO means the kernel's struct outgrew the
  buffer, so it also triggers the lsof fallback.
- The all-process scan takes processes and owners from one kern.proc.all
  snapshot: other users' processes are never asked, which removes the
  per-pid owner lookup, its race, and pid-list truncation.
- Each process is read once per scan. The scan has its own deadline (5 s
  per 200 tree pids, plus 10 s for all processes); running out fails a
  resolved check closed.
- PROC_PIDLISTFDS is sized first and buffers are reused across pids.
- physicalFootprint shares the proc_info call, and compile-time checks
  keep darwinPlatform a SocketProber and PeerResolver.
@vishr
vishr merged commit 4538781 into main Oct 31, 2026
6 checks passed
@vishr
vishr deleted the feat/darwin-libproc-sockets branch October 31, 2026 07:17
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