Skip to content

Each monitor comes at the scale the user picks, a built-in high-resolution panel at 2 by itself - #488

Merged
DevomB merged 95 commits into
mainfrom
display-scale
Oct 15, 2026
Merged

DevomB merged 95 commits into
mainfrom
display-scale

Conversation

@DevomB

@DevomB DevomB commented Oct 13, 2026

Copy link
Copy Markdown
Owner

What was wrong: dwl-config.h's one monitor rule gives every output scale 1, so a laptop's high-resolution panel (a 14" 2560x1600 one is about 216 dpi) draws everything at half the size it should be, and nothing let the user change a monitor's scale or place. The laptop design's monitor check (zones see no make, model or serial, and a second monitor in every zone) had no guest round.

What changed (built as ee proposed: no dwl or proxy change):

  • tools/desktop/kryptik-display.c: a client of wlroots' output management (zwlr_output_manager_v1, v4), which dwl serves to zone 0 alone. With no argument it lists the monitors (name, scale, mode, place, make and model, size). apply makes the user's lines in ~/.config/kryptik/display (NAME SCALE [X Y], * for any monitor no line names), or the default: 2 for a built-in panel (eDP, LVDS, DSI) of 192 dpi and 1200 lines or more, as GNOME decides, else 1. Only built-in panels are measured, as a television's EDID may give its aspect ratio as its size. --watch does the same at the start and on every done, which is how the choices survive logins and hotplug. It changes a monitor only when it is not as chosen and asks the same thing once, so it never fights the compositor's rounding, and a monitor the idle lock turned off (dwl stops listing it until it is on again) is never turned on by it. Each configuration names every head, as the protocol requires; a cancelled one is asked again with the next serial.
  • The chrome runs kryptik-display --watch at session start. kryptik display lists, sets (NAME SCALE [X Y]) and resets (NAME auto), then makes the choice at once.
  • The desktop recipe builds it from the output-management XML in wlroots' pinned tarball (as the lock takes its XML from dwl's), checks it runs, and the step's stamp names the file and the wlroots version. image-programs.txt lists usr/bin/kryptik-display.
  • wlprobe outputs: binds every wl_output (v4 at most) and prints each event.
  • gui-test, around the second monitor: the user's file names the first monitor at 1 and * at 2, and the new monitor comes at scale 2 with the first at 1 (second-head-scale-chosen). Through a zone's proxy both outputs are named output-N, with no make or model, one at scale 2 (zone-sees-heads-unnamed).
  • Docs: the laptop design's monitors section, a Monitors paragraph in the guide, the known-limitations line (no built-in panel in QEMU), the status row, and the roadmap's laptop bullet (now all built).

How it was checked: tools/tests/display.sh (new, 24 passed here) covers what kryptik display keeps, replaces and refuses (scale range, names, places, * with a place, outside the session), and fails on the old command, which has no display. stage-inputs.sh, bash -n and sh -n pass. The C is built only in the Distro run's desktop step (no compiler on this laptop by rule), which also runs it for its usage line; its own check is the two gui rounds. The panel default waits for physical hardware.

