From 94b042ca2aea15e6eff5a9365021cf5e766029d7 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Thu, 12 Nov 2026 22:02:46 +0000 Subject: [PATCH] Skip the process fd scan for a socket file with no listener A socket file that no listener is listed under falls back to matching listeners by the file they are bound to in each process's root, which scans every process fd. A VMM that died without unlinking its socket takes that path on every resolve. Probe the socket first: a refused connection proves no listener is bound to the file in any root. --- lib/hypervisor/socket_pid_linux.go | 19 +++++++++++++++++++ lib/hypervisor/socket_pid_linux_test.go | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/hypervisor/socket_pid_linux.go b/lib/hypervisor/socket_pid_linux.go index fd3605d20..163133fe7 100644 --- a/lib/hypervisor/socket_pid_linux.go +++ b/lib/hypervisor/socket_pid_linux.go @@ -7,12 +7,14 @@ import ( "errors" "fmt" "io/fs" + "net" "os" "path/filepath" "slices" "strconv" "strings" "syscall" + "time" ) var procDir = "/proc" @@ -20,6 +22,8 @@ var procDir = "/proc" // soAcceptcon marks a listening socket in /proc/net/unix (__SO_ACCEPTCON). const soAcceptcon = 0x10000 +const socketProbeTimeout = 100 * time.Millisecond + // ResolveProcessPID finds the process currently holding the listening Unix // socket for the given hypervisor control path, via the socket inode in // /proc/net/unix and each process's fd table. The fd scan requires the @@ -70,6 +74,12 @@ func socketMatcherForPath(socketPath string) (socketMatcher, error) { if statErr != nil || want.Mode()&os.ModeSocket == 0 { return nil, err } + // A refused connection proves no listener is bound to the file in any + // root, so a socket left behind by a dead VMM does not cost a scan of + // every process fd. + if socketRefusesConnections(socketPath) { + return nil, err + } listeners, listErr := listeningSocketPaths() if listErr != nil { return nil, listErr @@ -90,6 +100,15 @@ func socketMatcherForPath(socketPath string) (socketMatcher, error) { }, nil } +func socketRefusesConnections(socketPath string) bool { + conn, err := net.DialTimeout("unix", socketPath, socketProbeTimeout) + if err == nil { + _ = conn.Close() + return false + } + return errors.Is(err, syscall.ECONNREFUSED) +} + func processHoldsSocket(pid int, match socketMatcher) bool { fdEntries, err := os.ReadDir(filepath.Join(procDir, strconv.Itoa(pid), "fd")) if err != nil { diff --git a/lib/hypervisor/socket_pid_linux_test.go b/lib/hypervisor/socket_pid_linux_test.go index 54dbd75dd..89ca6e69f 100644 --- a/lib/hypervisor/socket_pid_linux_test.go +++ b/lib/hypervisor/socket_pid_linux_test.go @@ -190,6 +190,29 @@ func TestResolveProcessPIDMatchesSocketInProcessRoot(t *testing.T) { require.ErrorIs(t, err, ErrNoOwningProcess) } +func TestResolveProcessPIDSkipsProcessScanForSocketWithoutListener(t *testing.T) { + oldProcDir := procDir + procDir = t.TempDir() + t.Cleanup(func() { procDir = oldProcDir }) + + // A VMM that died without unlinking its socket leaves the file behind. + socketPath := filepath.Join(t.TempDir(), "fc.sock") + listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) + require.NoError(t, err) + listener.SetUnlinkOnClose(false) + require.NoError(t, listener.Close()) + + require.NoError(t, os.MkdirAll(filepath.Join(procDir, "net"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(procDir, "net", "unix"), nil, 0o644)) + // A process scan would fail on this unreadable fd instead of reporting no owner. + fdDir := filepath.Join(procDir, "123", "fd") + require.NoError(t, os.MkdirAll(fdDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(fdDir, "3"), nil, 0o644)) + + _, err = ResolveProcessPID(socketPath) + require.ErrorIs(t, err, ErrNoOwningProcess) +} + func TestResolveProcessPIDReportsMissingSocket(t *testing.T) { oldProcDir := procDir procDir = t.TempDir()