Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lib/hypervisor/socket_pid_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import (
"errors"
"fmt"
"io/fs"
"net"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"time"
)

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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions lib/hypervisor/socket_pid_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading