feat/hints a471fa4

peerexe.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close