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

+8 -2
View File
@@ -35,8 +35,9 @@ allowlist matches the external mapper's canonical subset:
Aliases such as `Prior`, uppercase text, shifted character names, AltGr,
dead/IME keys, arbitrary Unicode and unrecognized names remain unsupported.
For exact case/spelling, see `ValidRemoteKey` in
`internal/tentaclewire/remote_key.go`. No implicit modifier transitions, text
For exact case/spelling, see `input.ValidRemoteKeyName` in
`internal/input/remote_key_names.go`, used by `ValidRemoteKey` in the wire/SDK
layer. No implicit modifier transitions, text
insertion or fallback commands are synthesized.
Go exposes `RemoteKey`, `RemoteKeyRequest`, `ValidRemoteKey` and
@@ -105,6 +106,11 @@ concurrent idempotent release, overflow and retirement/replacement. The external
bridge verifies that every canonical mapper result satisfies the public SDK
contract. None of these tests injects a desktop key.
An unconnected [destination-key prerequisite](testing/remote-key-target.md) now
retains native codes through layout changes and uncertain effects, with a
quarantined Xorg resolver/backend. It is not attached to this mailbox and supplies
no independent process/watchdog cleanup or physical collision guarantee.
No capture event API, production Xorg input owner, independent native cleanup
helper, core escape/bypass integration or installed Remote keyboard command is
provided. Physical-source approval, physical collision/recovery, audible speech,
+4
View File
@@ -2,6 +2,10 @@
This is an isolated prototype and a record of limitations, not a Remote input
implementation. No reader input behavior or installed executable changes.
A subsequent [Go destination-key prerequisite](remote-key-target.md) now supplies
stored-code ownership, retryable cleanup and a quarantined native resolver. It
does not inherit this Python prototype's independent EOF/watchdog behavior and
is not attached to production input.
The source baseline is `a47253d697c1aa44dea80646cad99a4fe163c7f1` on `testing`,
with the existing secret-entry work preserved. The branch was checked against
its upstream on 2026-11-03 before these tests were added.
+110
View File
@@ -0,0 +1,110 @@
# Quarantined Remote destination-key prerequisite
The reader has a destination-key ledger and an unconnected Xorg backend.
`internal/input/remote_key_ledger.go` owns at most 32 native destination codes;
`internal/platform/xorg/remote_keys.go` resolves and applies the supported
logical keys on a dedicated X connection. No production code constructs this
target, and no reader input owner is attached to `remote.key`. The Remote
command still discards relay keyboard traffic and offers no keyboard grant.
This is a native prerequisite, not a usable input service. It must remain
quarantined until independent helper cleanup, input provenance and physical
collision handling are established.
## Ownership and cleanup
One ledger belongs to one serial native owner and helper lifetime. Its caller
must validate the original lease immediately before each effect, retire on
cancellation, and finish cleanup before allowing replacement input. The ledger
itself neither issues consent nor checks lease epochs. It must not be shared
between leases or mutated concurrently.
A new logical down resolves one destination code and rejects invalid codes,
another owned name resolving to that code, or an already-held server key. It
reserves the code before attempting the down, because a native failure can leave
the outcome uncertain. Duplicate downs are idempotent and perform no additional
injection; native repeat fidelity remains unfinished. An unowned up performs
no native operation. An owned up uses its reserved code without resolving the
name again, including after layout/Num Lock changes.
Mapping failure, collision, capacity overflow or native failure stops admission.
`Retire` attempts every owned up in reverse down order. Successful ups are
removed; failed ups retain their exact code and original relative order for
explicit cleanup retry. Retirement never resumes admission. Backend errors
become constant local errors, without native diagnostic text or key names.
The native wrapper closes its connection only after successful ledger cleanup.
Failed cleanup leaves the connection and pending identities available for retry.
Opening replacement input must wait for confirmed old cleanup; a new consent
epoch does not make an old native release harmless. Closing an XTEST connection
or killing this process does not release held keys. The earlier
[Python feasibility harness](remote-input-feasibility.md) demonstrates that
limitation; its separate EOF/watchdog mechanism is not implemented by this Go
target.
## Native resolution and limits
The logical-key vocabulary now belongs to `input.ValidRemoteKeyName`; the wire
and Go SDK validators delegate to it, preserving the existing IPC schema and
Python conformance. Native input therefore uses the same bounded names as the
public SDK, without depending on the Tentacle wire package.
The backend fetches a fresh XKB map/state for each new resolution and requires
one unambiguous matching destination code. It supports group zero only;
nonzero groups are unavailable. Shift/Caps state is removed for logical base-key
resolution while Num Lock is retained. Shift-only punctuation, unavailable or
ambiguous symbols are rejected. It does not synthesize modifiers, assume a
Windows scan code equals an X code, or rewrite the global keymap. The private
fixture changes maps and lock state explicitly to exercise these cases.
See the [XKB library specification](https://xorg.freedesktop.org/archive/current/doc/libX11/XKB/xkblib.html)
for `XkbGetMap`, `XkbGetState` and `XkbTranslateKeyCode`.
Lock-key downs are unavailable until lock synchronization and reader modifier
handling have a defined policy. This includes Caps Lock even when a reader
configuration would otherwise use it as a modifier. AltGr, IME/dead keys,
general group/level handling and exact remote autorepeat remain unfinished.
The already-held-key check is not atomic with injection. A later physical or
other-client press of an owned code is not separately represented by the global
bitmap. This backend cannot preserve that press during cleanup. Fresh map reads
also do not freeze configuration changes by other clients. No physical-device,
concurrent-map-change or same-code collision guarantee is claimed.
All native wrapper methods require one serial caller on a locked OS thread.
Xlib calls and the error guard can block or terminate a process after X-server
failure. The future production target must run in an independent helper, with
bounded inherited-descriptor IPC, watchdog/EOF cleanup, exclusive ownership,
provenance, policy cancellation and explicit failure reporting. Do not attach
this synchronous wrapper to the reader event loop or directly to its mailbox.
## Private verification
From the reader checkout:
```sh
go test -race ./internal/input ./internal/tentaclewire -count=1
python3 -B tests/native_remote_keys.py
python3 -B tests/native_remote_input.py
```
The new harness requires Go, Python, Xvfb, private D-Bus and `setxkbmap`. It starts
a 24-bit Xvfb server before private D-Bus using `native_test_session.py`, with a
disposable runtime directory. It never uses the active reader, display or
accessibility bus. Missing dependencies fail the harness rather than count as
skipped acceptance. The native tests skip in ordinary unflagged Go suites;
only an actual harness run establishes their private results.
Unit tests cover stored-code releases, idempotent downs, unowned ups, alias and
preexisting collisions, constant errors, held-key limits, uncertain down/up
outcomes, partial cleanup and retry order. Six new native Go race tests verify
owned modifier cleanup, preservation of an already-held key from another
synthetic client, US/German map replacement, Num Lock changes, unsupported-group
and shift-only-symbol rejection, and lock-key refusal. They inspect server key
state, not application command delivery or hardware behavior.
The new native harness, existing ten-experiment/six-native-test feasibility
harness, input/wire/Xorg race suites, focused host keyboard/lifecycle race tests,
external Remote race suite, affected-package vet, a separate reader build,
the existing speech regression harness and Tentacles `make check` passed.
No full reader suite, installed-binary behavior,
audible speech, physical input/Braille or live NVDA/Orca acceptance was performed.
+145
View File
@@ -0,0 +1,145 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import "errors"
const RemoteHeldKeyLimit = 32
var (
ErrRemoteKeyState = errors.New("remote input owner is retired")
ErrRemoteKeyUnavailable = errors.New("remote input key is unavailable")
ErrRemoteKeyCollision = errors.New("remote input key ownership is ambiguous")
ErrRemoteKeyBackend = errors.New("remote input operation failed")
ErrRemoteKeyLimit = errors.New("remote input held-key limit exceeded")
)
// RemoteKeyBackend belongs to one serial native owner. Set can fail after an
// effect reached the server; the ledger therefore reserves before down and
// retains ownership until up succeeds. This interface supplies no consent,
// watchdog, physical-source tracking or independent crash recovery. Resolve
// returns an Xorg destination code in 8..255, never a VK or scan code.
type RemoteKeyBackend interface {
Resolve(name string) (uint16, error)
Held(code uint16) (bool, error)
Set(code uint16, pressed bool) error
}
// RemoteKeyLedger retains destination codes, not source keycodes or relay VKs.
// One instance belongs to one native helper lifetime; its caller must serialize
// all methods, validate the original lease at delivery and retire on revoke.
type RemoteKeyLedger struct {
backend RemoteKeyBackend
held map[string]uint16
order []string
stopped bool
}
func NewRemoteKeyLedger(backend RemoteKeyBackend) (*RemoteKeyLedger, error) {
if backend == nil {
return nil, ErrRemoteKeyUnavailable
}
return &RemoteKeyLedger{backend: backend, held: make(map[string]uint16)}, nil
}
func (ledger *RemoteKeyLedger) fail(err error) error {
ledger.stopped = true
return err
}
func (ledger *RemoteKeyLedger) Apply(name string, pressed bool) error {
if ledger == nil || ledger.stopped {
return ErrRemoteKeyState
}
if !ValidRemoteKeyName(name) {
return ledger.fail(ErrRemoteKeyUnavailable)
}
code, owned := ledger.held[name]
if owned {
if pressed {
// Idempotent duplicate downs. Native repeat synthesis/lock policy
// remain unavailable; do not generate extra transitions here.
return nil
}
if err := ledger.backend.Set(code, false); err != nil {
return ledger.fail(ErrRemoteKeyBackend)
}
delete(ledger.held, name)
for index, held := range ledger.order {
if held == name {
copy(ledger.order[index:], ledger.order[index+1:])
ledger.order[len(ledger.order)-1] = ""
ledger.order = ledger.order[:len(ledger.order)-1]
break
}
}
return nil
}
if !pressed {
return nil // An unowned up must not release a physical/local key.
}
if name == "Caps_Lock" || name == "Num_Lock" || name == "Scroll_Lock" {
// Lock synchronization and reader-modifier handling need their own
// native policy. The quarantined backend must not toggle local locks.
return ledger.fail(ErrRemoteKeyUnavailable)
}
if len(ledger.held) == RemoteHeldKeyLimit {
return ledger.fail(ErrRemoteKeyLimit)
}
code, err := ledger.backend.Resolve(name)
if err != nil || code < 8 || code > 255 {
return ledger.fail(ErrRemoteKeyUnavailable)
}
for _, other := range ledger.held {
if other == code {
return ledger.fail(ErrRemoteKeyCollision)
}
}
held, err := ledger.backend.Held(code)
if err != nil {
return ledger.fail(ErrRemoteKeyBackend)
}
if held {
return ledger.fail(ErrRemoteKeyCollision)
}
ledger.held[name] = code
ledger.order = append(ledger.order, name)
if err := ledger.backend.Set(code, true); err != nil {
return ledger.fail(ErrRemoteKeyBackend)
}
return nil
}
// Retire stops admission permanently and releases only reserved target codes,
// in reverse down order. Failed ups retain their identity for explicit cleanup
// retry; a successful repeat call has no effects. Cleanup must complete before
// destroying the native backend. Never resume a retired ledger.
func (ledger *RemoteKeyLedger) Retire() error {
if ledger == nil {
return ErrRemoteKeyState
}
ledger.stopped = true
failed := false
for index := len(ledger.order) - 1; index >= 0; index-- {
name := ledger.order[index]
if err := ledger.backend.Set(ledger.held[name], false); err != nil {
failed = true
} else {
delete(ledger.held, name)
}
}
remaining := ledger.order[:0]
for _, name := range ledger.order {
if _, owned := ledger.held[name]; owned {
remaining = append(remaining, name)
}
}
clear(ledger.order[len(remaining):])
ledger.order = remaining
if failed {
return ErrRemoteKeyBackend
}
return nil
}
+217
View File
@@ -0,0 +1,217 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import (
"errors"
"reflect"
"strings"
"testing"
)
type ledgerTransition struct {
code uint16
down bool
}
type ledgerBackend struct {
codes map[string]uint16
held map[uint16]bool
effects []ledgerTransition
resolves []string
resolveError error
heldError error
setError func(uint16, bool) error
}
func (backend *ledgerBackend) Resolve(name string) (uint16, error) {
backend.resolves = append(backend.resolves, name)
return backend.codes[name], backend.resolveError
}
func (backend *ledgerBackend) Held(code uint16) (bool, error) {
return backend.held[code], backend.heldError
}
func (backend *ledgerBackend) Set(code uint16, down bool) error {
backend.effects = append(backend.effects, ledgerTransition{code, down})
// Model a backend that applies an effect before reporting uncertainty.
backend.held[code] = down
if backend.setError != nil {
return backend.setError(code, down)
}
return nil
}
func ledgerFixture(t *testing.T) (*RemoteKeyLedger, *ledgerBackend) {
t.Helper()
backend := &ledgerBackend{codes: map[string]uint16{"a": 38, "q": 24, "Shift_L": 50, "Control_R": 105}, held: make(map[uint16]bool)}
ledger, err := NewRemoteKeyLedger(backend)
if err != nil {
t.Fatal(err)
}
return ledger, backend
}
func applyLedgerKey(t *testing.T, ledger *RemoteKeyLedger, name string, down bool) {
t.Helper()
if err := ledger.Apply(name, down); err != nil {
t.Fatal(err)
}
}
func TestRemoteKeyLedgerRetainsDestinationThroughLayoutChange(t *testing.T) {
ledger, backend := ledgerFixture(t)
applyLedgerKey(t, ledger, "a", true)
backend.codes["a"] = 24
applyLedgerKey(t, ledger, "a", true) // Duplicate down is idempotent.
applyLedgerKey(t, ledger, "a", false)
applyLedgerKey(t, ledger, "a", false) // Unowned up has no native effect.
applyLedgerKey(t, ledger, "a", true)
if err := ledger.Retire(); err != nil {
t.Fatal(err)
}
want := []ledgerTransition{{38, true}, {38, false}, {24, true}, {24, false}}
if !reflect.DeepEqual(backend.effects, want) || !reflect.DeepEqual(backend.resolves, []string{"a", "a"}) {
t.Fatal("layout changes or duplicate transitions changed native ownership", backend.effects)
}
if len(ledger.held) != 0 || len(ledger.order) != 0 {
t.Fatal("successful retirement retained keys")
}
if err := ledger.Retire(); err != nil || len(backend.effects) != len(want) {
t.Fatal("successful retirement was not idempotent")
}
if !errors.Is(ledger.Apply("a", true), ErrRemoteKeyState) {
t.Fatal("retired ledger resumed")
}
}
func TestRemoteKeyLedgerRejectsCollisionsWithoutAdopting(t *testing.T) {
for _, collision := range []string{"preexisting", "alias"} {
t.Run(collision, func(t *testing.T) {
ledger, backend := ledgerFixture(t)
applyLedgerKey(t, ledger, "Control_R", true)
if collision == "preexisting" {
backend.held[38] = true
} else {
backend.codes["a"] = 105
}
if !errors.Is(ledger.Apply("a", true), ErrRemoteKeyCollision) {
t.Fatal("ambiguous key ownership accepted")
}
if err := ledger.Retire(); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(backend.effects, []ledgerTransition{{105, true}, {105, false}}) {
t.Fatal("cleanup adopted/released the rejected key", backend.effects)
}
if collision == "preexisting" && !backend.held[38] {
t.Fatal("cleanup released an unowned key")
}
})
}
}
func TestRemoteKeyLedgerFailedEffectsRetainCleanup(t *testing.T) {
for _, failingDown := range []bool{true, false} {
ledger, backend := ledgerFixture(t)
if !failingDown {
applyLedgerKey(t, ledger, "a", true)
}
backend.setError = func(uint16, bool) error { return errors.New("private native failure") }
if err := ledger.Apply("a", failingDown); !errors.Is(err, ErrRemoteKeyBackend) || strings.Contains(err.Error(), "private") {
t.Fatal("uncertain native operation did not fail with a constant error")
}
if ledger.held["a"] != 38 || !ledger.stopped {
t.Fatal("failed effect lost its reserved destination")
}
if !errors.Is(ledger.Retire(), ErrRemoteKeyBackend) || ledger.held["a"] != 38 {
t.Fatal("failed cleanup forgot its retryable key")
}
backend.codes["a"] = 99
backend.setError = nil
if err := ledger.Retire(); err != nil || len(ledger.held) != 0 || backend.held[38] {
t.Fatal("cleanup retry did not use the original destination", err)
}
if last := backend.effects[len(backend.effects)-1]; last != (ledgerTransition{38, false}) {
t.Fatal("cleanup retried a new mapping", last)
}
}
}
func TestRemoteKeyLedgerPartialCleanupPreservesOrder(t *testing.T) {
ledger, backend := ledgerFixture(t)
for _, name := range []string{"Shift_L", "Control_R", "a", "q"} {
applyLedgerKey(t, ledger, name, true)
}
backend.setError = func(code uint16, down bool) error {
if !down && (code == 105 || code == 24) {
return errors.New("fixture uncertainty")
}
return nil
}
if !errors.Is(ledger.Retire(), ErrRemoteKeyBackend) || !reflect.DeepEqual(ledger.order, []string{"Control_R", "q"}) {
t.Fatal("partial cleanup did not retain original retry order")
}
if !reflect.DeepEqual(backend.effects[4:], []ledgerTransition{{24, false}, {38, false}, {105, false}, {50, false}}) {
t.Fatal("cleanup did not attempt every owned key in reverse down order")
}
backend.setError = nil
if err := ledger.Retire(); err != nil || !reflect.DeepEqual(backend.effects[8:], []ledgerTransition{{24, false}, {105, false}}) {
t.Fatal("retry repeated successful cleanup or lost its order", err)
}
}
func TestRemoteKeyLedgerBoundsAndUnavailableKeys(t *testing.T) {
if _, err := NewRemoteKeyLedger(nil); !errors.Is(err, ErrRemoteKeyUnavailable) {
t.Fatal("accepted missing backend")
}
var absent *RemoteKeyLedger
if !errors.Is(absent.Apply("a", true), ErrRemoteKeyState) || !errors.Is(absent.Retire(), ErrRemoteKeyState) {
t.Fatal("nil ledger accepted work")
}
for _, name := range []string{"unknown private key", "Caps_Lock", "Num_Lock", "Scroll_Lock"} {
ledger, backend := ledgerFixture(t)
if err := ledger.Apply(name, true); !errors.Is(err, ErrRemoteKeyUnavailable) || len(backend.effects) != 0 || len(backend.resolves) != 0 {
t.Fatal("unsupported key or lock reached the native backend")
}
}
for _, code := range []uint16{0, 7, 256, 65535} {
ledger, backend := ledgerFixture(t)
backend.codes["a"] = code
if !errors.Is(ledger.Apply("a", true), ErrRemoteKeyUnavailable) || len(backend.effects) != 0 {
t.Fatal("invalid native code produced an effect")
}
}
ledger, backend := ledgerFixture(t)
for index := range RemoteHeldKeyLimit {
name := string(rune('a' + index))
if index >= 26 {
name = string(rune('0' + index - 26))
}
backend.codes[name] = uint16(8 + index)
applyLedgerKey(t, ledger, name, true)
}
backend.codes["6"] = 255
if !errors.Is(ledger.Apply("6", true), ErrRemoteKeyLimit) || len(backend.effects) != RemoteHeldKeyLimit {
t.Fatal("held-key overflow produced an extra effect")
}
if err := ledger.Retire(); err != nil || len(backend.effects) != 2*RemoteHeldKeyLimit {
t.Fatal("overflow lost bounded cleanup", err)
}
}
func TestRemoteKeyLedgerBackendFailuresArePrivate(t *testing.T) {
for _, operation := range []string{"resolve", "held"} {
ledger, backend := ledgerFixture(t)
failure := errors.New("private backend name and path")
if operation == "resolve" {
backend.resolveError = failure
} else {
backend.heldError = failure
}
if err := ledger.Apply("a", true); err == nil || strings.Contains(err.Error(), "private") || len(backend.effects) != 0 || len(ledger.held) != 0 {
t.Fatal("backend error leaked input or caused an effect")
}
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import "strconv"
// ValidRemoteKeyName defines the shared logical-key vocabulary. It grants no
// native capability and performs no layout/keycode lookup.
func ValidRemoteKeyName(name string) bool {
if len(name) == 1 && (name[0] >= 'a' && name[0] <= 'z' || name[0] >= '0' && name[0] <= '9') {
return true
}
if len(name) >= 2 && len(name) <= 3 && name[0] == 'F' {
number, err := strconv.Atoi(name[1:])
return err == nil && number >= 1 && number <= 24 && strconv.Itoa(number) == name[1:]
}
if len(name) == 4 && name[:3] == "KP_" && name[3] >= '0' && name[3] <= '9' {
return true
}
switch name {
case "BackSpace", "Tab", "Return", "Escape", "space", "Pause", "Print", "Menu",
"Caps_Lock", "Num_Lock", "Scroll_Lock", "Shift_L", "Shift_R", "Control_L", "Control_R",
"Alt_L", "Alt_R", "Super_L", "Super_R", "Page_Up", "Page_Down", "End", "Home",
"Left", "Up", "Right", "Down", "Insert", "Delete",
"KP_Begin", "KP_Enter", "KP_Page_Up", "KP_Page_Down", "KP_End", "KP_Home",
"KP_Left", "KP_Up", "KP_Right", "KP_Down", "KP_Insert", "KP_Delete",
"KP_Multiply", "KP_Add", "KP_Separator", "KP_Subtract", "KP_Decimal", "KP_Divide",
"semicolon", "equal", "comma", "minus", "period", "slash", "grave", "bracketleft",
"backslash", "bracketright", "apostrophe":
return true
}
return false
}
+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)
}
+3 -27
View File
@@ -6,7 +6,8 @@ package tentaclewire
import (
"encoding/json"
"strconv"
"git.stormux.org/storm/cthulhu/internal/input"
)
const RemoteKeyQueueSize = 256
@@ -25,32 +26,7 @@ type RemoteKeyRequest struct {
Key RemoteKey `json:"key"`
}
func ValidRemoteKey(key RemoteKey) bool {
name := key.Name
if len(name) == 1 && (name[0] >= 'a' && name[0] <= 'z' || name[0] >= '0' && name[0] <= '9') {
return true
}
if len(name) >= 2 && len(name) <= 3 && name[0] == 'F' {
number, err := strconv.Atoi(name[1:])
return err == nil && number >= 1 && number <= 24 && strconv.Itoa(number) == name[1:]
}
if len(name) == 4 && name[:3] == "KP_" && name[3] >= '0' && name[3] <= '9' {
return true
}
switch name {
case "BackSpace", "Tab", "Return", "Escape", "space", "Pause", "Print", "Menu",
"Caps_Lock", "Num_Lock", "Scroll_Lock", "Shift_L", "Shift_R", "Control_L", "Control_R",
"Alt_L", "Alt_R", "Super_L", "Super_R", "Page_Up", "Page_Down", "End", "Home",
"Left", "Up", "Right", "Down", "Insert", "Delete",
"KP_Begin", "KP_Enter", "KP_Page_Up", "KP_Page_Down", "KP_End", "KP_Home",
"KP_Left", "KP_Up", "KP_Right", "KP_Down", "KP_Insert", "KP_Delete",
"KP_Multiply", "KP_Add", "KP_Separator", "KP_Subtract", "KP_Decimal", "KP_Divide",
"semicolon", "equal", "comma", "minus", "period", "slash", "grave", "bracketleft",
"backslash", "bracketright", "apostrophe":
return true
}
return false
}
func ValidRemoteKey(key RemoteKey) bool { return input.ValidRemoteKeyName(key.Name) }
func ValidRemoteKeyRequest(request RemoteKeyRequest) bool {
return len(request.Lease) == 32 && request.Epoch != 0 && request.Sequence != 0 && ValidRemoteKey(request.Key)
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
# Copyright (c) 2026 Storm Dragon
# Authors: Storm Dragon, Angus Kola
# SPDX-License-Identifier: GPL-3.0-or-later
"""Exercise quarantined destination-key ownership on a private Xvfb server."""
import os
from pathlib import Path
import shutil
import sys
from native_test_session import run_private_session
def main():
for program in ("xvfb-run", "dbus-run-session", "setxkbmap", "go"):
if shutil.which(program) is None:
raise RuntimeError(f"Required private-test dependency is missing: {program}")
environment = dict(os.environ, CTHULHU_NATIVE_REMOTE_KEYS="1",
GOCACHE="/tmp/cthulhu-cache", GOPROXY="off")
command = ["xvfb-run", "-a", "-s", "-screen 0 1024x768x24", "dbus-run-session", "--",
"go", "test", "-race", "./internal/platform/xorg", "-count=1", "-timeout=60s",
"-run", "^TestNativeRemoteKeyTarget", "-v"]
return run_private_session(command, env=environment,
cwd=Path(__file__).resolve().parents[1], check=True).returncode
if __name__ == "__main__":
sys.exit(main())