SchOS/docs/roadmap.md
Nexoniarz c0806d1b64 net: a TCP/IP stack, and AF_INET on the sockets that were already there
lwIP 2.2.0, fetched by ports/lwip and compiled straight into the kernel.
Not written from scratch, and the reason is not the packet formats: it is
congestion control, retransmission, window management and twenty years of
corner cases that only show up against a real peer. Not FreeBSD's
netinet either, though the licence would have allowed it -- that stack is
not a library, it reaches into mbufs, epoch(9), netisr, VNET and uma(9),
so adopting it means porting FreeBSD's kernel infrastructure first. lwIP
was written for exactly this situation: a small kernel with a raw
Ethernet driver and a port layer of a few hundred lines.

It is kernel code, so nothing is installed into the sysroot and there is
no second build system: the top-level Makefile compiles the files it
wants against our port headers. The file list is explicit rather than a
wildcard, because lwIP ships IPv6, 6LoWPAN, PPP, SLIP, DNS-SD and a dozen
apps in the same tree, and building what is not configured yields either
dead code or a link error naming a port function nobody meant to write.
Its sources are unmodified and its warnings are silenced for its files
only -- patching them would mean maintaining a fork.

The port layer is ours: a lock, a clock, the netif that hands frames to
e1000, lwipopts.h, and a shim/ directory of tiny headers so lwIP's
<string.h> and friends resolve to the kernel's functions instead of the
host's, whose prototypes disagree and would otherwise be found on the
default include path.

NO_SYS=1, so lwIP has no threads of its own and one kernel thread feeds
it. A thread rather than the interrupt handler, because a call into lwIP
can reach the driver, the driver can sleep, and an interrupt handler
cannot -- so the interrupt still only clears the cause register and the
thread polls. kthread_create is new and generally useful: a process with
a kernel-only page directory, no files, no cwd, no parent, dispatched
from forkret, which can therefore sleep and be preempted like anything
else. DHCP obtains an address at boot and the console says which one.

AF_INET went in exactly where phase 2 said it would. socknew dispatches
on the domain, kernel/net/inet.c implements the internet side on lwIP's
raw API, and not one syscall changed shape: bind, connect, listen,
accept, read and write reach the same descriptors they always did. TCP
and UDP both work. cmd/nettest opens a connection, writes, and reads the
answer back.

Two things are owed and named here so they are not discovered later:
recvfrom does not yet report the source address, and the socket paths
poll on the timer tick instead of sleeping on the socket. Both are
follow-ups, not redesigns.

NOTICE carries lwIP's BSD-3 text. Clause 3 is no-endorsement, not the
old advertising clause, so it sits with Apache-2.0 without difficulty.

Verified: the tree builds clean, and the headless boot check passes with
27 [ok] and no failures with the stack compiled in and the thread
running -- which is the part that would break first if netinit deadlocked
or the thread starved anything. An end-to-end connection test belongs in
that suite and is the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:02:48 +02:00

27 KiB
Raw Blame History

Roadmap

Written 2026-08-04, from the plan: USB, GPU, fonts, networking and sockets, then a compositor (Schcom), a window manager (SchWM) and a desktop (SchDE), plus an installer and a .sip package format.

This file records two things that are easy to lose: the order the work has to happen in, and the decisions that constrain it.

Superseded in direction, not in order. SchOS is now aimed at being a system rather than a Unix-like kernel with a userland attached: a NeXTSTEP/ macOS-shaped layering, with POSIX kept complete but demoted from identity to personality, and a native API (SchAPI) above it. See architecture/schapi.md. That decision changes what the phases below are building towards; it does not reorder them, and it does not start until the tree builds and boots natively again.

That assumption about §1 has since been revised. NetBSD's TCP/IP stack was the intended starting point; the per-file licence check found that NetBSD's tcp_input.c and tcp_output.c carry the WIDE Project's 4-clause advertising clause, which Berkeley's rescission does not reach. FreeBSD's equivalent is clean, and lwIP is cleaner still and a far smaller port. See Phase 2 below and docs/licensing.md §3.


1. Where drivers can come from

BSD: yes. Linux: no. This is not a preference, it is a licence incompatibility, and it decides the whole driver strategy.

The reasoning — Apache-2.0's patent terms counting as "further restrictions" under GPLv2 §6, which is why the two cannot be combined and redistributed — is in licensing.md, along with the per-licence table and the NetBSD-specific rules. Read it before copying anything. The short version: BSD-2 and BSD-3 code can be used directly with a NOTICE entry; GPL-2.0-only code can never be, at any distance.

