Atv0.3.0Repositorytessera

docs/lock-and-greet.md

Release v0.3.0 · Default-branch version

Document

Lock screen and login greeter

tessera lock is an experimental session-lock client. It runs in a separate process and uses ext-session-lock-v1. Only successful authentication can send the unlock request. Closing the client, losing an output or encountering a PAM error leaves the compositor's lock in place.

The built-in locker is off by default pending security review. Swaylock remains the stock locker and recovery option. These examples describe an experimental configuration; they don't install a PAM policy or change a running session.

Appearance

The lock screen and greeter share their renderer, palette, clock, local PNG logo slot and input field. The greeter adds a username field, a session list containing Tessera, and confirmed restart/shutdown actions. Tab and Shift+Tab move between username and password; Enter in the username field moves to the password field. Native input freezes the public username at the focus key; characters typed after it stay in secret storage, including within one dispatch batch and while greetd prepares its first prompt. Returning to the username cancels the pending conversation, clears the password and preserves the username. Escape clears the lock field or cancels a greeter conversation. Password characters never become Sigil strings.

Both screens show a pointer cursor at the output scale, using a reachable cursor theme or a built-in arrow. Click a field to focus it or click Enter in the footer to submit. The greeter's session row cycles sessions; F2 does the same. Restart and shut down each require a second click or their matching function key to confirm. The confirmation must first be presented, followed by at least 500 ms before another press can confirm it. Rapid double-clicks or repeated function keys cannot bypass that delay. Greeter clicks wait until its opaque panel has been presented after the 800 ms fade. The lock footer has only its existing unlock action. Kiosk pointer buttons are forwarded only to the greeter's own view. While locked, native routing keeps pointer input on lock surfaces and away from the desktop.

The shared panel uses the F20 layout: JetBrains Mono 2.304 Regular at 16 px, with one 20 px time/date line 24 px below the panel. The unmodified font and full SIL OFL notice ship inside the executable. No system font installation is needed. With no logo, the panel is centered on the output; with a logo, it sits below it. Exact implemented geometry is in `design/lock-greet/MIDDLE.md`.

The greeter has the GE footer: Enter and keyboard layout on its first row, F11 restart and F12 shut down on its second. The lock uses the single-row G2 footer. Esc still clears the field, but has no visible hint. The layout label tracks the current XKB group, and a gold Caps Lock indicator appears inside the password field while Caps Lock is on. Both update as modifiers or groups change. Dots stay within the field, leaving room for the Caps indicator.

tessera lock --palette orpiment --wallpaper drift
tessera greet --palette orpiment --wallpaper facets \
  --logo /usr/share/branding/wordmark.png \
  --logo-x 784 --logo-y 358 --logo-width 352 --logo-height 112

--wallpaper none disables animation. Logos must be absolute local PNG paths, at most 8 MiB, with at most 4096 pixels on either axis and four million decoded pixels. No logo means no placeholder and no reserved logo box. Ordinary logo coordinates and sizes use output-local logical pixels and scale with the output.

Locking covers every output with an opaque surface. By default the field stays on the output under the pointer when locking starts, falling back to keyboard focus and then the first output. output: "all" mirrors the field on every output; an output name selects that display. The command-line equivalent is --output pointer, --output all or --output NAME. Removing the chosen output selects a remaining one. Passive outputs retain the clock and date. Passwords show one dot per Unicode character, counted in native memory and capped at the field's width; Backspace removes one character and Escape clears the field. The greeter uses the same password display. Output removal, new outputs and scale changes never authorize unlocking. The greeter uses one fullscreen client surface on --output NAME, or the first available output. Removing that output selects a remaining output. The greeter rejects the lock-only pointer and all choices.

