Commit graph SchOS/.claude
Author SHA1 Message Date
Nexoniarz
a053a96add signal: the return trampoline was Linux-only, and a test that proves the rest
Went looking for why `X=$(echo hi)` hangs every shell. Did not find it. Found
two other things, and built the instrument that rules out the largest suspect.

**The signal return trampoline was ABI-blind.** deliver_signals() manufactures
a return address for the handler -- seven bytes on the user stack reading
`mov eax, 15 ; syscall`. Fifteen is *Linux's* rt_sigreturn, which is right for
anything musl-linked, and that is every port and every shell. For a first-party
binary it is native syscall 15, which is `open`. So a handler in any cmd/
program ran, returned into the trampoline, called open() with whatever the
handler had left in its registers, got -ENOENT, and carried on executing the
stack bytes after the trampoline.

    pid 14 sigtest: trap 14 err 4 rip 0x46ca7 addr 0xfffffffffffffffe
      rax=fffffffffffffffe ... rsp=46ca0
      ustack: 46e30 46ca0 50f0000000fb8 0 0 0

rip is rsp+7 -- one past a seven-byte trampoline -- and the third word of the
stack is the trampoline's own bytes. It emits the number for the process's ABI
now; syscall_map is the thing that says which numbering this process speaks.

**SIG_BLOCK, SIG_UNBLOCK and SIG_SETMASK were not defined anywhere.**
sys_sigprocmask branched on bare 0, 1 and 2 with the names in comments, so
anything outside the kernel had to spell the number and hope it had the right
one. Defined in posix.h, used in the kernel.

**cmd/sigtest** asserts the SIGCHLD/sigsuspend contract every shell is built
on, and it is written so a permissive stub cannot pass: a blocked SIGCHLD stays
pending, sigsuspend delivers an *already pending* signal without spinning, the
handler runs exactly once, the mask is restored, the child is still reapable,
and a signal arriving mid-wait also wakes it. Eight checks, all green. The
kernel's half of that contract is now proven rather than assumed, which is what
lets the remaining bug be pursued somewhere else.

Along with it, `rt_sigsuspend` in lib/usys.S -- the syscall existed and the
first-party library simply never exposed it.

On the hang itself: it is not job control, not SMP, not zsh (mksh does it too),
and not any value the kernel returns -- a full syscall trace is in
docs/history/kernel.md section 7, which also records what the contradiction
narrows to. That section had said "root cause not yet isolated" with three
suspects; all three are now eliminated, and the note says what to do next
instead of what to guess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 23:56:34 +02:00
Nexoniarz
3185685997 syscall: three globals on the entry path, shared by every CPU
One to four processes died per boot with a fault they could not have caused:
`rip 0x0` -- an instruction fetch at zero, which a process does not branch to
-- or a dereference of `0xffffffffffffffea`, which is `-EINVAL`. It hit shells
and schinitd alike and had been carried for a long time as "zsh sometimes jumps
into .data".

`syscall_entry` has to swap FS_BASE from the user's TLS to the kernel's before
it can touch any `%fs:` data, and `wrmsr` destroys rax, rcx and rdx. Those have
to be parked somewhere first -- not on the user stack, which is untrusted and
has not been switched away from yet. They were parked in three globals:

    // Single-CPU scratch for syscall_entry FS swap
    // (safe while IF cleared by SYSCALL/SFMASK)
    uint64 _sc_rax, _sc_rcx, _sc_rdx;

IF being clear answers for an interrupt arriving on *this* CPU. It says nothing
whatever about a second CPU executing the same three stores at the same
instant, and with more than one processor entering syscalls that is not a rare
interleaving -- it is the ordinary one.

**%rcx is the user return address.** SYSCALL puts the user's rip there and
SYSRET takes it back from there. So the CPU that lost the race returned its
process into *another process's* instruction stream, with %rax -- the syscall
number on the way in, the return value on the way out -- swapped alongside it.
Both symptoms fall straight out of that: a zero handed over and followed, and
an errno used as a pointer.