There is a third source that is better than either: the specifications. xHCI, AHCI, virtio, NVMe, the Intel e1000 and i915 datasheets are all public documents with no licence attached to what you write from them. That is where kernel/drivers/xhci.c came from.

One caveat about "port it from BSD" as a plan, learned by looking: FreeBSD's sys/dev/usb is roughly 100k lines welded to newbus, devclass, bus_dma, callouts, taskqueues and FreeBSD's locking. Porting it means porting FreeBSD's device framework first and finding the USB stack somewhere underneath. NetBSD's is more portable and the same argument applies with a smaller multiplier. BSD is far more valuable as the reference you check yourself against than as the code you paste. Read it when the spec is ambiguous — which it is, often — and write your own.

Reading Linux for understanding is fine and normal. Translating a Linux driver function-by-function is not; the result is a derivative work whatever the variable names say.


2. Ordering, and why it is not the order it was asked in

The requested order was USB → GPU → fonts → network → GUI → installer. That is a reasonable order by interest. By dependency it wants one change:

Shared driver infrastructure comes before all of it. USB, any GPU beyond the VBE framebuffer, and any NIC all need the same three things, none of which existed:

  • a way to register an interrupt handler at runtime (kernel/dev/irq.c)
  • physically contiguous memory for rings and descriptors (kernel/core/dma.c)
  • PCI capability walking and MSI (kernel/dev/pci.c)

That work is done. It was the first step of USB, not a detour from it.

The other honest note: USB is the item that does least for the GUI. Under QEMU there is already a PS/2 keyboard and mouse, so USB buys real hardware support — which matters, but not for getting Schcom on screen. Sockets and raw input events are what actually block the compositor. USB is being done first because it was asked for first; the ordering below reflects that with the dependencies fixed.


3. The plan

Phase 1 — USB (done, bar the boot path)

  • Shared infrastructure: IRQ registration, DMA arena, PCI capabilities
  • xHCI: reset, DCBAA, command ring, event ring, scratchpad
  • Port enumeration, Enable Slot, Address Device, control transfers
  • Device descriptors read from real devices (USB2 and USB3 paths)
  • Configuration and interface descriptor parsing, SET_CONFIGURATION
  • Interrupt endpoints, driven from the IRQ handler
  • HID boot protocol: keyboard and mouse into the existing input paths
  • Bulk endpoints, bulk-only transport, SCSI, a block device at USBDISK_DEV — a superblock reads back through bread()
  • Hubs — route strings, hub slot contexts, per-port reset and power. A mouse behind a hub enumerates a tier deep, which is what proves it
  • Making a USB disk mountable and bootable (needs the root device to be selectable rather than compiled in)

Phase 2 — Sockets, raw input, and networking

Do sockets before the network stack. Unix domain sockets are what Schcom needs — clients and compositor have to pass messages and share buffers, and a pipe cannot carry an shm handle. Unix sockets are also far simpler than TCP/IP, so they are the right thing to build the socket layer's shape around.

  • socket/bind/listen/accept/connect/send/recv syscalls, at their real Linux numbers — they were NO_LINUX stubs, so a musl program calling socket() never even reached the stub
  • AF_UNIX SOCK_STREAM, socketpair, shutdown; names live in a table rather than the filesystem (see the comment at the top of kernel/net/unix.c for what that costs)
  • Raw input events — /dev/input/event0 and event1, Linux evdev layout, fed by the PS/2 keyboard and mouse and by USB HID. The console keeps its own path; both consumers see every key
  • SCM_RIGHTS for passing descriptors. Possibly unnecessary: an shm id is an integer and travels fine over a stream socket, so this waits until something actually needs to pass an open file
  • e1000 driver — kernel/drivers/net/e1000.c, from Intel's public manual 317453. MAC from EEPROM, transmit and receive rings, link detection. Run with -netdev user,id=n0 -device e1000,netdev=n0. Verified to carry real traffic: echo n > /proc/sysrq sends a hand-built ARP request and receives the gateway's reply, which exercises the descriptor formats, the DD handshake, the tail bookkeeping, the DMA addresses and the MAC filter in one exchange. That check came before any protocol stack on purpose -- a stack on an unproven driver fails in ways indistinguishable from its own bugs
  • ARP, IPv4, ICMP, UDP, then TCP -- done 2026-08-15, lwIP 2.2.0. Fetched by ports/lwip and compiled into the kernel; the port layer is kernel/net/lwip/ (about 400 lines: a lock, a clock, and the e1000 netif). NO_SYS=1, so lwIP has no threads of its own and one kernel thread (kernel/net/net.c, started by netinit) polls the NIC and runs its timers. DHCP obtains an address at boot. Verified end to end: nettest 10.0.2.2 9000 opens a TCP connection out of the guest, and the listener on the host receives HELLO-FROM-SCHOS while the guest receives the host's reply -- so the handshake, both data directions and the close all work, not just the driver. kthread_create was added for this and is generally useful.
  • Was: not written from scratch -- decided 2026-08-08. Writing TCP is the one part of this where doing it yourself is a bad trade: it is not the packet format that is hard, it is congestion control, retransmission, window management and twenty years of accumulated corner cases that only appear against a real peer. Preferred base: lwIP (BSD-3, no advertising clause, verified). It was written for exactly this situation -- a small OS with a raw Ethernet driver -- and its port layer is a handful of files. If BSD instead: FreeBSD, not NetBSD. NetBSD's tcp_input.c carries the WIDE Project's 4-clause advertising clause; FreeBSD's has none. But FreeBSD's sys/netinet is not a library -- it reaches into mbufs, epoch(9), netisr, VNET and uma(9), so taking it means porting FreeBSD's kernel infrastructure first. See docs/licensing.md §3.
  • AF_INET on top of the same socket layer -- done, exactly as planned: socknew dispatches on the domain, kernel/net/inet.c implements the AF_INET side on lwIP's raw API, and no syscall changed shape. TCP and UDP both work; recvfrom's source address and a proper sleep/wakeup instead of the current tick-poll are the two things still owed.

