Add quarantined Xorg destination-key ownership and cleanup

This commit is contained in:
ak47 committed 2026-11-05 21:56:14 -05:00
1 parent 792cac5d0b
commit cd9ae05bbe
10 files changed
+988 -29

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
/*
#include "commands_native.h"
#include <X11/XKBlib.h>
#include <X11/extensions/XTest.h>
#include <stdlib.h>
static Display *remote_key_open(void) {
Display *display = XOpenDisplay(NULL);
if (!display) return NULL;
int event, error, major, minor;
if (!XTestQueryExtension(display, &event, &error, &major, &minor)) {
XCloseDisplay(display);
return NULL;
}
return display;
}
static int remote_key_resolve(Display *display, const char *name) {
KeySym wanted = XStringToKeysym(name);
if (wanted == NoSymbol) return 0;
int result = 0;
cthulhu_x11_begin_read(display);
XkbStateRec state;
XkbDescPtr map = XkbGetMap(display, XkbAllClientInfoMask, XkbUseCoreKbd);
// Multi-group and level-three policy is not implemented. Never guess a
// physical key from an inactive group's symbol or a shifted-only symbol.
if (map && XkbGetState(display, XkbUseCoreKbd, &state) == Success && state.group == 0) {
unsigned int modifiers = state.mods & ~(ShiftMask | LockMask);
for (int code = map->min_key_code; code <= map->max_key_code; code++) {
unsigned int consumed;
KeySym actual;
if (XkbTranslateKeyCode(map, code, modifiers, &consumed, &actual) && actual == wanted) {
if (result) { result = 0; break; } // Ambiguous destination.
result = code;
}
}
}
if (map) XkbFreeKeyboard(map, XkbAllComponentsMask, True);
if (cthulhu_x11_end_read(display)) result = 0;
return result;
}
static int remote_key_held(Display *display, unsigned int code) {
char held[32];
cthulhu_x11_begin_read(display);
int success = XQueryKeymap(display, held);
int error = cthulhu_x11_end_read(display);
if (!success || error) return -1;
return !!((unsigned char)held[code / 8] & (1u << (code % 8)));
}
static int remote_key_set(Display *display, unsigned int code, int pressed) {
cthulhu_x11_begin_read(display);
int success = XTestFakeKeyEvent(display, code, pressed, CurrentTime);
int error = cthulhu_x11_end_read(display);
return success && !error;
}
*/
import "C"
import (
"unsafe"
"git.stormux.org/storm/cthulhu/internal/input"
)
// Quarantined native prerequisite, with no production caller. All methods
// require one serial caller on a locked OS thread. Xlib can block: the future
// production owner must live in an independent helper, never the reader loop.
type remoteKeyTarget struct {
backend *remoteKeyBackend
ledger *input.RemoteKeyLedger
}
type remoteKeyBackend struct{ display *C.Display }
func openRemoteKeyTarget() (*remoteKeyTarget, error) {
display := C.remote_key_open()
if display == nil {
return nil, input.ErrRemoteKeyUnavailable
}
backend := &remoteKeyBackend{display: display}
ledger, err := input.NewRemoteKeyLedger(backend)
if err != nil {
C.XCloseDisplay(display)
return nil, err
}
return &remoteKeyTarget{backend: backend, ledger: ledger}, nil
}
func (backend *remoteKeyBackend) Resolve(name string) (uint16, error) {
if backend.display == nil || !input.ValidRemoteKeyName(name) {
return 0, input.ErrRemoteKeyUnavailable
}
value := C.CString(name)
defer C.free(unsafe.Pointer(value))
code := C.remote_key_resolve(backend.display, value)
if code < 8 || code > 255 {
return 0, input.ErrRemoteKeyUnavailable
}
return uint16(code), nil
}
func (backend *remoteKeyBackend) Held(code uint16) (bool, error) {
if backend.display == nil || code < 8 || code > 255 {
return false, input.ErrRemoteKeyBackend
}
held := C.remote_key_held(backend.display, C.uint(code))
if held < 0 {
return false, input.ErrRemoteKeyBackend
}
return held != 0, nil
}
func (backend *remoteKeyBackend) Set(code uint16, pressed bool) error {
if backend.display == nil || code < 8 || code > 255 {
return input.ErrRemoteKeyBackend
}
var down C.int
if pressed {
down = 1
}
if C.remote_key_set(backend.display, C.uint(code), down) == 0 {
return input.ErrRemoteKeyBackend
}
return nil
}
func (target *remoteKeyTarget) apply(name string, pressed bool) error {
return target.ledger.Apply(name, pressed)
}
func (target *remoteKeyTarget) retire() error { return target.ledger.Retire() }
// Failed cleanup keeps the connection and ledger available for explicit retry.
// Closing a connection alone is not XTEST key cleanup.
func (target *remoteKeyTarget) close() error {
if err := target.retire(); err != nil {
return err
}
if target.backend.display != nil {
C.XCloseDisplay(target.backend.display)
target.backend.display = nil
}
return nil
}
@@ -0,0 +1,286 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package xorg
import (
"context"
"errors"
"os"
"os/exec"
"runtime"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
)
func remoteNativeThread(t *testing.T) {
t.Helper()
if os.Getenv("CTHULHU_NATIVE_REMOTE_KEYS") != "1" {
t.Skip("requires private native_remote_keys.py session")
}
runtime.LockOSThread()
t.Cleanup(runtime.UnlockOSThread)
remoteNativeLayout(t, "us")
t.Cleanup(func() { remoteNativeLayout(t, "us") })
}
func remoteNativeLayout(t *testing.T, layout string) {
t.Helper()
// Go cancels t.Context before cleanup; restoring the private keymap still
// needs its own bounded lifetime after that cancellation.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if output, err := exec.CommandContext(ctx, "setxkbmap", "-layout", layout).CombinedOutput(); err != nil {
t.Fatalf("private keymap update: %v: %s", err, output)
}
}
func remoteNativeState(t *testing.T, action string) {
t.Helper()
const script = `import ctypes as C, sys
x = C.CDLL('libX11.so.6')
signatures = {
'XOpenDisplay': ([C.c_char_p], C.c_void_p),
'XCloseDisplay': ([C.c_void_p], C.c_int),
'XSync': ([C.c_void_p, C.c_int], C.c_int),
'XStringToKeysym': ([C.c_char_p], C.c_ulong),
'XkbKeysymToModifiers': ([C.c_void_p, C.c_ulong], C.c_uint),
'XkbLockModifiers': ([C.c_void_p, C.c_uint, C.c_uint, C.c_uint], C.c_int),
'XkbLockGroup': ([C.c_void_p, C.c_uint, C.c_uint], C.c_int),
}
for name, (arguments, result) in signatures.items():
function = getattr(x, name)
function.argtypes, function.restype = arguments, result
display = x.XOpenDisplay(None)
assert display
try:
action = sys.argv[1]
if action.startswith('group-'):
assert x.XkbLockGroup(display, 0x100, int(action[-1]))
else:
mask = x.XkbKeysymToModifiers(display, x.XStringToKeysym(b'Num_Lock'))
assert mask
assert x.XkbLockModifiers(display, 0x100, mask, mask if action == 'num-on' else 0)
x.XSync(display, False)
finally:
x.XCloseDisplay(display)
`
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if output, err := exec.CommandContext(ctx, "python3", "-c", script, action).CombinedOutput(); err != nil {
t.Fatalf("private XKB state update: %v: %s", err, output)
}
}
func remoteNativeTarget(t *testing.T) *remoteKeyTarget {
t.Helper()
target, err := openRemoteKeyTarget()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := target.close(); err != nil {
t.Error(err)
}
})
return target
}
func remoteNativeHeld(t *testing.T, probe *remoteKeyTarget, code uint16, want bool) {
t.Helper()
if held, err := probe.backend.Held(code); err != nil || held != want {
t.Fatalf("native held state = %v, %v; want %v", held, err, want)
}
}
func TestNativeRemoteKeyTargetOwnedCleanup(t *testing.T) {
remoteNativeThread(t)
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
codes := map[string]uint16{}
for _, name := range []string{"Shift_L", "Control_R", "a"} {
code, err := target.backend.Resolve(name)
if err != nil {
t.Fatal(err)
}
codes[name] = code
if err := target.apply(name, true); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, code, true)
if err := target.apply(name, true); err != nil {
t.Fatal("duplicate down lost ownership", err)
}
}
if err := target.apply("a", false); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, codes["a"], false)
if err := target.retire(); err != nil {
t.Fatal(err)
}
for _, code := range codes {
remoteNativeHeld(t, probe, code, false)
}
if err := target.apply("a", true); !errors.Is(err, input.ErrRemoteKeyState) {
t.Fatal("retired native target resumed")
}
if err := target.close(); err != nil {
t.Fatal(err)
}
if _, err := target.backend.Resolve("a"); !errors.Is(err, input.ErrRemoteKeyUnavailable) {
t.Fatal("closed backend admitted a new lookup")
}
}
func TestNativeRemoteKeyTargetPreservesUnownedKey(t *testing.T) {
remoteNativeThread(t)
other, target, probe := remoteNativeTarget(t), remoteNativeTarget(t), remoteNativeTarget(t)
shift, err := other.backend.Resolve("Shift_L")
if err != nil {
t.Fatal(err)
}
control, err := target.backend.Resolve("Control_R")
if err != nil {
t.Fatal(err)
}
if err := other.apply("Shift_L", true); err != nil {
t.Fatal(err)
}
if err := target.apply("Shift_L", false); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, shift, true)
if err := target.apply("Control_R", true); err != nil {
t.Fatal(err)
}
if err := target.apply("Shift_L", true); !errors.Is(err, input.ErrRemoteKeyCollision) {
t.Fatal("already-held target was adopted", err)
}
if err := target.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, control, false)
remoteNativeHeld(t, probe, shift, true)
if err := other.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, shift, false)
}
func TestNativeRemoteKeyTargetLayoutChangeKeepsRelease(t *testing.T) {
remoteNativeThread(t)
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
before, err := target.backend.Resolve("y")
if err != nil {
t.Fatal(err)
}
if err := target.apply("y", true); err != nil {
t.Fatal(err)
}
remoteNativeLayout(t, "de")
after, err := target.backend.Resolve("y")
if err != nil || before == after {
t.Fatal("fresh XKB snapshot did not observe changed destination layout", err)
}
if err := target.apply("y", false); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, before, false)
remoteNativeHeld(t, probe, after, false)
if err := target.apply("y", true); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, after, true)
if err := target.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, after, false)
}
func TestNativeRemoteKeyTargetRejectsUnavailableAndLocks(t *testing.T) {
for _, name := range []string{"unknown private name", "Caps_Lock", "Num_Lock", "Scroll_Lock", "semicolon"} {
t.Run(name, func(t *testing.T) {
remoteNativeThread(t)
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
shift, err := target.backend.Resolve("Shift_L")
if err != nil {
t.Fatal(err)
}
if err := target.apply("Shift_L", true); err != nil {
t.Fatal(err)
}
if name == "semicolon" {
remoteNativeLayout(t, "de") // Semicolon needs Shift on this layout.
}
if err := target.apply(name, true); !errors.Is(err, input.ErrRemoteKeyUnavailable) {
t.Fatal("unsupported key was admitted", err)
}
if err := target.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, shift, false)
})
}
}
func TestNativeRemoteKeyTargetNumLockKeepsRelease(t *testing.T) {
remoteNativeThread(t)
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
remoteNativeState(t, "num-off")
t.Cleanup(func() { remoteNativeState(t, "num-off") })
code, err := target.backend.Resolve("KP_Insert")
if err != nil {
t.Fatal(err)
}
if _, err := target.backend.Resolve("KP_0"); !errors.Is(err, input.ErrRemoteKeyUnavailable) {
t.Fatal("resolver guessed a keypad number with Num Lock off")
}
if err := target.apply("KP_Insert", true); err != nil {
t.Fatal(err)
}
remoteNativeState(t, "num-on")
current, err := target.backend.Resolve("KP_0")
if err != nil || current != code {
t.Fatal("fresh resolver did not preserve keypad identity across Num Lock", err)
}
if _, err := target.backend.Resolve("KP_Insert"); !errors.Is(err, input.ErrRemoteKeyUnavailable) {
t.Fatal("resolver guessed keypad navigation with Num Lock on")
}
if err := target.apply("KP_Insert", false); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, code, false)
if err := target.apply("KP_0", true); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, code, true)
if err := target.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, code, false)
}
func TestNativeRemoteKeyTargetRejectsInactiveGroup(t *testing.T) {
remoteNativeThread(t)
target, probe := remoteNativeTarget(t), remoteNativeTarget(t)
shift, err := target.backend.Resolve("Shift_L")
if err != nil {
t.Fatal(err)
}
if err := target.apply("Shift_L", true); err != nil {
t.Fatal(err)
}
remoteNativeLayout(t, "us,de")
remoteNativeState(t, "group-1")
t.Cleanup(func() { remoteNativeState(t, "group-0") })
if err := target.apply("a", true); !errors.Is(err, input.ErrRemoteKeyUnavailable) {
t.Fatal("unimplemented group policy admitted a key", err)
}
if err := target.retire(); err != nil {
t.Fatal(err)
}
remoteNativeHeld(t, probe, shift, false)
}