The living texture runs in a lower-priority process, created before password storage is allocated. Drift uses the current motion at speed 2, with seed 23 and a 480x270 cell grid. Motion follows elapsed monotonic time, including when rendering slows down. The screen scales the cells with nearest-neighbor sampling. Integer scales copy each cell into its output rectangle directly; fractional scales use Cairo's nearest-neighbor filter. Facets and the copper palette retain their 320x180 pipeline. Easel 0.2.0 caches the GPU programs between frames. Mesa selects a headless GPU when one is available, with one llvmpipe thread for software rendering. The worker starts at sixteen frames per second; --texture-fps accepts 8, 12, 16 or 24 for comparison. Frames render ahead and publish on absolute monotonic deadlines. The client notices each new publication during its bounded dispatch poll instead of sampling with another frame timer. Input redraws never reset animation timing. Missed publication ticks are skipped after stalls or suspend. The cold frame is excluded. A lower median over a complete two-second rolling window decides permanent steps: 16 -> 12 -> 8 -> 1, with no recovery in the same process. An explicit 24 starts 24 -> 16. CPU budgets are 9.6% of each interval: 6 ms at 16, 8 ms at 12, 12 ms at 8 (4 ms at 24). Startup and a idle gap longer than max(500 ms, four intervals), excluding CPU work, receive two seconds of grace, then need a complete warm window. The client also grants grace while output presentation feedback is stale (VT inactivity) and at session handoff. The surfaceless worker continues independently of VT presentation; a worker pause receives the same grace. Each lower rung starts a fresh observation window. Sparse windows retain at least three observations, even if a very slow machine takes longer than two seconds to produce them. The client applies this policy only to animation paints; input and clock-only redraws do not bias it and remain immediate. Set TESSERA_SCREEN_TIMING=1 for numeric timing records and every step's window median, budget, sample count and costs, without account or input data. A lost renderer leaves the last complete frame or the opaque background, without interfering with authentication. Every texture frame is read once as a bounded regular-file snapshot, at most 256 KiB encoded, with an exact 480x270 or 320x180 header checked before Cairo decodes it. The worker writes opaque indexed PNGs when there are at most 256 colors, with an RGB fallback for richer images. Low compression and only IHDR, optional PLTE, IDAT and IEND chunks keep the files small and bounded. This keeps publishing within its frame budget. Both texture and logo snapshots retain only IHDR, PLTE, tRNS, IDAT and IEND chunks. Text, colour profiles and other metadata are removed without decoding, so compressed metadata cannot add allocations or work in the input loop. Chunk lengths and the final IEND boundary are checked before Cairo is called; unknown critical chunks and incomplete snapshots are rejected. The IDAT pre-pass also requires a complete zlib stream within the exact raw scanline size, including packed bit depths and Adam7 passes. Extra compressed pixels after the image are rejected before Cairo can inflate them. Rejected, replaced or truncated frames retain the last good texture or the opaque background; the input process never uses an unrestricted PNG loader.

The background uses a radial pool centered at y 595 in the greeter's 1080p coordinates, with strength 0.5 and texture strength 0.72. The lock centers it on its smaller content box: y 539 with the standard logo, or y 563.5 without a logo. The pool has a 200-pixel core and a 560-pixel smoothstep falloff, stretched horizontally by 1.8. Shading happens on the cell grid with a cached mask. The screen keeps at most one 64 MiB canvas and one 64 MiB shade mask; normal Drift masks occupy about 0.5 MiB. A canvas is reused only after every caller has released its frame. The client paints directly into an available Wayland SHM canvas. Busy buffers and canvases held by callers are excluded from reuse. A retained canvas keeps its mapping alive across resize and client shutdown; all retained mappings still count toward the 256 MiB allocation limit. This avoids a second full output copy without changing buffer-release ownership.

Opting into the locker

The lock record selects the built-in client explicitly:

(config-with default-config
  lock: (lock enabled?: #t
          palette: "orpiment"
          wallpaper: "drift"
          output: "pointer"
          logo: "/usr/share/branding/wordmark.png"
          width: 352 height: 112))

