Skip to content

Desktop proof: the compositor stack over #565 (not for merging) - #585

Draft
DevomB wants to merge 168 commits into
mainfrom
proof-compositor-stack
Draft

DevomB wants to merge 168 commits into
mainfrom
proof-compositor-stack

Conversation

@DevomB

@DevomB DevomB commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Not for merging. A desktop proof of the compositor stack over the first desktop proof (#565).

What it holds.

The merge's three conflicts.

One change no conflict showed. #566's chrome watcher reads MICREC=/run/kryptik-sound/control/microphone and asks the mixer only when that record changes. With the stack the mixer writes marks/microphone, so the watcher never saw a change and the microphone's window never opened. Here MICREC is marks/microphone. The stack's own merge of main carries the same change in #561.

Checked before pushing. Every required name in gui-test.sh's list (148) is reported by gui-check.sh, none twice. The dwl edits parse, and no conflict markers are left.

What the run should show. The stack's checks: dwl-execs-nothing, dwl-without-group, seat-is-dwls, dwl-confined-to-its-files (with the signal scope), desktop-memory-bound. The audit INFO of dwl's calls outside its list decides whether the filter refuses them next. Everything #565 shows should still pass.

Distro is dispatched on this branch only after #565's proof, and on the integrator's word.

DevomB added 30 commits October 14, 2026 23:01
… pages dwl faulted for a zone go with it

A page dwl reads before the zone writes it is charged to dwl, and a zone that
kept its descriptor kept that page after the pool was gone, which no bound on
live pools held. The proxy now keeps its own copy of each pool's descriptor and
punches the pool's range (fallocate, FALLOC_FL_PUNCH_HOLE) once the pool and
its buffers are deleted and no surface shows one. It refuses a pool it could
not punch (not open for writing, sealed against writes, not shared memory) and
seals each one it takes against a later write seal. The pools of a connection
that ended are punched a second later, once dwl has read the hang-up, and count
against the zone until then. The proxy's call list gains fstatfs and fallocate.
The pool tests use memfds, as clients do.
…y hold, so 32 clients still fit

The proxy keeps a copy of each wl_shm pool's descriptor to punch it, up to MAX_SHM_POOLS_PER_ZONE (256), and reserves them: under kryptikd's limit as it was, 32 clients' worth plus a few, it would have taken 30 clients. The limit grows by 256, and the descriptor test holds it to 32.
… two sessions still fit

The proxy now reserves 256 descriptors for the copies of its zone's pools; under the test's old limit of 300 one session fit, not two.
…le are punched by the last

pool_fd read the seals and only then added F_SEAL_SEAL, so a client
thread could add F_SEAL_FUTURE_WRITE between the two. Its mapping keeps
working under that seal, and the punch then fails with EPERM, leaving
the pages dwl faulted charged to dwl. The seal now comes first, which
makes the read final, and a failed read refuses the pool.

wl_shm.create_pool takes any descriptor, so two pools may share one
file, and punching the first to go cleared pages the other still shows.
Each pool now keeps its file's device and inode. One that goes while
another on its file remains hands that one its range and its charge,
and the last punches the widest. So a large pool released before a
small one on the same file stays counted until its pages are freed.

A punch that fails is logged through the main loop's rate-limited log.
The proxy's filter allows fstat, and fallocate only for a punch that
keeps the size.
…xt sharp at 1.5

At a fractional output scale a zone's client could only draw at the next
whole scale and have the compositor scale it down, which blurs text and
costs memory. The proxy now lets wp_fractional_scale_manager_v1 through,
and dwl's zone sockets offer it too: the compositor tells each window the
scale of its output in 120ths, and Firefox, which uses the protocol by
default, draws at that scale through wp_viewporter. It tells a zone only
its output's scale, which wl_output already gives it rounded up.

The proxy's tables are generated again with fractional-scale-v1.xml from
wayland-protocols 1.49, the others unchanged byte for byte. A proxy test
passes the request and the scale; wlprobe scale asks for it, and the
desktop suite's zone window hears it through its proxy.
…nstead of starting a second

With no remote, a second firefox in a zone whose browser is open only
says that Firefox is already running. dwl now has an action for the
browser key, focusorlaunch: it takes the zone's window whose app_id is
the proxy's kryptik.<zone>.firefox-default, matched with the zone of the
client's socket, views its first tag on its monitor when it is not shown
and gives it the keyboard, and starts the zone's Firefox only when there
is no such window. The claimed app_id is named once in dwl-config.h.

The menu keeps every launch, as a persistent zone's passphrase is asked
on its terminal. dwl's status names the zones whose browser has a window,
which the chrome records in browsers; when it names the zone, b writes
the zone's name to browser-front in the session's runtime directory
instead. dwl takes that request only from a regular file of the session
user's that no one else may write, in a directory of the user's that no
one else may write, removes it before acting, and only ever puts a window
in front. The focus record gains the window's app_id and the tags viewed.

The desktop suite presses the key from another tag: tag 3 is viewed,
Firefox has the keyboard, and no second Firefox starts. A request root
wrote is removed unheard, the user's puts Firefox in front, and with the
zone stopped the key starts Firefox.
… stamped it

The browser key matches that app_id; printed where Firefox has the
keyboard, it shows the value dwl-config.h should name whatever the later
checks find.
…nd the design names the case left

As the chrome writes it, so the check does not depend on the suite's own
umask. A press of the key or b while the zone's Firefox is starting,
before its window shows, still starts a second one. The app_id is the
program name GTK takes, which Firefox sets to its remoting name.
…s shown and has the keyboard

A zone file may say [desktop] hold_awake = true. It is off by default;
personal and work set it, untrusted sets it false beside its reason, and the
nic zone is refused it. kryptikd starts the zone's proxy with --hold-awake
from it, and only then does the proxy let zwp_idle_inhibit_manager_v1
through. The protocol tables gain idle-inhibit-unstable-v1.xml from
wayland-protocols 1.49, beside the six hashes they already name.

zone-colours.h carries the same setting for dwl, from the same files. dwl
offers the inhibitor on such a zone's socket alone, and counts one only from
such a zone, on a mapped window shown on a lit output, while a window of that
zone has the keyboard and the session is unlocked. A hold lasts four hours
from when it began or a key or button was last pressed. dwl's status names
the zone holding the screen awake, for the chrome. A keyboard focus change,
an output's power and the cap's timer each check the hold again.

Upstream dwl 4847f97 comes as patch 0003: a zone can now keep an inhibitor on
a surface whose window it unmaps, and the check reads that surface's pointer.

wlprobe gains an inhibit mode and an idle mode, and the desktop suite checks
that personal's window holds zone 0's idle timer off, that the hold ends when
a zone 0 window or the lock takes the keyboard, and that untrusted is offered
no inhibitor.
…own, and the design says when a hold begins again

Both were joined to the line before by a line continuation the edit did not mean. A hold that a lid close, a suspend or the lock ended counts again, with its four hours anew, once its window has the keyboard back after the unlock.
…unts, and the chrome names the holder

kryptik-lock --idle waited on ext-idle-notify-v1's input-only notification,
so no hold could keep the session unlocked. dwl now counts a hold only from
a zone whose file allows it, on a shown window with the keyboard, for four
hours from the last key or button press. So all three of the watcher's
timers wait on the notification that heeds inhibitors, and bind version 1,
which has it.

The chrome records dwl's holdawake line in the session's runtime directory,
as it records browsers, and the menu names the zone holding off the lock
and the screens going dark. The desktop suite checks the record during the
hold and after it ends, and that a 2 s idle lock started during the hold
is still waiting 6 s later.
…l counts

It still said the idle lock counted input alone, the protocol's version 2,
which this branch changed.
…ry zone window in the suite does

On tag 1 the chrome's zone 0 window has the keyboard, so a zone window maps behind it and gets none until the user picks it. The hold's block waits for the window to draw and has the host press Alt+j, which reaches it whether zone 0's window is there or not.
…er-focus-or-launch

# Conflicts:
#	build/guest-tests/gui-check.sh
#	tools/desktop/kryptik-chrome
#	tools/image/gui-test.sh
…ld-awake

# Conflicts:
#	compartments/kryptikd/src/zone.rs
#	compartments/kryptikd/src/zone/tests.rs
#	tools/desktop/wlprobe.c
…ake, beside the microphone's and the camera's holders

A hold's usual window is a video in front, which covers the chrome that would name the holder. The bar takes it from dwl's own count; the marks' look each second draws it again when the holder changes.
A zone name is at most 12 bytes, so the name, the microphone's, the camera's and the awake marks take 71 with the terminator; 64 cut the last.
dwl ran in the root cgroup with no limit, and a zone's pool pages that dwl reads before the zone
writes them are charged to dwl: a zone could make the compositor hold memory outside every limit.

The session's last step, kryptik-launch --desktop, asks the launch daemon to move it into
kryptik-desktop, then becomes dwl, so everything the desktop starts is inside. The group sits beside
kryptik/ rather than in it, under a memory.max of a quarter of the memory and 1 GiB at least.
The daemon moves only the asker, and refuses root, a pid another uid holds and any process in a
zone's or a proxy's cgroup. A desktop it cannot bound starts anyway, and the session log says so.

Checked by cgroup.rs's unit test, launch-desktop.py, serve.sh's S3h and gui-check's
desktop-memory-bound. launch-desktop.py checks that the launcher becomes the desktop in its own
process whatever the daemon answers.
wlprobe charge keep commits one unwritten 4 MiB buffer in untrusted, then, 4 s after it is drawn,
drops the buffer and its pool and unmaps the window while it keeps the pool's descriptor, as a zone
may. gui-check reads the desktop cgroup's memory.current before, while dwl shows the buffer, and
once the pool is gone, and prints the rises on a desktop-charge line: the measurement the design
asked for. It is what the proxy's punch will change: until the proxy punches the range, the pages
dwl faulted stay the desktop's.

tools/tests/desktop-tools.sh compiles wlprobe.c and kryptik-launch.c with the stage's hardening
flags and checks their usage, so a change to either is caught in CI before a Distro build.
A zone that outlives the desktop and keeps its pools' descriptors keeps their pages charged to the
desktop's group: they are shmem, which nothing reclaims without swap. The next login's dwl joined
that same group and could start at its bound. join_desktop now removes the group first. rmdir
succeeds on an unpopulated group whatever it still has charged, and its charges then count at the
root. EBUSY keeps a group an earlier desktop's processes still hold.
dwl-zone-borders.py makes about seventy exact-string edits to dwl 0.8, and CI checked only that they
apply as text: a C error in them showed hours into a Distro build, in stage 04.

tools/tests/dwl-compile.sh fetches the pinned wlroots, wayland-protocols and dwl, trying each source
as fetch-sources.sh does, and keeps each only with sources.lock's hash. It writes the two headers
meson would generate for wlroots and every protocol's server header, then applies
build/patches/dwl-0.8 and the zone-border edits in stage 04's order with its config. It compiles
dwl.c and util.c with dwl's own warning flags, -fsyntax-only, against the pinned headers. A
negative control, a member that does not exist, must fail. The compartment job installs the system
headers and runs it; without them it exits 77.
…dwl's filter refuses exec

The first part of the compositor's own confinement (docs/design/compositor-separation.md, step
three). Once the status command runs, dwl forks a spawner. The spawner closes everything but one
end of a socket, names itself dwl-spawner, is not dumpable and ends with dwl. It runs only the
commands in dwl-config.h's spawncmds, each as dwl's spawn did, sent to it by their index.

