Skip to content

Commit c20a10b

Browse files
authored
fix(linux): stop X11 hotkeys and the KDE geometry helper from sharing what they should not (#1527)
1 parent 8075782 commit c20a10b

4 files changed

Lines changed: 960 additions & 37 deletions

File tree

internal/adapter/hotkeys/linux/x11_cgo.go

Lines changed: 127 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ type x11HotkeyBinding struct {
2929
}
3030

3131
type x11HotkeyState struct {
32+
// displayMu serializes every Xlib call made on display.
33+
//
34+
// Xlib is thread-safe only when XInitThreads runs before the first
35+
// XOpenDisplay, and nothing in this process calls it. Manager.mu guards the
36+
// Go maps and says nothing about the connection, so without this lock a
37+
// registration on a caller's goroutine and the poll loop on its own would
38+
// be inside the same connection buffers at once: XPending flushes the
39+
// output buffer and reads from the socket, which is exactly what the
40+
// XGrabKey/XSelectInput/XFlush sequence beside it is doing.
41+
//
42+
// It is never held across a call that waits for a key. The loop polls with
43+
// neru_hotkeys_pending and only calls XNextEvent when an event is already
44+
// queued, so the longest a registration can wait behind it is one
45+
// non-blocking read — holding it across a bare XNextEvent would wedge
46+
// registration until the user pressed something.
47+
displayMu sync.Mutex
48+
3249
display *C.Display
3350
root C.Window
3451
bindings map[ports.HotkeyID]x11HotkeyBinding
@@ -38,35 +55,115 @@ type x11HotkeyState struct {
3855
once sync.Once
3956
}
4057

41-
var x11States sync.Map
58+
// x11IgnoredModifierMasks are the lock modifiers a grab has to be repeated
59+
// under, because the server reports them in a key event's state and a grab
60+
// naming a different state does not match: Lock is CapsLock, and Mod2 is where
61+
// NumLock conventionally sits.
62+
//
63+
// The set is stated once so a grab and the ungrab that undoes it cannot drift:
64+
// an ungrab that misses a mask leaves a hotkey grabbed after Neru forgot it.
65+
func x11IgnoredModifierMasks() [4]C.uint {
66+
return [4]C.uint{0, C.Mod2Mask, C.LockMask, C.Mod2Mask | C.LockMask}
67+
}
4268

43-
func (m *Manager) registerX11Hotkey(hotkeyID ports.HotkeyID, keyString string) error {
44-
state, err := m.ensureX11State()
69+
// grab resolves keyString against the connection's keymap and installs the
70+
// grabs for it, as one critical section.
71+
//
72+
// The resolve belongs inside the lock rather than beside it: XKeysymToKeycode
73+
// is an Xlib call on this same connection, so it is one of the calls being
74+
// serialized, not a pure computation that happens to precede them.
75+
func (s *x11HotkeyState) grab(keyString string) (x11HotkeyBinding, error) {
76+
s.displayMu.Lock()
77+
defer s.displayMu.Unlock()
78+
79+
keycode, modifiers, err := parseX11Hotkey(s.display, keyString)
4580
if err != nil {
46-
return err
47-
}
48-
49-
keycode, modifiers, parseErr := parseX11Hotkey(state.display, keyString)
50-
if parseErr != nil {
51-
return parseErr
81+
return x11HotkeyBinding{}, err
5282
}
5383

54-
for _, mask := range []C.uint{0, C.Mod2Mask, C.LockMask, C.Mod2Mask | C.LockMask} {
84+
for _, mask := range x11IgnoredModifierMasks() {
5585
C.XGrabKey(
56-
state.display,
86+
s.display,
5787
C.int(keycode),
5888
modifiers|mask,
59-
state.root,
89+
s.root,
6090
C.True,
6191
C.GrabModeAsync,
6292
C.GrabModeAsync,
6393
)
6494
}
65-
C.XSelectInput(state.display, state.root, C.KeyPressMask)
66-
C.XFlush(state.display)
6795

68-
state.bindings[hotkeyID] = x11HotkeyBinding{keycode: C.int(keycode), modifiers: modifiers}
69-
state.ids[x11BindingKey(keycode, modifiers)] = hotkeyID
96+
C.XSelectInput(s.display, s.root, C.KeyPressMask)
97+
C.XFlush(s.display)
98+
99+
return x11HotkeyBinding{keycode: C.int(keycode), modifiers: modifiers}, nil
100+
}
101+
102+
// ungrab releases the grabs grab installed for one binding.
103+
func (s *x11HotkeyState) ungrab(binding x11HotkeyBinding) {
104+
s.displayMu.Lock()
105+
defer s.displayMu.Unlock()
106+
107+
for _, mask := range x11IgnoredModifierMasks() {
108+
C.XUngrabKey(
109+
s.display,
110+
binding.keycode,
111+
binding.modifiers|mask,
112+
s.root,
113+
)
114+
}
115+
116+
C.XFlush(s.display)
117+
}
118+
119+
// nextEvent takes one queued event off the connection, reporting false when
120+
// none was waiting.
121+
//
122+
// The pending check and the read are one critical section because they are one
123+
// sequence: XPending answering "an event is queued" is only a fact about this
124+
// connection while nothing else is draining it, and an XNextEvent whose event
125+
// another thread has taken blocks until the next one arrives — with displayMu
126+
// held, which is the one way this lock could wedge registration.
127+
func (s *x11HotkeyState) nextEvent() (C.XEvent, bool) {
128+
s.displayMu.Lock()
129+
defer s.displayMu.Unlock()
130+
131+
var event C.XEvent
132+
133+
if C.neru_hotkeys_pending(s.display) == 0 {
134+
return event, false
135+
}
136+
137+
C.XNextEvent(s.display, &event)
138+
139+
return event, true
140+
}
141+
142+
// closeDisplay tears down the connection. Callers must have stopped the poll
143+
// loop first — the lock orders this against a registration, not against a read
144+
// of a display that has been freed.
145+
func (s *x11HotkeyState) closeDisplay() {
146+
s.displayMu.Lock()
147+
defer s.displayMu.Unlock()
148+
149+
C.XCloseDisplay(s.display)
150+
}
151+
152+
var x11States sync.Map
153+
154+
func (m *Manager) registerX11Hotkey(hotkeyID ports.HotkeyID, keyString string) error {
155+
state, err := m.ensureX11State()
156+
if err != nil {
157+
return err
158+
}
159+
160+
binding, grabErr := state.grab(keyString)
161+
if grabErr != nil {
162+
return grabErr
163+
}
164+
165+
state.bindings[hotkeyID] = binding
166+
state.ids[x11BindingKey(C.uint(binding.keycode), binding.modifiers)] = hotkeyID
70167

71168
return nil
72169
}
@@ -86,15 +183,7 @@ func (m *Manager) unregisterX11Hotkey(hotkeyID ports.HotkeyID) {
86183
return
87184
}
88185

89-
for _, mask := range []C.uint{0, C.Mod2Mask, C.LockMask, C.Mod2Mask | C.LockMask} {
90-
C.XUngrabKey(
91-
state.display,
92-
binding.keycode,
93-
binding.modifiers|mask,
94-
state.root,
95-
)
96-
}
97-
C.XFlush(state.display)
186+
state.ungrab(binding)
98187

99188
delete(state.ids, x11BindingKey(C.uint(binding.keycode), binding.modifiers))
100189
delete(state.bindings, hotkeyID)
@@ -120,7 +209,7 @@ func (m *Manager) unregisterAllX11Hotkeys() {
120209
close(state.stopCh)
121210
<-state.doneCh
122211

123-
C.XCloseDisplay(state.display)
212+
state.closeDisplay()
124213
x11States.Delete(m)
125214
})
126215
}
@@ -135,6 +224,10 @@ func (m *Manager) ensureX11State() (*x11HotkeyState, error) {
135224
return state, nil
136225
}
137226

227+
// XOpenDisplay and the root-window read are the two Xlib calls here that do
228+
// not go through displayMu, and the only ones that need not: they run
229+
// before the state exists to lock, before it is published to x11States and
230+
// before the poll loop that would share the connection is started.
138231
display := C.XOpenDisplay(nil)
139232
if display == nil {
140233
return nil, derrors.New(
@@ -167,18 +260,19 @@ func (m *Manager) runX11HotkeyLoop(state *x11HotkeyState) {
167260
default:
168261
}
169262

170-
// Use XPending to check for queued events instead of calling the
171-
// blocking XNextEvent directly. This allows the stop channel to be
172-
// checked between iterations, preventing a goroutine leak and a
173-
// use-after-free when unregisterAllX11Hotkeys closes the display.
174-
if C.neru_hotkeys_pending(state.display) == 0 {
263+
// nextEvent polls with XPending instead of calling the blocking
264+
// XNextEvent directly. This allows the stop channel to be checked
265+
// between iterations, preventing a goroutine leak and a
266+
// use-after-free when unregisterAllX11Hotkeys closes the display —
267+
// and it is what lets every Xlib call on this connection share one
268+
// lock without a registration ever waiting on a keystroke.
269+
event, hasEvent := state.nextEvent()
270+
if !hasEvent {
175271
time.Sleep(x11HotkeyPollInterval)
176272

177273
continue
178274
}
179275

180-
var event C.XEvent
181-
C.XNextEvent(state.display, &event)
182276
if C.neru_xevent_type(&event) != C.KeyPress {
183277
continue
184278
}

0 commit comments

Comments
 (0)