Its optional x:, y: and output: fields become client arguments. When disabled, the existing lock-command: remains authoritative. Explicitly enabling it also changes the stock swayidle lock words to tessera lock --background, with shell-quoted appearance arguments. That invocation returns only after the compositor's locked acknowledgement, so swayidle can finish its before-sleep handshake. A failed launch or missing acknowledgement starts swaylock -f as a fallback. The private native acknowledgement socket verifies the connecting PID against the designated client with SO_PEERCRED; a same-UID sibling or renderer cannot satisfy it. The client sends only after its live lock object's locked event. Socket descriptors are close-on-exec, and the client opens its sending connection after spawning the renderer and closes it immediately after sending. Running scripts/idle-lock without --builtin still invokes swaylock -f.

The fixed PAM service is tessera. The client binds the account to its real UID, rejects a PAM identity change, and requires authentication, account validation and PAM cleanup to succeed. It doesn't open a PAM login session or change credentials. A missing service, expired account, unsupported conversation or allocation error denies unlocking. Configure the system's screen-lock PAM stack for this service before trying it in an isolated session.

Authentication runs in a separate native helper with a ten-second monotonic deadline. It closes inherited descriptors, including the Wayland connection, before calling PAM, locks its private copy of the secret pages, and loses its result descriptor on exec. The parent erases its password copy immediately after forking and verifies the helper's kernel message credentials. Timeout kills only that helper PID, discards its result and shows Authentication timed out; try again. The compositor stays locked. Child reaping is nonblocking; shutdown never joins a PAM thread. The helper also receives SIGKILL if its owning client dies. The installed PAM stack remains responsible for any external services or descendants it creates.

Recovery from another terminal

A live owner blocks replacement lockers, even if its authentication is stuck. The native abandoned-lock recovery path becomes available only after that owner exits. Keep another login terminal available during qualification.

  1. Switch to another VT with Ctrl+Alt+F2 and log in as the session's user. Know the locked session's Wayland socket name before testing; the new VT's display environment may be empty or refer to another session.
  2. Run ps -u "$(id -u)" -o pid,ppid,comm,args. Find the owning tessera lock client. A PAM helper's command name is tessera-pam; its PPID identifies its owning client. The texture process is a separate child. Confirm the exact client PID and intended session before signaling it.
  3. Set locker_pid to that PID and run kill -TERM -- "$locker_pid". Check that it exited. If it remains after a few seconds, use kill -KILL -- "$locker_pid" and confirm it is gone. Do not kill the compositor or use a broad process-name match. The desktop remains locked.
  4. Start a replacement for that same display, for example:
   XDG_RUNTIME_DIR="/run/user/$(id -u)" WAYLAND_DISPLAY=wayland-your-session swaylock -f

Replace the socket name with the one for the locked session. After its PAM policy is qualified, tessera lock --wallpaper none is another replacement command. Merely starting either command while the old owner is alive will fail. Recovery does not require unlocking or restarting the compositor.

The headless regression stalls fake PAM, verifies a replacement is refused while the old client lives, terminates that exact client, and authenticates a replacement on the same private display. It also checks automatic PAM timeout and a new attempt. Physical VT switching requires separate hardware testing.

greetd setup

Run tessera greet as the unprivileged greeter account. greetd supplies GREETD_SOCK, performs PAM and starts the selected user's tessera session. Tessera's greeter host loads no user configuration, starts no session helpers and opens no expression socket. It admits the greeter child's first toplevel and closes other clients' toplevels. Use repeatable --session NAME=COMMAND options to supply sessions in display order. Explicit commands must be absolute paths; the greeter does not check whether the greeter account can access a user's session script. Quote a value that includes arguments:

tessera greet --session Tessera=/home/user/.dotfiles/bin/desktop-session \
  --session 'Debug=/opt/tessera/bin/tessera start --log-level debug'

The option parser groups words using single or double quotes. Outside single quotes, a backslash escapes any following character, including inside double quotes: "a\qb" becomes aqb. This differs from POSIX shell parsing. Single quotes preserve backslashes literally. An unfinished quote or trailing backslash is an error; variables and command substitutions are never expanded by the option parser.