dwl then sets no-new-privileges and a seccomp filter that refuses execve, execveat, ptrace and
process_vm_writev, and kills calls of another architecture or ABI. A dwl made to run code reaches no
program but the table's, and cannot steer the spawner. gui-check's dwl-execs-nothing reads Seccomp: 2
and NoNewPrivs: 1 in dwl's status and finds its spawner child; every key the suite presses goes
through the spawner.
wlroots 0.19.3's version.h.in names WLR_VERSION_STR, _MAJOR, _MINOR and _MICRO with #mesondefine,
not @var@, which the placeholder check caught on its first run in CI.
Its second run in CI found the check working end to end, negative control included, and two errors
that were the host's: Kryptik's edits call wl_client_get_user_data and wl_client_set_user_data, which
libwayland 1.23 added, and Ubuntu 24.04's headers are 1.22. The script now fetches the pinned
wayland (1.26.0) as it does wlroots, writes its wayland-version.h and its core protocol's server
header, and puts its src/ before the host's include path.
DevomB added a commit that referenced this pull request Oct 16, 2026
DevomB added 12 commits October 16, 2026 08:18
… run sound-tools.sh

#570's tools/tests/sound-tools.sh compiles the sound's C programs against alsa-lib, and exits 77 without its headers, which CI's lint job installs. Acceptance runs the host suites with --strict, where a suite that did not run fails the item: the desktop proof 38909683518 failed build/host-suites on that alone (92 suites passed, sound-tools did not run). HOST_PACKAGES feeds the apt install only, no cache key.
dwl-compile.sh and dwl-headless.sh build wlroots and dwl from pinned sources with headers only CI's Compositor build job installs. Elsewhere they exit 77, which run-tests.sh --strict, as acceptance runs it, counts as a failure. They are left to that job, as the compositor and desktop-identity suites are left to theirs.
With the levels kept in /run/kryptik-sound/control, control holds more than the socket. The same change goes into the stack's own merge of main.
kryptik-launch returns once the zone runs, so waiting on the launch did not wait for the probe, which keeps its window 10 s. The socket checks after it then found untrusted still running and could not start their own, and failed with no list to read.
…rd back

