v0.5.0 089fa12

scm_linux.go RawHistoryFolder

1//go:build linux
2// +build linux
3
4package wlturbo
5
6import (
7	"errors"
8	"fmt"
9	"io"
10	"net"
11	"sync"
12	"syscall"
13	"unsafe"
14
15	"golang.org/x/sys/unix"
16)
17
18// maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
19// A socket read that carries more than this is reported as a protocol failure
20// instead of silently dropping descriptors.
21const maxRecvFDs = 64
22
23type controlBuffer struct{ data []byte }
24
25// controlBufferPool provides control-message buffers sized for maxRecvFDs.
26var controlBufferPool = sync.Pool{
27	New: func() interface{} {
28		return &controlBuffer{data: make([]byte, unix.CmsgSpace(maxRecvFDs*4))}
29	},
30}
31
32// readChunk reads one chunk of bytes from the display connection and queues any
33// ancillary file descriptors that arrive with it. A single read never defines a
34// message boundary; callers frame the returned bytes themselves.
35//
36// Descriptors are owned by the connection and consumed in arrival order by
37// Event.Fd, which is what the Wayland wire protocol requires: an fd belongs to
38// the argument position in the byte stream, not to whichever message happens to
39// end first.
40func (d *Display) readChunk() (int, error) {
41	if d.unix == nil {
42		n, err := d.conn.Read(d.recvBuf)
43		if n < 0 {
44			n = 0
45		}
46		return n, err
47	}
48
49	buffer := controlBufferPool.Get().(*controlBuffer)
50	defer controlBufferPool.Put(buffer)
51	oob := buffer.data
52
53	n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
54	if n < 0 {
55		n = 0
56	}
57
58	if oobn > 0 {
59		fds, perr := parseFDs(oob[:oobn])
60		d.queueFDs(fds)
61		if perr != nil {
62			err = &ProtocolError{Kind: "malformed_control", Err: perr}
63		}
64	}
65
66	if flags&unix.MSG_CTRUNC != 0 {
67		// Descriptors were dropped by the kernel, so the connection can no
68		// longer be demarshalled correctly: treat it as a protocol failure so
69		// the caller closes the descriptors that did arrive.
70		err = &ProtocolError{Kind: "control_truncated", Err: ErrMalformedFrame}
71	}
72	return n, err
73}
74
75// parseFDs extracts the file descriptors carried by socket control messages.
76// Descriptors parsed before an error are returned so the caller can close them.
77func parseFDs(oob []byte) ([]int, error) {
78	var fds []int
79	for len(oob) > 0 {
80		if len(oob) < unix.SizeofCmsghdr {
81			return fds, fmt.Errorf("short control header")
82		}
83		var hdr unix.Cmsghdr
84		// Parse each header independently: a malformed later header must not
85		// discard rights already decoded from earlier messages.
86		if err := readControlHeader(oob, &hdr); err != nil {
87			return fds, err
88		}
89		length := int(hdr.Len)
90		if length < unix.SizeofCmsghdr || length > len(oob) {
91			return fds, fmt.Errorf("invalid control length %d", length)
92		}
93		if hdr.Level == unix.SOL_SOCKET && hdr.Type == unix.SCM_RIGHTS {
94			message := syscall.SocketControlMessage{Header: syscall.Cmsghdr{Len: uint64(length), Level: int32(hdr.Level), Type: int32(hdr.Type)}, Data: oob[unix.SizeofCmsghdr:length]}
95			parsed, err := syscall.ParseUnixRights(&message)
96			if err != nil {
97				return fds, fmt.Errorf("parse unix rights: %w", err)
98			}
99			fds = append(fds, parsed...)
100		}
101		step := unix.CmsgSpace(length - unix.SizeofCmsghdr)
102		if step > len(oob) {
103			break
104		} // final control message may omit trailing padding
105		oob = oob[step:]
106	}
107	return fds, nil
108}
109
110func readControlHeader(data []byte, hdr *unix.Cmsghdr) error {
111	// Linux cmsghdr has a native-endian size_t followed by two int32s.
112	if len(data) < unix.SizeofCmsghdr {
113		return fmt.Errorf("short control header")
114	}
115	*hdr = *(*unix.Cmsghdr)(unsafe.Pointer(&data[0]))
116	return nil
117}
118
119// queueFDs is called only by the active Dispatch reader.
120func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) }
121
122// closePendingFDs is called under recvMu (including during shutdown).
123func (d *Display) closePendingFDs() {
124	fds := d.pendingFDs
125	d.pendingFDs = nil
126	for _, fd := range fds {
127		if fd >= 0 {
128			_ = unix.Close(fd)
129		}
130	}
131}
132
133// CloseSentFD releases a descriptor after a complete successful Wayland send.
134func CloseSentFD(fd int) error { return unix.Close(fd) }
135
136// sendmsgWithFDs sends a message, attaching file descriptors when present.
137// A non-nil guard runs under the send lock, so its checks and the write are
138// ordered atomically with every other request on the connection.
139func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error {
140	d.sendMu.Lock()
141	defer d.sendMu.Unlock()
142
143	if d.closed.Load() {
144		return net.ErrClosed
145	}
146	if len(fds) > 0 && d.unix == nil {
147		return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
148	}
149	if guard != nil {
150		if err := guard(); err != nil {
151			return err
152		}
153	}
154	var n int
155	var err error
156	if len(fds) == 0 {
157		n, err = d.conn.Write(buf)
158	} else {
159		n, _, err = d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
160	}
161	if err == nil && n != len(buf) {
162		err = io.ErrShortWrite
163	}
164	if err != nil {
165		// A failed or partial write desynchronizes the stream: shut the socket
166		// down so every later send and receive fails instead of corrupting it.
167		_ = d.conn.Close()
168	}
169	return err
170}
171
172// Memory mapping helpers for shared memory buffers
173
174// CreateAnonymousFile creates an anonymous file for shared memory
175func CreateAnonymousFile(size int64) (fd int, err error) {
176	// Try memfd_create first (Linux 3.17+)
177	fd, err = unix.MemfdCreate("wlclient-shm", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
178	if err == nil {
179		// Set file size
180		err = unix.Ftruncate(fd, size)
181		if err != nil {
182			_ = unix.Close(fd)
183			return -1, err
184		}
185
186		// Add seals to prevent resizing
187		_, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS,
188			unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL)
189		if err != nil {
190			_ = unix.Close(fd)
191			return -1, err
192		}
193
194		return fd, nil
195	}
196
197	// Fallback to O_TMPFILE if available
198	fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600)
199	if err == nil {
200		err = unix.Ftruncate(fd, size)
201		if err != nil {
202			_ = unix.Close(fd)
203			return -1, err
204		}
205		return fd, nil
206	}
207
208	// Final fallback: create temp file and unlink
209	name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid())
210	fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)
211	if err != nil {
212		return -1, err
213	}
214
215	// Unlink immediately
216	_ = unix.Unlink(name)
217
218	// Set size
219	err = unix.Ftruncate(fd, size)
220	if err != nil {
221		unix.Close(fd)
222		return -1, err
223	}
224
225	return fd, nil
226}
227
228// MapMemory maps a file descriptor into memory
229func MapMemory(fd int, size int) ([]byte, error) {
230	return unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
231}
232
233// UnmapMemory unmaps memory
234func UnmapMemory(data []byte) error {
235	return unix.Munmap(data)
236}

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 1

Tags 8

↑↓ moveenter openesc close