F2 cycles sessions while either field has focus. greetd 0.10.3 joins its command array into a shell command. Tessera quotes each argument for that shell, so spaces, empty arguments and shell metacharacters reach the chosen program unchanged. Without these options the single entry remains Tessera running tessera through the user's PATH. This quoting assumes greetd's join-into-shell command handling; a daemon that executes the array directly is not supported by this option. An authentication error returns to the username prompt for another attempt, including when greetd reports that the failed PAM worker has already exited during cancellation. The session environment includes TESSERA_LOGIN_SESSION=1, so a no-argument Tessera invocation starts and publishes its login session even when greetd's stdin is not a TTY. A session wrapper must execute the updated binary. Tessera clears the marker after capturing the login decision, before starting session children. Nested displays keep their separate session handling.

The console between greeter and session

greetd 0.10.3 starts the session after the greeter exits and switches its VT to text mode before executing the command. That prevents the greeter from guaranteeing that its last DRM frame stays visible. Tessera clears only its own active controlling VT's text buffer and hides the text cursor before a successful handoff. The incoming session repeats this before native startup, so the fallback is black while DRM starts. It does not change VT, keyboard or graphics modes. Nested and headless backends skip terminal access.

A restarted greeter restores the cursor before taking over. A failed session startup and normal session shutdown also restore it. If the new executable cannot start at all, greetd restarts the greeter, which restores a usable text console even if its own graphics startup then fails. This does not suppress new kernel messages, external programs that reset the cursor, or a hardware modeset blank. A physical VT test is needed to judge that interval.

TESSERA_HANDOFF lines include a common monotonic millisecond clock. Compare greeter-start-session-ack, greeter-client-exit, greeter-native-joined, session-entry, session-native-start, session-native-ready and session-first-present across the greeter and session logs. The last stamp comes from the first actual output presentation, rather than a buffer commit or a ready notification. Their differences separate client shutdown, native teardown, greetd/profile/exec work, module loading and DRM startup/presentation.

If kernel console output remains visible, an administrator can consider vt.global_cursor_default=0 to hide the default text cursor and quiet or a lower console loglevel= to reduce messages. These are system-wide choices with troubleshooting tradeoffs. Tessera does not change kernel arguments.

It initializes the fixed Ctrl+Alt+F1 through F12 session-switch cache and configures each arriving keyboard with the fixed US keymap, without loading user configuration. A headless test drives the native escape path with a physical-keyboard substitute and stubs only the hardware VT action. Qualify Ctrl+Alt+Fn on the actual seat before using the greeter as a login screen.

On Guix, add a terminal to greetd-service-type, using the Tessera package provided by your channel:

