1package wayland
2
3import (
4 "bufio"
5 "errors"
6 "fmt"
7 "os"
8 "runtime"
9 "strconv"
10 "strings"
11
12 "golang.org/x/sys/unix"
13)
14
15// peerExe names the executable behind a client connection, in two steps so a
16// caller can keep the pidfd and resolve the executable again at each
17// decision.
18type peerExe interface {
19 // Pidfd returns a pidfd of the peer of the connected unix socket sockFD
20 // (SO_PEERPIDFD). It pins the peer process: its pid cannot be recycled
21 // while the file is open. errNoPidfd means the kernel cannot give one.
22 Pidfd(sockFD int) (*os.File, error)
23 // Exe returns the path of the executable running as the process behind
24 // pidfd, and its pid. A deleted executable, or a process that has
25 // exited, is an error.
26 Exe(pidfd *os.File) (exe string, pid int, err error)
27}
28
29var (
30 errExeDeleted = errors.New("executable deleted")
31 // errNoPidfd is a kernel without SO_PEERPIDFD (before Linux 6.5): the
32 // peer cannot be identified safely.
33 errNoPidfd = errors.New("kernel has no SO_PEERPIDFD")
34)
35
36// linuxPeerExe resolves the peer through a pidfd (SO_PEERPIDFD, Linux 6.5+):
37//
38// 1. The pidfd pins the peer process: its pid cannot be recycled while the
39// descriptor is open. The pid is read from /proc/self/fdinfo/<pidfd>.
40// 2. /proc/<pid>/exe is read.
41// 3. pidfd_send_signal(pidfd, 0) then proves the process still exists. If it
42// had exited before the read, the pid could have named someone else, and
43// the answer is refused.
44//
45// There is no fallback to the SO_PEERCRED pid: it has no such proof, so a
46// kernel without SO_PEERPIDFD identifies nobody.
47type linuxPeerExe struct {
48 // noPidfd simulates a kernel without SO_PEERPIDFD (tests).
49 noPidfd bool
50}
51
52func (l linuxPeerExe) Pidfd(sockFD int) (*os.File, error) {
53 if l.noPidfd {
54 return nil, errNoPidfd
55 }
56 pidfd, err := unix.GetsockoptInt(sockFD, unix.SOL_SOCKET, unix.SO_PEERPIDFD)
57 switch {
58 case err == nil:
59 return os.NewFile(uintptr(pidfd), "pidfd"), nil
60 case errors.Is(err, unix.ENOPROTOOPT) || errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EOPNOTSUPP):
61 return nil, errNoPidfd
62 default:
63 return nil, fmt.Errorf("SO_PEERPIDFD: %w", err)
64 }
65}
66
67func (linuxPeerExe) Exe(pidfd *os.File) (string, int, error) {
68 defer runtime.KeepAlive(pidfd)
69 return exeOfPidfd(int(pidfd.Fd()))
70}
71
72// exeOfPidfd runs the pidfd procedure above.
73func exeOfPidfd(pidfd int) (string, int, error) {
74 pid, err := pidOfPidfd(pidfd)
75 if err != nil {
76 return "", 0, err
77 }
78 exe, err := readExe(pid)
79 // After the read: a process that is gone may have let its pid be reused.
80 if alive := unix.PidfdSendSignal(pidfd, 0, nil, 0); alive != nil {
81 return "", pid, fmt.Errorf("peer exited: %w", alive)
82 }
83 return exe, pid, err
84}
85
86// pidOfPidfd reads the Pid: line of /proc/self/fdinfo/<pidfd>: -1 once the
87// process has exited, 0 when it is outside this pid namespace.
88func pidOfPidfd(pidfd int) (int, error) {
89 f, err := os.Open("/proc/self/fdinfo/" + strconv.Itoa(pidfd))
90 if err != nil {
91 return 0, err
92 }
93 defer f.Close()
94 sc := bufio.NewScanner(f)
95 for sc.Scan() {
96 if v, ok := strings.CutPrefix(sc.Text(), "Pid:"); ok {
97 pid, err := strconv.Atoi(strings.TrimSpace(v))
98 if err != nil || pid <= 0 {
99 return 0, fmt.Errorf("peer pid not visible (%q)", strings.TrimSpace(v))
100 }
101 return pid, nil
102 }
103 }
104 return 0, errors.New("pidfd fdinfo has no Pid")
105}
106
107// readExe reads /proc/<pid>/exe and refuses a deleted executable.
108func readExe(pid int) (string, error) {
109 exe, err := os.Readlink("/proc/" + strconv.Itoa(pid) + "/exe")
110 if err != nil {
111 return "", err
112 }
113 if strings.HasSuffix(exe, " (deleted)") {
114 return "", errExeDeleted
115 }
116 return exe, nil
117}