1package wlturbo
2
3import (
4 "bytes"
5 "errors"
6 "net"
7 "os"
8 "strings"
9 "sync"
10 "sync/atomic"
11 "testing"
12 "time"
13
14 "golang.org/x/sys/unix"
15)
16
17// captureConn records every write so tests can compare wire bytes.
18type captureConn struct {
19 mu sync.Mutex
20 writes [][]byte
21}
22
23func (c *captureConn) Read([]byte) (int, error) { return 0, net.ErrClosed }
24func (c *captureConn) Write(p []byte) (int, error) {
25 c.mu.Lock()
26 defer c.mu.Unlock()
27 c.writes = append(c.writes, append([]byte(nil), p...))
28 return len(p), nil
29}
30func (c *captureConn) Close() error { return nil }
31func (c *captureConn) LocalAddr() net.Addr { return testAddr{} }
32func (c *captureConn) RemoteAddr() net.Addr { return testAddr{} }
33func (c *captureConn) SetDeadline(time.Time) error { return nil }
34func (c *captureConn) SetReadDeadline(time.Time) error { return nil }
35func (c *captureConn) SetWriteDeadline(time.Time) error { return nil }
36
37func (c *captureConn) last(t *testing.T) []byte {
38 t.Helper()
39 c.mu.Lock()
40 defer c.mu.Unlock()
41 if len(c.writes) == 0 {
42 t.Fatal("nothing was written")
43 }
44 return c.writes[len(c.writes)-1]
45}
46
47func (c *captureConn) count() int {
48 c.mu.Lock()
49 defer c.mu.Unlock()
50 return len(c.writes)
51}
52
53func newCaptureEnv() (*Display, *captureConn, *BaseProxy) {
54 c := &captureConn{}
55 d := newDisplay(c)
56 parent := &BaseProxy{id: d.AllocateID(), context: d.context, version: 3}
57 d.context.Register(parent)
58 return d, c, parent
59}
60
61// RequestArgs and Request must put identical bytes on the wire for every
62// argument kind, including padding boundaries and nil objects.
63func TestRequestArgsMatchesBoxedRequest(t *testing.T) {
64 d, c, parent := newCaptureEnv()
65 obj := &BaseProxy{id: 77, context: d.context}
66 var nilObj Object
67 cases := []struct {
68 name string
69 boxed []interface{}
70 typed []Arg
71 }{
72 {"none", nil, nil},
73 {"words", []interface{}{uint32(0xdeadbeef), int32(-5), Fixed(-300)},
74 []Arg{ArgUint(0xdeadbeef), ArgInt(-5), ArgFixed(-300)}},
75 {"string_pad0", []interface{}{"abc"}, []Arg{ArgString("abc")}},
76 {"string_pad3", []interface{}{"a"}, []Arg{ArgString("a")}},
77 {"string_empty", []interface{}{""}, []Arg{ArgString("")}},
78 {"array_pad", []interface{}{[]byte{1, 2, 3, 4, 5}}, []Arg{ArgArray([]byte{1, 2, 3, 4, 5})}},
79 {"array_nil", []interface{}{[]byte(nil)}, []Arg{ArgArray(nil)}},
80 {"object", []interface{}{Object(obj)}, []Arg{ArgObject(obj)}},
81 {"nil_object", []interface{}{nilObj}, []Arg{ArgObject(nil)}},
82 {"mixed", []interface{}{uint32(9), "hello", Object(obj), int32(2), []byte("xy"), Fixed(256)},
83 []Arg{ArgUint(9), ArgString("hello"), ArgObject(obj), ArgInt(2), ArgArray([]byte("xy")), ArgFixed(256)}},
84 }
85 for _, tc := range cases {
86 t.Run(tc.name, func(t *testing.T) {
87 if err := d.context.Request(Request{Proxy: parent, Opcode: 5, Name: "x.r"}, tc.boxed...); err != nil {
88 t.Fatal(err)
89 }
90 want := c.last(t)
91 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 5, Name: "x.r"}, tc.typed...); err != nil {
92 t.Fatal(err)
93 }
94 got := c.last(t)
95 if !bytes.Equal(got, want) {
96 t.Fatalf("typed %x != boxed %x", got, want)
97 }
98 if len(got)%4 != 0 {
99 t.Fatalf("unaligned message: %d", len(got))
100 }
101 })
102 }
103}
104
105// Numeric words are written little-endian (native wire order on supported
106// targets) after a header carrying object, opcode and size.
107func TestRequestArgsWireLayout(t *testing.T) {
108 d, c, parent := newCaptureEnv()
109 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 2, Name: "x.r"}, ArgUint(1), ArgInt(-1), ArgString("hi")); err != nil {
110 t.Fatal(err)
111 }
112 want := []byte{
113 byte(parent.ID()), 0, 0, 0, 2, 0, 24, 0,
114 1, 0, 0, 0,
115 0xff, 0xff, 0xff, 0xff,
116 3, 0, 0, 0, 'h', 'i', 0, 0,
117 }
118 if got := c.last(t); !bytes.Equal(got, want) {
119 t.Fatalf("got %x want %x", got, want)
120 }
121}
122
123func TestRequestArgsMalformed(t *testing.T) {
124 d, c, parent := newCaptureEnv()
125 child := &BaseProxy{context: d.context}
126 tooBig := strings.Repeat("a", 0xffff)
127 for _, tc := range []struct {
128 name string
129 args []Arg
130 }{
131 {"zero_arg", []Arg{ArgUint(1), {}}},
132 {"string_too_long", []Arg{ArgString(tooBig)}},
133 {"array_too_long", []Arg{ArgArray(make([]byte, 0xffff))}},
134 {"total_too_large", []Arg{ArgArray(make([]byte, 0xfff0)), ArgArray(make([]byte, 0x20))}},
135 } {
136 t.Run(tc.name, func(t *testing.T) {
137 before := c.count()
138 args := append([]Arg{ArgObject(child)}, tc.args...)
139 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.bad", Child: child}, args...); err == nil {
140 t.Fatal("malformed request succeeded")
141 }
142 if c.count() != before {
143 t.Fatal("bytes written for a malformed request")
144 }
145 if _, ok := d.objects.Load(child.ID()); ok {
146 t.Fatal("child left registered after failed send")
147 }
148 })
149 }
150 // The largest 4-byte aligned message (0xfffc) still goes through.
151 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.ok"}, ArgArray(make([]byte, 0xfffc-HeaderSize-4))); err != nil {
152 t.Fatalf("largest array: %v", err)
153 }
154 if got := c.last(t); len(got) != 0xfffc {
155 t.Fatalf("size = %d", len(got))
156 }
157}
158
159func TestRequestArgsLifecycle(t *testing.T) {
160 d, c, parent := newCaptureEnv()
161
162 // Version, child allocation and inheritance.
163 late := &BaseProxy{context: d.context}
164 err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.late", Since: 4, Child: late}, ArgObject(late))
165 if !errors.Is(err, ErrVersionTooLow) {
166 t.Fatalf("since=4 on v3 = %v, want ErrVersionTooLow", err)
167 }
168 if late.ID() != 0 || c.count() != 0 {
169 t.Fatal("refused request allocated a child or wrote")
170 }
171 child := &BaseProxy{context: d.context}
172 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.make", Child: child}, ArgObject(child)); err != nil {
173 t.Fatal(err)
174 }
175 if child.ID() == 0 || child.Version() != 3 {
176 t.Fatalf("child id=%d version=%d", child.ID(), child.Version())
177 }
178 if _, ok := d.objects.Load(child.ID()); !ok {
179 t.Fatal("child not registered")
180 }
181
182 // Destructor: claimed exactly once; later requests fail without writing.
183 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}); err != nil {
184 t.Fatal(err)
185 }
186 n := c.count()
187 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}); err == nil {
188 t.Fatal("second destructor succeeded")
189 }
190 if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.after"}, ArgUint(1)); err == nil {
191 t.Fatal("request after destructor succeeded")
192 }
193 if c.count() != n {
194 t.Fatal("bytes written after destructor")
195 }
196
197 // A marshal failure leaves a destructor's proxy registered.
198 p2 := &BaseProxy{id: d.AllocateID(), context: d.context, version: 1}
199 d.context.Register(p2)
200 if err := d.context.RequestArgs(Request{Proxy: p2, Opcode: 9, Name: "x.destroy", Destructor: true}, Arg{}); err == nil {
201 t.Fatal("zero Arg accepted")
202 }
203 if err := d.context.CheckProxy(p2); err != nil {
204 t.Fatalf("proxy lost after marshal error: %v", err)
205 }
206
207 // Foreign and closed contexts.
208 other := &BaseProxy{id: 5, context: NewContext(d)}
209 if err := d.context.RequestArgs(Request{Proxy: other, Name: "x.r"}); err == nil {
210 t.Fatal("foreign proxy accepted")
211 }
212 d.context.closed.Store(true)
213 if err := d.context.RequestArgs(Request{Proxy: p2, Name: "x.r"}); err == nil {
214 t.Fatal("closed context accepted")
215 }
216}
217
218// Descriptors are closed after a successful send and stay with the caller on
219// failure, exactly as with Request.
220func TestRequestArgsFDOwnership(t *testing.T) {
221 sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
222 if err != nil {
223 t.Fatal(err)
224 }
225 fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
226 ca, err := net.FileConn(fa)
227 fa.Close()
228 if err != nil {
229 t.Fatal(err)
230 }
231 cb, err := net.FileConn(fb)
232 fb.Close()
233 if err != nil {
234 t.Fatal(err)
235 }
236 defer cb.Close()
237 d, err := ConnectFromConn(ca)
238 if err != nil {
239 t.Fatal(err)
240 }
241 defer d.Close()
242 parent := &BaseProxy{id: d.AllocateID(), context: d.Context(), version: 1}
243 d.Context().Register(parent)
244
245 open := func() int {
246 f, err := os.Open(os.DevNull)
247 if err != nil {
248 t.Fatal(err)
249 }
250 fd, err := unix.Dup(int(f.Fd()))
251 f.Close()
252 if err != nil {
253 t.Fatal(err)
254 }
255 return fd
256 }
257 isOpen := func(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
258
259 fd := open()
260 if err := d.Context().RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.fd", FDs: []int{fd}}, ArgString("m"), ArgFD(), ArgUint(4)); err != nil {
261 t.Fatal(err)
262 }
263 if isOpen(fd) {
264 t.Fatal("descriptor not closed after successful send")
265 }
266 body := make([]byte, 8+8+4)
267 if n, oob, err := readWithOOB(cb.(*net.UnixConn), body); err != nil || n != len(body) || oob == 0 {
268 t.Fatalf("peer read n=%d oob=%d err=%v", n, oob, err)
269 }
270
271 fd = open()
272 defer unix.Close(fd)
273 if err := d.Context().RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.fd", FDs: []int{fd}}, ArgFD(), Arg{}); err == nil {
274 t.Fatal("zero Arg accepted")
275 }
276 if !isOpen(fd) {
277 t.Fatal("descriptor closed after failed send")
278 }
279}
280
281func readWithOOB(c *net.UnixConn, buf []byte) (n, oobn int, err error) {
282 oob := make([]byte, unix.CmsgSpace(4))
283 n, oobn, _, _, err = c.ReadMsgUnix(buf, oob)
284 return
285}
286
287// The number of ArgFD markers must equal len(FDs). A mismatch is rejected
288// before the child is allocated, the guard runs or anything is written; the
289// caller keeps its descriptors and a destructor proxy stays unclaimed.
290func TestRequestArgsFDMarkerMismatch(t *testing.T) {
291 d, c, parent := newCaptureEnv()
292 fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
293 if err != nil {
294 t.Fatal(err)
295 }
296 defer unix.Close(fd)
297 isOpen := func() bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
298
299 for _, tc := range []struct {
300 name string
301 fds []int
302 args []Arg
303 destructor bool
304 }{
305 {"fd_without_marker", []int{fd}, []Arg{ArgUint(1)}, false},
306 {"fd_without_any_args", []int{fd}, nil, false},
307 {"marker_without_fd", nil, []Arg{ArgFD(), ArgUint(1)}, false},
308 {"two_markers_one_fd", []int{fd}, []Arg{ArgFD(), ArgFD()}, false},
309 {"one_marker_two_fds", []int{fd, fd}, []Arg{ArgFD()}, false},
310 {"destructor_fd_without_marker", []int{fd}, nil, true},
311 {"destructor_marker_without_fd", nil, []Arg{ArgFD()}, true},
312 } {
313 t.Run(tc.name, func(t *testing.T) {
314 before := c.count()
315 child := &BaseProxy{context: d.context}
316 args := tc.args
317 r := Request{Proxy: parent, Opcode: 1, Name: "x.fd", FDs: tc.fds, Destructor: tc.destructor}
318 if !tc.destructor {
319 r.Child = child
320 args = append([]Arg{ArgObject(child)}, args...)
321 }
322 if err := d.context.RequestArgs(r, args...); err == nil {
323 t.Fatal("mismatched fd markers accepted")
324 }
325 if c.count() != before {
326 t.Fatal("bytes written")
327 }
328 if child.ID() != 0 {
329 t.Fatal("child allocated")
330 }
331 if !isOpen() {
332 t.Fatal("caller descriptor closed")
333 }
334 if err := d.context.CheckProxy(parent); err != nil {
335 t.Fatalf("proxy claimed or lost: %v", err)
336 }
337 })
338 }
339
340 // Legacy semantics are unchanged: Request does not count placeholders.
341 if err := d.context.Request(Request{Proxy: parent, Opcode: 1, Name: "x.legacy"}, uintptr(3)); err != nil {
342 t.Fatalf("legacy uintptr placeholder without FDs: %v", err)
343 }
344}
345
346// A typed request racing a destructor on the same proxy either is written
347// before the destructor or fails without writing; nothing follows the
348// destructor on the wire.
349func TestRequestArgsConcurrentWithDestructor(t *testing.T) {
350 for i := 0; i < 50; i++ {
351 d, c, parent := newCaptureEnv()
352 var wg sync.WaitGroup
353 var ok atomic.Int32
354 for g := 0; g < 4; g++ {
355 wg.Add(1)
356 go func() {
357 defer wg.Done()
358 if d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.r"}, ArgUint(1)) == nil {
359 ok.Add(1)
360 }
361 }()
362 }
363 var destroyed atomic.Int32
364 for g := 0; g < 2; g++ {
365 wg.Add(1)
366 go func() {
367 defer wg.Done()
368 if d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}) == nil {
369 destroyed.Add(1)
370 }
371 }()
372 }
373 wg.Wait()
374 if destroyed.Load() != 1 {
375 t.Fatalf("destructor sent %d times, want exactly 1", destroyed.Load())
376 }
377 c.mu.Lock()
378 writes := c.writes
379 c.mu.Unlock()
380 if int32(len(writes)) != ok.Load()+1 {
381 t.Fatalf("%d writes for %d requests + destructor", len(writes), ok.Load())
382 }
383 for j, w := range writes {
384 isDestructor := w[4] == 9
385 if isDestructor != (j == len(writes)-1) {
386 t.Fatalf("destructor not last on the wire: write %d of %d", j, len(writes))
387 }
388 }
389 }
390}