(service greetd-service-type
  (greetd-configuration
    (allow-empty-passwords? #f)
    (terminals
      (list
        (greetd-terminal-configuration
          (terminal-vt "7")
          (default-session-user "greeter")
          (default-session-command
            (greetd-user-session
              (command (file-append tessera "/bin/tessera"))
              (command-args '("greet" "--palette" "orpiment"))
              (xdg-session-type "wayland")))))))))

These records come from (gnu services base); see Guix's base services. The greeter needs a working seat backend and access to its runtime directory. Keep another login terminal available while qualifying a new display manager.

On NixOS, with your Tessera derivation bound to tessera:

services.greetd = {
  enable = true;
  settings.default_session = {
    user = "greeter";
    command = "${tessera}/bin/tessera greet --palette orpiment --power-command systemctl";
  };
};

# For the separate experimental locker, after reviewing its PAM policy:
security.pam.services.tessera = {};

The NixOS module creates the greeter account and greetd PAM service. The locker uses its own service. See the greetd module. The authenticated user's environment must put tessera on PATH.

Boot handoff

The first committed greeter buffer contains only the handoff background, logo and optional waterline. The form and living texture appear after its frame callback. A loader can supply a physical-pixel rectangle:

tessera greet --palette orpiment \
  --logo /usr/share/branding/wordmark-704x224.png \
  --handoff-color '#0E0E0C' \
  --handoff-x 1568 --handoff-y 716 \
  --handoff-width 704 --handoff-height 224 \
  --handoff-line 864

At 3840x2160 and scale 2, this matches a 352x112 logical wordmark at (784,358) and a gold waterline at logical y=432. Use the same decoded asset, mode, color and physical placement on both sides. The kiosk stages an opaque background and exact logo/waterline patches before enabling each output. Its first buffer commit is checked at 3840x2160, scale 2. A frame callback is not proof of physical presentation; framebuffer-to-DRM takeover still needs hardware qualification with the loader, mode and display driver you will ship.

Security review

Review the native client, PAM bridge, greetd transport and kiosk policy together. The default must stay disabled until the review and its required fixes land.

  • PAM: real UID lookup, fixed service name, rejection of identity changes, null-password policy, account expiration, unsupported prompts, every error return, helper lifecycle and pam_end handling. Test with a dedicated PAM service and dummy credentials, never a real account password.
  • Password memory: bounded locked/nondumpable native pages, core-dump limits, process dumpability, UTF-8 editing, clearing on focus/seat loss, wiping after authentication, and framed-writer wiping on partial writes and errors. PAM owns its response allocations after the conversation returns; those copies are ordinary heap allocations and need particular scrutiny. The VM receives only masked field state, never secret text.
  • Process boundaries: start the renderer before allocating secrets; audit all later forks, including power helpers and PAM's own helpers. Check renderer failure, parent death, hung PAM, shutdown and child reaping without exposing or unlocking the desktop.
  • Lock ownership: only the accepted PAM result for this live lock object can unlock. Review refusal before the locked event, a finished event during PAM, SIGKILL, abandoned-lock restart budgets, simultaneous lockers, suspend/resume, output addition/removal and scale changes. Recovery must keep the native lock.
  • Wayland buffers: allocation bounds, two-buffer release tracking, resizing, removal while buffers are in flight, opaque initial coverage and failed commits.
  • greetd: length limits, little-endian framing, short reads/writes, malformed JSON, disconnects, prompt sequencing, cancellation, deadlines, starting only after authentication and exiting only after start-session acknowledgement.
  • Kiosk: protocols: 0 disables optional protocols, but baseline compositor, shm, seat, output, xdg-shell, subcompositor, viewporter, fractional scale, relative pointer, data-device, presentation timing and security-context globals remain; hardware renderers may add their DMA-BUF globals. Review that inventory, socket permissions, trusted PID lifetime, extra toplevels, popups/subsurfaces, clipboard behavior and the absence of config, REPL, Xwayland, virtual input, capture and output-management endpoints.
  • Presentation and input: local bounded PNG decoding, keymaps and layout changes, masking, focus transitions, animation budgets and responsiveness during slow rendering. Qualify DRM takeover separately from the first client buffer.

An accepted unlock waits for a Wayland sync acknowledgment before the client exits, bounded to one second. A flush alone cannot confirm server processing.

Development checks

scripts/build-screen-client builds libtessera-screen.so against Wayland, xkbcommon, wayland-cursor and PAM. Use the pinned Guix environment. scripts/run-screen-dev accepts explicit TESSERA_SCREEN_DEPS and TESSERA_SCREEN_RUNTIME paths and keeps compilation artifacts under build/.

The new checks use their own headless runtime directories and fake credentials: scripts/test-screen-all is the full entry point. It creates a private D-Bus, runtime/config/data directories and a fake swaylock before starting any fixture. Keep build-time dependency/runtime paths explicit; apply the host's allowed CPU affinity and priority when invoking it.

Individual fixtures include scripts/test-screen-headless, scripts/test-screen-greetd, scripts/test-screen-interaction, scripts/test-screen-kiosk, scripts/test-screen-kiosk-pointer, scripts/test-screen-review-sabotage, scripts/test-screen-first-output, scripts/test-screen-ui, scripts/test-screen-sabotage, test/screen-model.sgl and test/screen-render.sgl. The UI fixture builds a private host copy that substitutes dummy physical keys; the production host continues to reject synthetic keyboard input while locked. The fake PAM implementation is linked only into test artifacts. It is never part of the production library.

scripts/test-screen-launch checks background acknowledgement and fallback using child-process doubles. scripts/test-screen-regressions runs the existing lock, startup and theme fixtures on a private D-Bus with private runtime and configuration directories. Use that launcher for lifecycle tests.