diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index b439161..b5d5bfa 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -1,7 +1,8 @@ - [SchOS rename + git](schos-rename-and-git.md) — Schunix is SchOS and the kernel is SchIFT; the tree is under local git, and the two bugs the rename caused. - [Cross-platform build](schos-cross-platform-build.md) - Windows, Linux, macOS and BSD all build it; clang everywhere, and the four host assumptions that used to break. - [Memory sync](schos-memory-sync.md) - memory lives in the repo at .claude/memory/; tools/memory-sync.sh moves it between machines. -- [The Linux machine](schos-linux-machine.md) - Fedora laptop at /home/nexoniarz/Documents/SchOS; the three things only it can test. +- [The Linux machine](schos-linux-machine.md) - Fedora laptop at /home/nexoniarz/Documents/SchOS; the three things only it can test, all now green. +- [Bugs the Fedora move exposed](schos-fedora-move-bugs.md) - mkfs.c's DT_* ordering, and a stack walker that page-faulted on an all-ones frame pointer. - [Performance and init](schos-performance-and-init.md) - BSIZE must equal SCHFS_BLKSIZE (4-34x), and why Ctrl-C could kill pid 1. - [Trusting bootcheck](schos-bootcheck-trust.md) - it silently stopped running; count the [ok] lines, never just grep FAIL. - [schinit](schos-schinit.md) - process 1 is launchd-shaped; job manifests, and why setsid() cost the shell its terminal. diff --git a/.claude/memory/schos-fedora-move-bugs.md b/.claude/memory/schos-fedora-move-bugs.md new file mode 100644 index 0000000..f2c2517 --- /dev/null +++ b/.claude/memory/schos-fedora-move-bugs.md @@ -0,0 +1,53 @@ +--- +name: schos-fedora-move-bugs +description: "The two latent bugs the Fedora move exposed on 2026-08-09 — mkfs.c's DT_* ordering and the faulting stack walker in getstackpcs" +metadata: + node_type: memory + type: project + originSessionId: d4821aa9-2e49-40da-91e7-90ef9ed9997e + modified: 2026-08-09T17:26:47.236Z +--- + +Moving to the Fedora laptop ([[schos-linux-machine]]) found two bugs that had +been latent for a long time. Neither is Fedora-specific; a different host and a +freshly built userland simply drove code Windows never reached. + +## 1. `tools/mkfs.c` would not compile at all + +The `DT_UNKNOWN` / `DT_DIR` / `DT_REG` fallbacks sat **above** `#include +`, with the other compatibility shims. glibc spells those as +enumerators (with matching macros), so `#ifndef DT_UNKNOWN` asked before the +header was read, always answered "missing", and defined them — after which the +header's own `enum { DT_UNKNOWN = 0, ... }` expanded to `enum { 0 = 0, ... }` +and died on a syntax error pointing at the `#define`, not the cause. + +The fallbacks now live **below** the include, with a comment saying why. +`` cannot move up instead: it has to follow the `#undef dirent` that +ends the `#define dirent schfs_dirent` block, or it defines xv6's struct +dirent rather than the host's. mingw-w64 never hit this because it genuinely +lacks the constants. + +## 2. The stack walker page-faulted, in `kernel/core/spinlock.c` + +Symptom: `cp` panicked the kernel with `unexpected trap 14 ... cr2=0x7`. + +`getstackpcs()` walked the frame chain and dereferenced `rbp[1]` without +validating `rbp`. The guard rejected `0`, anything below `KERNBASE`, and +`0xffffffff` — but the value that actually turns up is all-ones, +`0xffffffffffffffff`, which is **above** `KERNBASE` and passed. `rbp[1]` then +wrapped to address `0x7`, exactly the reported `cr2`. The `0xffffffff` test was +dead code on x86-64: it is far below `KERNBASE`, so the previous check already +caught it. + +Fixed by bounding `rbp` from above (`> (addr_t)-16`) and requiring 8-byte +alignment, rather than naming another magic constant. + +**Why this one matters more than it looks.** `getcallerpcs()` runs on *every* +lock acquisition and again from the trap handler, so a walker that can fault +replaces a real diagnosis with a second, more confusing one. A backtrace helper +must never be able to fault. + +**Why it surfaced now:** `make ports` rebuilt toybox, and the new `cp` is not +the stale binary that had been staged in `sysroot/`. It drove +`fileread` → `ilock` → `acquiresleep` → `acquire` → `getcallerpcs` on a path +the old one did not. Rebuilding a stale userland is itself a test. diff --git a/.claude/memory/schos-linux-machine.md b/.claude/memory/schos-linux-machine.md index b24e4b2..ce6e023 100644 --- a/.claude/memory/schos-linux-machine.md +++ b/.claude/memory/schos-linux-machine.md @@ -1,8 +1,10 @@ --- name: schos-linux-machine -description: The Fedora laptop, its project path and memory key, and the three things only it can test -metadata: - node_type: project +description: "The Fedora laptop, its project path and memory key, and the three things only it can test" +metadata: + node_type: memory + originSessionId: d4821aa9-2e49-40da-91e7-90ef9ed9997e + modified: 2026-08-09T17:26:30.874Z --- Nexoniarz works from **two machines** and moved the project to the second on @@ -25,15 +27,23 @@ session that learned something. matters: 1. **`make ports`** builds musl, toybox and mksh from source. Windows cannot do - it at all — the build skips ports and uses whatever is staged in `sysroot/`, - which is why `sysroot/bin/sh` still carries the pre-rename symbol - `__schunix_tp`. + it at all — the build skips ports and uses whatever is staged in `sysroot/`. 2. **The SchFS round-trip test** needs symlinks and hard links, which Git Bash - cannot create, so it skips on Windows. **It has never actually run** — the - one part of SchFS with no coverage. + cannot create, so it skips on Windows. 3. **KVM.** A Ryzen box has SVM, so QEMU runs at native speed. Deliberately not the Makefile default: the suite's timings were set under TCG. +**All three ran for the first time on 2026-08-09, and the baseline is green:** +`make ports` (the rebuilt `sysroot/bin/sh` no longer carries `__schunix_tp`), +`make`, `make schfs-check` — round-trip included, collision pass and all — +and `bootcheck.ps1` at 27/27, then 108 `[ok]` with +`-Fb -Tty -Usb -Sock -Evdev -Ps`. Expect one "no prompt" on `wc -c` in the +extended run; the check it feeds still passes, so it is pacing, not a fault. + +**The move exposed two latent bugs, both fixed the same day** — see +[[schos-fedora-move-bugs]]. Neither was Fedora's fault; the host and the +freshly built userland simply drove paths Windows never did. + **PowerShell has no Fedora package**, and `tools/windows/bootcheck.ps1` is the test suite. pwsh runs it on Linux fine; it comes from Microsoft's repo or the GitHub tarball. Nothing else needs it. diff --git a/kernel/core/spinlock.c b/kernel/core/spinlock.c index 7b593db..1c0ec01 100644 --- a/kernel/core/spinlock.c +++ b/kernel/core/spinlock.c @@ -70,7 +70,24 @@ getstackpcs(addr_t *rbp, addr_t pcs[]) int i; for(i = 0; i < 10; i++){ - if(rbp == 0 || rbp < (addr_t*)KERNBASE || rbp == (addr_t*)0xffffffff) + // This walks a frame chain that may be garbage -- it runs from + // getcallerpcs() on every lock acquisition, and again from the trap + // handler, where the thing being diagnosed is often the reason the stack + // is unwalkable. A fault here replaces the real diagnosis with a second, + // more confusing one, so every dereference is checked first. + // + // The old test also compared against 0xffffffff, which on x86-64 is + // unreachable: it is far below KERNBASE and the check above already + // rejected it. The value that actually turns up is all-ones, + // 0xffffffffffffffff, which is *above* KERNBASE and sailed straight + // through -- rbp[1] then wraps to address 0x7 and page-faults. Bound rbp + // from above rather than naming another magic constant, so any frame + // pointer near the top of the address space is rejected whatever it is. + if(rbp == 0 || rbp < (addr_t*)KERNBASE || (addr_t)rbp > (addr_t)-16) + break; + // A frame pointer is 8-byte aligned. An unaligned one is not a frame, + // and following it walks off into arbitrary memory. + if(((addr_t)rbp & 7) != 0) break; pcs[i] = rbp[1]; rbp = (addr_t*)rbp[0]; diff --git a/tools/mkfs.c b/tools/mkfs.c index d820079..d89fb55 100644 --- a/tools/mkfs.c +++ b/tools/mkfs.c @@ -41,21 +41,6 @@ #define bzero(p, n) memset((p), 0, (n)) #define bcopy(src, dst, n) memmove((dst), (src), (n)) -// struct dirent's d_type is a BSD/glibc extension; mingw-w64 has no such -// field. The loops below already handle DT_UNKNOWN by probing with opendir(), -// which is the portable answer and was written for filesystems that decline to -// report a type -- so on Windows every entry simply takes that path. -#ifndef DT_UNKNOWN -#define DT_UNKNOWN 0 -#define DT_DIR 4 -#define DT_REG 8 -#endif -#ifdef _WIN32 -#define DIRENT_TYPE(dp) DT_UNKNOWN -#else -#define DIRENT_TYPE(dp) ((dp)->d_type) -#endif - #define stat xv6_stat #define sigset_t xv6_sigset_t #define dirent schfs_dirent @@ -69,6 +54,29 @@ // After the #undef above, so this gets the host's struct stat and not xv6's. #include +// struct dirent's d_type is a BSD/glibc extension; mingw-w64 has no such +// field. The loops below already handle DT_UNKNOWN by probing with opendir(), +// which is the portable answer and was written for filesystems that decline to +// report a type -- so on Windows every entry simply takes that path. +// +// This block must stay *below* , not up with the other compatibility +// shims. glibc spells these as enumerators with matching macros, so asking +// #ifndef before the header is read always says "missing" -- and then the +// header's own `enum { DT_UNKNOWN = 0, ... }` expands to `enum { 0 = 0, ... }` +// and the build dies on a syntax error a long way from the cause. The header +// cannot move up either: it has to follow the `#undef dirent` above, or it +// would define the xv6 struct dirent instead of the host's. +#ifndef DT_UNKNOWN +#define DT_UNKNOWN 0 +#define DT_DIR 4 +#define DT_REG 8 +#endif +#ifdef _WIN32 +#define DIRENT_TYPE(dp) DT_UNKNOWN +#else +#define DIRENT_TYPE(dp) ((dp)->d_type) +#endif + #ifndef static_assert #define static_assert(a, b) do { switch (0) case 0: case (a): ; } while (0) #endif