Phase 3 — GPU

"Basic GPU driver" has a right answer and a wrong one. The wrong one is starting with Intel or AMD hardware: modern GPU drivers are enormous and most of the size is mode setting, power management and a command submission model that only pays off once there is something to submit.

  • virtio-gpu, 2D. kernel/drivers/virtio_gpu.c, written from the OASIS specification. Full path: GET_DISPLAY_INFO, RESOURCE_CREATE_2D, RESOURCE_ATTACH_BACKING, SET_SCANOUT, TRANSFER_TO_HOST_2D, RESOURCE_FLUSH. Bound as the display class ahead of the VBE framebuffer. Run with -vga std -device virtio-gpu-pci; keep -vga std, because the bootloader's splash goes through VBE and there is no virtio path that early
  • A /dev/dri-style device with mode setting, so resolution stops being whatever the bootloader asked VBE for
  • Double buffering and vblank — Schcom needs both to not tear
  • Only then: real hardware, and only 2D/blit acceleration

Phase 4 — Fonts

This is a bigger job than it sounds and it should be sized honestly. A TTF/OTF rasteriser needs: cmap/glyf/loca/head/hmtx table parsing, quadratic béziers for TrueType outlines and cubic ones for OpenType/CFF, a scanline rasteriser with anti-aliasing, and a glyph cache.

The bigger structural problem is that the console assumes a fixed 8×16 cell grid throughout — vga_putc, the cursor, the ANSI erase operations. Variable width text is not a font change, it is a text-rendering layer.

  • The text layer — include/text.h + kernel/dev/text.c. Glyph metrics, a struct surface target so a compositor can draw off-screen, 8-bit coverage with real blending, UTF-8, and font_ops so a rasteriser plugs in as a second provider. This was the actual blocker: the font was never the problem, the character-cell grid was.
  • Port the console onto it and delete the cell grid
  • A TrueType rasteriser — kernel/dev/ttf.c, glyf outlines, composite glyphs, cmap format 4, anti-aliased scanline fill. Host harness at tools/ttftest.c
  • A glyph cache — direct-mapped over ASCII into a 48 KB pool
  • Load a font from disk and point the console at it, deleting the cell grid
  • CFF/Type 2 charstrings, so OTF fonts work too (a separate font_ops provider; a bytecode interpreter, not an extension of the glyf path)
  • A rasteriser. stb_truetype.h is public domain and therefore usable as-is if wanted; writing one is also reasonable and more interesting
  • The typefaces, decided 2026-08-09. Both verified, not assumed: Roboto for the desktop, window manager and compositor -- Apache-2.0, the same licence as SchOS, so it needs nothing beyond a NOTICE entry. JetBrains Mono for the terminal and TTY -- SIL OFL 1.1, and notably with no Reserved Font Name declared, so it may be modified and still called JetBrains Mono. That matters more than it sounds: pre-rasterising a font into bitmaps at build time is a modification, and an RFN would have forbidden keeping the name on the result. OFL's one real constraint is that the font may not be sold on its own and must stay under OFL; bundling it in an Apache-2.0 system is exactly the case it is written for.
  • Text layout (advance widths, kerning) as a library in lib/
  • The console keeps its fixed grid; proportional text belongs to the GUI

