A point release with a stack of real fixes, the start of a GUI-subsystem refactor, and one apology.

Default root password (the apology)

I forgot to put the default root password anywhere visible in the 1.0.0 release. People who organically tried to boot Aegis over the past few days couldn’t find it — sorry about that. It’s:

Username: root
Password: forevervigilant

The README has it now, the homepage has it now. Single-line oversight, “how do I even use this thing” friction. Genuinely my bad.

The bug that almost shipped

On bare metal, Lumen would draw the desktop exactly once and then freeze. Mouse cursor stuck, keyboard ignored, no progress. QEMU never triggered it. The kind of thing where you spend ten minutes thinking “weird hardware quirk” before catching yourself.

It was two AF_UNIX bugs interacting:

  1. fcntl(F_SETFL, O_NONBLOCK) didn’t propagate to AF_UNIX sockets. The kernel’s sys_fcntl updated sock_t.nonblocking for AF_INET but never touched unix_sock_t.nonblocking. Lumen’s listen socket — which it explicitly set non-blocking — was actually still blocking at the kernel level.
  2. AF_UNIX sockets had no .poll callback. sys_poll fell through to the permissive default (always returns POLLIN), so Lumen’s event loop called accept() every tick. Combined with bug #1, the very first accept() blocked forever and took the whole compositor with it.

Fixed in kernel/syscall/sys_file.c and kernel/net/unix_socket.c. New unix_vfs_poll() reports readiness based on accept queue (LISTENING), peer ring buffer (CONNECTED), and peer state (POLLHUP).

stsh was secretly /bin/sh

/etc/passwd says the root shell is /bin/stsh but logging in dropped you into /bin/sh. Not a fallback path, not a config bug — the kernel was loading the wrong binary.

