v0.4.0 b6be3a8

captureexe.go RawHistoryFolder

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}

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