Files
voidkontrol/docs/research/void-linux.md
T

30 KiB

VoidKontrol: Void Linux runtime, access, and packaging research

Research date: 2026-09-21. Decision ticket: Establish the Void Linux runtime, permissions, and packaging requirements.

This is a researched platform contract and a set of recommendations, not an implemented or hardware-tested controller. The user's accepted boundary is the currently observed USB presentation first, with other modes admitted only after separate validation; firmware flashing and factory-reset UI remain outside v1. The supplied control specification owns protocol requirements. No packages were installed, services restarted, udev rules changed, or device reports exchanged during this research.

Findings that change the starting assumptions

  1. Hidraw support is already present on this host. The running kernel reports CONFIG_HIDRAW=y; the Swarm has /dev/hidraw4 and /dev/hidraw5, with interface 1 at the latter path. Both are currently 0600 root:root. The present barrier is access policy and protocol evidence, not a demonstrated need to replace the kernel. Node numbers are observations, never identifiers to hard-code. See the local evidence below and Linux hidraw documentation.
  2. CONFIG_HIDRAW=m is not a valid alternative. HIDRAW is a Boolean Kconfig option; hidraw.o is linked into the HID core, which itself can be a module. Consequently, failure of modprobe hidraw does not diagnose missing support. The supplied spec's earlier inference required a dated correction, now recorded in the device evidence. Linux Kconfig, HID Makefile
  3. Void-native does not require a permanently root daemon. A dedicated system account can receive access to the matching hidraw node and run under runit's chpst. This preserves the spec's privileged service boundary while restricting its privilege to device access and its private data directory. It is a design recommendation grounded in Void service packaging and chpst, not a claim that the daemon has been built.
  4. Distro availability and upstream buildability differ. WebKitGTK 4.1 and Qt 6 have relevant current Void templates, including musl handling. The current electron35 template is explicitly marked broken, even though the host's cached repository index lists an Electron binary. Noctalia installed here is locally packaged; the inspected upstream Void tree contains no Noctalia template. Package inventory, local evidence below.

Evidence and method

The required Context7 lookup resolved Void Linux to the official /void-linux/void-docs collection, then fetched runit service and session/seat management documentation. Those results were checked against complete relevant Void Handbook pages, the Void packaging manual, package source, Linux source, and upstream eudev/runit/polkit specifications. There was no Context7 quota failure or fallback to uncited recollection.

Package claims below refer to upstream void-linux/void-packages commit 954278b83979958bcfdff05f22cf699c9dc20346 retrieved on the research date. The recursive tree response was not truncated. This is a source snapshot, not a promise that every architecture's current binary mirror is synchronized.

Read-only local evidence, collected in the active session:

Check Observed result Implication
/etc/os-release, uname -a, ps -p 1 -o comm= Void; x86_64; Linux 7.2.6_1; PID 1 runit This machine can validate x86_64 glibc/runit, not other targets.
xbps-query -p pkgver glibc / installed package listing glibc-2.41_1 Do not describe this as a musl test.
zcat /proc/config.gz CONFIG_HIDRAW=y, CONFIG_HID=m, CONFIG_HID_GENERIC=m, CONFIG_USB_HID=m Hidraw is compiled into the loaded HID core.
lsusb -d 258a:010c; /sys/class/hidraw/*/device/uevent 258a:010c, BY Tech Kreo Swarm; hidraw4 interface 0; hidraw5 interface 1; hid-generic Normal kernel HID ownership is intact. No feature ioctl was performed.
ls -l /dev/hidraw4 /dev/hidraw5 Both crw------- root root, no ACL indicator The ordinary desktop user has no device access through these nodes.
udevadm info --query=property --name=/dev/hidraw5 Only DEVNAME, DEVPATH, MAJOR, MINOR, SUBSYSTEM Do not assume ID_VENDOR_ID or ID_USB_INTERFACE_NUM is already populated on this hidraw node.
udevadm info --attribute-walk --name=/dev/hidraw5 bInterfaceNumber=01 on the USB interface; VID/PID/product on the higher USB device A single rule combining all those ATTRS matches cannot work; use separate parent-matching stages.
loginctl show-session 2 Local, active, seat0, Wayland; LockedHint=no Active-session authorization can be tested here. This is not a seatd-only setup.
Selected environment XDG_CURRENT_DESKTOP=niri, XDG_RUNTIME_DIR=/run/user/1000, WAYLAND_DISPLAY=wayland-1 UI runs in a graphical user session; system daemon must not inherit these assumptions.
Installed package listing / /var/service/ runit 2.3.1_1, eudev 3.2.14_2, elogind 252.39_1, D-Bus 1.16.2_2, polkit 127_1; dbus and udevd enabled Elogind is operational despite no explicit elogind service symlink, consistent with D-Bus activation.
Installed UI packages GTK3 3.24.52_1, GTK4 4.22.4_1, Qt6 6.11.2_1, libwebkitgtk60-2.50.4_1, IBM Plex fonts GTK4 WebKit being installed does not satisfy a GTK3 WebKit 4.1 dependency.
Installed desktop packages niri 26.04_1, noctalia 5.1.0_1 Version-specific desktop integration remains a separate research decision.
xbps-query -p repository noctalia Local void-packages/hostdir/binpkgs repository This installation is not proof of an official Noctalia package.
xbps-query -Rs webkit / electron35 Cached index lists libwebkit2gtk41-2.50.4_1 and electron35-35.7.2_1 Index metadata only; no installation or fresh mirror synchronization was performed.

Service lifetime and privilege

Void uses runit; packaged service directories live in /etc/sv/, and linking a directory into /var/service/ enables immediate start and subsequent supervision. The service's executable run script must exec a foreground process. A check script reports readiness, finish runs after termination, and a log subservice can receive output. An administrative down file prevents automatic start. Void services

Recommended service contract:

  • Keep one system service, provisionally voidkontrold (the spec calls the responsibility swarm75d). The UI and CLI are ordinary users. The service runs as _voidkontrol:_voidkontrol, with no membership in input, video, wheel, or a general hardware group. chpst -u removes inherited supplementary groups. A root supervisor creating a runtime directory does not require the long-lived service process to be root. chpst
  • Have the run script create /run/voidkontrol with explicit owner and mode, then change credentials and exec the daemon. Permanent private state belongs under /var/lib/voidkontrol, created by packaging. This follows Void's distinction between service-created volatile directories and make_dirs for persistent directories. Void packaging manual
  • With D-Bus IPC, the package's system-bus policy should grant only the service account ownership of the selected bus name. Startup waits for/retries the system bus; do not add a second D-Bus process or depend on a user's session bus. With a Unix socket instead, use kernel-authenticated peer credentials and an equivalent per-call authorization check. The final IPC choice is a decision, not a requirement imposed by Void. Void sessions
  • Readiness means the daemon can serve a diagnostic/capability request even when the keyboard is unplugged. Device absence should be an application state, not a restart loop. A system service must work without niri, Noctalia, DISPLAY, or XDG_RUNTIME_DIR; runit's clean environment makes this separation particularly important. Void services
  • Handle SIGTERM: stop accepting mutations, bound the wait for an outstanding transaction, durably record any uncertain outcome, close the device and IPC, and exit. sv down sends TERM and CONT; sv stop waits by default for seven seconds, and force variants may kill on timeout. Do not rely on finish to restore hardware or guarantee completion after power loss. sv manual
  • Let runit restart a crashed daemon, but obey the spec's no-auto-write rule: an interrupted transaction becomes uncertain and requires fresh readback. Restart, hotplug, login, wake, and service upgrade must never replay saved frames or apply a profile automatically. This is a project invariant from the supplied control spec, not distro behavior.
  • Use bounded logs with an explicit logging setup. Void installs no syslog daemon by default, while the packaging vsv helper creates a vlogger subservice by default. Decide between that documented syslog prerequisite and an explicit svlogd log subservice; do not silently depend on journald. Logs should contain transaction metadata/errors, not keystrokes, macro contents, complete backups, or raw transport buffers. Void logging, vsv documentation

Device discovery and narrow udev access

Linux recommends discovering hidraw nodes through libudev instead of assuming a stable /dev/hidrawN; hidraw feature-report ioctls use the control endpoint. For the spec's report ID 6, a 519-byte payload requires a 520-byte ioctl buffer, whose first byte is report ID 6. This is a framing distinction, not permission to send the currently unverified command payloads. Plain hidraw read() receives interrupt input reports, and plain write() is not a substitute for the specified feature-report exchange. Linux hidraw

Required project invariants remain: no input-device grabs, no kernel HID detach/unbind, no access to the unrelated 5-byte feature report, no arbitrary raw-report RPC, and validation of VID/PID, product, interface, usage collection, report IDs/sizes, and authenticated device identity before writes. A udev rule can reduce access, but cannot replace that protocol gate or physical-layout validation. Interface 1 also contains keyboard input reports; daemon code must not turn device access into a general key-event collector.

Recommended access model: give the service account access to only the matching interface and mediate all users through IPC. Do not grant ordinary users membership in the service account's group or install MODE="0666". Do not add TAG+="uaccess" for this controller: elogind processes that tag to grant active-user device access, which would let a GUI or unrelated user process bypass the policy-owning daemon. Existing administrator-added device ACLs/rules must be diagnosed; this package cannot prevent root from opening the device. elogind seat rule, eudev rule language

The following is a candidate rule design for later isolated testing, not a rule installed or verified with udev on this host:

# Example package path: /usr/lib/udev/rules.d/70-voidkontrol.rules
SUBSYSTEM!="hidraw", GOTO="voidkontrol_end"
ACTION=="remove", GOTO="voidkontrol_end"
ENV{.VOIDKONTROL_MATCH}=""
ATTRS{idVendor}=="258a", ATTRS{idProduct}=="010c", ATTRS{product}=="Kreo Swarm", ENV{.VOIDKONTROL_MATCH}="1"
ENV{.VOIDKONTROL_MATCH}=="1", ATTRS{bInterfaceNumber}=="01", GROUP="_voidkontrol", MODE="0660"
LABEL="voidkontrol_end"

The first matching stage evaluates attributes on the USB device; the second evaluates the interface ancestor. Eudev requires all parent matches within a single rule to match the same ancestor. Dot-prefixed temporary properties are not stored in the database or exported. The rule intentionally does not assume ID_USB_INTERFACE_NUM is populated, since it is absent in this host's hidraw properties. eudev rule language, local attribute walk above.

A service-held lock prevents two VoidKontrol instances from controlling the same keyboard, but is not a claim of kernel-enforced exclusivity against root or another driver. Root/administrator access remains outside the ordinary-user isolation boundary. Reconnect must rediscover and revalidate the descriptor; USB topology and a hidraw index are not durable identity. The lack of a serial number makes multiple identical devices a separate identity decision.

Active-user policy, elogind, and seatd

Void distinguishes the system and per-session D-Bus buses. Elogind manages login sessions, device access, and power; it needs system D-Bus and may activate on demand. Seatd only manages seats. Seatd does not replace elogind's login session information, its uaccess processing, or XDG_RUNTIME_DIR creation. Void recommends elogind or turnstile for runtime-directory management and documents a seatd plus turnstile alternative. Void session management

Void's current polkit template explicitly builds with -Dsession_tracking=elogind. Polkit defines separate defaults for any caller, inactive local callers, and active local callers, and provides a system-bus unique-name subject. These are usable on Void without systemd. Void polkit template, polkit policy, polkit subject

Recommended v1 policy, pending the explicit security decision:

  • Use the actual authenticated IPC sender, never a UID/PID/username supplied in request JSON. With system D-Bus, authorize the caller's unique bus name. The UI, CLI, and Noctalia integration use the same policy.
  • Allow customization only from the active local session associated with the keyboard's seat; deny remote, inactive, and unidentifiable callers. Recheck authorization when a queued mutation begins, and invalidate it on session or seat change. Read-only diagnostics may be less restricted; configuration exports/macros contain user data and need separate privacy treatment.
  • If the product must deny writes while locked, check the session lock state explicitly and test the actual locker integration. Active=yes alone does not express the desired locked-screen policy. This report's host was unlocked; it did not validate lock-state propagation.
  • Do not use a silent "any local group member" fallback when elogind or polkit is unavailable. Report authorization unavailable, or implement an explicit administrator-approved alternate mode after a separate security decision. A group alone does not prove active seat ownership.

An elogind-based session is the smallest evidenced release baseline here. Supporting a seatd-only session with identical active-user guarantees is an additional feature requiring an independently specified authority; it is not an automatic consequence of running on Void. This baseline recommendation does not require replacing a working user's seat manager or altering the host.

Storage and XBPS lifecycle

The XDG specification separates config, data, state, cache, and runtime files. Defaults are ~/.config, ~/.local/share, ~/.local/state, and ~/.cache; runtime storage must be user-owned mode 0700, local, and tied to login lifetime. Only absolute XDG paths are valid. XDG Base Directory specification

Recommended ownership:

Data Location/owner recommendation Reason
UI preferences, selected theme $XDG_CONFIG_HOME/voidkontrol, desktop user User preferences follow XDG config semantics.
User-created named configurations/exports $XDG_DATA_HOME/voidkontrol or explicitly chosen export path, user Portable user data; writes performed by the client.
UI session/history state $XDG_STATE_HOME/voidkontrol, user Persisted application state, independent of device transactions.
Service backup and pending-transaction journal /var/lib/voidkontrol, _voidkontrol, private The service must not depend on a logged-in user's environment or accept an arbitrary root-write path over IPC.
Service locks/socket, if used /run/voidkontrol, explicit private/service policy Recreated each service start; no backups here.
User-only IPC/instance state, if needed $XDG_RUNTIME_DIR/voidkontrol, user, private Session lifetime and ownership required by XDG.

Import validates a bounded document supplied by the client; export returns data to the authorized client, which writes the selected user path. Do not make the service open arbitrary paths supplied by an unprivileged caller. Restore must validate schema, device/firmware compatibility, and capability bounds, preserve a pre-change backup, then use the normal serialized/readback transaction path. User data must survive an uninstall; a separate explicitly requested purge can be considered later. These are proposed application rules supporting the supplied spec's backup/restore requirement.

For native packaging, provide an xbps-src template with release source and checksum, correct hostmakedepends/makedepends/runtime dependencies, licenses, desktop entry/icon, daemon/CLI, service directory, udev rule, and chosen IPC policy. Void templates are Bash descriptions compiled into XBPS packages; vsv installs a service directory and supervision links, not the running /var/service enablement link. Use system_accounts="_voidkontrol" with a dynamically allocated UID/GID, a non-login shell, make_dirs for persistent directories, and conf_files for administrator configuration. New system accounts must use an underscore prefix. Void packaging manual

The native build path is a separate xbps-src masterdir. The upstream README documents ./xbps-src pkg <name> and same-architecture musl builds via ./xbps-src -A x86_64-musl pkg <name>. Build as an ordinary user with the documented chroot method. Pin source and application dependency lockfiles; record the package tree revision and resolved dependency versions. Do not claim bit-for-bit reproducibility merely because a template builds. void-packages README, Cargo build style

Lifecycle requirements and native behavior:

  • Install must not implicitly enable hardware writes, start a GUI as root, enroll all users in a group, or change desktop configuration. Document the separate service-enable step. Verify account and rule availability before starting the daemon; bootstrap the current attached device permissions by a controlled replug or targeted administrator action during installation QA.
  • XBPS does not automatically restart updated services. Preserve that behavior and document the required controlled daemon restart; negotiate IPC version compatibility so a newly installed UI can explain an old running service. Void XBPS handbook
  • Package removal needs an explicit tested service-stop/disable procedure, including in-flight transaction handling and stale enablement links. INSTALL/REMOVE scripts must distinguish real removal from upgrade and work with alternate installation roots. Avoid unrelated host mutations or deleting backups. Void packaging manual
  • Void's system-account trigger disables rather than deletes the service account on real removal and does not do that for upgrades. The mkdirs trigger uses rmdir, not recursive deletion, for package-created directories. Preserve that non-destructive model. Account trigger, mkdirs documentation
  • A locally built XBPS repository can be used for initial distribution; a remotely shared repository and packages must be signed. Official Void repository acceptance is a separate process, not required for being a native XBPS package. Void repositories, signing documentation

UI runtime and libc options

Void officially supports musl as well as glibc, but software portability and binary availability vary; i686 musl binaries and musl multilib are not offered. Source-level compatibility is not release qualification. Void musl handbook

Candidate component Current source evidence Consequence for VoidKontrol
Rust daemon rust/cargo 1.98.0 templates, explicit musl handling; Void has a Cargo build style Viable native backend candidate; build dependencies must come from the selected target toolchain. This is not a language decision. Rust, Cargo, build style
Direct hidraw or HIDAPI hidraw backend hidapi 0.14.0 template uses eudev/libusb build dependencies If HIDAPI is selected, choose its hidraw backend explicitly and prove no kernel detach path exists. Direct Linux ioctls are also a viable small implementation. HIDAPI package, Linux API
GTK3/WebKitGTK 4.1 web UI host gtk+3 3.24.52 and libwebkit2gtk41 2.50.4; libwebkit2gtk41-devel exports webkitgtk-4.1 headers/pkg-config; Wayland enabled Available foundation for the Carbon researcher's Tauri option. GTK4's libwebkitgtk60 is a different ABI, not a substitute. GTK3, WebKit
WebKit on musl x86_64-musl/aarch64-musl target patterns enable JIT; sampling profiler is glibc-only; bubblewrap and xdg-dbus-proxy enabled by default Musl is not ruled out. Validate the chosen wrapper/build, webview sandbox, accessibility, portal dialogs, and rendered Carbon components on actual Void musl before promising support. Do not disable the webview sandbox to pass tests. WebKit
Qt 6 native UI qt6-base 6.11.2, Wayland enabled; separate qt6-wayland-client; musl-aware checks; declarative/QML packages present Technically viable native UI, but Carbon widget fidelity/accessibility implementation cost belongs to the design decision. Qt base
Qt 6 WebEngine host Built from qt6-pdf 6.11.2; musl compatibility dependency; Widevine disabled on musl; word-size/architecture restrictions Technically possible web host, but Chromium build/runtime surface is substantially larger; do not assume every Void architecture. Qt WebEngine source
Electron electron35 35.7.2 permits x86_64*/aarch64*, includes musl compatibility, but explicitly broken with Python >=3.14 Existing binary availability is not a reproducible native build path. Resolve packaging and maintenance before choosing it for a native Void v1. Electron template
niri and Noctalia Upstream niri 26.04 template enables dbus,xdp-gnome-screencast, no systemd feature; Noctalia absent from this source snapshot, locally packaged here Do not hard-require systemd activation or claim Noctalia is in official Void repositories. Version the supported integration separately. niri template, source snapshot, local evidence above

