Skip to content

Claude/labwc eclipse desktop 08f1ft - #414

Open
Pryancito wants to merge 2302 commits into
rcore-os:masterfrom
Pryancito:claude/labwc-eclipse-desktop-08f1ft
Open

Claude/labwc eclipse desktop 08f1ft#414
Pryancito wants to merge 2302 commits into
rcore-os:masterfrom
Pryancito:claude/labwc-eclipse-desktop-08f1ft

Conversation

@Pryancito

Copy link
Copy Markdown

No description provided.

claude and others added 30 commits July 28, 2026 17:03
…n itimer

QEMU bisection (repair-canary kernel, smp=1) shows plain external commands and
fork+exec chains WITHOUT an alarm all survive (/bin/busybox true, ls, env true,
sh -c 'exec true'), while anything using timeout (setitimer/alarm) crashes --
and timeout 5 true crashes immediately (the 5s timer never fires), so it is the
ARMING of the itimer, not its delivery. Fatal #GP is an IRQ delivered right
after an sti (lock guard pop_off) in lookup_inode_at, because the executor
kernel stack is already corrupted; the mangled slot is a saved timer_tick
return address. sys_setitimer/TimerHeap/arm_itimer are bounds-clean by
inspection, so the bug is in the arm_itimer->timer_set->NAIVE_TIMER/timer_tick
interaction with the executor stack.
…y), not just arm

Distinguishing hang from crash via QMP screendumps: timeout 5 true and
sh -c 'true & wait' only HANG (VM alive) -- a separate reaping bug -- while
timeout -s TERM 1 sleep 5, where the 1s timer actually fires and delivers
SIGALRM->SIGTERM to the blocked sleep, CRASHES (QMP gone). So the corruptor is
the timer-IRQ signal-delivery path (arm_itimer callback -> deliver_timer_signal
in timer_tick/hard IRQ -> lock_linux + signal_set force-waking a blocked task),
not the arming. Corrects the previous commit's 'arming' conclusion.
…m signal cascade

A stack-scan (dts_scan, reverted) bracketing deliver_timer_signal's entry and
its find_process/thread_ids/signals.insert/signal_set found ZERO corruption
under the crashing repro, yet the VM still crashed. So the itimer's IRQ-context
delivery is clean. The corruption is downstream: timeout's SIGALRM handler frame
setup (handle_signal/setup_uspace) and/or delivering the terminating SIGTERM to
the still-running, nanosleep-blocked child. Records the narrowed next targets
(handle_signal, sys_kill/send_signal_to_process, Process::exit /
PROCESS_TERMINATED callback).
…poses fs-node UAF

The intermittent-corruption crash (repro: timeout -s TERM 1 sleep 5) triple-
faulted DURING exception delivery: the #GP raised on the corrupt executor stack
could not push its frame -> #DF -> triple fault, silently (nothing on serial).

- Give #GP (vector 13) a dedicated IST stack (mirrors the existing #DF IST1) so
  the handler runs on known-good stack instead of triple-faulting.
- In the #GP handler, repair a mangled saved kernel RIP (0xffffff00_00xxxxxx with
  its top byte overwritten -> a  to a non-canonical addr) and resume; tight
  signature (kernel .text pointer, mangled top byte) so it never fires on a real
  #GP or a user address.

Result: the repro no longer silently dies -- it reaches a diagnosable panic:
  [KERNEL PAGE FAULT] vaddr=0x97 rip=0xffffff00001bfee7
  -> <rcore_fs_mountfs::MNode as INode>::metadata, a vtable call through a
     corrupt inner-inode pointer (rcx=0x87). So the true root cause is a
     corrupted/use-after-freed filesystem MNode inode, reached via a path lookup
     during the signal cascade -- matching the original Arc<MountFS>::drop_slow
     symptom. docs/README-crash-repro.md updated with the disasm + next step.
Re-running the repro: one run reaches the diagnosable MNode::metadata #PF panic,
another still silently triple-faults. The wild write sprays small garbage
(0x01/0x0a/0x87) across stack return addresses AND heap Arc/vtable pointers, so
fault interception can't reliably keep the machine alive -- the real fix is to
stop the wild write. Mitigation kept as pure hardening + it surfaced the root
cause.
…memset victim

Two decisive new findings on the intermittent kernel corruption, plus hardening:

- Simpler reproducer: `cat /proc/self/exe > /dev/null` perturbs the same state
  with NO signal handler and NO itimer. It usually hangs silently and ~1 boot in
  ~15 surfaces a clean kernel page fault. This removes the entire signal/timer
  half of the hypothesis space -- the corruption lives in the plain
  exec/path-lookup / /proc/self/exe read machinery.

- The wild write is a `memset` (compiler_builtins set_bytes), and memset is a
  VICTIM: the faulting destination 0xffffff9c0169fbc8 is a valid executor-stack
  heap address with one byte flipped (byte4 0x00->0x9c); the implied span rules
  out a runaway length. Same tiny-value byte-spray signature as the mangled
  return address (top byte ff->01) and the MNode inode vtable (->0x87). All three
  are the same primary writer scattering small byte values across live pointers;
  each fault is wherever a corrupted pointer is next USED.

Instrumentation / hardening left in place:
- kernel-private #PF handler (zCore/src/handler.rs) now walks the frame-pointer
  chain and raw-scans the stack for kernel code pointers via the blocking serial
  writer, using rbp/rsp captured by kstats::note_fault_regs (set in the arch trap
  entry). Prints [kfault-bt] frames naming the memset's caller when the
  diagnosable-fault variant lands.