Base: guide-laptop-checks (#485), the top of the laptop stack, which this shares files with; it retargets to main when the stack lands.

DevomB added 30 commits October 13, 2026 05:51
…or, in the zone that holds it

ADR-025 hands a zone a descriptor on the partition the user gave it, and
needs a reader there that parses the filesystem on that descriptor:
mtools, exfat-fuse and ntfs-3g take paths, and reopening a block device
by path is refused to the zone.

tools/stick is that reader, a crate of its own, since kryptikd keeps to
libc alone (ADR-010). It is fatfs 0.3.6, without chrono, behind
kryptik-stick: no arguments says what the zone was given, then list
[DIR], get PATH [DEST], put FILE [DIR]. The descriptor comes from the
zone's broker (`stick`, answered `ok ro|rw WHAT` with SCM_RIGHTS), or
--device FILE for an image. A read-only grant refuses every write
whatever fatfs asks. Nothing on either side is written over. Names from
the stick reach the terminal with control characters as `?`, and reach a
path only as one component. A tree nested past 32 directories is
refused. A put is synced to the device before it is reported. FAT's
dates come from the zone's clock as UTC, held to 1980-2107.

CI tests it and builds it for musl. The Distro workflow builds it beside
kryptikd, held to the same path check, and stage 04 installs it
(KRYPTIK_STICK_BIN, the stick step). The licence texts of fatfs,
bitflags, byteorder and log are in build/licences, and
check-rust-licences.sh reads its lock.
The stick reader parses a FAT filesystem in the zone, but nothing in zone 0
could hand it a stick: the design (removable-storage.md) has kryptikd make a
device-mapper linear mapping of the one partition the user picks and pass it
to the zone through its broker, never mounting it.

stick.rs decides which block devices may be given (a partition of a USB disk
or SD card, or such a disk with no partition table; held by nothing, mounted
nowhere, not on a disk the system's own mappings use), makes the mapping
read-only unless the user chose to write, records the grant under
/run/kryptik-stick, and on eject wipes the mapping's table so every read and
write on a descriptor the zone kept fails, then removes it once the last
closes. A zone file says `[storage] removable = false` to take no stick; the
vault's does, and the net zone never takes one.

The broker answers `stick` with the zone's descriptor and `ok ro|rw WHAT`;
the launch daemon has `sticks`, `stick-grant ZONE PART ro|rw` and
`stick-eject ZONE` for the chrome, and `info` says what a zone holds; root
has `kryptikd stick list|grant|eject`. A zone's stick goes back when the zone
stops.

Checked with unit tests on a stand-in /sys of the kernel's shape (a USB
partition named with its vendor, model, size and GPT name; a stick with no
partition table; an SD card; an NVMe disk refused; a held, mounted or
system-disk partition refused; bad names), the grant record round trip, the
mapping lookup, the zone key, and the broker parse.
…s suite plugs one in

A stick plugged in now opens a window of the chrome naming each partition
by what sysfs says of it; a letter and a zone's number give it to that zone
to read, or with w to write too, and e with the number takes it back. The
menu's d opens the same window, and its zone lines say which zone holds a
stick. kryptik-launch gains --sticks, --stick-grant and --stick-eject for it.

Eject now writes out what the zone left unsynced, then wipes the mapping's
table and drops what was cached of the stick: a wiped table alone left the
pages the zone had read readable through a descriptor it kept. A stick
pulled out without eject loses its grant the next time the sticks are
listed, which the chrome does whenever a block device comes or goes, and a
grant is refused while a program in the zone still holds the mapping of a
stick taken back from it.

zones-test.sh plugs a FAT stick into the running guest through QMP
(vm-drive.py's plug: and unplug: steps, run-ovmf.sh --usb-port) and runs
guest-tests/stick-check.sh between the plugs: zone 0 mounts and probes
nothing; vault and the net zone are refused; untrusted, given it read-only,
lists it and copies a file off whole, cannot write, and has no node for it;
another zone asking is refused; eject frees the partition at once and a
descriptor the zone kept reads only EIO; what the zone puts with a write
grant is on the image the host reads after pulling it out; a program
holding the stick when it is pulled out reads EIO and the grant ends with
no oops; and the install medium plugged in offers its ESP, which the reader
lists, and its root, which the reader refuses. chrome-sticks.sh checks the
window and the watcher against a stand-in launcher, and vm-drive.py's test
the plug steps against a stand-in QMP socket.
If lvm2's rules ship, 13-dm-disk.rules runs blkid on every device-mapper
device as it appears or changes, so a grant would have zone 0 parse the
stick's superblock as root through kryptik-stick-ZONE, which
59-kryptik-removable.rules keeps eudev from doing on the partition itself.
12-kryptik-stick.rules sets device-mapper's disable flags for those
mappings, by their sysfs name and by the name 10-dm.rules imports.

stick-check.sh's new round, after the grant: eudev holds no ID_FS_
property for the mapping or the partition.
A video call in a zone had the microphone and no camera: the kernel built no
video support. ADR-026 grants the camera as the microphone is granted.

The kernel builds Video4Linux and uvcvideo as modules, filtered to cameras
and test drivers, with no camera button input device; modprobe.d/camera.conf
keeps uvcvideo from loading by alias, and stage 05 checks both. vivid is
built as a module for the zones suite to stand in for a camera, without its
framebuffer; the hardening checker's finding on it is accepted, with why.

kryptikd's camera.rs: a zone that may hold the camera (a new [camera] use
key; vault's file says no, and the net zone never does) gets at start a
directory of its own from a tmpfs that allows device nodes, bound at
/run/kryptik/camera without nodev, /dev/video0 linking into it, and a
Landlock rule for it. A grant loads uvcvideo, binds again what an earlier
grant's end let go of, finds the first capture node in sysfs and makes it
there, owned by the zone's root, mode 0600. Its end, by the user, the
zone's stop or the screen lock, removes the node, unbinds the driver from
the device so a kept descriptor fails, and unloads uvcvideo unless
something still holds it, which the chrome then says.

The launch daemon has camera-grant and camera-revoke, kryptik-launch
--camera-grant and --camera-revoke, root kryptikd camera grant|revoke|
status. The chrome's menu has k<N> and k and a Camera line, a window says
which zone holds the camera for as long as it does, and kryptik-lock takes
the camera back as the screen locks.

Checked: camera.rs unit tests on a stand-in sysfs (the first capture node,
not a metadata one, numbered as numbers, its name shown plainly; the device
and bus to let go of; the record round trip), the zone key, and
chrome-camera.sh against a stand-in launcher. zones-test step 2c runs
guest-tests/camera-check.sh with vivid: no load by alias, no camera before a
grant, vault and net refused, the grant's node and a captured frame, the
zone's stop ending it, another zone finding none, k alone failing a kept
descriptor and unloading uvcvideo, a grant to a running zone. gui-check's
lock round: the screen locking takes it back.
A mapping ejected while its zone still held the stick is removed by
device-mapper once the zone's last descriptor closes, from a workqueue, so
the eject kryptikd runs at the zone's stop can find it and then lose it
between two dmsetup calls, and logged that as a failure. Eject now treats a
mapping that is gone as done, says whether a grant ended rather than
whether a mapping was there, and still clears a mapping left without a
grant.

stick-check.sh's rounds that look for the mapping to be gone wait up to ten
seconds for that removal instead of looking once.
The chrome opens its camera window within a second or two of a grant, and
a window that maps takes the keyboard: granted after the round's zone 0
window had it, the camera window could take it before the lock did, which
the round would read as the lock's doing. Granted first, and waited for,
the window is under the round's own.
A directory tree nested one level past MAX_DEPTH stops the copy off the
stick with the limit named, where a stick built to loop would otherwise
recurse until the zone's stack or disk ran out.
Every walk of the stick's directories, path lookup included, now goes
through one loop that stops with "the stick's filesystem is malformed"
past FAT's 65,536 entries in a directory, or past as many entries or bytes
as the volume has room for in one command. fatfs follows a cluster chain
that loops, so a directory could be endless (list grew its list until the
zone's memory ran out), and directories aliasing an ancestor multiplied
under get, past what the depth limit stops, until the zone's disk was full.
put looks for the name within the same bounds before fatfs walks the
directory to add to it.

A name holding a control character is no file name here: FAT allows none,
and the errors that name a path here carried it to the terminal. Errors
show paths as names are shown, and the marks that turn text around show as
'?' too.

The stick's row in stage 04 sits above kryptikd's comment, which is
kryptikd's.
… as kryptikd is

Stage 06's strict audit flags every static Rust binary for both, and lists
kryptikd and the proxy by name; kryptik-stick is built the same way.
…wn aliases

kmod's modprobe skips a blacklisted module's own aliases with or without
-b, so a dry run by the alias names nothing either way and could not show
the alias belongs to uvcvideo; modinfo reads it from the module.
…he camera

A zone holding the microphone or the camera has a window of the chrome's
that says so, but a window the user puts in full screen covers it, and only
dwl's bar above that window stays in sight: a zone could take a grant, wait
for full screen, and record with nothing on screen saying so.

The bar now reads, after the zone's name, "mic ZONE" and "cam ZONE" for
whichever zones hold them, on every fullscreen window, not only the holder's.
dwl learns them from two files through one inotify descriptor on its event
loop, and draws every fullscreen bar again on a change: the camera's grant
record, which kryptikd writes as root, and the microphone's holder, which
the mixer now writes into its own control directory (uid 87, 0640, group
kryptik) at its start and on every grant and revoke. dwl takes a file only if
it is a regular file of its writer's that no one else may write, and a name
only if it is a zone dwl knows; anything else marks nothing. kryptikd's
launch daemon mounts the camera's tmpfs as it starts, so the compositor's
watch is on it and not on the directory beneath.

gui-test, with vivid as the camera: untrusted fullscreen, granted the camera
and recorded as the microphone's holder, its bar reads "untrusted mic
untrusted cam untrusted" pixel for pixel; both taken back, and a record of
the microphone's holder put in place by root rather than the mixer, the bar
names the zone alone. The sound suite checks the mixer's record on the grant
and once the zone's stop has ended it.
…a directory made anew drops

An inotify watch belongs to the directory's inode: the mixer's control
directory made again, or anything mounted over a watched directory, ends
the watch, and every later grant went unmarked until dwl restarted. dwl now
also reads both records once a second, draws the fullscreen bars again only
when a holder changed, and sets both watches again each time, which keeps a
watch that is there as it is.

The gui round makes the mixer's directory anew before the records are
written, so the marks it checks come through a directory dwl did not first
watch, and puts the mixer's own back after.
…Bluetooth and input methods, and the laptop bullet rewritten
…ooth and input methods, and the laptop bullet rewritten
DevomB added 27 commits October 14, 2026 21:25
…, and the desktop suite checks it

# Conflicts:
#	docs/status.md
…ke, and the desktop suite checks it

# Conflicts:
#	docs/status.md
…s awake, and the desktop suite checks it

# Conflicts:
#	docs/status.md
…used, and the second head's screenshot is checked at the scale it is shown at
…ore lock-0's window opens, and the desktop suite is named beside the zones suite as loading vivid
…out round lists the sticks only once the kernel has let the stick's partition go
…deo0, as no other is granted the camera; the camera suite is named in the image's program list
…ard, so untrusted's fullscreen has ended for Alt+e to give back
… card pulled out, so a moved microphone and a pulled USB card no longer leave a loop spinning
…hes, a zone's sched and ioprio calls refused, proxy-calls ending at a proxy's exit
…pens on the second head

zone-sees-heads-unnamed launched wlprobe outputs in untrusted while the
havoc window the checks before it opened still ran there, so the launch
was refused: a zone runs once. The probe now comes first, its zone is
stopped, and havoc opens on the second head as before, where the
screenshot and the unplug checks after it need it.
@DevomB
DevomB changed the base branch from guide-laptop-checks to main October 15, 2026 12:31
@DevomB
DevomB marked this pull request as ready for review October 15, 2026 12:31
@DevomB
DevomB merged commit 7373ad8 into main Oct 15, 2026
11 checks passed
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.

1 participant