Skip to content

Feat/vt layer seat0 - #287

Merged
hodgesds merged 5 commits into
mainfrom
feat/vt-layer-seat0
Sep 8, 2026
Merged

Feat/vt layer seat0#287
hodgesds merged 5 commits into
mainfrom
feat/vt-layer-seat0

Conversation

@hodgesds

@hodgesds hodgesds commented Sep 8, 2026

Copy link
Copy Markdown
Owner

No description provided.

hodgesds and others added 5 commits September 7, 2026 16:58
systemd-logind treats seat0 as VT-owning: a display manager allocates a VT via
VT_OPENQRY, passes that vtnr to CreateSession, and logind marks the session
whose VTNr equals the active VT as the seat's active session. Only the active
session is handed DRM/input device fds via TakeDevice.

NARF had no VT layer — the VT ioctls returned ENOTTY and there was no
/sys/class/tty/tty0/active — so logind logged "not /sys/class/tty/tty0/active
which is broken, ignoring", could not determine the active graphical session,
and refused TakeDevice. The greeter compositor then fell back to a direct open
of /dev/dri/card0, which failed (EACCES), found no DRM device, and the screen
stayed black.

Add a minimal logical VT layer (one active VT at a time; no hardware console
switching, which a VM never needs):

- filesystem/src/vt.rs: active-VT + allocation + per-VT switch mode + per-VT
  owner, plus /sys/class/tty/tty0/active contents.
- DevConsole::ioctl implements VT_OPENQRY / VT_GETSTATE / VT_ACTIVATE /
  VT_WAITACTIVE / VT_GETMODE / VT_SETMODE / VT_RELDISP / KDSETMODE, and
  set_owners so logind can chown a session's VT to its user (also clears the
  "Cannot change owner of /dev/tty1" / "Failed to restore VT" warnings).
- /dev/ttyN lookup for N in 2..=63.
- sysfs::populate_tty_class() registers /sys/class/tty/tty0/active as a
  Stage::Late initcall, reading the active VT so it agrees with VT_GETSTATE.

Boot-verified on a real CachyOS + Plasma greeter: logind now determines the
active session, TakeDevice hands the compositor the card0 fd, and kwin brings up
its GPU, output (Virtual-1), and input. 5 filesystem/vt smoke tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2nAdDEUHUuoSmZhESwfae
sys_read polled the read future with poll_blocking() unconditionally, which
PARKS the task on a Pending result, and only checked O_NONBLOCK /
nonblock_read_eagain AFTER (reached only when the ops returns Ready(WouldBlock)).
An ops whose read() future returns Pending-when-empty — an evdev input node,
which blocks internally on an empty ring (see devfs_input::nonblock_read_eagain)
— therefore parked a non-blocking read forever instead of returning EAGAIN.

This is exactly the case devfs_input's nonblock_read_eagain() comment intends to
cover ("makes sys_read poll the read future once and return EAGAIN on an empty
ring rather than spin-pumping poll_blocking"), but the check sat after
poll_blocking, so it was never reached.

libinput opens /dev/input/eventN O_RDWR|O_NONBLOCK and reads them; a compositor
(kwin_wayland) that hit an empty evdev ring parked in read() and never returned
to its event loop, so no frame was ever presented and the screen stayed black
even after logind handed it the DRM device.

Fix: for a non-blocking read on a stream/char device (nonblocking() &&
(is_stream() || nonblock_read_eagain())), poll the read future once and map
Pending to WouldBlock so the EAGAIN path fires; keep poll_blocking (park) for
blocking reads and regular files. Socket/eventfd/pipe non-blocking reads already
return Ready(WouldBlock) and are unaffected (their smoke tests still cover that
path); boot-smoke clean. Boot-verified on the CachyOS Plasma greeter: kwin
advances from parking at input setup all the way through startup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2nAdDEUHUuoSmZhESwfae
Page migration copied the source frame's contents into the destination
BEFORE tearing down and cross-CPU-invalidating the old leaf. Two SMP
hazards followed:

- A user store that raced the copy (through a still-writable/-cached leaf
  on another CPU, or the owning CPU when a foreign task drives the move)
  landed in the source after the snapshot and was lost — a torn write that
  surfaced as heap garbage in the migrated page.
- Worse, `flush_region_broadcast` gates its cross-CPU shootdown on
  `is_vm_shared()`. That gate assumes the caller is a thread of the AS, so
  a single-threaded process could only be resident on the calling CPU. The
  migrator (compaction, `migrate_pages(2)`) runs from a FOREIGN task: the
  victim may be resident on any other CPU, which then kept translating to a
  frame already returned to the buddy allocator — a cross-process
  use-after-free of physical memory.

Reorder to Linux's `migrate_pages` sequence (try_to_migrate → copy →
remove_migration_ptes): unmap the leaf and invalidate it on every CPU
(`unmap_4kb` broadcasts through the shootdown hook and ack-waits) BEFORE
copying, then install the new leaf. A racing access now takes a
not-present fault, spins on the region lock the migrator holds (servicing
shootdown IPIs while it spins), and retries against the published leaf.
The broadcast is unconditional, not `vm_shared`-gated, because the
migrator is not a thread of the target AS.