- MNode poison + inode-pointer guard (vendor/rcore-fs-mountfs) returns EIO and
  logs the clobber pattern instead of dereferencing a garbage vtable; verified it
  does not false-positive on normal fs traffic. Also refined the diagnosis: the
  MNode canary fired 0/6 boots, so the primary corruption victim is the executor
  stack, not heap fs nodes.

Docs updated in docs/README-crash-repro.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…self/exe recursion

busybox standalone re-executes applets via execve("/proc/self/exe"), and
sys_execve stored that LITERAL string as the new image's execute_path. The
magic link then became self-referential: lookup_inode_at("/proc/self/exe")
reads execute_path() == "/proc/self/exe" and recurses without bound. The
coroutine stack is a guard-page-less 128 KiB heap allocation, so the runaway
recursion silently writes thousands of stack frames DOWNWARD into neighbouring
heap allocations -- spraying saved return addresses (the "mangled top byte"
ff->01/0a #GP RIPs), ASCII "/proc/self/exe" bytes, and small values (the 0x87
MNode vtable) over other tasks' state.

This one bug was every observed symptom: the `timeout -s TERM 1 sleep 5`
triple fault (busybox timeout spawns sleep via the same /proc/self/exe re-exec,
so the child's execve itself recursed), the deterministic `cat /proc/self/exe`
hang, the memset-victim #PF, and ALSO the "separate" `timeout 5 true` shell
hang -- confirmed same root cause by the fix.

Fix, two layers:
- sys_execve (linux-syscall/src/task.rs): canonicalize -- when the exec path is
  "/proc/self/exe", substitute the CURRENT execute_path (the real binary path)
  before loading/storing; the literal magic string is never stored again.
- lookup_inode_at (linux-object/src/fs/mod.rs): recursion guard -- an empty or
  self-referential execute_path returns ELOOP like a real symlink cycle.

Verified in QEMU (one boot, zero corruption banners):
- cat /proc/self/exe: hang (14/14 boots) -> exit 0
- timeout -s TERM 1 sleep 5 (x3): triple fault -> exit 143 each, kernel alive
- timeout 5 true: shell hang -> exit 0
- env/readlink/ls regression checks pass; children now named "busybox" not "exe"

Docs: full deduction chain + residual non-fatal quirks recorded in
docs/README-crash-repro.md. Recommended follow-up: guard page under executor
stacks so any future runaway recursion faults cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…ddress 0

The VMAR's first-fit search (find_free_area) started at offset 0, so a process
whose low address space is unmapped could get an anonymous mmap(NULL, ...)
placed AT ADDRESS 0. Userspace treats the returned 0 as a valid pointer; every
syscall null-check then bounces it with EFAULT. Observed as `dd bs>=4096`
failing "Bad address" on any input: glibc placed dd's I/O buffer at 0x0 and
read(0, NULL, 4096) was rejected ([read-efault] + [mmap-trace] instrumentation
pinned it: "anon len=0x1000 -> 0x0").

Fix: a min_offset floor threaded through map_ext_min -> determine_offset ->
find_free_area, applied ONLY by the Linux sys_mmap path (both anon and file
branches) with MMAP_MIN_ADDR = 64 KiB (the Linux vm.mmap_min_addr default).
Every candidate in find_free_area is clamped to the floor so a mapping ending
below it cannot reintroduce a low address. MAP_FIXED is exempt, like Linux.

Scoping matters: a first attempt put the floor globally in determine_offset and
shifted the ELF loader's app sub-VMAR off base 0, which relocated every non-PIE
image (absolute vaddrs 0x400000+) and SIGSEGV'd all of userspace. allocate()
and all non-mmap callers therefore keep floor 0; the constraint is documented
at both sites.

Also keeps a [read-efault] diagnostic in sys_read (fires only on error) that
names the failing layer (inode read vs copy-out) -- it is what caught this.

Verified in QEMU: dd bs=4096/8192/65536 all pass (before: EFAULT); boot, ls,
env, fork/exec all normal; `timeout -s TERM 1 sleep 5` still exits 143 with
the kernel alive; `cat /proc/self/exe` still exits 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
sys_read converted a read chunk STARTING with byte 0x03 (ETX) into a terminal
interrupt whenever the fd was 0 -- without checking that stdin is a terminal.
Any program reading binary data through a pipe on stdin was killed by a
spurious SIGINT whenever a chunk began with 0x03 (guaranteed within a couple
of MB of binary): `cat /bin/busybox | wc -c`, tar/gzip pipelines, X startup
scripts piping data.

Terminal interrupts belong to the line discipline, and both the VT `Stdin`
(termios ISIG, stdio.rs) and the PTY slaves (pty.rs, devfs/pty.rs) already
convert VINTR into SIGINT for the foreground pgrp themselves -- the syscall-
level check was pure downside. Removed, with a comment explaining why it must
not come back.

Note: `^C` at the *serial* console prompt was only ever handled cosmetically by
this hack (it fired when the shell read the byte -- it never interrupted a
foreground `sleep`, before or after). Serial input does not run through the
termios line discipline; routing it there is a separate follow-up. VT and PTY
consoles are unaffected.

Verified in QEMU: cat/dd binary pipelines now complete (2124608 bytes, exit 0;
before: SIGINT/exit 130); crash-repro regression `timeout -s TERM 1 sleep 5`
still exits 143 with the kernel alive; cat /proc/self/exe, dd bs=4096, ls/env
all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
The executor (coroutine) stacks are guard-page-less 128 KiB heap allocations:
a runaway kernel call chain overflows them silently into neighbouring heap
allocations, and the resulting corruption is near-impossible to attribute (the
/proc/self/exe self-reference recursion cost exactly that hunt). The existing
canary at the stack base was only checked AFTER a future yielded -- an
overflowing task never yields, so it never fired.

Now the timer IRQ checks the currently-running executor's base canary on every
tick (x86_64 trap handler -> executor::check_current_executor_canary): any deep
overflow passes through the canary words, so future runaway recursion panics
with a labelled "[stack-canary] COROUTINE STACK OVERFLOW" banner (executor id,
task id, stack base) within ~4 ms instead of corrupting the heap silently.

try_lock discipline throughout (hard-IRQ context): if the interrupted code
holds the runtime lock the check simply skips that tick. Cost: one try_lock +
4 volatile reads per tick.

Verified in QEMU: full regression suite green (binary pipelines, timeout crash
repro exit 143, cat /proc/self/exe, dd bs=4096, ls/env), zero false positives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

Claude/labwc eclipse desktop 08f1ft
…physical size

Real-hardware startx test (RTX 2060 SUPER + 32" 1366x768 TV over HDMI) showed
two display-sizing gaps: the whole system ran at a hardcoded 1024x768 (4:3
stretched across a 16:9 panel) because rboot.conf pinned resolution=1024x768,
and Xorg computed DPI from a fake 270x203mm physical size (the 96-DPI guess)
instead of the panel's real 885x497mm.

rboot:
- New `resolution=auto` (config Resolution enum: Keep/Auto/Exact). Auto reads
  the display's EDID (which rboot already fetches) and picks the GOP mode
  matching the EDID-preferred detailed timing; if the firmware does not offer
  it, falls back to the largest offered mode (firmware-validated against the
  display, and a TV upscales its standard timings far better than it stretches
  a small 4:3 mode).
- `resolution=WxH` no longer PANICS ("graphic mode not found") when the mode
  is not offered -- it warns and keeps the current mode. A config value could
  previously brick boot on any machine whose firmware lacked that mode.
- EDID is read before mode selection (was after).

kernel (DRM GETCONNECTOR):
- Physical dimensions now prefer the EDID when the connector reports none:
  exact mm from the preferred detailed timing (bytes 66-68), else cm from
  bytes 21/22, else the old 96-DPI guess. DPI-aware clients get real numbers.

Both rboot.conf files (template + shipped) switch to resolution=auto.

Verified in QEMU: boots at 1920x1080 (auto picked the largest GOP mode; was
1024x768), clean console render, full regression suite green (timeout repro
exit 143, pipelines, dd, cat /proc/self/exe). rboot fmt/clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

feat(boot/drm): EDID-driven display sizing -- resolution=auto + real …
Two "impossible" unwrap panics were reported from real-hardware/QEMU runs:
- thread.rs:79 (lock_linux) during startx on a pre-fix kernel build
- process.rs:138 (linux()) once during boot

Both are ext-Box downcast failures: every process/thread the Linux layer
creates carries its ext and ext is immutable, so a failure means either a
kernel-internal Zircon object leaked into a Linux-only path or the ext Box was
corrupted (the /proc/self/exe recursion bug produced exactly this signature on
old builds). The bare `Option::unwrap()` panic named neither the object nor the
reason, making the reports unactionable.

Replace both with descriptive panics that print the pid/tid, process name and
the two possible causes. No behavioural change on the happy path.

(Current HEAD boots clean 5/5 in QEMU with zero panics; the boot-time report is
believed to be from a stale kernel ELF, but if it ever recurs the panic will
now identify the process.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

diag(task): name the culprit when linux()/lock_linux() downcasts fail
Real-hardware report: a mixed ESP (new rboot.conf with `resolution=auto`, old
BootX64.efi without auto support) died before drawing anything --
`panicked at src/config.rs:66: ParseIntError { kind: InvalidDigit }` -- because
the resolution parser unwrapped `"auto".parse::<usize>()`. The config file is
user-editable and version-skewed in practice, so the parser must degrade, not
panic.

Hardened every value parser in rboot's config:
- resolution: malformed/unknown values warn and fall back to Auto (which
  itself falls back to keeping the current mode when EDID/GOP give nothing).
- kernel_stack_address / kernel_stack_size / physical_memory_offset:
  malformed numbers warn and keep the built-in default. The hex parser also
  no longer slices `&value[2..]` blind (out-of-bounds panic on short values)
  and accepts values with or without the 0x prefix.

Verified: rboot builds, fmt/clippy clean; QEMU boots to the shell at the
auto-selected 1920x1080 with the hardened binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

fix(rboot): config parser must never brick boot
…n hardware)

startx was a manual runtime chore: a fresh install / QEMU boot came up to a bare
shell and the user had to `apk add` xorg-server, an input driver and fonts by
hand -- and a real-hardware run showed xf86-input-libinput simply missing, so X
started with no keyboard or mouse.

New xtask module `xorg` populates the rootfs at BUILD time via
`apk add --root <rootfs>` (Alpine's supported offline root-install: no chroot,
no running target), pulling the whole stack: xorg-server (built-in modesetting,
which this kernel's DRM scheme drives), xf86-input-libinput, xinit,
mesa-dri-gallium + mesa-gl, xkeyboard-config/setxkbmap/xkbcomp, the base fonts
X refuses to start without (font-misc-misc/cursor/encodings/dejavu), xterm and
xrandr/xset.

Two-image coverage:
- Full rootfs (installed btrfs / real hardware): gets everything -> startx works
  out of the box.
- Live initramfs (QEMU boots this, not the disk): xorg::copy_into_live copies
  the X-owned trees in UNCAPPED (LIVE_KEEP omits usr/bin+usr/lib and the per-file
  cap would truncate them), excluding usr/lib/dri so the RAM image stays bounded
  -- X runs without GL in QEMU, full software GL on the installed system.

Robustness:
- apk 3.x (the Chimera static binary this repo ships) needs --initdb: the empty
  v2-style db mod.rs writes makes it abort "Failed to open apk database".
  Verified: with --initdb + an absolute --cache-dir the command reaches the
  network cleanly (only the sandbox's mirror 403 blocks it here).
- Best-effort like nvidia_firmware: an offline build / unreachable mirror /
  missing package warns and still produces a bootable image.
- Persistent gitignored cache (ignored/apk-cache) + no forced --update-cache, so
  a later OFFLINE build reuses the .apks a prior online build fetched.
- Knobs: ECLIPSE_XORG=0 skips it; ECLIPSE_XORG_PACKAGES="..." overrides the set
  (non-Alpine repos); ECLIPSE_XORG_LIVE=0 keeps the installer initramfs lean.

xtask builds; fmt/clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

feat(build): bake the X.Org stack into the image (works in QEMU and o…
…x was never baked)

`make image` calls `make(false)`, whose incremental path (taken whenever
rootfs/<arch> already exists -- the common case, and one is even checked in)
RETURNED before desktop::install / xorg::install. Those ran only on a
from-scratch `clear` build, so an ordinary rebuild shipped an image with no X
server: the freshly-built QEMU image booted to "sh: startx: not found".

- Call desktop::install + xorg::install on the incremental path too (before its
  early return). apk-add of already-present packages is a no-op, so repeat
  builds stay cheap; desktop::install is idempotent config writes.
- xorg::install now verifies the result and reports LOUDLY: on success it prints
  that the server/startx/libinput driver are present; on failure it prints a
  boxed, unmissable notice explaining that the image will say
  "startx: not found" and how to fix it (network to the mirror, or
  ECLIPSE_XORG_PACKAGES to match the repo's names). No more silent skip.

xtask builds; fmt/clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

fix(build): run Xorg/desktop install on incremental builds too (start…
…: not found" cause)

The build normally runs as an unprivileged user (`make` on a dev box). apk 3.x
refuses to create a package database as non-root without --usermode, so the
build-time `apk add --root` exited immediately with
"ERROR: Use --usermode to allow creating database as non-root" and was warned
past -- shipping an image with no X server ("sh: startx: not found"), regardless
of network. (This is what the user hit; a direct run of the exact command
reproduced the error.)

apk also REFUSES --usermode when run as root ("--usermode not allowed as root"),
so the flag can't just always be passed. Detect the euid via `id -u` (no new
crate dep) and pass --usermode only when non-root; a root/CI build keeps the
privileged path.

xtask builds; fmt/clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

fix(build): pass apk --usermode for non-root builds (the real "startx…
…rg was baked in)

`make qemu` handed QEMU the ESP *directory* via `-drive fat:rw:$(esp)`, whose
built-in FAT emulation is FAT16 and caps at ~504 MiB. Baking the X.Org stack
into the live initramfs pushed the ESP past that, so QEMU aborted before boot:
"Directory does not fit in FAT16 (capacity 516.06 MB)".

justrun (x86_64) now builds a real FAT32 image sized to the ESP contents
(du + 128 MiB) with mkfs.vfat + mtools and boots `-drive file=$(esp_img)` --
the same kind of image a real hardware ESP is. Falls back to the old
`fat:rw:$(esp)` directory method when those tools are absent (fine for small
images / minimal hosts), selected at parse time via `have_vfat`.

Verified the image build in isolation: a 642 MiB payload (133 MiB zcore +
380 MiB Xorg initramfs) mkfs/mmd/mcopy's cleanly and all EFI files land in the
image; `make -n` shows QEMU now points at esp.img. No change to real-hardware
install (install-eclipse writes a real 1 GiB FAT32 ESP already).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

fix(qemu): boot a real FAT32 ESP image, not fat:rw dir (broke once Xo…
The build-time `apk add --root rootfs/x86_64 --initdb` clobbered the base
system: --initdb starts from an empty database, so to satisfy xorg-server apk
pulls the ENTIRE dependency closure (musl, libc, ld-musl, base libs) and writes
it over Eclipse's hand-staged busybox/musl. The result booted to every VT shell
SIGSEGV'ing identically (jump to a garbage PC 0x6100 through the broken loader),
one of which tripped the new descriptive panic "Thread::lock_linux(): ... has no
LinuxThread ext" during teardown.

Fix: install into a throwaway staging root (ignored/xorg-stage), then copy ONLY
the X-owned trees into the real rootfs — usr/bin, usr/lib, usr/libexec,
usr/share, etc/fonts, etc/X11 — never bin/ lib/ sbin/ or base /etc. The base
loader/libc/busybox are preserved; X is purely additive (musl lives in /lib,
which is skipped, so usr/lib carries only the X + mesa libs). Also create the
`libc.musl-x86_64.so.1 -> ld-musl-x86_64.so.1` alias the Alpine X binaries link
against, additively, so they resolve libc against the base's own musl.

xtask builds; fmt/clippy clean.

NOTE for anyone with an already-broken rootfs from the previous build: clear it
(`rm -rf rootfs/x86_64`) before rebuilding so the base is re-staged fresh — the
incremental build path does not restore a clobbered /lib/ld-musl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

fix(build): install Xorg into a staging root, not over the base rootfs
… run

After the staging fix the base boots clean, but Alpine's dynamically-linked X
binaries still crashed: `mcookie` (run by startx) SIGSEGV'd at pc=0x1bd0
("Couldn't create cookie"). Cause: they are linked against Alpine's musl, but
the rootfs shipped Eclipse's own musl-cross `ld-musl-x86_64.so.1`; a
newer-musl binary on the older loader jumps to a bogus low PC.

musl keeps a stable, BACKWARD-compatible ABI, so make Alpine's musl the single
loader: copy just `lib/ld-musl-x86_64.so.1` out of the (already-fetched)
dependency closure into the real rootfs (0755), replacing Eclipse's. Eclipse's
older base binaries keep running on the newer loader; the Alpine X binaries get
the musl they were built against. This is the "one musl for everything" the user
asked for, done with a single file rather than clobbering the whole base (which
is what broke boot before). The `libc.musl-x86_64.so.1` soname alias is created
regardless of which loader is in place. ECLIPSE_XORG_MUSL=0 opts out.

xtask builds; fmt/clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

feat(build): use Alpine's musl as the one loader so Alpine X binaries…
With the Alpine-musl loader in place, `mcookie` now creates the xauth cookie
and the Alpine Xorg binary actually runs — far enough to try opening its log
and abort fatally: "Cannot open log file /var/log/Xorg.0.log". The staged base
has no /var/log. Create it in the full rootfs (xorg staging) and in the minimal
live/installer root (build_live_rootfs), which is what boots under QEMU. Xorg
creates /tmp/.X11-unix itself at runtime, so only /var/log is needed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
Pryancito and others added 30 commits August 5, 2026 22:11
…ao0ib

fix(lunarbar): agent-review findings — keycodes, popup zone, stale-in…
A dynamically-linked crash (labwc and its libraries) left only
`unhandled page fault @ … pc=0x7f…` plus a `Done(139)` — the PC is an absolute
address with no indication of which mapping it belongs to, so it could not be
symbolised. Add `VmAddressRegion::describe_addr`, which locates the mapping
containing an address and returns its base, the byte offset into the backing
file (VMO), and the backing object's name (a file path for a file mapping,
already set by the file get_vmo path; empty for anonymous memory).

The SIGSEGV handler now prints, for both the faulting PC and the bad data
address:

  [crash] pid=N pc 0x7f… in /usr/lib/libwlroots.so.13 + 0x1234 (map base 0x7f…)

so the exact fault can be located offline with `addr2line -e <file> 0x<off>` /
`objdump`, turning an opaque compositor `Done(139)` into a pinned crash site.
Anonymous mappings (heap/stack/JIT) print as `[anon] + off`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

diag(crash): name the library + offset a userspace SIGSEGV faults in
init wires a supervised service's stdio to /dev/null, so a black-screen labwc
bring-up left no diagnostics anywhere — impossible to tell an output problem
(no DRM output / mode) from an input one (libinput found no pointer) from a
plain render issue. The labwc wrapper now redirects the real binary's stdout
and stderr to /tmp/labwc.log, so `cat /tmp/labwc.log` after a failed or blank
start shows wlroots' backend/output/connector discovery, the libinput devices
it opened, and any errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

diag(labwc): capture the compositor's log to /tmp/labwc.log
This reverts commit a3418cc.
labwc runs stably now (the SIGSEGV is gone) but the screen is black with no
cursor, and /tmp/labwc.log is empty — labwc's default WLR_ERROR level logs
nothing when it starts without errors, i.e. it likely composited fine and the
frame just never reached the display. Since the software cursor is drawn INTO
the frame (WLR_NO_HARDWARE_CURSORS=1), "black" and "no cursor" are one symptom:
the composited frame isn't being scanned out.

Two diagnostics to localise it in a single rebuild:

- labwc wrapper now passes `-d` (labwc's documented debug flag → wlroots
  WLR_DEBUG), so /tmp/labwc.log shows backend/output/mode/page-flip activity —
  telling us whether wlroots even creates an output and page-flips.

- `present_now` logs, one-shot: the first present (fb id, crtc, active vs
  graphics VT, software-KMS path) and the first VT-gated drop. This separates
  "compositor never presents" (no present log) from "presents are suppressed
  because a text VT is foreground" (drop log) from "presents happen but the
  blit doesn't show" (present log, no drop, still black).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

diag(labwc,drm): full wlroots debug log + one-shot present/VT-gate trace
`copy_into_live` (the step that stages usr/bin + usr/lib + the glibc loader
trees into the RAM-resident live root QEMU actually boots) only ran when Xorg
was installed in the full rootfs. labwc reached the live initramfs only as a
side effect of that Xorg sweep — so a labwc-only build, or one where the Xorg
apk step failed, booted with the compositor binary and libwlroots ABSENT.

The wrapper (usr/local/bin/labwc, which IS in LIVE_KEEP) then found no
/usr/bin/labwc, printed "real binary not found (apk add labwc)" and exited
127, and eclipse-init's respawn loop restarted it forever — a black screen
with the compositor never actually present, easy to misdiagnose as an output
or input bug when the binary was never there to begin with.

Gate `copy_into_live` on `have_xorg || have_labwc` instead of `have_xorg`
alone, and move the Xorg apk-enabled check (`enabled()`) onto the `have_xorg`
term only, so `ECLIPSE_XORG_LIVE` remains the master switch but a labwc-only
build (or `ECLIPSE_XORG=0`) still gets its compositor staged into the live
root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
Recoge los hallazgos concretos de la busqueda de fallos: aliasing UB en
ROUTES_STORAGE con dos NICs, deteccion de fin de TX por TDH en vez del
bit DD, offsets de FEXTNVM6/7 que apuntan a MMIO reservado, SECRC sin
activar fuera de PCH, IDs igb que el resto del driver no puede manejar,
reensamblado RX inalcanzable, y una decena mas de severidad media/baja.
Sin correcciones aplicadas todavia.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFLBAPffWxDbLjrj9TEoW4
docs(e1000e): auditoria de bugs del driver e1000e.rs
Cambios en drivers/src/net/e1000e.rs:
- ROUTES_STORAGE: Box::leak por-NIC en vez de static mut compartido
  (aliasing UB + rutas cruzadas entre tarjetas).
- can_send()/send(): deteccion de fin de TX por el bit DD del descriptor
  en vez de una lectura de TDH (Intel documenta que TDH refleja prefetch,
  no el write-back real); init_tx pre-marca el anillo TX con DD=1.
- FEXTNVM6/FEXTNVM7: offsets corregidos a 0x00010/0x000E4 (antes caian en
  MMIO reservado junto a PBA y los workarounds de ULP/SPT eran no-ops).
- RCTL_SECRC: incondicional (antes solo en is_pch(), dejando 4 bytes de
  FCS pegados a cada frame en 82574L/QEMU).
- matched(): se retiran los IDs de familia igb (I210/I211) que el resto
  del driver no puede manejar (RXDCTL nunca se habilita para ellos).
- poll_pending: auto-recuperacion si la cola de jobs diferidos evictó el
  bottom-half y dejo el flag/IMS atascados.
- process_rx_slot: tope de reensamblado real (antes BUF_SIZE hacia la
  rama de fusion inalcanzable) y contabiliza el descarte en rx_dropped.
- tx_dropped/rx_csum_bad: campos por instancia en vez de estaticos de
  archivo compartidos entre NICs.
- Nombre de interfaz: incluye device/function del PCI para no colisionar
  entre dos NICs en el mismo bus.
- CTRL_FRCSPD/CTRL_FRCDPX: bits corregidos (11/12, estaban al reves).
- FWSM_FW_VALID: constante duplicada y sin uso, eliminada.
- TIPG: IPGR2 corregido a 6 (antes 12).
- TXDCTL.QUEUE_ENABLE: log de aviso si el latch no llega a activarse.

Anade el modulo de test tx_ring_tests (4 tests) que ejercita el nuevo
camino DD-bit de TX contra un NIC simulado. cargo test --features mock
e1000e: 11/11 OK. cargo build (no_std real): OK. cargo clippy: limpio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFLBAPffWxDbLjrj9TEoW4
…k-screen cause

Root cause of the labwc black screen / missing cursor, found by reproducing it
end-to-end in QEMU with a glibc labwc/wlroots stack: the in-kernel ELF loader
(`zircon_object::util::elf_loader::relocate`, which eagerly relocates the ELF
interpreter itself before it ever runs — this kernel's substitute for a real
ld.so's userspace self-bootstrap) silently SKIPPED `R_X86_64_IRELATIVE`
(type 37) relocations, leaving those GOT slots at zero.

glibc's own dynamic linker links against CPU-feature-dispatched IFUNC routines
(optimized memcpy/memset/strlen/…) even for internal use during its own
bootstrap, so a real glibc interpreter's `.rela.plt` legitimately carries many
IRELATIVE entries. Every call through an unresolved one jumped to address 0.
This explained the exact observed symptom: the interpreter bootstraps far
enough to load and run the main program, and labwc logged correctly through
its whole config/output-setup phase (none of which happened to call a broken
slot) — modeset even succeeded — but the first render pass, heavy on
memcpy/memset for the software (pixman) blit path, hit one and the process
went silently dark. No crash, no log line: just nothing ever drawn.

Unlike every other relocation type here (which write a fixed address),
IRELATIVE's value is the RETURN VALUE of *calling* the resolver — exactly what
a real ld.so does for its own IFUNCs (glibc's `elf_ifunc_invoke`). Implemented
by actually running the resolver: a brief, synchronous ring-3 excursion into
the target address space using the same primitive every thread already uses
(`UserContext::enter_uspace`), with a deliberately-unmapped sentinel return
address so the resolver's `ret` traps into a recognisable, clean instruction-
fetch page fault instead of jumping into garbage. A hardware interrupt (the
timer) arriving mid-call is serviced and the same call is resumed exactly
where it left off, precisely as the normal thread-execution loop already does
for ordinary user code. Any OTHER trap aborts the whole batch rather than
writing a possibly-bogus value into a live GOT slot — the same fail-closed
contract `relocate()` already has for every other error path.

`relocate()` gains a second `scratch_vmar` parameter: the interpreter's own
sub-VMAR is sized exactly to its LOAD segments with zero spare room for the
resolver's throwaway stack page, so the scratch mapping borrows space from the
PARENT address space (passed in from both `loader/mod.rs` call sites) instead.

x86_64-only (the only arch this was reproduced and verified on); every other
architecture keeps the previous skip-and-warn behaviour unchanged.

Verified in QEMU with the full glibc labwc/wlroots/lunarbg/lunarbar/foot
desktop stack: before this fix, labwc modesets and then goes silent (black
screen, no cursor); after, lunarbg renders the wallpaper, lunarbar's panels
come up, foot's terminal starts, and the compositor shows a fully rendered
desktop with a visible cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
`present_now`'s one-shot "first present" log (added earlier this session)
passed `DRM_STATE.lock().graphics_vt` directly as a `warn!()` argument. Rust
extends a temporary's lifetime to the end of its ENCLOSING STATEMENT, so that
MutexGuard stayed alive for the whole macro call — including the log line's
formatting and serial write, not just the field read. `present_now` takes the
same lock again a few lines later for VT-gating; on a slow serial console that
window was wide enough to strand another CPU on it, tripping the >8s deadlock
detector (`HOLDER cpu=N at drm.rs:818`, the line this diagnostic added).

Read `graphics_vt` into a local first, so the guard drops immediately after
the field read — before `warn!` ever runs — matching how the field is read
everywhere else in this file. Found while reproducing the labwc black screen
end-to-end in QEMU: this diagnostic's own bug intermittently wedged the
compositor's very first present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

Claude/labwc eclipse desktop 08f1ft
fix(e1000e): corrige los 13 bugs de la auditoria previa
A labwc build that hangs right after "Loading user-specified backends due to
WLR_BACKENDS: drm,libinput" (the log line printed just before wlroots opens
its libseat session) is most likely blocked in the libseat<->seatd handshake —
but seatd's own stdout/stderr was wired to /dev/null by init like every other
supervised service's, leaving no visibility into whether seatd ever accepted
the connection, what backend/permission state it saw, or where the handshake
actually stalled. Same fix as the labwc wrapper's /tmp/labwc.log: redirect
seatd's output (with -l debug for full protocol tracing) to /tmp/seatd.log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
…8f1ft

diag(seatd): capture seatd's own log to /tmp/seatd.log
El camino RX no agrupaba ningun acceso MMIO por paquete: cada frame
recibido pagaba su propio round-trip completo, sin importar cuantos
llegaran en la misma rafaga. Bajo QEMU (el objetivo de pruebas habitual
de este driver) cada acceso MMIO tipicamente dispara una VM exit
completa, asi que esta sobrecarga por-paquete domina el coste real de
mover los bytes.

Cambios en drivers/src/net/e1000e.rs, todos en el camino RX:

- receive() cachea RDH una sola vez por llamada en vez de releerlo por
  MMIO en cada iteracion del bucle de drenaje (hasta 2 lecturas por
  iteracion antes). process_rx_slot ya no relee RDH: el invariante lo
  garantiza quien llama.
- El doorbell RDT se difiere (rx_doorbell_dirty) y se agrupa con
  flush_rx_doorbell(), llamado una vez por rafaga en poll_with_irq_hint
  (tras iface.poll()) y en NetScheme::recv(). Se elimina tambien la
  lectura de flush sincrona que seguia a cada escritura de RDT.
- ensure_rx_armed_if_link_up ya no relee STATUS por MMIO cuando el
  enlace ya se sabe activo (se invoca en cada poll).
- La invalidacion de cache del buffer RX usa la longitud real del frame
  en vez de BUF_SIZE completo (2048B) — nada lee mas alla de esa
  longitud.

Nuevo test rx_doorbell_is_batched_not_rung_per_packet que verifica el
agrupamiento explicitamente. Los 13 tests existentes (RX, TX, coherency
bench) siguen en verde; cargo build (no_std real) y clippy limpios.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFLBAPffWxDbLjrj9TEoW4
perf(e1000e): agrupa el doorbell RDT y elimina MMIO redundante en RX
La fila DISK del banco corria en el cwd del shell, que en ambos harnesses
es una raiz en RAM: medias la cache de paginas, no un filesystem sobre un
dispositivo de bloque. Esto adjunta una imagen btrfs identica a los dos
kernels por el MISMO controlador ich9-ahci, la monta y mide alli, asi que
el numero refleja las rutas de escritura/lectura/metadatos de btrfs sobre
el driver AHCI y nada mas.

Tres piezas:

- `ROOTKEEP=1` (linux-object/fs): conserva el medio de arranque como / y
  se salta el auto-pivot. Sin el, el arranque agarra como raiz CUALQUIER
  btrfs/ext2 de disco completo que encuentre -- correcto para un sistema
  instalado, pero secuestra / en cuanto se adjunta un disco de datos. Con
  el, el disco extra queda sin montar para que el usuario lo monte donde
  quiera. Es un arreglo real, no solo para el banco.

- `-d IMG` en los dos harnesses (scripts/qemu-{,linux-}bench.sh): adjunta
  una imagen raw por su propio ich9-ahci, fuera de la ruta de arranque, de
  modo que el dispositivo bajo prueba es inequivoco. El harness de Linux
  ademas descomprime y carga la pila modular libahci+ahci+btrfs desde el
  /lib/modules del anfitrion antes de montar (ahci y btrfs son modulos en
  el kernel de la distro; libata/sd_mod son builtin).

- scripts/qemu-btrfs-bench.sh: formatea una imagen maestra con el layout
  exacto que la crate btrfs in-tree de Eclipse emite y lee
  (-O ^free-space-tree, crc32c, nodos de 16K), da una copia fresca a cada
  kernel (QEMU escribe in situ), corre --only disk sobre el montaje btrfs
  en ambos y los imprime lado a lado.

Verificado de extremo a extremo: Eclipse monta /dev/sda btrfs y
escribe/lee; Linux carga los modulos, monta el mismo /dev/sda y
escribe/lee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwhtMqzF6Lq18WnrhJD3zR
…zada

La comparacion pareada (misma imagen btrfs, mismo ich9-ahci, working set
de 128 MiB) sobre disco real en vez de la cache de paginas. Eclipse pierde
en todo y catastroficamente en lectura: secuencial 2,8 vs 379 MB/s (135x),
aleatoria 4K 35 vs 29132 IOPS (828x), 28 ms por lectura de 4K.

La causa es la ruta de I/O de bloque, tres factores multiplicandose, los
tres ausentes en Linux: AHCI de profundidad de cola 1 por sondeo (un
comando en vuelo, busy-wait antes del siguiente; Linux usa NCQ x32 con
interrupciones), cache de bloque de 8 MiB contra 128 MiB de working set
(94% de fallos; Linux cachea en la page cache del tamano de la RAM), y
amplificacion de metadatos de btrfs (cada lectura logica de 4K recorre
dos B-trees de nodos de 16K, y con la cache pequena cada una dispara
varias lecturas fisicas a profundidad 1).

La escritura sufre menos (5,7x) por agrupacion y la latencia de fsync
empata: el coste esta en el volumen de lecturas concurrentes, no en la
barrera de durabilidad. Se documentan las tres palancas por orden de
impacto/coste (cache mas grande = una constante; NCQ+IRQ = rediseno del
driver; readahead secuencial), medibles con el harness nuevo de una
pasada.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwhtMqzF6Lq18WnrhJD3zR
…aults; virtio-blk multi-sector panic

btrfs's per-inode read-extent cache always extended its scan from the OLD
high-water mark to the new offset, and never shrank: a page fault landing
past the cached range (exactly what a dynamic linker's scattered access into
a large mmap'd .so looks like) forced a full extent-tree walk over the
entire abandoned gap, and kept every extent seen so far in a Vec that gets
linearly rescanned on every subsequent read. Cost grew with how far into the
file a fault landed, not with how many pages were actually touched. Jumping
past the cached range now restarts the scan at the requested offset instead
of dragging the gap along, so a scattered access pattern costs work
proportional to what it actually reads.

Verified against the existing btrfs-rs test suite (unchanged, all passing)
and a host-side benchmark against a real generated rootfs.btrfs image.

Separately, VirtIoBlk::{read,write}_block forwarded whatever buffer length
BlockScheme callers passed straight to the vendored virtio-drivers crate,
whose VirtIOBlk::{read,write}_block hard-assert exactly 512 bytes and moves
one sector per call. linux-object's BlockByteDevice/CachedDevice issue
multi-sector requests (e.g. the 64 KiB read-ahead) to any BlockScheme, which
would panic the kernel the first time it hit virtio-blk. Split multi-sector
requests into per-sector calls, matching how the AHCI driver already handles
arbitrary-length buffers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nKUQTKTwoqrwgwQk7DiP9
…perf-889jeu

fix(btrfs+virtio-blk): O(touched-range) extent rescans on mmap page f…
…128x

El comparativo btrfs sobre AHCI dio lectura aleatoria de 4K a 35 IOPS
(28 ms/op), 828x tras Linux. La causa NO era el driver: `dd` crudo sobre
/dev/sda da 108 MB/s en bloques grandes. Era la politica de cache y
readahead de CachedDevice sobre el.

Dos fallos, ambos medidos:

1. El readahead se aplicaba a TODA lectura menor que la ventana, aleatoria
   incluida. Cada lectura de 4K arrastraba la ventana entera (1 MiB = 256x
   de mas) y expulsaba justo los metadatos de btrfs que la siguiente
   lectura volvia a pedir -- puro thrash. Ahora el readahead solo se
   dispara en continuacion byte-exacta de la lectura anterior: un stream
   secuencial colapsa en un comando grande por ventana; un paseo aleatorio
   pide solo sus 4K y deja el cache lleno de los metadatos que reusara.

2. El cache era de 8 MiB contra working sets mucho mayores. Subido a
   64 MiB, cubre los metadatos de btrfs y buena parte de los datos
   calientes.

Medido en QEMU, misma imagen btrfs sobre el mismo AHCI:

    lectura aleatoria 4K   35 -> 4472 IOPS     128x
    latencia aleatoria     28380 -> 224 us     127x
    escritura secuencial   7,7 -> 31,9 MB/s    4,1x
    lectura secuencial     2,8 -> 10,3 MB/s    3,7x
    fsync (mejor)          6,43 -> 1,72 ms
    crear ficheros         134 -> 240 files/s

Probada y descartada por medida la deteccion floja de secuencialidad
("hacia delante dentro de una ventana"): hundia el aleatorio 10x
(4472 -> 446 IOPS) porque btrfs si mete lecturas aleatorias dentro de una
ventana; la version estricta gano con datos. Arranque normal verificado
sin regresion con el cache mayor.

Queda abierta la lectura secuencial (10 vs 108 MB/s crudos): los
metadatos intercalados de btrfs rompen la cadena secuencial estricta, asi
que muchas lecturas no disparan readahead. Necesitaria estado de readahead
por-stream, no una constante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwhtMqzF6Lq18WnrhJD3zR
…peed-9fkd4w

Claude/eclipse os processing speed 9fkd4w
This branch never merged master's 1b1d289 ("mejoras a drivers, velocidad,
procesamiento y demas"), so has_ready()'s lock-contention fallback and
AHCI's zero-copy DMA are already in their safe state here -- but a future
merge from master could silently reintroduce either:

- has_ready(): that commit flipped the try_lock() failure fallback from
  true to false, contradicting its own doc comment and reintroducing a
  lost-wake class of bug (a CPU can halt through a wake it hasn't
  observed yet, with no timer backstop in the executor's idle path).
- AHCI zero-copy DMA: that commit re-enabled a path deliberately disabled
  with `&& false` since June, with no coherence/pinning audit against
  real hardware.

The equivalent functional fix (flip false back to true, redisable the
DMA path) still needs to land on master itself, where 1b1d289 actually
lives -- a direct push there was blocked by this session's auto-mode
guard, so it's pending a separate approval/route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHCn5RNRcwR5PY1sYBHY2S
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.

3 participants