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}