1// Package peerexe names the executable behind a process through a pidfd.
2//
3// A pidfd pins its process: the pid cannot be recycled while the descriptor
4// is open. ExeOfPidfd reads the executable through that pin and proves the
5// process still existed after the read, so the answer is never the exe of a
6// process that took over a recycled pid. There is no fallback to a bare pid,
7// which has no such proof.
8package peerexe
9
10import (
11 "bufio"
12 "errors"
13 "fmt"
14 "os"
15 "strconv"
16 "strings"
17
18 "golang.org/x/sys/unix"
19)
20
21var (
22 // ErrExeDeleted: the executable of the process was removed.
23 ErrExeDeleted = errors.New("executable deleted")
24 // ErrNoPidfd is a kernel without SO_PEERPIDFD (before Linux 6.5): the
25 // peer cannot be identified safely.
26 ErrNoPidfd = errors.New("kernel has no SO_PEERPIDFD")
27)
28
29// Pidfd returns a pidfd of the peer of the connected unix socket sockFD
30// (SO_PEERPIDFD). The caller closes it. ErrNoPidfd means the kernel cannot
31// give one.
32func Pidfd(sockFD int) (*os.File, error) {
33 pidfd, err := unix.GetsockoptInt(sockFD, unix.SOL_SOCKET, unix.SO_PEERPIDFD)
34 switch {
35 case err == nil:
36 return os.NewFile(uintptr(pidfd), "pidfd"), nil
37 case errors.Is(err, unix.ENOPROTOOPT) || errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EOPNOTSUPP):
38 return nil, ErrNoPidfd
39 default:
40 return nil, fmt.Errorf("SO_PEERPIDFD: %w", err)
41 }
42}
43
44// Open returns a pidfd of pid (pidfd_open). The caller closes it. A pid
45// learned from a source that can be outrun by recycling (a D-Bus
46// GetConnectionUnixProcessID reply) must be confirmed against the source
47// again after Open, so the pidfd names the same process.
48func Open(pid int) (*os.File, error) {
49 fd, err := unix.PidfdOpen(pid, 0)
50 if err != nil {
51 return nil, fmt.Errorf("pidfd_open(%d): %w", pid, err)
52 }
53 return os.NewFile(uintptr(fd), "pidfd"), nil
54}
55
56// ExeOfPidfd returns the executable and the pid of the process behind pidfd:
57//
58// 1. The pid is read from /proc/self/fdinfo/<pidfd>.
59// 2. /proc/<pid>/exe is read.
60// 3. pidfd_send_signal(pidfd, 0) then proves the process still exists. If it
61// had exited before the read, the pid could have named someone else, and
62// the answer is refused.
63//
64// A deleted executable is ErrExeDeleted. The caller keeps pidfd open (and
65// alive, runtime.KeepAlive if it holds an *os.File) during the call.
66func ExeOfPidfd(pidfd int) (string, int, error) {
67 pid, err := pidOfPidfd(pidfd)
68 if err != nil {
69 return "", 0, err
70 }
71 exe, err := readExe(pid)
72 // After the read: a process that is gone may have let its pid be reused.
73 if alive := unix.PidfdSendSignal(pidfd, 0, nil, 0); alive != nil {
74 return "", pid, fmt.Errorf("peer exited: %w", alive)
75 }
76 return exe, pid, err
77}
78
79// pidOfPidfd reads the Pid: line of /proc/self/fdinfo/<pidfd>: -1 once the
80// process has exited, 0 when it is outside this pid namespace.
81func pidOfPidfd(pidfd int) (int, error) {
82 f, err := os.Open("/proc/self/fdinfo/" + strconv.Itoa(pidfd))
83 if err != nil {
84 return 0, err
85 }
86 defer f.Close()
87 sc := bufio.NewScanner(f)
88 for sc.Scan() {
89 if v, ok := strings.CutPrefix(sc.Text(), "Pid:"); ok {
90 pid, err := strconv.Atoi(strings.TrimSpace(v))
91 if err != nil || pid <= 0 {
92 return 0, fmt.Errorf("peer pid not visible (%q)", strings.TrimSpace(v))
93 }
94 return pid, nil
95 }
96 }
97 return 0, errors.New("pidfd fdinfo has no Pid")
98}
99
100// readExe reads /proc/<pid>/exe and refuses a deleted executable.
101func readExe(pid int) (string, error) {
102 exe, err := os.Readlink("/proc/" + strconv.Itoa(pid) + "/exe")
103 if err != nil {
104 return "", err
105 }
106 if strings.HasSuffix(exe, " (deleted)") {
107 return "", ErrExeDeleted
108 }
109 return exe, nil
110}