Report suspected vulnerabilities privately to the maintainer at kleinpanic@gmail.com. Please do not open a public issue for undisclosed security problems.
xrandrw is a local, single-user desktop daemon. It runs as the logged-in
user and talks to the local X server and (optionally) the local dwm window
manager. It is not a network service and grants no privileges. The trust
boundary that receives untrusted bytes is the dwm-ipc endpoint
(/tmp/dwm.sock), which is hardened in src/xrandrw/dwmipc.py (SEC-01): every
reply is bounded before allocation (MAX_REPLY_SIZE), every socket op is
timed with a single total wall-clock deadline (no unbounded block, no
slow-trickle hold), every parse/transport error is funnelled to the single
DwmIpcUnavailable type (feature silently degrades OFF), and the socket path
is pre-checked to be a current-uid-owned socket before connecting.
The following are deliberate design choices appropriate to the local single-user posture above, not defects:
-
subscribe()returns an un-lifecycle-managed socket by design, and currently has no caller at all.dwmipc.subscribe()performs the SUBSCRIBE handshake and returns the live, still-open socket to its caller. It does not read, loop, or close it; whoever calls it owns the EVENT-stream consumption loop and the close/cleanup.As shipped, nothing in xrandrw calls it. The relocation lifecycle landed in Phase 10 built on the existing RandR-driven watch loop plus request/response dwm-ipc calls, so the dwm EVENT stream is never subscribed to and no such socket is ever opened in normal operation.
subscribe()remains part of the client's public surface for external users of the module and for a possible future event-driven path. Its unmanaged-socket contract is therefore a documented property of an unused entry point rather than a live resource risk; it should be revisited if and when an in-tree caller is added. -
run_command/subscribereturn values are not shape-validated by design. Unlikeget_monitors/get_dwm_client(which run strict shape validators),run_command's decoded result and thesubscribehandshake are returned as-decoded. They remain bounded by two hard limits at the untrusted boundary: theMAX_REPLY_SIZEcap (enforced before any allocation) and JSON-only decoding (a non-JSON body raisesDwmIpcUnavailable). Callers that need a specific shape validate it themselves; adding a rigid schema here would couple the client to dwm command-result shapes that are Phase 10 concerns. -
Residual socket-path trust after the owner/type check.
dwmipcverifies/tmp/dwm.sockis a socket owned by the current uid (viaos.lstat+S_ISSOCK, rejecting symlinks) before connecting. This defeats a foreign user pre-placing a file or socket at the path. It does not defend against a same-uid process pre-binding the path (which could already act as the user directly) — that is out of scope for a single-user desktop where all the user's own processes are already inside the trust boundary. dwm creates the socket as the same user, so the normal case is unaffected.
The relocation lifecycle (src/xrandrw/relocate.py) is the only component that
mutates window state. It stays inside the same local single-user trust
boundary and reuses the hardened dwmipc client for every round-trip. The
following are deliberate, documented choices for this phase.
-
T-10-SC — Dependency posture (unchanged from Phase 4/8). Phase 10 adds no new third-party runtime dependency. Window control uses the already-audited
python-xlibseam (ownDisplayper call, never raises past the seam) and the Phase-8dwmipcclient (bounded reply size, single total wall-clock deadline, uid/type-checked socket path, all errors funnelled toDwmIpcUnavailable→ feature degrades OFF). No network surface is introduced; the trust boundary is still the local dwm-ipc endpoint. -
T-10-04-D — Synchronous in-loop restore (W2 tradeoff, now bounded). The restore cycle runs synchronously inside the single-threaded watch
select()loop rather than on a worker thread (the locked no-thread decision, W2). Its cost is bounded by a modest per-callipc_timeout(_RELOCATE_IPC_TIMEOUT, 0.25s, threaded into every dwm-ipc call includingcapture_windows) and then_monitors-bounded tagmon hop count, with the cycle duration logged (relocate_cycle_done). WR-01 further bounds worst-case shutdown latency: the cycle checks the watch-loopstop_evtat the head of the per-window loop and bails after the current window on SIGTERM, so a slow-but-connected dwm can no longer delay shutdown behind a full batch (nor mask it from the watchdog). -
_displacedeviction policy (WR-02) in place. Displaced-window records are swept on every steady-state settle: any record whose(pid, starttime)no longer resolves to a live process (dead process or reused PID, checked against/procwith no window control) is dropped (relocate_displaced_evict). This bounds the in-memory map so a permanently-removed output or an exited process cannot leak records forever in a long-lived daemon. -
Fullscreen state is captured but not restored (IN-03, known limitation). A displaced window's
is_fullscreenflag is recorded but intentionally not reapplied on restore. dwm supports_NET_WM_STATE_FULLSCREEN, but a headless-testable restore of that state could not be validated against the test doubles this phase, so it is omitted rather than shipped unverified. A window that was fullscreen on the removed output returns to its saved monitor/tag/floating-state/geometry but not its fullscreen flag. This is a functional gap, not a security risk; tracked for a follow-up phase.