Killing the background job ended the shell around su, not the probe under it, so the zone 0 window stayed in front with the keyboard and hold-ends-at-lock saw none come back. The window is ended by its command, as the suite's other probes are.
… any pages the proxy left

memory.current counts dwl's own allocations too, and #565's proof showed it rising after the pool went (+2024 KiB shown, +2748 KiB gone), which says nothing either way about the punch. The pool is shmem, so the desktop cgroup's shmem in memory.stat shows its pages alone; the proxy's log says if a punch failed.
DevomB added a commit that referenced this pull request Oct 16, 2026
It replaces main as this branch's source of #566 and #572.

- kryptik-sound.c: marks/'s HOLDER_PATH beside #572's LEVELS_PATH and
  KEPT_PATH, as rehearsed.
- wlprobe.c: the resolution recorded before, #546's charge probe beside
  the stack's.
- dwl-zone-borders.py: #565's desktop set laid over this branch the way
  #585's merge of the two (82ea9b4) laid it over the stack's top. The
  result differs from that merge by exactly the lines #562 to #584
  change above this branch.

kryptik-sound.c and wlprobe.c equal #585's 700731c.
DevomB added a commit that referenced this pull request Oct 16, 2026
…says control keeps the levels

#566's watcher reads MICREC each second. This branch moved the mixer's
record of the microphone's holder to /run/kryptik-sound/marks, so after
the merge the watcher read a file nothing wrote any more, and the
microphone window never opened. MICREC now names marks/microphone,
which anyone on the host reads. compositor-separation.md says control
keeps the socket and the levels, as #572 keeps them there.

Checked: MICREC's lines and the design's paragraph equal #585's
700731c byte for byte, as does kryptik-sound.c.
Back from tty2, dwl answers clients as soon as its seat is enabled again, and libinput reopens the keyboards through the seat a moment later. The single Alt+p the host sends on GT KEY-MENU-AFTER-VT could arrive in between and be lost, as in the compositor proof's run (menu windows: 1 before Alt+p, 1 after). The check now waits until dwl holds as many /dev/input/event descriptors as before the switch, says so when they do not come back, and names both counts if no menu opens.
DevomB added a commit that referenced this pull request Oct 16, 2026
… a keyboard added after it confines itself takes keys

wlroots 0.19.3 gives each keyboard its keymap in a file made by shm_open
under /dev/shm. dwl's Landlock rules refused that create, so a keyboard
libinput added after dwl confined itself had no keymap. wlroots then left
it out of dwl's keyboard group, and its keys reached nothing. This covers
the switch back from the administration console, a keyboard plugged in
after login, and one found again after a sleep.

The rule beneath /dev/shm grants what that path does: create, open for
reading and writing, open again read-only, truncate and unlink.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants