Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/53-viewtop-audit-open.md
Fimeg f5122cb59d docs: retire the task queue's dead branches
Twelve completed or superseded task records leave the live queue; the byte-identical rename duplicate is removed.\n\nThe authority and somatic source records now point at SAF, while tasks 33, 48, 49, 53, 60, and 76 say what is actually left.
2026-08-17 12:27:53 -04:00

16 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.

Follow-up faults measured 2026-08-16

The original ten findings below remain closed. Hardware use exposed the next compositor batch, and this task owns the defects so the dated handoff can return to archive:

  • transition.rs clamps shift_to at 1.0, leaving the shell's resistance past the overview detent inert. Raise the compositor ceiling to the agreed 1.22 and keep its clamp test honest.
  • control.rs serves one request and closes. A persistent client needs a loop to EOF and one BufReader; constructing a reader per line can discard bytes it already read past the newline.
  • Firefox is configured at 540 logical pixels and draws 504 while kitty obeys exactly. Instrument the configure/commit geometry; do not infer the missing strip from shell layout.
  • Firefox and Bitwarden popup menus render but take no taps. Position arithmetic has been checked against smithay; the next evidence is an instrumented build logging which surface client_surface_under selects.
  • in_layer stops at the top layer whose bounding box contains the point. If that surface's input region rejects, it returns None instead of trying the next layer down. A transparent overlay can therefore consume a tap without receiving it.

The package-owned shell and wrapper-target cleanup remain TASK-76. Motion architecture and the measured lossy transport live in souveraine/saf/surface/01-motion.md.

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 single path.exists(), and both compositor callers (evidence::Reporter::spawn, spawn_heartbeat) took NotFound as 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, and observe_authority was 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 own sensor_health.touch: unknown. Fixed in 6c9614c — poll every 500 ms until the socket appears, loud on the way in and out; NoRuntimeDir still fails at once, because that is not a late daemon. touch: live now.

  • delivered was a lie. touch_down on a locked session answered {"delivered": true} while resolve had withheld the contact and nothing had been sent to anyone. The field was is_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 in bed1ed6, re-measured delivered: false.

  • A drift warning that fired on the normal path. prune_workspaces derives 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's new_with_filter, the wlr one through GlobalDispatch:: can_view with the seat uid carried in the global's data (ListGate), because can_view is an associated function with no self and 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_toplevel runs on every focus change, and while smithay's send_title/send_app_id drop an unchanged value themselves, send_done does not — and done is what a client relayouts on. So the change is detected first and done is 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 is Stopped, 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 are error!, a window that has not committed a buffer yet is debug! 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: asks Act::Compose like every other composition verb, and state reports borders so 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 retilekick_renderframe_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 new Privileged::WindowList filter. That was the deployment risk worth naming: 2026-08-03 closing five bind filters left the phone unable to lock.
  • state reports borders — the read-back §4 asked for.
  • a slot outside the namespace: invalid_argument, "slot 4294967295 is outside the agent's namespace of 64".
  • unplace end to end on a live window: was_floating: falseplacewas_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.sh green 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).