Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/53-viewtop-audit-open.md
Fimeg 1d9f62974e task-53: mark the tiled Firefox fix pushed
3431223 supplies tiled edges and closes the 504-of-540 draw; 41f07b1 removes 3,034 false touch-frame warnings.\n\nBoth are on ViewTop main. Package and glass proof remain separate.
2026-08-17 12:29:30 -04:00

293 lines
16 KiB
Markdown

# 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.
- **Source-fixed and pushed in `3431223`, delivery proof pending:** Firefox was
configured at 540 logical pixels and drew 504 because the compositor supplied
size without tiled-edge state, so Firefox retained its CSD shadow. Viewtop now
publishes all four tiled edges for tiled windows and clears them on `place`;
that commit also retiles the remaining split when one window floats.
- 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.
Also source-fixed and pushed: `41f07b1` sends `wl_touch.frame` only when that
contact group reached a client. Shell-consumed contacts had produced 3,034
false warnings in one session and buried useful evidence beneath them.
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 `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 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: 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.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).