detached 01f9a3d

fix(xwm): check root ownership before claiming WM state and never adopt the check window

commit
01f9a3dc878eaf166043aeb0680410dab2f6fe1f
author
brice · · 2026-10-22 12:11 UTC
committer
brice · · 2026-10-22 17:54 UTC
parent
69b4382
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +40 -4
M internal/adapters/wayland/xwm/events.go +2 -2
@@ -132,7 +132,7 @@ func (x *XWM) onError(e *xturbo.XError) {
132132 func (x *XWM) onCreateNotify(ev *xturbo.Event) {
133133 e := xproto.CreateNotifyOf(ev)
134134 xid := uint32(e.Window())
135- if x.tab.lookup(xid) != 0 {
135+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
136136 return
137137 }
138138 s := x.insert(xid)
@@ -184,7 +184,7 @@ func (x *XWM) onTreeReply(r *xturbo.Reply) {
184184 children := q.Children()
185185 for i := 0; i < children.Len(); i++ {
186186 xid := uint32(children.At(i))
187- if x.tab.lookup(xid) != 0 {
187+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
188188 continue
189189 }
190190 s := x.insert(xid)
@@ -132,7 +132,7 @@ func (x *XWM) onError(e *xturbo.XError) {
132 func (x *XWM) onCreateNotify(ev *xturbo.Event) {132 func (x *XWM) onCreateNotify(ev *xturbo.Event) {
133 e := xproto.CreateNotifyOf(ev)133 e := xproto.CreateNotifyOf(ev)
134 xid := uint32(e.Window())134 xid := uint32(e.Window())
135- if x.tab.lookup(xid) != 0 {135+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
136 return136 return
137 }137 }
138 s := x.insert(xid)138 s := x.insert(xid)
@@ -184,7 +184,7 @@ func (x *XWM) onTreeReply(r *xturbo.Reply) {
184 children := q.Children()184 children := q.Children()
185 for i := 0; i < children.Len(); i++ {185 for i := 0; i < children.Len(); i++ {
186 xid := uint32(children.At(i))186 xid := uint32(children.At(i))
187- if x.tab.lookup(xid) != 0 {187+ if xid == x.wmWin || x.tab.lookup(xid) != 0 {
188 continue188 continue
189 }189 }
190 s := x.insert(xid)190 s := x.insert(xid)
M internal/adapters/wayland/xwm/xwm.go +20 -2
@@ -13,6 +13,7 @@ import (
1313 "encoding/binary"
1414 "errors"
1515 "fmt"
16+ "io"
1617
1718 "bnema.dev/xturbo"
1819 "bnema.dev/xturbo/composite"
@@ -80,7 +81,7 @@ type XWM struct {
8081 }
8182
8283 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
83- x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots)}
84+ x := &XWM{conn: conn, log: log.WithField("component", "xwm"), root: conn.Root(), tab: newTable(initialSlots)}
8485 x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}
8586 x.fit()
8687 return x
@@ -195,6 +196,17 @@ func (x *XWM) boot() error {
195196 var rootMask xproto.ChangeWindowAttributesValues
196197 rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
197198 x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)
199+ // Only one client may select SubstructureRedirect on the root: wait for
200+ // the answer before touching anything another window manager owns.
201+ if err := b.Err(); err != nil {
202+ return fmt.Errorf("xwm: encode root mask: %w", err)
203+ }
204+ if err := conn.Sync(&x.h); err != nil {
205+ return fmt.Errorf("xwm: root mask: %w", err)
206+ }
207+ if x.bootErr != nil {
208+ return x.bootErr
209+ }
198210 composite.RedirectSubwindows(b, root, composite.RedirectManual)
199211
200212 var wv xproto.CreateWindowValues
@@ -205,6 +217,8 @@ func (x *XWM) boot() error {
205217 xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)
206218 x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
207219 x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
220+ // CurrentTime and no MANAGER announcement: this Xwayland is private to
221+ // neferwl and nobody waits on these selections.
208222 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)
209223 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)
210224
@@ -311,7 +325,11 @@ func (x *XWM) fail(err error) {
311325 if x.closed {
312326 return
313327 }
314- x.log.Warn().Err(err).Msg("X11 window management stopped")
328+ if errors.Is(err, io.EOF) || errors.Is(err, xturbo.ErrClosed) {
329+ x.log.Info().Err(err).Msg("X11 window management stopped: Xwayland closed the connection")
330+ } else {
331+ x.log.Warn().Err(err).Msg("X11 window management stopped")
332+ }
315333 x.Close()
316334 }
317335
@@ -13,6 +13,7 @@ import (
13 "encoding/binary"13 "encoding/binary"
14 "errors"14 "errors"
15 "fmt"15 "fmt"
16+ "io"
16 17
17 "bnema.dev/xturbo"18 "bnema.dev/xturbo"
18 "bnema.dev/xturbo/composite"19 "bnema.dev/xturbo/composite"
@@ -80,7 +81,7 @@ type XWM struct {
80 }81 }
81 82
82 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {83 func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
83- x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots)}84+ x := &XWM{conn: conn, log: log.WithField("component", "xwm"), root: conn.Root(), tab: newTable(initialSlots)}
84 x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}85 x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}
85 x.fit()86 x.fit()
86 return x87 return x
@@ -195,6 +196,17 @@ func (x *XWM) boot() error {
195 var rootMask xproto.ChangeWindowAttributesValues196 var rootMask xproto.ChangeWindowAttributesValues
196 rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)197 rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
197 x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)198 x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)
199+ // Only one client may select SubstructureRedirect on the root: wait for
200+ // the answer before touching anything another window manager owns.
201+ if err := b.Err(); err != nil {
202+ return fmt.Errorf("xwm: encode root mask: %w", err)
203+ }
204+ if err := conn.Sync(&x.h); err != nil {
205+ return fmt.Errorf("xwm: root mask: %w", err)
206+ }
207+ if x.bootErr != nil {
208+ return x.bootErr
209+ }
198 composite.RedirectSubwindows(b, root, composite.RedirectManual)210 composite.RedirectSubwindows(b, root, composite.RedirectManual)
199 211
200 var wv xproto.CreateWindowValues212 var wv xproto.CreateWindowValues
@@ -205,6 +217,8 @@ func (x *XWM) boot() error {
205 xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)217 xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)
206 x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})218 x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
207 x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})219 x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
220+ // CurrentTime and no MANAGER announcement: this Xwayland is private to
221+ // neferwl and nobody waits on these selections.
208 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)222 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)
209 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)223 xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)
210 224
@@ -311,7 +325,11 @@ func (x *XWM) fail(err error) {
311 if x.closed {325 if x.closed {
312 return326 return
313 }327 }
314- x.log.Warn().Err(err).Msg("X11 window management stopped")328+ if errors.Is(err, io.EOF) || errors.Is(err, xturbo.ErrClosed) {
329+ x.log.Info().Err(err).Msg("X11 window management stopped: Xwayland closed the connection")
330+ } else {
331+ x.log.Warn().Err(err).Msg("X11 window management stopped")
332+ }
315 x.Close()333 x.Close()
316 }334 }
317 335
M internal/adapters/wayland/xwm/xwm_test.go +18 -0
@@ -137,6 +137,13 @@ func TestBootWMExists(t *testing.T) {
137137 x, err := Boot(fd, testLog())
138138 require.ErrorIs(t, err, ErrWMExists)
139139 require.Nil(t, x)
140+ // Nothing the running window manager owns was touched.
141+ for _, r := range p.requests() {
142+ require.NotEqual(t, uint8(opSetSelectionOwner), r.major)
143+ if r.major == opChangeProperty {
144+ require.NotEqual(t, uint32(peerRoot), r.u32(4))
145+ }
146+ }
140147 }
141148
142149 func TestBootNoComposite(t *testing.T) {
@@ -238,6 +245,17 @@ func TestCreateAndDestroyManaged(t *testing.T) {
238245 _ = src
239246 }
240247
248+// The X server reports the XWM's own check window to it like any other
249+// child of the root; it is never adopted.
250+func TestOwnWindowNeverAdopted(t *testing.T) {
251+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
252+ p.createNotify(x.wmWin, false)
253+ const marker = 0x500003
254+ p.createNotify(marker, true)
255+ readUntil(t, x, func() bool { return x.Slot(marker) != 0 })
256+ require.Zero(t, x.Slot(x.wmWin))
257+}
258+
241259 func TestOverrideRedirectNeverReachesHost(t *testing.T) {
242260 x, p, _, _, _ := attached(t) // the mock fails the test on any host call
243261 p.createNotify(0x500002, true)
@@ -137,6 +137,13 @@ func TestBootWMExists(t *testing.T) {
137 x, err := Boot(fd, testLog())137 x, err := Boot(fd, testLog())
138 require.ErrorIs(t, err, ErrWMExists)138 require.ErrorIs(t, err, ErrWMExists)
139 require.Nil(t, x)139 require.Nil(t, x)
140+ // Nothing the running window manager owns was touched.
141+ for _, r := range p.requests() {
142+ require.NotEqual(t, uint8(opSetSelectionOwner), r.major)
143+ if r.major == opChangeProperty {
144+ require.NotEqual(t, uint32(peerRoot), r.u32(4))
145+ }
146+ }
140 }147 }
141 148
142 func TestBootNoComposite(t *testing.T) {149 func TestBootNoComposite(t *testing.T) {
@@ -238,6 +245,17 @@ func TestCreateAndDestroyManaged(t *testing.T) {
238 _ = src245 _ = src
239 }246 }
240 247
248+// The X server reports the XWM's own check window to it like any other
249+// child of the root; it is never adopted.
250+func TestOwnWindowNeverAdopted(t *testing.T) {
251+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
252+ p.createNotify(x.wmWin, false)
253+ const marker = 0x500003
254+ p.createNotify(marker, true)
255+ readUntil(t, x, func() bool { return x.Slot(marker) != 0 })
256+ require.Zero(t, x.Slot(x.wmWin))
257+}
258+
241 func TestOverrideRedirectNeverReachesHost(t *testing.T) {259 func TestOverrideRedirectNeverReachesHost(t *testing.T) {
242 x, p, _, _, _ := attached(t) // the mock fails the test on any host call260 x, p, _, _, _ := attached(t) // the mock fails the test on any host call
243 p.createNotify(0x500002, true)261 p.createNotify(0x500002, true)

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 23

Tags 10

↑↓ moveenter openesc close