Per-CPU words at %gs:32..48 now. %gs is usable there, before the FS swap, for
exactly the reason RESTORE_KERNEL_FS can already read %gs:0: GS base is per-CPU
in hardware, seginit() sets it once, and userspace cannot change it --
sys_arch_prctl() implements ARCH_SET_FS and ARCH_GET_FS and rejects the rest.

Measured rather than argued. Both variants built once, `build/esp` saved for
each, the same pristine schfs.img restored before every boot, -smp 4:

    three globals     10 boots    10 faults, in five of the ten
    three per-CPU     18 boots     0 faults

A _Static_assert pins all seven per-CPU offsets, because trapasm.S spells them
as literals and cannot include a header -- a slot that moves would leave a
register quietly holding somebody else's value, which is the hardest possible
thing to find.

**This is the second bug of this exact shape in this file.** The first was
`kernel_fs_base`, a single global every CPU wrote during its own seginit(), so
the last writer won and every CPU restored one CPU's per-CPU block;
RESTORE_KERNEL_FS reads %gs:0 for that reason. The lesson generalises and is
now written down in both places: **on the entry path, a global is a bug.**

Closes the row added to Gate 0 four commits ago. It was the right row to put
first -- a compositor built on this would have failed in ways indistinguishable
from its own bugs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 22:56:45 +02:00
Nexoniarz
953f9c5c3c branding: one mark, and PSOGM draws it
Guru and SchCotie are gone. Shift is the only mark, and the crash screen draws
it where the face used to be.

Guru was an icon -- a pictogram with a face, denoting one state -- that lived
on the crash screen and nowhere else. Its argument was identity: `:(` is
recognisably Microsoft's, and a crash screen is the single screenshot most
likely to leave the machine, so it is the worst place to be wearing somebody
else's face. That part holds, and it is kept in the branding README because it
is the argument against any second mark. What does not follow is that the
answer is a *new* mark. Shift already signs the system, it is already what the
machine drew on the splash it started with, and a reader who has seen it twice
before the crash recognises it faster than a face they have never seen.

SchCotie was a mascot, and never existed: the file said so itself under "what
this does not cover". A brief for a character nobody is about to draw is not
worth carrying.

So `draw_guru()` and its SVG-space geometry leave panic.c, the two `guru.svg`
files are deleted, and PSOGM calls gfx_draw_logo() -- the same include/logo.h
mask the loader splash and the boot console composite. At its native 128 px:
there is no resampler in the crash path, and the face was scalable in a place
that only ever drew it at one size.

The name stays. "Purple Screen of Guru Meditation" is the Amiga nod, which
panic.c's own header already called out as a separate thing from the mark.

**And taking the screenshot to check it caught a regression from the TTF
console work.** `gfx_draw_char()` -- the pixel-positioned path, which only
PSOGM uses -- still walked `font_data[c][row]` for `row < CHAR_HEIGHT`.
font_data has exactly 16 rows per glyph; CHAR_HEIGHT is the *TrueType* cell
height now, about 21. So every line of the crash report drew five rows of the
next character's bitmap underneath itself, and it looked like each line had
been printed twice. The grid path had been updated when the cell size stopped
being a constant; this one had not, because nothing routine draws text in pixel
coordinates. Both go through one draw_glyph_px() now, and the bitmap's own
geometry is spelled BITMAP_W/BITMAP_H so it cannot be mistaken for a cell size
again.

Two stale claims corrected while in there, in panic.c's header and in
graphics.md: PSOGM has not dimmed-and-floated over the old screen for a while
-- it clears to full purple, and the file's own code comment says why -- and
there is no longer "a face that is obviously a cartoon".

The website's crash section shows the mark in flat white rather than its own
violets, which is what the kernel actually draws: PSOGM composites a coverage
mask in one colour, and those violets sit close enough to #4B1D9A to
half-disappear against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:35:58 +02:00
Nexoniarz
e577791e5e memory: the SMP crash is SMP-only, and what the font work cost
Two notes worth keeping out of the commit log because they are about where to
start next time rather than what changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:05:52 +02:00
Nexoniarz
57aff40228 docs: four directories, and a rule for when a thing leaves the roadmap
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.