Font licensing: public domain or very permissive only. No SIL OFL if it can be avoided, no "free for personal use", no CC-BY-SA. The constraint is the same one that governs drivers (§1): a font file is redistributed inside every SchOS image, so its licence attaches to the image.

Safe choices, in rough order of usefulness:

  • CC0 / public domain — best case, nothing to comply with.
  • MIT / BSD / Apache-2.0 fonts — fine, needs a NOTICE entry.
  • SIL OFL — usable but has a real string attached: a modified font must be renamed, and OFL fonts cannot be sold on their own. Workable, but read it before committing one.

Whatever is chosen, the licence goes in NOTICE before the file is committed, not after.

Phase 5 — The GUI, as separate projects

See projects/README.md. Schcom, SchWM and SchDE live outside the kernel tree on purpose.

  • Schcom — the compositor. Owns the framebuffer, composites client buffers, routes input. Needs: unix sockets, shm (done), raw input events, and a GPU driver with double buffering.
  • SchWM — window management on top of Schcom's protocol
  • SchDE — panel, launcher, settings

Raw input is the missing kernel piece nobody thinks of until it bites: keyboard events currently only arrive through the tty line discipline, cooked and line-buffered and delivered to one foreground process. A compositor needs key-down/key-up with modifiers and no line discipline at all — an evdev-style /dev/input.

Phase 6 — Installer and .sip

.sip (SchOS Installation Package), the stated model being WIM in Windows PE: a file-based image rather than a block image, so one file can hold a whole filesystem, be applied to a partition of any size, and hold several images that share their common files.

The pieces for this mostly exist already. tools/mkschfs.c does content dedup with an (st_dev, st_ino) fast path; SchFS's B-tree is content addressed. A sensible .sip is: a header, a file index (path, metadata, content hash), and a blob store keyed by hash, with compression per blob.

  • .sip format specification, written down before any code
  • tools/mksip — capture a directory tree into a .sip
  • Kernel or userspace apply path
  • An installation environment that boots from the ISO, partitions, applies the .sip, and installs the bootloader

Phase 7 — The full-stack acceptance test (served its purpose; removed)

For a while this phase was a ported copy of a well-known 1993 shooter, kept as an acceptance test rather than a feature. The reasoning was sound and is worth keeping even though the test is gone: a large program that somebody else wrote, which exercises /dev/fb0, /dev/input/event0, mmap, the ELF loader, gettimeofday and a fixed-rate tic loop simultaneously and notices when any of them lie, is a much harsher test than anything written in-house — because it was not written against our bugs.

It found real ones, all of which are fixed and covered by narrower tests now:

  • fcntl(F_SETFL, O_NONBLOCK) was a documented no-op, so the first read blocked forever and looked exactly like a hung renderer.
  • /dev/input/eventN replayed its backlog to each new opener, handing a program the keystrokes that had launched it as input.
  • Without EVIOCGRAB, a full-screen program and the shell behind it both read the same keystrokes.

It was removed once it stopped finding anything: the port was ~100k lines of third-party source under licences that can never ship in an Apache-2.0 image, and carrying it had a standing cost — 33 MB of non-redistributable data staged into sysroot/, which is linked into the kernel binary for a ramdisk-root build. That combination is a licensing accident waiting to happen, for a test that was passing every time.

What replaced it: tools/windows/bootcheck.ps1 with its -Sock, -Evdev and -Ps modes, plus tools/windows/splashshot.ps1 for the boot path. These are narrower and do not prove the whole stack composes, which is the honest cost of the removal.

The lesson worth keeping is the shape of the test, not the program: the next full-stack test should be something large that we did not write, driven to a real pixel-level assertion rather than a log line. tools/windows/splashshot.ps1's colour counting exists because a log-only harness once passed on a blank screen.

Phase 8 — The system layer: schinit, firmware, and a crash channel

Three ideas parked here rather than started, because each one is a project and the phases above are the agreed order. Recorded so they are not re-derived from scratch later.

