| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
906318f2b6 |
X on the console: /dev/tty0, a palette, and startx that works
Xorg finds its VT through /dev/tty0, and without one ran on none: the console went on drawing into the framebuffer over the X screen. tty0 is the terminal on screen, as on Linux. FBIOPUTCMAP succeeds, as it does on a true-colour Linux framebuffer, instead of 256 logged errors per start. schos-base gives root a .xserverrc with -dumbSched, Xorg's switch for input on the main loop: SchOS has no threads, and Xorg crashed in pthread_join on its input thread when the first client left. It adds the hostname to /etc/hosts, so xauth can resolve schos:0. zsh loses PROMPT_SP: on a serial terminal narrower than the console its padding wrapped and left a # above every prompt. bootcheck knows zsh's prompt and matches without colour codes, so busybox's ls reads as text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 |
||
|
|
20cc673a41 |
net: a network *class*, so the stack stops knowing what an e1000 is
The TCP/IP stack reached straight into the first driver anyone wrote.
net.c called e1000_present(), the lwIP glue was named netif_e1000.c and
called e1000_transmit(), and include/defs.h declared the driver's
internals to the whole kernel. The second NIC driver would therefore
have arrived with a near-identical copy of the glue file and a patch to
the protocol stack, which is the arrangement include/device.h was
written to prevent -- the e1000 even said so, with `.ops = 0, // no
class vtable yet; there is no net class`.
There is now. A driver publishes a struct netdev_ops -- transmit,
receive, mac, link_up -- and is finished: it never mentions lwIP, and
nothing above it learns its name. The display class was already this
shape; this is the same shape for networking, and the two now read the
same way in kernel/dev/device.c.
What that made possible immediately:
- One netif per bound device instead of one per driver. lwIP's glue is
netif_eth.c, it takes a struct device, and it holds up to NETIF_MAX
interfaces. net.c attaches every network device it finds and starts
DHCP on each, because which interface the network answers on is not
ours to decide.
- /proc/netif lists all of them, one block of fields each, with the
interface name and the driver that claimed it.
- The ARP self-test moved from the e1000 to the class. Nothing in it
was chip-specific, and it is the first thing anyone will want to run
against the next driver. Still `echo n > /proc/sysrq`.
- Private headers live with their driver: e1000.h was in include/,
where exactly one file could use it. The driver build now adds the
driver's own directory to the include path.
The e1000 also drives more than one card. Its probe() refused the second
outright, because there was a single static instance -- so a machine
with two of them drove one and silently ignored the other. Instances are
an array now, and the instance is hung on dev->priv by a new optional
`attached` hook that the bus calls once the device exists. probe() runs
before device_add(), so there was previously nowhere to put it; that is
the whole reason the hook exists rather than a line inside probe.
QEMU gets an explicit NIC on every target (QEMU_NET). The default device
happens to be an e1000 on this machine type today, which is exactly the
sort of thing that stays true until a QEMU release changes it and the
network stops being exercised without anyone noticing.
Verified:
- bootcheck -Net passes, 48 [ok], no failures -- including the moved
self-test and two new checks that /proc/netif names both the
interface and the driver behind it, which together say the stack
went through the class rather than through a driver it was compiled
to know.
- Two NICs attached by hand (-device e1000 twice, second with its own
MAC): eth0 and eth1 both attach, each reports its own MAC in
/proc/netif, and each gets its own DHCP lease. Before this commit
the same machine produced one interface.
- make schfs-check still passes in full.
One trap worth recording: moving a header broke the build for anyone
with stale .d files -- make stopped with "No rule to make target
'include/e1000.h'". CFLAGS gains -MP, which emits a phony target per
header so a moved or deleted one can never wedge the build again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a9ff1166b5 |
net: recvfrom says who sent it, and a datagram is delivered whole
The second of the two things the stack owed. recvfrom accepted the address arguments and ignored them, so every caller was told nothing about where a datagram came from -- which for UDP is most of the point: a server that cannot see the sender cannot reply to it. Fixing it properly meant fixing something worse that was sitting underneath. UDP payloads went into the same byte ring TCP uses, with no record of where one datagram ended and the next began, so two datagrams that arrived together came back from one read as one. That is a bug whose appearance depends on timing and whose blame lands on the sender. Datagrams now carry a header alongside the bytes: source address, source port, length. Sixteen of them per socket, in a ring beside the existing one. Whole buffers would have been 16 sockets x 16 datagrams x an MTU of static memory for something almost always empty; only the boundaries need keeping, and those are eight bytes each. A datagram too big for the buffer it is read into is truncated and the remainder discarded, rather than left in the ring to be delivered as the head of the next one. One that arrives with the queue full is dropped whole -- that is UDP's own contract, and half a datagram in the ring would corrupt every one after. An empty queue and a zero-length datagram are kept apart, because a sender really can send zero bytes and the right answer to that is 0 rather than "nothing has arrived yet". sendto grew the mirror image: a destination address on a datagram socket, which is what a server replying to whoever just wrote to it needs. On TCP a destination is ignored rather than refused -- the connection decides where the bytes go, and programs pass one out of habit. For a stream, recvfrom's source is read from the pcb: it is the peer, and it is the same answer for every byte. Verified: bootcheck -Net now covers UDP too, 45 [ok] and no failures. The address check is real rather than cosmetic -- nettest deliberately leaves its sockaddr uninitialised, so a kernel that filled in nothing would print stack rubbish instead of 10.0.2.2 and the port the responder is actually on. The boundaries are checked by byte count: the responder sends 20 bytes and then 16, and the guest reports reading 20 and then 16. Treated as a stream the first read would have taken all 36. Left for later, and deliberately: accept() still does not report the peer's address, and getsockname/getpeername do not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f46fb41966 |
test: the network is checked by the suite now, and /proc/netif says what it did
The TCP/IP stack landed with its end-to-end test performed by hand: start a listener, boot, type nettest, read the output. That is evidence once and nothing afterwards, and it is exactly the kind of check that quietly stops being run. bootcheck -Net does the whole exchange. A listener runs on this host on a port picked by binding one and letting go, the guest is told to connect out to 10.0.2.2 at that port, and both ends are asserted: the guest reports what it received, the listener reports what arrived. One side alone proves much less than it looks -- a stack that completes a handshake and then drops every byte satisfies any check made from inside the guest. The listener is a background job, a separate process, because this harness has one hard rule: nothing may block the writer feeding QEMU's stdin. A socket accept is precisely the wait that deadlocked two earlier attempts at reading QEMU's output. It answers the protocol nettest speaks and then closes -- the close is load-bearing, since nettest reads until end of file and a listener that stayed open would hang the guest rather than fail it. The NIC is spelled out (-netdev user + -device e1000) rather than left to QEMU's default. Other runs can take the default and still answer "did a driver bind"; a run that asserts on an address should be reading its own configuration. /proc/netif is new, and the address is why. Until now the only report was a line on the console at boot, which userland cannot read and a test can only find by scraping the transcript. It gives interface, MAC, state, address, netmask, gateway, MTU and frame counts. Named netif rather than net on purpose: /proc/net is a *directory* on Linux, and a plain file there would give ENOTDIR to anything reaching for /proc/net/dev, which is worse than not having it. The counters are kept at the lwIP seam rather than in the driver, because a frame the driver received and the netif dropped never reached the stack. Verified: 38 [ok], no failures. Eleven of them are new -- lwIP up, DHCP obtaining 10.0.2.15 from SLIRP (a number nothing in the guest knows in advance, so it is the exchange completing rather than a guess), the four /proc/netif fields, frames counted in both directions, the handshake, the reply read back, and the listener confirming HELLO-FROM-SCHOS arrived. One bug found by writing it: $netPort was chosen after the command list was built, so the guest ran `nettest 10.0.2.2` with no port and printed its usage. An unset PowerShell variable interpolates to nothing rather than failing, which is worth remembering next time a command in this file looks right and behaves oddly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ed77244df0 |
boot/uefi: the system information panel, and the offset that faked a version
ui_panel was written for this screen during the loader restyle and then never called -- dead code sitting in boot/common/ui.c waiting for the row that would use it. This is that row: F8 -> System Information, showing firmware vendor and revision, UEFI revision, CPU brand, usable memory, current mode and whether ACPI was found. Everything is read when the screen is drawn rather than captured at startup, because the resolution row above it can have changed the mode, and a panel still reporting what the machine booted with is worse than no panel at all. Two facts are deliberately read through the same code that feeds the kernel -- efi_type_to_e820() for what counts as usable memory, and find_rsdp() for ACPI. A panel with its own idea of either would be a second implementation to keep in step, and the ACPI line exists precisely because a missing RSDP means no MADT, no IOAPIC and no SMP, which has already been debugged once from the far end. The screenshot caught a bug in the screen it was taken to verify: UEFI revision read "21587.2286". EFI_TABLE_HEADER begins with a UINT64 Signature, so Revision is at offset 8 and I had read offset 4 -- the upper half of "IBI SYST", formatted as a version number. It now reads 2.7 under OVMF, which is correct. Verified by driving the menu over the QEMU monitor and reading the pixels: five rows with the hint updated to 1-5, the panel centred, and every value cross-checkable -- EDK II, firmware 1.0, UEFI 2.7, "QEMU Virtual CPU version 2.5+" against -cpu qemu64, 1018 MB of the 1 GB given, 1280x800 matching what fbshot captures, ACPI present. The highlight sits 9 px inside both panel borders, symmetric. F8 has a 1.5 s window, so the capture presses it repeatedly through boot rather than guessing when OVMF finishes -- and only F8, since any other key breaks the wait and boots. Also: every ESP drive now says format=raw. QEMU warned about probing the format on each of the fifteen or so places that attach it, which is new noise on what is now the only boot path. 110 [ok], no failures, no warnings. |
||
|
|
651899220c |
boot: the BIOS loader is gone
1449 lines of boot/bios/, tools/sign.pl and tools/padstage2.pl, plus the build rules, the disk image and the ISO entry that existed to carry them. Everything since roughly 2012 boots UEFI, and CSM -- the firmware-side BIOS emulation that lets a legacy loader run on such a machine at all -- has been going away since Intel dropped support for it in 2020. So this is not a path being retired early; it is one whose remaining hardware is already the exception. The cost was never the code, it was the tax on everything near it. Stage 2 had a 65-sector ceiling it overflowed silently -- 25440 bytes against 24576 -- and because the kernel is written at sector 1 + STAGE2_SECTORS, an overflowing loader was overwritten by the very kernel it was loading. The resolution list could not be shared with UEFI either: VBE mode setting is INT 10h, so matching it needed unreal mode, which is the riskiest code in the tree for a feature that already worked elsewhere. What replaces schos.img is the staged ESP, handed to QEMU as a virtual FAT volume. That is a development arrangement and not an installable disk; a real GPT image with a real FAT partition is the installer's job, and until it exists nothing here writes a disk a machine boots unaided. Secure Boot is untouched too -- the loader is unsigned. The ISO keeps one El Torito entry, the FAT image holding loader and kernel. It never booted from a USB stick via BIOS anyway: the firmware presents the stick as a hard disk while stage2main.c read the kernel back over ATAPI. The 32-bit multiboot entry in entry.S now has no caller and is left in place. Deleting kernel entry code is its own change with its own way of failing silently, and it should not ride along with this one. Ported with it: run.ps1, the six screenshot tools, and schcom_test.sh, each with its own OVMF varstore copy so no run inherits another's UEFI variables. Two things this shook out. OVMF now belongs in `all`: nothing boots without firmware, `make clean` wipes build/ including the copy fetched from the host, and a clean followed by a build left every run script throwing. And it cannot be listed as a prerequisite at the top of the file -- make expands prerequisites when it reads the line, and $(OVMF_CODE) is defined further down, so naming it there appends nothing and says nothing. Verified from a clean tree: 110 [ok] and no failures on the full suite, the ISO boots with no hard disk attached, schfs-check passes, schcom's end-to-end pixel test passes, and fbshot captures a real 1280x800 frame. |
||
|
|
0007853baa |
test: the suite runs on UEFI, and stops testing the BIOS loader's taste
bootcheck has only ever booted through the BIOS loader. That is the path being removed, so the suite had to reach OVMF before the removal rather than after it -- deleting the boot path first would leave nothing running during the work that needs watching most. -Uefi swaps the drives for a pflash pair and the staged ESP, keeping the same layout: ESP on index 0, the SchFS volume on index 1. Every check runs unaltered, so a difference between the two transcripts is a difference in firmware and not in what was asked. The varstore is copied per run; sharing one would let a run that changed a UEFI variable decide how the next one behaves. Both paths now report 110 [ok] with no failures. Three checks did fail on UEFI, and were right to. They asserted xres 800, yres 600 and line_length 3200 -- the mode boot/bios/stage2.S asks VBE for. Under OVMF the firmware picks, here 1280x800 with a 5120-byte stride, which is correct and self-consistent. The checks were pinning the BIOS loader's fixed choice, and could do so only because that choice was fixed. They now assert the invariants the original comment was actually after: a plausible mode, a stride that covers a full row, and a width that is a multiple of 8 -- the last being the mode that sheared under OVMF, where the firmware advertised 1366 and the hardware scanned out 1360. Also reaped a QEMU left holding schfs.img by a run started outside the harness, which its pidfile reaper could not have known about. |
||
|
|
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> |
||
|
|
4e0478c21a |
bootcheck: reap a QEMU left behind by a killed run
The `finally` stops QEMU on every path the script controls, but not when the script itself is killed -- a CI timeout, Ctrl-C at the wrong moment, pkill. QEMU then keeps running with no parent. Because these tests run under TCG rather than KVM, a spare emulator pinning a core slows every later run, and one was found still executing the guest at 86% of a core eight minutes after its parent was gone. That is the bad kind of failure: the suite does not break, it gets slower, and commands drift past Wait-Prompt's timeout until the next one is typed into a busy shell and the transcript interleaves. The check that then fails is unrelated to whatever actually changed. Record the pid on startup and reap the previous run's leftover before starting a new one, the same self-healing approach already used for the monitor port. The name is checked before killing, because pids are recycled and killing an unrelated process would be worse than the leak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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.
|
||
|
|
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. |
||
|
|
686d5cf006 |
init survives stray signals; commands are 4-34x faster
Two bugs reported from actual use, plus a test harness that had stopped testing. init could be killed by Ctrl-C, taking the machine with it. SIGINT goes to the foreground process group, init is pgid 1, and any window where tty_fg_pgrp is 1 -- before a shell claims the terminal, after one exits, or between the two while the key is held -- lands it on init. init has no handler, so killed was set, exit() ran, and exit() panics outright on initproc. Spamming Ctrl-C at a frozen program is exactly the input that finds that window. POSIX and Linux both discard a default-action signal for pid 1; init_ignores() now does the same, SIGKILL included, because init is not killable from userspace on any Unix. Everything was slow because BSIZE was 1024 while a SchFS block is 4096, so every block cost four IDE requests, each with its own interrupt and sleep/wakeup. exec() of the 800 KB toybox binary meant ~800 round trips. The giveaway: `seq 1 200`, which touches no file and prints nothing, cost the same 2.0s as `cat /bin/toybox`, and `ls | wc -l` cost exactly double -- both dominated by loading the same binary twice. BSIZE is 4096 now, one request per SchFS block, and NBUF is 256 blocks (1 MB) rather than 90 KB -- smaller than the binary every command execs, so nothing was ever still resident. ideinit() issues SET MULTIPLE MODE, which is what makes a 4096-byte transfer legal: READ/WRITE MULTIPLE move the drive's multiple count per interrupt and nothing had ever set it, which is the real reason idestart() carried a sector_per_block > 7 panic. seq 1 200 2.02s -> 0.06s ls /bin 2.76s -> 0.68s ls /bin | wc -l 4.68s -> 0.73s And the harness. bootcheck called Find-Qemu from a param() default, which is bound before any function in the body exists -- so it died on its first line and printed an error containing neither "FAIL" nor "Exception", the two things being grepped for. It had not run since it was made host-portable. Worse, an earlier index-based edit had deleted the Expect blocks for -Tty, -Usb, -Sock, -Ps and -Evdev, so five of the seven modes asserted nothing at all. Restored, and the USB run gets its own 16 MB build/usbstick.img: it had been pointed at build/fs.img, which nothing builds since SchFS became root, so QEMU refused to start. 242 checks across seven modes, all passing, and make schfs-check green. |
||
|
|
754d96ebc5 |
tools: group scripts by what runs them
tools/windows/ .ps1 (pwsh, so usable from Linux and macOS too)
tools/posix/ .sh
tools/python/ .py
tools/ .c and .pl
The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.
Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.
Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:
- schfs_fsck.c did not compile under mingw at all: --extract uses
POSIX mkdir(path,mode), symlink() and link(), none of which mingw
has. That took the whole tool with it, and `make schfs-check` with
it. mkdir maps to _mkdir; symlink and link fail loudly rather than
writing a regular file in their place, because an extracted tree
that differs from the image while reporting success defeats the
point of --extract.
- the round-trip test then failed at `ln -s`, since Git Bash cannot
create symlinks without developer mode. It skips with a message now.
Not a fudge: mkschfs's mingw build cannot see a symlink either, so
the test would have compared a tree with none against an image with
none and called it a pass -- coverage reporting itself as present
while being absent.
Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
|
Renamed from tools/bootcheck.ps1 (Browse further)