A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.

So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.

schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.

Three corrections found on the way, all load-bearing:

  * projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
    sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
  * The root README said "No TCP/IP" months after the stack landed.
  * history/kernel.md sec.7 was marked "in progress" on a problem that process
    groups solved.

Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 17:13:35 +02:00
Nexoniarz
58296b3376 memory: the SchFS and platform roadmap, and the 614 MB ceiling
Records the direction set out today -- snapshots, scrub, compression and
encryption for SchFS; one shared home for the format; a unified second
stage under both firmwares; audio -- and the two requests that were
pushed back on, with the reasoning, so neither gets re-litigated from
scratch: 128-bit addressing buys nothing over the 64 ZB already
addressable, and SchFS cannot live in projects/ because the kernel has to
mount it to boot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:53:32 +02:00
Nexoniarz
50e61e431b memory: Schcom v0, and the order change that put it before the installer
Records that the GUI stack moved ahead of the installer, that Schcom v0
runs and how to prove it, the decisions behind the protocol shape
(Wayland-shaped, MIT so linkable, shm id rather than SCM_RIGHTS for now),
and the one-display-device trap in the screendump test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 23:12:56 +02:00
Nexoniarz
7b2d4037a4 Rebuild when RAMROOT is toggled, and add an opt-in KVM target
`make ramroot` wiped the objects on the way in, and nothing wiped on the
way out. So a plain `make` afterwards kept every RAMDISK_ROOT object and
linked the 24 MB ramdisk back into a kernel that no longer used it: 25 MB
where a clean build is 836 KB. It still booted -- from the ramdisk -- and
the only outward symptom was stage2 having thirty times as much to load
through PIO, one sector at a time. That is the "booting got slower"
report this came from.

A stamp file records which setting the objects were built with, and the
tree is dropped when it changes in either direction.

qemu-kvm is opt-in and the default stays TCG, because the suite's timings
were established there. Measured with the guest timing itself,
`ls /bin | wc -l` goes 2.9s -> 1.1s. Boot does not improve and gets
slightly worse: every PIO port access in stage2 is a VM exit under KVM.

Retracts a claim from earlier today. The "2.2x speedup" attributed to the
region-based allocator was a measurement artifact -- the probe sent a
command, slept a fixed 50s on the host, then read /proc/uptime, so it
timed the sleep rather than the command, through a guest clock that drifts
under load. Bracketing the command inside one guest command line shows no
measurable difference. The e820 change is still correct; it is simply not
a performance change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:46:00 +02:00
Nexoniarz
85f0ebf802 memory: the installer is scoped, and the live CD is off the list
Records that a mountable ISO 9660 root was called off -- the ISO already
boots a complete system from the ramdisk, so it is the live environment
and an installer makes a mountable disc redundant, which drops the ATAPI
driver and ISO 9660 filesystem entirely.

Also records the installer's shape (both firmwares from the start, .sip
before anything installs), the five-stage dependency chain, and the 4 GB
limit the raw block device carries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:20:20 +02:00
Nexoniarz
b428a50145 memory: UEFI reaches userspace, and the ISO grows a hierarchy
Records the four bugs between "boots to cpu0: starting" and "boots to an
interactive shell", the new ioremap rule for device registers, and what
the disc now contains -- including that it is still not a live CD, and
what that would take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:04:12 +02:00
Nexoniarz
6e23d2a466 bootcheck: make the prompt wait survive a slow guest
Three bugs, all of which surfaced as a failed check somewhere other than
the command that was actually slow.

Wait-Prompt waited a fixed 20 s. These tests run under TCG, not KVM, and
a cold `ls /bin | wc -l` takes 23 s on the Fedora laptop measured with
the guest's own /proc/uptime -- it was over the wall every time. Raised
to 60 s; a long timeout only costs time when something is genuinely
wrong, a short one costs a false failure elsewhere.

