Feat/vt layer seat0 - #287
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.