`relocate_leaf` becomes the single relocation core; `migrate_page_to_node`
and `migrate_huge_page_to_node` are folded onto the same order (the huge
path via a new ungated `flush_range_all_cpus`), and the now-redundant
post-copy `flush_region_broadcast` calls are dropped.

Adds `smoke_migrate_frame_flushes_before_copy`: a shootdown-hook shim
plays the last racing writer into the OLD frame when the migration
invalidates the page; the test fails if the copy ran before the flush.
Also adds `paging::shootdown_hook`/`clear_shootdown_hook` (test needs to
wrap+restore the live hook) and `AddressSpace::debug_page_backing` (a
read-only per-page backing accessor for fault forensics).

memory/migrate kernel tests: all 8 green, including the new one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjBBDaiGFmT7TX65yfS12G
`copy_user_cstr_checked` read the path bytes from userspace successfully
(every `copy_from_user` returned Ok) and then returned EFAULT purely
because the bytes were not valid UTF-8. Linux (`fs/namei.c::getname_flags`)
treats a path as an opaque byte string and never validates UTF-8: it
resolves the exact bytes and, for a name no file has, returns ENOENT. A
readable non-UTF-8 pointer is not a bad address, so EFAULT was wrong and
Linux-divergent — every path-taking syscall (openat, newfstatat, …)
inherited it.

NARF's VFS is UTF-8-keyed and cannot store a non-UTF-8 name, so a lossy
decode yields a name that matches no real entry → NotFound → ENOENT: the
same observable result Linux gives for a non-representable path. A genuine
copy fault still returns EFAULT (unchanged), and a valid UTF-8 path is
unaffected.

Found while chasing a KDE-greeter abort: fontconfig scans a directory,
builds `<dir>/<name>` paths, and openat()s them with `ignore_missing`; a
stray non-UTF-8 tail made NARF answer EFAULT where Linux answers ENOENT,
turning a "skip this entry" into a hard error. (The greeter abort has a
separate upstream cause — this only removes the spurious fault.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjBBDaiGFmT7TX65yfS12G
The old handler accepted the registration and returned 0 on the rationale
that NARF was "a cooperative single-CPU kernel with no preemption
mid-sequence" — no longer true (NARF is preemptive SMP). NARF never writes
the rseq `cpu_id`/`cpu_id_start` fields on return-to-user and never aborts a
critical section to `rseq_cs.abort_ip` on preemption or CPU migration, so
faking success is actively unsafe: glibc >= 2.35 registers rseq at thread
start and, on success, publishes `__rseq_size != 0` and trusts the ABI
area — so `sched_getcpu()`'s fast path and rseq per-CPU allocators read a
`cpu_id` that never advances and run critical sections un-restarted across a
real CPU migration, silently corrupting per-CPU state.

Return -ENOSYS, exactly as a kernel that does not implement rseq: glibc
leaves rseq unregistered (`__rseq_size == 0`) and falls back to the
`getcpu(2)` path, correct on any CPU count. Spec updated; the abi test flips
from asserting ok(0) to asserting ENOSYS (smoke_abi_sched_rseq_unimplemented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjBBDaiGFmT7TX65yfS12G
@hodgesds
hodgesds merged commit 7e8e8e0 into main Sep 8, 2026
5 checks passed
@hodgesds
hodgesds deleted the feat/vt-layer-seat0 branch September 8, 2026 01:45
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