It also matched a prompt anywhere in the transcript. Between writing a
command and the guest echoing it the log still ends with the previous
prompt, so it could return before the command had started and the next
one was typed on top of it. Anchor the match to text produced after the
command was sent.

On timeout it slept two seconds and typed regardless, which is what
turned one slow command into an interleaved transcript and a failure
attributed to a later check. Wait for output to stop arriving instead.

Report any command that never produced a prompt next to the result:
after one overrun every later check is reading a transcript that may be
interleaved, and that should not be something you have to notice in the
scrollback.

With this, -Fb -Tty -Usb -Sock -Evdev -Ps is 108 [ok], 0 misses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:18:09 +02:00
Nexoniarz
c55e65de5e memory: the e820 work, and why the extended suite flaps
Records the measured effect of the region-based allocator (2.2x on a cold
ls, 6 s off the boot), the two pre-existing UEFI blockers found while
verifying it, and the three confounds that each produced a confident
wrong answer about guest speed in one session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:05:34 +02:00
Nexoniarz
15331d4399 Fix two bugs the Fedora move exposed
Neither is Fedora-specific. A different host and a freshly built
userland simply drove code that Windows never reached.

tools/mkfs.c did not compile at all. The DT_UNKNOWN/DT_DIR/DT_REG
fallbacks sat above <dirent.h>, so on glibc -- which spells them as
enumerators -- the #ifndef asked before the header was read, always
answered "missing", and then the header's own enum expanded to
`enum { 0 = 0, ... }`. Move the fallbacks below the include. The
include cannot move up instead: it has to follow the #undef that ends
the `#define dirent schfs_dirent` block.

getstackpcs() page-faulted. It dereferenced rbp[1] without validating
rbp, and the guard rejected 0xffffffff -- dead code on x86-64, already
caught by the KERNBASE test -- while the value that actually turns up
is all-ones, which is above KERNBASE. rbp[1] then wrapped to 0x7 and
took trap 14. `cp` panicked the kernel every time. Bound rbp from
above and require 8-byte alignment.

That second one masks other bugs: getcallerpcs() runs on every lock
acquisition and again from the trap handler, so a walker that can
fault replaces a real diagnosis with a bogus one.

Baseline on the Fedora laptop is now green: make ports, make,
make schfs-check including the round-trip that had never run, and
bootcheck at 27/27 plus 108 [ok] across the extended suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 19:29:21 +02:00
Nexoniarz
dbb88064c1 docs: correct the layout block, and note what a copy leaves behind
README's layout predated the BIOS/UEFI split. moving-to-linux.md now
says what is gitignored and therefore absent from a clone: dist/ holds
61 MB of upstream tarballs, so copy it or let the build re-fetch them --
make ports verifies each against a known SHA-256 either way.
2026-08-09 17:25:42 +02:00
Nexoniarz
d41a0b1618 memory: the UEFI loader 2026-08-09 17:13:44 +02:00
Nexoniarz
64aa13886d memory: schinit and the BIOS/UEFI split 2026-08-09 16:36:54 +02:00
Nexoniarz
baede13847 schinit: a launchd-shaped process 1
Milestone 1 of three: manifests, RunAtLoad, KeepAlive with throttling,
and reaping. Replaces the hardcoded fork-a-shell loop in cmd/init, which
stays in the tree for now.