Recommended release-matrix decision: qualify x86_64 glibc + runit + eudev + elogind + system D-Bus + the accepted niri/Noctalia versions first, because that is the actual available hardware environment. Keep musl support architecturally possible and add an x86_64-musl build/smoke lane early. Whether musl must also block the v1 release is a user-facing support promise still to settle, not a fact inferred from "Void Linux." Other CPU architectures likewise require an explicit matrix and evidence; none was tested here.

Requirements and acceptance matrix

"Required" below means required by the supplied spec/user boundary or necessary to support the selected native platform contract. "Recommended" marks a design choice that the decision map must adopt or replace explicitly.

Area Requirement or recommendation Smallest sufficient release proof Current status
Kernel Required: effective HIDRAW support, intact kernel typing Running config and matched descriptor; normal typing/media/LED behavior before/after control Config/nodes confirmed; no control test performed
Device access Required: service can open only intended interface under ordinary service credentials Install rule in disposable setup; interface 1 accessible, interface 0/unrelated devices denied; replug and node renumbering Two-stage candidate researched, not installed/tested
Ownership Required: one controller transaction at a time Concurrent UI/CLI clients and attempted second daemon; no interleaving Not implemented
Authorization Recommended: active local caller/seat policy, fail closed Active, inactive, remote, locked, multi-seat, denied, and missing authority cases; repeat with queued requests Elogind active session observed only
Service lifecycle Required: runit foreground process and bounded shutdown; no automatic writes sv start/stop/restart; crash in each transaction phase; no-device boot; reconnect/resume Native requirements researched
Backup/restore Required: durable complete backup before first write; no uninstall data loss Interrupted write/restart followed by validated explicit restore; install/upgrade/remove/reinstall Awaiting protocol and implementation
GUI native behavior Required for promised matrix: Wayland launch as user, no root dialog loop Clean target installation, app launcher, scaling/input/accessibility, portal file dialogs, service-unavailable state Runtime packages inventoried only
Packaging Required: self-contained XBPS dependency declaration and reproducible recipe Clean xbps-src build; clean-machine install; upgrade/old-daemon handshake; removal preserving data No application/template exists yet
glibc Recommended primary release target Build/package/UI/service tests on x86_64 glibc and real keyboard Host available
musl Decide release promise; never label supported from compilation alone Separate musl package build plus real graphical/service/control tests Source plausibility established; no musl environment tested
Noctalia Required integration for accepted version, independent daemon Locally reproducible package/runtime provenance and versioned client integration Locally packaged version observed; desktop researcher owns integration

Remaining decisions and implementation evidence

  • Adopt the primary glibc/elogind matrix or make musl and/or seatd-only support release blockers. The research identifies consequences; it cannot choose this support promise on behalf of the user.
  • Choose system D-Bus with existing Void polkit/elogind versus a Unix socket with equivalent session authorization. Define locked-screen, multiple-session, same-seat, and administrator override policy explicitly.
  • Choose the UI runtime after the Carbon and desktop findings are combined. Package source evidence supports several options; no GUI stack was built.
  • Prove the proposed udev rule against real eudev events in an isolated test setup, and specify safe package install/update/remove handling. Read-only inspection was deliberately insufficient to claim that operational proof.
  • Set durable transaction/backup format and compatibility/recovery rules; decide identity behavior for identical no-serial devices. The protocol capture and authenticated-device evidence remain prerequisites for writes.

{"content_type":"terminal","tokens_before":7229,"tokens_after":7229,"token_count_basis":"o200k_base","ratio":0,"basis":"inferred"}