kernel/fs/initrd.c had /bin/stsh aliased to the /bin/sh blob. A leftover from before stsh existed as a real binary, when we wanted the path to resolve to something. VFS open order is initrd-first, ext2-fallback, so execve("/bin/stsh") always loaded the sh blob from initrd and never the real stsh on disk. Removed the alias. Now you actually get stsh — capability-aware prompt (# if you hold CAP_DELEGATE, $ otherwise), tab completion, the caps/sandbox/grant builtins. There’s a regression test (tests/tests/stsh_default_shell_test.rs) that runs the stsh-only caps builtin after login and panics loud if /bin/sh is what answers.

Three more bugs the GUI installer caught

The 1.0.0 GUI installer mapped the framebuffer directly via sys_fb_map and rendered on top of the desktop, which meant it wasn’t really a window — no chrome, no z-order, no input through the compositor. I wanted to fix that for 1.0.1: make it a proper Lumen window via a new external-window protocol over AF_UNIX. That’s the stack the original Lumen-freeze bugs broke into.

Building that out smoked out three more deep bugs, all of which had been silently lurking:

1. stb_truetype rasterizer assertion (Bastion crash). Assertion failed: z->ey >= scan_y_top (stb_truetype.h:3350). Subpixel positioning + fp rounding produced glyph edges with ey < scan_y_top on the very first scanline. Upstream stb clamps this only when j == 0 && off_y != 0, but the same hazard fires at off_y == 0 too — Bastion’s small-glyph BakeFontBitmap path always triggers the unclamped case. Made the clamp unconditional. Fixed in user/lib/glyph/stb_truetype.h.

2. AF_UNIX ring buffer math (lumen_connect EIO). The probe binary I wrote to test the new protocol kept getting lumen_connect failed (-5) on the very first byte. Turned out UNIX_BUF_SIZE was 4056 — matching the pipe ring “for symmetry” — but the ring math (head - tail) & (UNIX_BUF_SIZE - 1) only computes a real modulo when the size is a power of two. 4055 = 0b1111_1101_0111 — bits 3 and 5 are clear. So 8 & 4055 == 0. The 8-byte hello write put 8 bytes in the client’s ring (ring_head=8, ring_tail=0), but the server’s ring_used() computed (8 - 0) & 4055 = 0 and returned EAGAIN. The poll callback used a different check (raw head != tail) and correctly reported POLLIN — producing a perfect “poll said ready, read returned EAGAIN” race on every connect. Bumped to 4096.

3. mmap memfd size check (window_create NULL). With (2) fixed, lumen_connect worked but lumen_window_create returned NULL. The server was creating a memfd for the shared pixel buffer, calling ftruncate(memfd, 80000) (200×100×4), then mmap(NULL, 80000, ..., MAP_SHARED, memfd, 0) — and getting EINVAL. sys_mmap rounds len up to a page boundary at the top (80000 → 81920), then later compared len > mf->size for MAP_SHARED. 81920 > 80000 → EINVAL. The page allocation was fine; only the bookkeeping was wrong. Compare against page_count * 4096 instead.

After all three: gui-installer connects to Lumen via AF_UNIX, gets a memfd-backed pixel buffer, renders into it, sends LUMEN_OP_DAMAGE, and Lumen blits it into a normal composited window with chrome and a close button — same as terminals. Capability gate dropped: it no longer needs CAP_KIND_FB.

Mouse + keyboard for proxy windows

When the gui-installer hit the desktop, two more wires were missing:

  • Lumen wasn’t dispatching keystrokes to proxy windows. The main loop wrote bytes directly to focused->tag (= PTY master fd). Proxy windows have no PTY (tag = -1), so keys went nowhere. Fixed: invoke focused->on_key() if it exists; terminals’ on_key writes to PTY internally, proxy windows’ on_key sends LUMEN_EV_KEY. Both paths converge.
  • gui-installer wasn’t handling mouse clicks. The original installer was keyboard-only, so the “Next” button was just a visual hint. After the Lumen port mouse events arrived but were dropped. Added a hit-test that synthesizes Enter on a click in the advance-button area — same action keyboard Enter triggers.

Citadel dock split into a separate process

This is the start of a longer arc: cracking Lumen open into smaller subsystems that can each grow on their own. The dock is the first thing out the door.

In 1.0.0 the dock was statically linked into Lumen and rendered as an overlay callback after windows composited. In 1.0.1 it’s its own binary at /bin/citadel-dock, talks to Lumen over the same AF_UNIX protocol the gui-installer uses, and asks Lumen to spawn built-ins via a new LUMEN_OP_INVOKE message. Vigil starts it as a graphical-mode service. Lumen no longer knows what a dock is.

The protocol grew two opcodes: LUMEN_OP_CREATE_PANEL (a chromeless, non-focusable, bottom-anchored window) and LUMEN_OP_INVOKE (ask Lumen to run a named built-in like "terminal" or "widgets" — eventually those will be separate binaries too). Cost: lost the frosted-glass blur effect, since the dock can’t see the pixels behind itself anymore. Acceptable downgrade for now.

The longer plan is to keep peeling: terminal, taskbar, file manager, settings — each its own process, each potentially its own project/repo. cairo-dock-style. This release is the first cut.

Use-after-free in proxy window cleanup

While testing the new protocol I left a debug probe service auto-launching in graphical mode. The probe creates a window, presents a frame, exits — and on bare metal the very next mouse interaction page-faulted Lumen. Page fault, RIP in user code, CR2 in the mmap region — classic UAF.

Two underlying bugs:

  • lumen_server_hangup and handle_destroy_window were calling glyph_window_destroy(pw->win) after comp_remove_window(comp, pw->win) — but comp_remove_window already destroys the window. Double free. Removed the second destroy.
  • comp_remove_window cleared c->focused if the removed window was focused, but didn’t clear c->drag_win or c->content_drag_win. Any subsequent mouse event would dereference the stale pointer. Cleared both.

Also: the close button now invokes on_close if set (proxy windows use this to send LUMEN_EV_CLOSE_REQUEST to their owner instead of being destroyed unilaterally), so external clients can show “are you sure?” dialogs.

Small things

  • lumen running inside a Lumen terminal would map the framebuffer twice and produce two cursors. Lumen now sets LUMEN_RUNNING=1 in its environment; nested launches refuse to start with: lumen: you're already using lumen, pal.
  • glyph_window_t.tag initialized to -1 instead of defaulting to 0 — fd 0 is a valid file descriptor and was indistinguishable from “uninitialized” in the dispatch checks.

Get it

Download v1.0.1 ISO — same shape as 1.0.0 (live boot, graphical default, text-mode in GRUB), with all of the above baked in.

If you find more bugs — and you will — file an issue at exec/aegis. Security findings can also go privately to execxd@icloud.com.

Forever vigilant.