detached 6ef41f0

fix(server): expose global metadata before advertisement filtering

commit
6ef41f01db89dd894c9cf7aea587c52ac1ef18e4
author
brice · · 2026-10-22 05:56 UTC
parent
e13fc36
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
4/4
3 files · +167 -7
M README.md +5 -1
@@ -25,7 +25,11 @@ Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https
2525 `Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
2626 and bind each global; passing `nil` removes the filter. Set it before clients
2727 receive restricted globals. Global handles are stable, including during creation;
28-filters must not create globals or block.
28+filters must not create globals or block. Filter by `g.Interface()` (for example,
29+`g.Interface() != "xwayland_shell_v1" || c == xwaylandClient`), not a captured
30+handle assigned by `AddGlobal`: creation can invoke the filter before that
31+assignment. `Interface()` and `Version()` expose stored metadata without
32+allocation, including during creation.
2933
3034 `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
3135 writable interest initially disabled. `Source.SetWritable(true)` arms writable
@@ -25,7 +25,11 @@ Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https
25 `Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see25 `Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
26 and bind each global; passing `nil` removes the filter. Set it before clients26 and bind each global; passing `nil` removes the filter. Set it before clients
27 receive restricted globals. Global handles are stable, including during creation;27 receive restricted globals. Global handles are stable, including during creation;
28-filters must not create globals or block.28+filters must not create globals or block. Filter by `g.Interface()` (for example,
29+`g.Interface() != "xwayland_shell_v1" || c == xwaylandClient`), not a captured
30+handle assigned by `AddGlobal`: creation can invoke the filter before that
31+assignment. `Interface()` and `Version()` expose stored metadata without
32+allocation, including during creation.
29 33
30 `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with34 `Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
31 writable interest initially disabled. `Source.SetWritable(true)` arms writable35 writable interest initially disabled. `Source.SetWritable(true)` arms writable
M server/display.go +12 -3
@@ -139,10 +139,19 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
139139
140140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
141141 type Global struct {
142- c uintptr
143- d *Display
142+ c uintptr
143+ d *Display
144+ interfaceName string
145+ version int32
144146 }
145147
148+// Interface returns the advertised interface name without allocating. It is
149+// available even in filter calls made synchronously during AddGlobal.
150+func (g *Global) Interface() string { return g.interfaceName }
151+
152+// Version returns the advertised version, including during AddGlobal filtering.
153+func (g *Global) Version() int32 { return g.version }
154+
146155 // SetGlobalFilter controls global visibility and binding for each client. nil
147156 // removes the filter. Install it before advertising restricted globals; it does
148157 // not retract announcements already sent. The callback must not block or create
@@ -177,7 +186,7 @@ func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*
177186 live.nextBind++
178187 token := live.nextBind
179188 live.binds[token] = bindFn
180- g := &Global{d: d}
189+ g := &Global{d: d, interfaceName: iface.Name, version: version}
181190 d.pendingGlobal = g
182191 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
183192 d.pendingGlobal = nil
@@ -139,10 +139,19 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
139 139
140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
141 type Global struct {141 type Global struct {
142- c uintptr142+ c uintptr
143- d *Display143+ d *Display
144+ interfaceName string
145+ version int32
144 }146 }
145 147
148+// Interface returns the advertised interface name without allocating. It is
149+// available even in filter calls made synchronously during AddGlobal.
150+func (g *Global) Interface() string { return g.interfaceName }
151+
152+// Version returns the advertised version, including during AddGlobal filtering.
153+func (g *Global) Version() int32 { return g.version }
154+
146 // SetGlobalFilter controls global visibility and binding for each client. nil155 // SetGlobalFilter controls global visibility and binding for each client. nil
147 // removes the filter. Install it before advertising restricted globals; it does156 // removes the filter. Install it before advertising restricted globals; it does
148 // not retract announcements already sent. The callback must not block or create157 // not retract announcements already sent. The callback must not block or create
@@ -177,7 +186,7 @@ func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*
177 live.nextBind++186 live.nextBind++
178 token := live.nextBind187 token := live.nextBind
179 live.binds[token] = bindFn188 live.binds[token] = bindFn
180- g := &Global{d: d}189+ g := &Global{d: d, interfaceName: iface.Name, version: version}
181 d.pendingGlobal = g190 d.pendingGlobal = g
182 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)191 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
183 d.pendingGlobal = nil192 d.pendingGlobal = nil
M server/xwm_test.go +150 -3
@@ -46,7 +46,7 @@ func registry(t testing.TB, d *Display, fd int) int {
4646 if _, err := unix.Write(fd, request[:]); err != nil {
4747 t.Fatal(err)
4848 }
49- if wlEventLoopDispatch(d.loop, 0) < 0 {
49+ if xwmDispatch(d) < 0 {
5050 t.Fatal("dispatch failed")
5151 }
5252 wlDisplayFlushClients(d.c)
@@ -123,7 +123,7 @@ func TestWritableInterest(t *testing.T) {
123123 t.Fatal(err)
124124 }
125125 dispatch := func() {
126- if wlEventLoopDispatch(d.loop, 0) < 0 {
126+ if xwmDispatch(d) < 0 {
127127 t.Fatal("dispatch failed")
128128 }
129129 }
@@ -224,6 +224,153 @@ func BenchmarkFDCallback(b *testing.B) {
224224 b.ReportAllocs()
225225 b.ResetTimer()
226226 for b.Loop() {
227- wlEventLoopDispatch(d.loop, 0)
227+ xwmDispatch(d)
228228 }
229229 }
230+
231+func TestLateRestrictedGlobal(t *testing.T) {
232+ d := xwmDisplay(t)
233+ allowed, a := xwmClient(t, d)
234+ _, denied := xwmClient(t, d)
235+ if registry(t, d, a) != 0 || registry(t, d, denied) != 0 {
236+ t.Fatal("unexpected globals")
237+ }
238+ iface := &Interface{Name: "xwm_late_restricted", Version: 2}
239+ if err := NewInterfaces(iface); err != nil {
240+ t.Fatal(err)
241+ }
242+ calls, binds := 0, 0
243+ policy := func(c Client, g *Global) bool {
244+ calls++
245+ if g.Version() != 2 {
246+ t.Error("creation-time version unavailable")
247+ }
248+ return g.Interface() != iface.Name || c == allowed
249+ }
250+ d.SetGlobalFilter(policy)
251+ g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ })
252+ if err != nil {
253+ t.Fatal(err)
254+ }
255+ if calls != 2 {
256+ t.Fatalf("creation-time calls = %d", calls)
257+ }
258+ wlDisplayFlushClients(d.c)
259+ var buf [4096]byte
260+ n, err := unix.Read(a, buf[:])
261+ if err != nil || n < 16 {
262+ t.Fatalf("allowed announcement: n=%d err=%v", n, err)
263+ }
264+ name := binary.NativeEndian.Uint32(buf[8:12])
265+ if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 {
266+ t.Fatalf("denied announcement: n=%d err=%v", n, err)
267+ }
268+ if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 {
269+ t.Fatalf("metadata policy allocs = %g", allocs)
270+ }
271+ // Guessing a hidden global's numeric name must not bypass the filter.
272+ strLen := len(iface.Name) + 1
273+ padded := (strLen + 3) &^ 3
274+ req := make([]byte, 24+padded)
275+ binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry
276+ binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind
277+ binary.NativeEndian.PutUint32(req[8:12], name)
278+ binary.NativeEndian.PutUint32(req[12:16], uint32(strLen))
279+ copy(req[16:], iface.Name)
280+ binary.NativeEndian.PutUint32(req[16+padded:], 2)
281+ binary.NativeEndian.PutUint32(req[20+padded:], 3)
282+ if _, err := unix.Write(denied, req); err != nil {
283+ t.Fatal(err)
284+ }
285+ if xwmDispatch(d) < 0 {
286+ t.Fatal("dispatch")
287+ }
288+ wlDisplayFlushClients(d.c)
289+ if binds != 0 {
290+ t.Fatal("denied bind reached handler")
291+ }
292+ n, err = unix.Read(denied, buf[:])
293+ if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 {
294+ t.Fatalf("expected wl_display.error: n=%d err=%v", n, err)
295+ }
296+}
297+
298+func TestWritableBackpressureAndRemoval(t *testing.T) {
299+ d := xwmDisplay(t)
300+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
301+ if err != nil {
302+ t.Fatal(err)
303+ }
304+ defer unix.Close(fds[0])
305+ defer unix.Close(fds[1])
306+ if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil {
307+ t.Fatal(err)
308+ }
309+ var payload [4096]byte
310+ total := 0
311+ for attempts := 0; attempts < 1024; attempts++ {
312+ n, err := unix.Write(fds[0], payload[:])
313+ if err == unix.EAGAIN {
314+ break
315+ }
316+ if err != nil {
317+ t.Fatal(err)
318+ }
319+ total += n
320+ }
321+ if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN {
322+ t.Fatalf("not backpressured: %v", err)
323+ }
324+ writes := 0
325+ var s *Source
326+ s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() {
327+ writes++
328+ if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 {
329+ t.Errorf("resume: n=%d err=%v", n, err)
330+ }
331+ if err := s.SetWritable(false); err != nil {
332+ t.Error(err)
333+ }
334+ s.Remove() // Removal from a real C-dispatched callback.
335+ })
336+ if err != nil {
337+ t.Fatal(err)
338+ }
339+ if err := s.SetWritable(true); err != nil {
340+ t.Fatal(err)
341+ }
342+ xwmDispatch(d)
343+ if writes != 0 {
344+ t.Fatal("writable fired before peer drained")
345+ }
346+ for remaining := total; remaining > 0; {
347+ n, err := unix.Read(fds[1], payload[:])
348+ if err != nil || n == 0 {
349+ t.Fatalf("drain: n=%d err=%v", n, err)
350+ }
351+ remaining -= n
352+ }
353+ xwmDispatch(d)
354+ if writes != 1 || s.c != 0 {
355+ t.Fatalf("writes=%d source=%x", writes, s.c)
356+ }
357+ xwmDispatch(d)
358+ if writes != 1 {
359+ t.Fatal("removed source dispatched again")
360+ }
361+}
362+
363+func TestSetWritableKernelError(t *testing.T) {
364+ t.Skip("A live source owns a private duplicated fd; the public API cannot invalidate it. Removed sources are no-ops. Forcing epoll_ctl failure requires a fake or corrupting libwayland-owned state.")
365+}
366+
367+// The Go runtime can interrupt epoll_wait with its preemption signal.
368+func xwmDispatch(d *Display) int32 {
369+ for attempt := 0; attempt < 16; attempt++ {
370+ result := wlEventLoopDispatch(d.loop, 0)
371+ if result >= 0 || lastErrno() != unix.EINTR {
372+ return result
373+ }
374+ }
375+ return -1
376+}
@@ -46,7 +46,7 @@ func registry(t testing.TB, d *Display, fd int) int {
46 if _, err := unix.Write(fd, request[:]); err != nil {46 if _, err := unix.Write(fd, request[:]); err != nil {
47 t.Fatal(err)47 t.Fatal(err)
48 }48 }
49- if wlEventLoopDispatch(d.loop, 0) < 0 {49+ if xwmDispatch(d) < 0 {
50 t.Fatal("dispatch failed")50 t.Fatal("dispatch failed")
51 }51 }
52 wlDisplayFlushClients(d.c)52 wlDisplayFlushClients(d.c)
@@ -123,7 +123,7 @@ func TestWritableInterest(t *testing.T) {
123 t.Fatal(err)123 t.Fatal(err)
124 }124 }
125 dispatch := func() {125 dispatch := func() {
126- if wlEventLoopDispatch(d.loop, 0) < 0 {126+ if xwmDispatch(d) < 0 {
127 t.Fatal("dispatch failed")127 t.Fatal("dispatch failed")
128 }128 }
129 }129 }
@@ -224,6 +224,153 @@ func BenchmarkFDCallback(b *testing.B) {
224 b.ReportAllocs()224 b.ReportAllocs()
225 b.ResetTimer()225 b.ResetTimer()
226 for b.Loop() {226 for b.Loop() {
227- wlEventLoopDispatch(d.loop, 0)227+ xwmDispatch(d)
228 }228 }
229 }229 }
230+
231+func TestLateRestrictedGlobal(t *testing.T) {
232+ d := xwmDisplay(t)
233+ allowed, a := xwmClient(t, d)
234+ _, denied := xwmClient(t, d)
235+ if registry(t, d, a) != 0 || registry(t, d, denied) != 0 {
236+ t.Fatal("unexpected globals")
237+ }
238+ iface := &Interface{Name: "xwm_late_restricted", Version: 2}
239+ if err := NewInterfaces(iface); err != nil {
240+ t.Fatal(err)
241+ }
242+ calls, binds := 0, 0
243+ policy := func(c Client, g *Global) bool {
244+ calls++
245+ if g.Version() != 2 {
246+ t.Error("creation-time version unavailable")
247+ }
248+ return g.Interface() != iface.Name || c == allowed
249+ }
250+ d.SetGlobalFilter(policy)
251+ g, err := d.AddGlobal(iface, 2, func(Client, uint32, uint32) { binds++ })
252+ if err != nil {
253+ t.Fatal(err)
254+ }
255+ if calls != 2 {
256+ t.Fatalf("creation-time calls = %d", calls)
257+ }
258+ wlDisplayFlushClients(d.c)
259+ var buf [4096]byte
260+ n, err := unix.Read(a, buf[:])
261+ if err != nil || n < 16 {
262+ t.Fatalf("allowed announcement: n=%d err=%v", n, err)
263+ }
264+ name := binary.NativeEndian.Uint32(buf[8:12])
265+ if n, err := unix.Read(denied, buf[:]); err != unix.EAGAIN || n > 0 {
266+ t.Fatalf("denied announcement: n=%d err=%v", n, err)
267+ }
268+ if allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) }); allocs != 0 {
269+ t.Fatalf("metadata policy allocs = %g", allocs)
270+ }
271+ // Guessing a hidden global's numeric name must not bypass the filter.
272+ strLen := len(iface.Name) + 1
273+ padded := (strLen + 3) &^ 3
274+ req := make([]byte, 24+padded)
275+ binary.NativeEndian.PutUint32(req[0:4], 2) // wl_registry
276+ binary.NativeEndian.PutUint32(req[4:8], uint32(len(req))<<16) // bind
277+ binary.NativeEndian.PutUint32(req[8:12], name)
278+ binary.NativeEndian.PutUint32(req[12:16], uint32(strLen))
279+ copy(req[16:], iface.Name)
280+ binary.NativeEndian.PutUint32(req[16+padded:], 2)
281+ binary.NativeEndian.PutUint32(req[20+padded:], 3)
282+ if _, err := unix.Write(denied, req); err != nil {
283+ t.Fatal(err)
284+ }
285+ if xwmDispatch(d) < 0 {
286+ t.Fatal("dispatch")
287+ }
288+ wlDisplayFlushClients(d.c)
289+ if binds != 0 {
290+ t.Fatal("denied bind reached handler")
291+ }
292+ n, err = unix.Read(denied, buf[:])
293+ if err != nil || n < 12 || binary.NativeEndian.Uint32(buf[:4]) != 1 || binary.NativeEndian.Uint32(buf[4:8])&0xffff != 0 {
294+ t.Fatalf("expected wl_display.error: n=%d err=%v", n, err)
295+ }
296+}
297+
298+func TestWritableBackpressureAndRemoval(t *testing.T) {
299+ d := xwmDisplay(t)
300+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
301+ if err != nil {
302+ t.Fatal(err)
303+ }
304+ defer unix.Close(fds[0])
305+ defer unix.Close(fds[1])
306+ if err := unix.SetsockoptInt(fds[0], unix.SOL_SOCKET, unix.SO_SNDBUF, 4096); err != nil {
307+ t.Fatal(err)
308+ }
309+ var payload [4096]byte
310+ total := 0
311+ for attempts := 0; attempts < 1024; attempts++ {
312+ n, err := unix.Write(fds[0], payload[:])
313+ if err == unix.EAGAIN {
314+ break
315+ }
316+ if err != nil {
317+ t.Fatal(err)
318+ }
319+ total += n
320+ }
321+ if _, err := unix.Write(fds[0], payload[:]); err != unix.EAGAIN {
322+ t.Fatalf("not backpressured: %v", err)
323+ }
324+ writes := 0
325+ var s *Source
326+ s, err = d.AddFDRW(fds[0], func() { t.Error("unexpected readable") }, func() {
327+ writes++
328+ if n, err := unix.Write(fds[0], payload[:1]); err != nil || n != 1 {
329+ t.Errorf("resume: n=%d err=%v", n, err)
330+ }
331+ if err := s.SetWritable(false); err != nil {
332+ t.Error(err)
333+ }
334+ s.Remove() // Removal from a real C-dispatched callback.
335+ })
336+ if err != nil {
337+ t.Fatal(err)
338+ }
339+ if err := s.SetWritable(true); err != nil {
340+ t.Fatal(err)
341+ }
342+ xwmDispatch(d)
343+ if writes != 0 {
344+ t.Fatal("writable fired before peer drained")
345+ }
346+ for remaining := total; remaining > 0; {
347+ n, err := unix.Read(fds[1], payload[:])
348+ if err != nil || n == 0 {
349+ t.Fatalf("drain: n=%d err=%v", n, err)
350+ }
351+ remaining -= n
352+ }
353+ xwmDispatch(d)
354+ if writes != 1 || s.c != 0 {
355+ t.Fatalf("writes=%d source=%x", writes, s.c)
356+ }
357+ xwmDispatch(d)
358+ if writes != 1 {
359+ t.Fatal("removed source dispatched again")
360+ }
361+}
362+
363+func TestSetWritableKernelError(t *testing.T) {
364+ t.Skip("A live source owns a private duplicated fd; the public API cannot invalidate it. Removed sources are no-ops. Forcing epoll_ctl failure requires a fake or corrupting libwayland-owned state.")
365+}
366+
367+// The Go runtime can interrupt epoll_wait with its preemption signal.
368+func xwmDispatch(d *Display) int32 {
369+ for attempt := 0; attempt < 16; attempt++ {
370+ result := wlEventLoopDispatch(d.loop, 0)
371+ if result >= 0 || lastErrno() != unix.EINTR {
372+ return result
373+ }
374+ }
375+ return -1
376+}

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 14

↑↓ moveenter openesc close