1//go:build linux
2// +build linux
3
4package wlturbo
5
6import (
7 "errors"
8 "fmt"
9 "golang.org/x/sys/unix"
10 "sync"
11 "syscall"
12)
13
14// maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
15// A socket read that carries more than this is reported as a protocol failure
16// instead of silently dropping descriptors.
17const maxRecvFDs = 64
18
19// controlBufferPool provides control-message buffers sized for maxRecvFDs.
20var controlBufferPool = sync.Pool{
21 New: func() interface{} {
22 return make([]byte, unix.CmsgSpace(maxRecvFDs*4))
23 },
24}
25
26// readChunk reads one chunk of bytes from the display connection and queues any
27// ancillary file descriptors that arrive with it. A single read never defines a
28// message boundary; callers frame the returned bytes themselves.
29//
30// Descriptors are owned by the connection and consumed in arrival order by
31// Event.Fd, which is what the Wayland wire protocol requires: an fd belongs to
32// the argument position in the byte stream, not to whichever message happens to
33// end first.
34func (d *Display) readChunk() (int, error) {
35 if d.unix == nil {
36 n, err := d.conn.Read(d.recvBuf)
37 if n < 0 {
38 n = 0
39 }
40 return n, err
41 }
42
43 oob := controlBufferPool.Get().([]byte)
44 defer controlBufferPool.Put(oob)
45
46 n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
47 if n < 0 {
48 n = 0
49 }
50
51 if oobn > 0 {
52 fds, perr := parseFDs(oob[:oobn])
53 d.queueFDs(fds)
54 if perr != nil && err == nil {
55 err = perr
56 }
57 }
58
59 if flags&unix.MSG_CTRUNC != 0 && err == nil {
60 // Descriptors were dropped by the kernel, so the connection can no
61 // longer be demarshalled correctly: treat it as a protocol failure so
62 // the caller closes the descriptors that did arrive.
63 err = &ProtocolError{Kind: "control_truncated", Err: ErrMalformedFrame}
64 }
65 return n, err
66}
67
68// parseFDs extracts the file descriptors carried by socket control messages.
69// Descriptors parsed before an error are returned so the caller can close them.
70func parseFDs(oob []byte) ([]int, error) {
71 scms, err := syscall.ParseSocketControlMessage(oob)
72 if err != nil {
73 return nil, fmt.Errorf("parse control message: %w", err)
74 }
75
76 var fds []int
77 for i := range scms {
78 scm := &scms[i]
79 if scm.Header.Type != syscall.SCM_RIGHTS {
80 continue
81 }
82 parsed, err := syscall.ParseUnixRights(scm)
83 if err != nil {
84 return fds, fmt.Errorf("parse unix rights: %w", err)
85 }
86 fds = append(fds, parsed...)
87 }
88 return fds, nil
89}
90
91// queueFDs appends descriptors to this connection's pending list.
92func (d *Display) queueFDs(fds []int) {
93 if len(fds) == 0 {
94 return
95 }
96 d.fdMu.Lock()
97 d.pendingFDs = append(d.pendingFDs, fds...)
98 d.fdMu.Unlock()
99}
100
101// nextFD removes and returns the oldest pending descriptor.
102func (d *Display) nextFD() (int, bool) {
103 d.fdMu.Lock()
104 defer d.fdMu.Unlock()
105 if len(d.pendingFDs) == 0 {
106 return -1, false
107 }
108 fd := d.pendingFDs[0]
109 d.pendingFDs = d.pendingFDs[1:]
110 return fd, true
111}
112
113// closePendingFDs closes every descriptor this connection still holds.
114func (d *Display) closePendingFDs() {
115 d.fdMu.Lock()
116 fds := d.pendingFDs
117 d.pendingFDs = nil
118 d.fdMu.Unlock()
119
120 for _, fd := range fds {
121 if fd >= 0 {
122 _ = unix.Close(fd)
123 }
124 }
125}
126
127// sendmsgWithFDs sends a message, attaching file descriptors when present.
128func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
129 d.sendMu.Lock()
130 defer d.sendMu.Unlock()
131
132 if len(fds) == 0 {
133 _, err := d.conn.Write(buf)
134 return err
135 }
136 if d.unix == nil {
137 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
138 }
139
140 _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
141 return err
142}
143
144// Memory mapping helpers for shared memory buffers
145
146// CreateAnonymousFile creates an anonymous file for shared memory
147func CreateAnonymousFile(size int64) (fd int, err error) {
148 // Try memfd_create first (Linux 3.17+)
149 fd, err = unix.MemfdCreate("wlclient-shm", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
150 if err == nil {
151 // Set file size
152 err = unix.Ftruncate(fd, size)
153 if err != nil {
154 _ = unix.Close(fd)
155 return -1, err
156 }
157
158 // Add seals to prevent resizing
159 _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS,
160 unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_SEAL)
161 if err != nil {
162 _ = unix.Close(fd)
163 return -1, err
164 }
165
166 return fd, nil
167 }
168
169 // Fallback to O_TMPFILE if available
170 fd, err = unix.Open("/dev/shm", unix.O_TMPFILE|unix.O_RDWR|unix.O_CLOEXEC, 0600)
171 if err == nil {
172 err = unix.Ftruncate(fd, size)
173 if err != nil {
174 _ = unix.Close(fd)
175 return -1, err
176 }
177 return fd, nil
178 }
179
180 // Final fallback: create temp file and unlink
181 name := fmt.Sprintf("/dev/shm/wlclient-%d", unix.Getpid())
182 fd, err = unix.Open(name, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC, 0600)
183 if err != nil {
184 return -1, err
185 }
186
187 // Unlink immediately
188 _ = unix.Unlink(name)
189
190 // Set size
191 err = unix.Ftruncate(fd, size)
192 if err != nil {
193 unix.Close(fd)
194 return -1, err
195 }
196
197 return fd, nil
198}
199
200// MapMemory maps a file descriptor into memory
201func MapMemory(fd int, size int) ([]byte, error) {
202 return unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
203}
204
205// UnmapMemory unmaps memory
206func UnmapMemory(data []byte) error {
207 return unix.Munmap(data)
208}