schinit, a launchd-shaped init. The point of copying launchd rather than sysvinit is not the config format, it is demand launching: launchd holds the listening socket or watches the path, and starts the job when something actually arrives. That inverts the dependency graph — jobs stop needing to declare what must start before them, because the thing they depend on answers on first use. SchOS already has the two pieces that makes possible: AF_UNIX sockets (kernel/net/unix.c) and a real /proc. What is missing is a supervisor that can hold a socket on a job's behalf and hand it over on exec, plus a job description format. Prerequisite for this being worth anything: ps and top must be able to enumerate processes, or the supervisor is unobservable.

A crash screen you can work in, and a QR code channel out of it. PSOGM (kernel/core/panic.c) currently renders and stops. The idea is two staged things, and the staging matters:

  • An interactive mode — "a small OS inside the crash". This is the right framing and it is worth stating precisely, because it decides the design. At panic time the kernel is the only thing running, which is exactly why it can offer what a live system cannot: walk the stack, read another process's memory, dump page tables, inspect the process table — with no scheduler and no locking to respect, because nothing else can race you.

    What makes it a system rather than a print statement is that it keeps the base machine's drivers. Concretely, the ones that work with interrupts off: the VGA/framebuffer (vga_draw_pixel is a plain memory write), the PS/2 keyboard (pollable via port 0x60/0x64), and the serial port. Those three are a complete console. Everything else — IDE, USB, the timer, anything that sleeps or takes a sleeplock — is off limits, because the lock it needs may be precisely the one held by the CPU that just died.

    So the shape is a polled event loop over preallocated state: no kalloc, no sleep/wakeup, no acquire on anything a live path might hold, one static command buffer, and a driver set restricted to the three above. That restriction is not a limitation to work around later; it is the invariant that makes the mode trustworthy when everything else is not. Anything that wants more should be answered by rebooting into a recovery environment instead.

    A useful precedent to read but not copy: DDB (NetBSD/FreeBSD's in-kernel debugger) is exactly this, and its hard-won rule is the same one — a debugger that can deadlock is worse than no debugger.

  • A QR code. Done. kernel/core/qr.c (byte mode, EC level L, versions 1–6, no allocation) and a card on the right of the crash screen. A panic report is worth nothing if it cannot leave a machine whose network stack is dead, and a phone camera is an out-of-band channel needing no driver on our side. Deliberately not a URL: it encodes the report itself and reaches nothing.

    It ended up unconditional rather than behind Enter, which is better — it costs nothing to always draw, and requiring a keypress would mean the one case where input is least trustworthy is also the case where the report is hardest to retrieve. Verified end-to-end by make crashshot && make crashqr, which decodes the symbol out of the captured framebuffer.

UEFI. Not a feature so much as the point at which the current bootloader stops being extendable: it is 16-bit real mode, INT 10h VBE, and PIO ATA, none of which exist on a UEFI-only machine. The realistic shape is a second loader — an EFI application that gets the framebuffer from GOP, the memory map from GetMemoryMap(), loads the kernel off a FAT ESP, and enters at the same place boot/bios/stage2main.c does — rather than a port of the existing one. Worth noting that the splash work just done survives that transition: it is a mask, a palette and a layout, none of which care how the pixels were found. What does not survive is every assumption in stage2main.c about where things live in low memory (see docs/layout.md).

SchFS: all five phases are done (2026-08-15)

Phases 1-3 were finished earlier. Phase 3¾ (the commit barrier, uberblock fallback, deferred writes), phase 4 (extent refcounts and reflink) and phase 5 (attributes, the index and the query) all landed on 2026-08-15. See docs/architecture/schfs.md, which is the detailed record.

SchFS beyond SchOS

Three ideas parked 2026-08-08, in the order they should happen.

An on-disk format version, and an upgrade path. This one is not optional and gets harder every day it is postponed: the moment an image exists that someone cares about, changing the layout means being able to read the old one. The superblock already carries a version field and a CRC32c; what is missing is a policy — a documented rule for what a version bump means, a refusal to mount something newer than the driver understands (rather than misreading it), and an explicit upgrade step. Cheap now, expensive later, and it blocks the other two: a second implementation is only meaningful against a format that is pinned.

A Linux implementation. The realistic first form is FUSE, not a kernel module — it needs no GPL entanglement (libfuse is LGPL and used across a process boundary), it can reuse the read/write logic already in tools/mkschfs and tools/schfs_fsck rather than being written twice, and it turns "does our filesystem work" into something testable with fsx and xfstests, which is a far harsher examination than anything in this tree. A Linux kernel module is a different project and carries the licence problem that licensing.md describes.

The missing features. Worth listing honestly before adding any: no journal replay across an unclean shutdown beyond what CoW gives for free, no snapshots (despite the B-tree being copy-on-write, which is most of the work), no extended attributes, no sparse-file support, and no online fsck. Snapshots are the one where the existing design already pays for most of the cost.


3b. Known broken

Two things that are wrong now, both reported 2026-08-08 and both diagnosed rather than merely observed. Neither blocks the GUI work, which is why they are here and not in a phase.

SMP is broken (fixed 2026-08-08)

SMP works. One, two and four processors all boot; every CPU reports in with cpu%d: starting, and /proc/cpuinfo lists them all. smp_start_aps in kernel/core/main.c is 1.

Five distinct bugs, each hidden behind the one before it, and all invisible under QEMU because it publishes only one processor in the legacy MP table. VMware published the real count, which is why the triple fault only ever appeared there.

CPU enumeration mpinit() read the 1994 MP table. kernel/arch/x86_64/acpi.c now reads the ACPI MADT.
triple fault entry64high had no AP branch — every AP took the BSP's stack and ran kmain() twice. Now tests EBX == 1.
panic("acquire") seginit() called kalloc() before %fs existed; kalloc locks, locking reads cpu. BSP now preallocates each AP's page.
panic("sched locks") kernel_fs_base was one global read by trapasm.S on every entry, so all CPUs shared one per-CPU block. Now %gs:0.
null proc in sleep() cpu/proc were __thread — per thread, so the compiler cached the thread pointer across sched(), but %fs is per CPU. Now mycpu()/myproc().
#GP in syscall_entry The new per-CPU slots landed on %fs:(8), the syscall scratch word — the same memory as %gs:8. Moved to +16 and +24.

The __thread one is the lesson worth keeping: __thread is wrong for per-CPU kernel data. It means per thread, and the compiler is entitled to cache the thread pointer across calls on that basis. A kernel thread changes CPU at every sched(), so the assumption is false for exactly the variables that most need to be right. include/proc.h carries the disassembly that proves it.

Per-CPU block layout, shared with assembly and not to be reordered:

+0   self-pointer     x86-64 TLS ABI (%fs:0)
+8   syscall scratch  trapasm.S parks %rsp here
+16  struct cpu *
+24  struct proc *

The kernel demands 224 MB of RAM (fixed 2026-08-08)

kernel/arch/x86_64/e820.c reads the firmware's memory map and sets phystop from it. The floor went from 224 MB to 32 MB, and the kernel now sees all the memory a machine actually has: 512 MB in gives MemTotal: 524160 kB, where before it reported 224 MB whatever was installed.

Three details worth keeping:

  • It takes the end of the usable region containing the kernel, not the highest usable address anywhere. A memory map has reserved holes in it, and freerange() walks straight from the end of the kernel image to phystop handing out every page between — so the global maximum would put firmware tables on the free list, failing later and elsewhere and looking like anything but a memory map problem.
  • The ceiling is 3 GB, because kvmalloc() maps 0–4 GB as four 1 GB pages and marks the top one uncached for PCI BARs.
  • Below PHYSTOP_MIN it prints why and stops, rather than triple faulting in silence. At 24 MB: e820: only 23 MB usable; SchOS needs at least 24 MB.

The original text is kept below because the reasoning still applies to the SMP half of §3b, which is the same shape of bug: a constant nobody revisited.

The kernel demanded 224 MB of RAM

Symptom as reported: the OS needs ~256 MB, which is absurd next to Windows 98 or an early Linux.

Diagnosis: it is one constant. PHYSTOP in include/memlayout.h is 0xE000000 — 224 MB — inherited unchanged from xv6, and main() hands P2V(PHYSTOP) straight to kinit1()/dma_init(). The kernel therefore assumes that much physical memory exists and walks off the end of RAM if it does not. Measured 2026-08-08: boots at 224M and above, triple faults at 192M and below. Nothing is actually being used — the requirement is entirely fictional.

The fix is already half-built. boot/bios/stage2.S collects a full E820 memory map in real mode and leaves it at VBE_INFO_ADDR + 0x200 (count) and + 0x208 (24-byte entries); boot/bios/stage2main.c already parses it to print detected RAM. The kernel never reads it. So: have main() take the top of the highest usable E820 region instead of the constant, clamp it to what the direct map covers, and keep PHYSTOP only as a fallback for the case where the firmware returned nothing. That should drop the floor to something in the tens of megabytes and is a contained change.


4. Kept out of scope on purpose

  • Anything that needs pushing or publishing. This work stays local.