ADR-0003: Read macOS sockets through libproc instead of running lsof¶
- Status: Accepted (amends ADR-0001 for macOS)
- Date: 2026-10-20
Context¶
On macOS, gopsutil lists sockets by running lsof and parsing its text output
(ADR-0001). That works, but it starts a process for every listing (every 2 seconds
in the TUI), depends on lsof's output format, and drops some entries it can't
parse.
lsof itself reads sockets through libproc in libSystem: proc_listallpids,
proc_pidinfo(PROC_PIDLISTFDS) and proc_pidfdinfo(PROC_PIDFDSOCKETINFO). The
structures are declared in the public <sys/proc_info.h>.
Calling libSystem normally requires cgo, which would break our release builds:
macOS binaries are cross-compiled with CGO_ENABLED=0.
Decision¶
- Call the three libproc functions directly with purego, which loads C functions without cgo. It is already a dependency through gopsutil.
- Decode
struct socket_fdinfoin portable Go, using offsets taken from the macOS SDK. They are identical on arm64 and x86_64, and a test re-checks them with clang against the SDK on every macOS CI run. - Keep the gopsutil/
lsofpath as an automatic fallback if the native call fails. - Keep gopsutil for everything else (process names, users, command lines, other OSes).
Consequences¶
- ✅ No
lsofprocess per listing. Warmportcheck listwent from about 20 ms to under 10 ms; the socket listing itself from about 43 ms to 4 ms. - ✅ Same visibility as
lsof: own processes without root, all processes as root. A live-system integration test asserts both report the same listening sockets. - ✅ Still
CGO_ENABLED=0, so cross-compiled releases are unchanged. - ⚠️ We depend on the layout of
struct socket_fdinfo. It is part of Apple's public ABI and has been stable for years; the SDK layout test would catch a change, and thelsoffallback limits the blast radius. - ⚠️ Pointers passed through purego must be
unsafe.Pointerto Go buffers, neveruintptr, or the kernel can write into a stale copy. The call sites are commented and audited.