Skip to content
Closed
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
29 changes: 20 additions & 9 deletions lib/hypervisor/socket_pid_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ type socketMatcher func(pid int, target string) (bool, error)
// socketMatcherForPath matches the listener bound to socketPath. A VMM that
// runs in its own root, such as a jailed firecracker, binds its socket inside
// that root, so /proc/net/unix lists the in-root path and socketPath is a link
// to it. When no listener is listed under socketPath, a listener matches if
// its path, resolved in the holding process's root, is the file socketPath
// names.
// to it. When socketPath is such a link and no listener is listed under it, a
// listener bound to the link target's name at the root matches if its path,
// resolved in the holding process's root, is the file socketPath names.
//
// Any other socketPath with no listener is reported as ErrNoOwningProcess
// without scanning process fds, so a stale socket left by a dead VMM stays
// cheap to resolve.
func socketMatcherForPath(socketPath string) (socketMatcher, error) {
socketRef, err := socketRefForPath(socketPath)
if err == nil {
Expand All @@ -66,14 +70,21 @@ func socketMatcherForPath(socketPath string) (socketMatcher, error) {
if !errors.Is(err, ErrNoOwningProcess) {
return nil, err
}
linkTarget, linkErr := os.Readlink(socketPath)
if linkErr != nil {
return nil, err
}
want, statErr := os.Stat(socketPath)
if statErr != nil || want.Mode()&os.ModeSocket == 0 {
return nil, err
}
listeners, listErr := listeningSocketPaths()
listeners, listErr := listenersBoundTo("/" + filepath.Base(linkTarget))
if listErr != nil {
return nil, listErr
}
if len(listeners) == 0 {
return nil, err
}
return func(pid int, target string) (bool, error) {
path, ok := listeners[target]
if !ok {
Expand Down Expand Up @@ -174,12 +185,12 @@ func pidBySocket(socketPath string, match socketMatcher, ownerPID int) (int, err
return 0, fmt.Errorf("resolve process pid for %s: %w", socketPath, ErrNoOwningProcess)
}

// listeningSocketPaths maps each listening socket with a filesystem path to that path.
func listeningSocketPaths() (map[string]string, error) {
// listenersBoundTo maps each listening socket bound to path to that path.
func listenersBoundTo(path string) (map[string]string, error) {
paths := make(map[string]string)
err := scanListeners(func(ref, path string) error {
if strings.HasPrefix(path, "/") {
paths[ref] = path
err := scanListeners(func(ref, bound string) error {
if bound == path {
paths[ref] = bound
}
return nil
})
Expand Down
38 changes: 38 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,44 @@ func TestResolveProcessPIDMatchesSocketInProcessRoot(t *testing.T) {
require.ErrorIs(t, err, ErrNoOwningProcess)
}

func TestResolveProcessPIDSkipsFDScanForStaleSocket(t *testing.T) {
oldProcDir := procDir
procDir = t.TempDir()
t.Cleanup(func() { procDir = oldProcDir })

// A jailed VMM's listener is live, and a process with an unreadable fd
// would turn any fd scan into an error instead of ErrNoOwningProcess.
require.NoError(t, os.MkdirAll(filepath.Join(procDir, "net"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(procDir, "net", "unix"), []byte("00000000: 00000002 00000000 00010000 0001 01 5000 /api.sock\n"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(procDir, "123"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(procDir, "123", "fd"), nil, 0o644))

dir, err := os.MkdirTemp("/tmp", "spid")
require.NoError(t, err)
t.Cleanup(func() { _ = os.RemoveAll(dir) })
staleSocket := func(path string) {
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
require.NoError(t, err)
listener.SetUnlinkOnClose(false)
require.NoError(t, listener.Close())
}

// A dead unjailed VMM leaves its socket file behind.
unjailed := filepath.Join(dir, "fc.sock")
staleSocket(unjailed)
_, err = ResolveProcessPID(unjailed)
require.ErrorIs(t, err, ErrNoOwningProcess)

// A dead jailed VMM leaves a link to a socket no listener is bound to.
jailRoot := filepath.Join(dir, "jail")
require.NoError(t, os.MkdirAll(jailRoot, 0o755))
staleSocket(filepath.Join(jailRoot, "vsock.sock"))
jailed := filepath.Join(dir, "jailed.sock")
require.NoError(t, os.Symlink(filepath.Join(jailRoot, "vsock.sock"), jailed))
_, err = ResolveProcessPID(jailed)
require.ErrorIs(t, err, ErrNoOwningProcess)
}

func TestResolveProcessPIDReportsMissingSocket(t *testing.T) {
oldProcDir := procDir
procDir = t.TempDir()
Expand Down
Loading