14 KiB
TASK 53 — what an audit of the 2026-08-03 compositor work left open
Status: the ten findings are fixed, pushed, and on the phone as a package
(souveraine-viewtop 0.1.0.r74.g6c9614c20471-1 = viewtop 6c9614c, built on
archdev from main, pacman -U'd 2026-08-04, pacman -Qkk clean). It has
since survived a cold boot and phone-triage.sh is green on every check.
Created 2026-08-04 from an adversarial read of
ba4bd03..c98f4c8 and souveraine 266af98..914e48f. Repo:
souveraine-viewtop unless noted.
Three more defects the hardware found that four gates did not, which is the whole argument for this section existing. The third was found by re-checking after the work was already called done:
-
A missing sessiond was a verdict, not a wait — and it cost the phone its power button.
Sessiond::discover()is a singlepath.exists(), and both compositor callers (evidence::Reporter::spawn,spawn_heartbeat) tookNotFoundas final and returned. On the cold boot at 10:00:51 viewtop looked 0.46 s into its life and lost a sub-second race with sessiond's user unit. For the next half hour every button edge, gesture, wake, panel report and touch report went into a channel nobody read, andobserve_authoritywas never fed — doctrine §4's frozen-first-answer, which the 2026-08-02 session fixed and this re-entered by the front door. Ground truth: sessiond's ownsensor_health.touch: unknown. Fixed in6c9614c— poll every 500 ms until the socket appears, loud on the way in and out;NoRuntimeDirstill fails at once, because that is not a late daemon.touch: livenow. -
deliveredwas a lie.touch_downon a locked session answered{"delivered": true}whileresolvehad withheld the contact and nothing had been sent to anyone. The field wasis_down, and a withheld contact is still tracked on purpose — evidence is about the finger, not about where the event ended up. Holding a slot and reaching a client are two questions;delivers()is the second one. Fixed inbed1ed6, re-measureddelivered: false. -
A drift warning that fired on the normal path.
prune_workspacesderives membership from the surface table, so it dropped the closing window's own entry and reported "membership outlived its windows" on every window close. A signal that fires when nothing is wrong is not a signal — §4's rule pointed at itself. The ordinary close removes its own entry now and the sweep warns only on real drift (6446386).
Six findings from that audit were fixed the same day (the strip not moving, agent touch reaching the lock surface, the stale settle stranding the strip, the shared wake latch, workspace-blind focus, the missing workspace verb, per-window capture returning nothing). These were the rest.
The audit's own method note is the thing to keep: every one of these passed
all four gates. A wedged compositor reports shell=active with zero errors,
so "green" proves nothing about behaviour. The fixes below passed four gates and
264 tests too. Launch an app and read the log.
1. Two hit tests that disagree — FIXED
toggle_float and drag_window used self.space.element_under(point);
client_surface_under undoes the strip shift and the pose. So a three-finger
carry picked a different window than a touch did, whenever anything was posed or
the strip was off an integer — §11's second-decider shape in miniature, getting
louder as pose gets used.
Now windows_under() is the one hit test: it walks the space topmost-first,
applies window_origin (the strip) and unpose (the pose), and yields every
window under the point with the point in that window's own coordinates. An
iterator rather than one answer, because the two callers want different amounts
of it — the touch path walks past a window whose input region misses, the carry
takes the first.
window_origin is also what frame_elements reads, so the picture and the
touch cannot drift.
2. Agent contacts are unbounded and never cleaned up — FIXED
SLOTS_PER_ORIGIN = 64. Origin::slot() is fallible now and refuses anything
past it, so Router::active is bounded by construction rather than by a client
lifetime the control socket does not have (it is a line per connection). The
refusal surfaces at serve_intent as invalid_argument naming the bound.
That also kills the sharper half: Origin::Agent.slot() was
saturating_add(1 << 16), so u32::MAX stayed u32::MAX and reached
wl_touch's int id as -1 — the no-slot sentinel. Wrapping instead of
saturating would have been worse: her slot 64 would alias slot 0 and retarget a
contact somebody else holds.
Three tests pin it, including that the cap stays smaller than the gap between the two namespaces.
3. ext_foreign_toplevel_list_v1 had no bind filter, and its handles were empty — FIXED
Both halves, and they had to land together: filling the titles is what would have made the missing filter matter, since the list was mitigated only accidentally — by the titles never being set.
Privileged::WindowList, applied to both window lists: the standard one through smithay'snew_with_filter, the wlr one throughGlobalDispatch:: can_viewwith the seat uid carried in the global's data (ListGate), becausecan_viewis an associated function with noselfand no closure to capture. Same ceiling as the other four — any process of the session's user still binds — and TASK-41's grant is what narrows it.- Titles and app_ids on the standard handles, edge-triggered. Note the
earlier attempt (
0eda51e) was reverted for a protocol event storm:publish_toplevelruns on every focus change, and while smithay'ssend_title/send_app_iddrop an unchanged value themselves,send_donedoes not — anddoneis what a client relayouts on. So the change is detected first anddoneis sent only when something moved.
Still owed and NOT this task's: titles cross while the session is locked. That is a disclosure gate, asked at a different moment than a bind filter, and it is TASK-51 §5's.
4. Errors that are indistinguishable from nothing happening — FIXED
DEVICE-STATE-MACHINE.md §10's rule. The protocol's failure vocabulary is only
three words wide (Unknown, BufferConstraints, Stopped), so what the client
is told cannot always be specific — but what the log says can be.
capture_window_for: a window that closed isStopped, which is what it is. The other two are compositor bugs and warn with what did not match.copy_window_into/copy_output_into: every exit logs, at the severity it deserves — no renderer and no output areerror!, a window that has not committed a buffer yet isdebug!and not a fault.drag_window: three logged returns, and the axis latch (§7) is what stops a failed pick from letting the same gesture flip to strip mode later.fullscreen_request: refuses out loud, and only where it can (§6).toplevel_destroyed: see §5 and §10 below.border: asksAct::Composelike every other composition verb, andstatereportsbordersso a chain can read back what is in force.
5. focus_border ignored pose, and its fallback was the whole panel — FIXED
The border now follows the posed rectangle — the same arithmetic pose_origin
does, kept in logical pixels because the zone it clamps against is logical.
The fallback was the worse half. registry.output() is not a conservative
default, it is the largest possible wrong answer, and toplevel_destroyed
reached it every time: registry.remove then retile → kick_render →
frame_elements, all before space.refresh() had dropped the element. One
frame of full-zone grey slab, and a full-screen hit box on a dead window in the
same frame. Now window_extent() falls back to the window's own geometry, and
toplevel_destroyed unmaps the element first so "destroyed" is true for every
reader at once.
6. fullscreen_request arranged the scene on a client's say-so — FIXED, and the gate question was the wrong one
Both doors go through take_fullscreen_room / give_fullscreen_room_back now.
The disagreement the audit found was real and is closed: Ask::Fullscreen from
the dock minted no workspace, so whether your video got its own room depended on
which thing you tapped.
It does not ask client_may_arrange, and that is deliberate. That predicate
answers "may a client dictate its own geometry" — what move_request and
resize_request are — and it always says no. Fullscreen is not that: it is a
state the protocol defines and the compositor implements however it likes, and
refusing it would leave the client waiting on a configure that never comes. What
was genuinely missing is the lock: minting a room moves the view, which
changes what the operator finds when they authenticate, and a client does not
get to decide that from behind a lock surface. So disclosure_locked() refuses
it and says so.
7. Touch-cancel and a fourth finger left the drag latched — FIXED
abandon_drag() clears dragging, drag_axis, strip_drag and
strip_samples, and releases the strip at zero velocity rather than
snapping it — a cancel means the hand is gone, and the spring should take it
from where the hand left it. touch_cancel calls it: the recogniser forgetting
its contacts never moved the strip it was dragging, and those are two different
pieces of state.
The palm case is closed by matching fingers >= 3 on Drag and any count on
DragEnd. peak_fingers is max-concurrent, so a fourth contact renamed every
later event and neither arm matched — the strip stayed latched half-way with
nothing in flight to walk it home. A drag still only begins at exactly three.
And the axis is latched on both outcomes. It used to be inferred from
whether strip_drag was set, so a vertical drag that began over the wallpaper
latched nothing and a later event could flip it into scrolling the world. A
latch set on only one of two outcomes is not a latch. drag_axis_of() is a free
function with four tests; ties go to the window, as the more recoverable answer.
8. Doc comments that asserted the opposite of the code — FIXED
The three displaced by insertions (copy_output_into's doc on
session_is_toplevel, frame_elements' on focus_border, and the duplicated
#[cfg] in Viewtop::new) are back where they belong. The four that said false
things — the registry carrying "size, not position", the hit test "owing" the
inverse mapping, publish_toplevel not carrying the standard handles' strings
(now it does), and foreign_toplevel.rs's claim that the standard list is
deliberately not served — now describe the code.
WindowOverview.qml's header (souveraine) said "viewtop has no workspaces at
all", written the same night workspace.rs landed. Corrected, including what
is now true and undrawn: the strip is readable over the control socket and this
overview does not show it.
9. A floated window could not rejoin the layout — FIXED
unplace on the wire, in the verb table, and in serve_intent. place() still
pins a window out of the tiling permanently and on purpose — dragging a window
somewhere is saying where it should be — but the one-way door has a return now.
It answers was_floating, because idempotent success and a window that was
never floated are the same call and different facts.
It belongs on the wire rather than back on the gesture: it is the inverse of a
verb she already has (§13 makes geometry hers), and the binding that used to
carry toggle_float is sessiond's now.
10. Render churn — FIXED
kick_render moved out of retile_workspace and into retile, once. A strip
drag lays out two or three workspaces per motion event and each used to queue
its own frame.
Also here rather than in a section of its own: prune_workspaces() derives
membership from the surface table instead of maintaining it beside one. A
destroy that could not resolve an id used to leave the on_workspace entry
behind, and an entry behind is a room that can never look empty again —
one in the strip forever, holding nothing, costing a swipe every time you cross
it.
Acceptance
Verified on the phone 2026-08-04, session locked, over the control socket and with real clients:
- the session comes up and locks — lock granted and attested to
peer:7 (uid 1000), every layer surface mapped, and no refusal from the newPrivileged::WindowListfilter. That was the deployment risk worth naming: 2026-08-03 closing five bind filters left the phone unable to lock. statereportsborders— the read-back §4 asked for.- a slot outside the namespace:
invalid_argument, "slot 4294967295 is outside the agent's namespace of 64". unplaceend to end on a live window:was_floating: false→place→was_floating: true. §9's one-way door has a return.- two windows tile on one workspace, closing each leaves the strip at 1 with no
phantom room, and the log is clean — zero warnings that are not smithay's own
two (
drm master,mode property blob). - and it survived a cold boot:
phone-triage.shgreen on all six sections,Class=user,LockedHint=yes, lock-then-blank holding.
Method note for the next session: all of that was measured while locked,
over the control socket, with clients launched as
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 setsid kitty &. They
map and tile behind the lock surface, disclosing nothing, because the scene
stays lock-only. socat is not installed on the phone; a four-line python
AF_UNIX client is the way in.
Still owed, and it needs the PIN — the 40 is the user's, so an agent cannot open the session to check the rest:
- three-finger tap → the window action sheet through sessiond (rebound from the overview 2026-08-05, TASK-55 — it now carries the tapped window), and a three-finger carry;
- a window posed to 0.5 with the border around the window;
- fullscreen from the app and from the dock landing on the same workspace;
- a palm down mid-drag, and confirming the strip still settles.
Connects to
TASK-51 (viewtop → daily driver; §5's disclosure gate on the window list is the
half deliberately left there), TASK-50 (done; §1 and §5 here were its
consequences), TASK-41 (§3's bind filter is uid-only until the attested grant),
DEVICE-STATE-MACHINE.md §10 (§4 here is that rule),
SESSION-AUTHORITY-DOCTRINE.md §11 (§1 and §6).