Jobs are sectioned key=value files in /etc/schinit/*.job, kept in the
repo under etc/ and installed into the sysroot -- sysroot/ is generated
and gitignored, so anything written only there is lost on a fresh clone,
which is already true of /etc/passwd and should not become true of the
boot configuration. Unknown keys and sections are ignored rather than
fatal: a manifest written for a newer schinit must not stop this one
booting the machine.

Socket sections are parsed and recorded but not yet held. That is
milestone 2 and it is the whole reason for copying launchd rather than
sysvinit -- the supervisor holds the listener and starts the job on first
connect, which inverts the dependency graph and makes runlevels a
non-concept. The key exists now so manifests need not change later.

getdents64 is exposed to userland for this. The kernel has had it since
the Linux ABI landed, but nothing declared it in user.h or lib/usys.S, so
first-party programs had no way to list a directory at all.

Two things found by testing rather than reasoning:

  - setsid() gives a job its own session, and a session has no
    controlling terminal. The console then delivers input only to the
    foreground process group it tracks, so the shell came up, echoed
    every keystroke through the line discipline, and executed none of
    them -- indistinguishable from a hung shell. ControllingTerminal=yes
    claims it with TIOCSCTTY/TIOCSPGRP; off by default, since a daemon
    has no business seizing the console.
  - the first bootstrap() dropped /dev/input/event0-1 and most of /proc,
    because it was transcribed from the part of the old init that had
    been read rather than all of it. -Evdev caught it.

build/usbstick.img is built by `all` now: without it QEMU refuses the
-drive and the entire -Usb run produces no output, which reads as a
broken USB driver.

Seven suites green: base 27, Tty 51, Ps 32, Sock 64, Fb 36, Usb 31,
Evdev 29. make schfs-check passes.
2026-08-09 16:36:13 +02:00
Nexoniarz
08a1a9033a boot: separate BIOS from UEFI
boot/bios/ now holds the MBR, the El Torito stub and the real-mode stage
2; boot/uefi/ is empty and documented.

Separate directories rather than #ifdefs, because the two share almost
nothing. A BIOS loader lives in 16-bit real mode, reaches the firmware
through software interrupts, and walks itself up into long mode. A UEFI
loader is a 64-bit PE that the firmware has already placed in long mode
with paging on, calling services through a table of function pointers.
What they do share is the hand-off contract, which boot/README.md now
writes down: ELF segments placed, long mode, a 24-byte-entry memory map
at E820_MAP_ADDR, framebuffer geometry at VBE_INFO_ADDR.

That contract is the point of documenting it now. UEFI's GetMemoryMap
returns EFI_MEMORY_DESCRIPTORs, and the right move is to translate them
into the entries kernel/arch/x86_64/e820.c already parses rather than
teach the kernel a second format.

Build output paths under $(BUILD)/boot are unchanged; only sources moved.
2026-08-09 16:00:03 +02:00
Nexoniarz
8a4ac03ea9 memory: performance, init signal safety, and trusting the harness 2026-08-09 15:55:11 +02:00
Nexoniarz
66340e720e Rewrite run.ps1; it was five kinds of stale
run.ps1 is a .ps1 that does not live under tools/, so every sweep that
fixed the other launchers went straight past it:

  - hdb defaulted to build/fs.img, which nothing builds since SchFS
    became the root, so it pointed at a file that does not exist. QEMU
    came up with no usable disk on device 1 and the kernel panicked with
    SCHFS_ROOT -- "no SchFS volume on the root device". That is exactly
    the failure this script exists to prevent.
  - a -SchFS switch built around the old arrangement, where SchFS was a
    secondary volume and needed `make ramroot` to free device 1. There
    is no such arrangement now; every boot is a SchFS boot.
  - `cd /d/DEV/SchOS` hardcoded into the build invocation. It survived
    the Schunix -> SchOS rename by luck and would break on any clone.
    Derived from $PSScriptRoot now.
  - a hardcoded scoop QEMU path, and build\ backslash literals.
  - only -vga std, where the Makefile also attaches virtio-gpu-pci.

The magic check is SchFS-only now and prints the bytes it actually read
when it fails.

SchFS itself is fine: it mounts on 1, 2 and 4 CPUs, and all seven
bootcheck suites pass.
2026-08-09 14:49:47 +02:00
Nexoniarz
3182da3965 Carry project memory in the repo, with tools/memory-sync.sh
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:

  - D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
    invisible until moved across by hand.
  - Development happens from Windows and from Linux, and each looks in a
    different place for the same checkout.

Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.

The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
2026-08-09 14:27:22 +02:00