Compare commits

..
Author SHA1 Message Date
Nexoniarz
2e3deb5390 Name the process behind 'permission denied'; record what is unfinished
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 18:06:38 +01:00
Nexoniarz
d650ff433b udev: PCI bridges as parents, USB interfaces, the controller's own address, and the attributes lsusb reads
PCI devices now nest under the bridge they sit behind (from each bridge's
secondary..subordinate window), so a controller behind a root port is where
Linux would put it. USB interfaces (1-5:1.0) are directories with class,
number, a driver link and a uevent with INTERFACE= and a MODALIAS, announced
after the drivers have been offered the device and removed before it. The xHCI
driver tells the USB core its PCI address when it comes up, replacing the guess
that bus N was the Nth controller. Devices and root hubs carry version,
bMaxPacketSize0, bNumInterfaces, bmAttributes and the rest; lsusb -t from
Alpine's usbutils now draws the tree.

Checked on q35 with an xHCI behind a pcie-root-port and a USB keyboard on each
controller. Plain lsusb still fails: libusb wants usbfs, and refuses a kernel
that calls itself 0.1.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 17:54:13 +01:00
Nexoniarz
2aef583aad Twelve login terminals, root only, console fixes, a faster build, a Niveus splash, and X back on evdev
- tty1-tty12 all run login; the image has only root. /dev/tty7..12 could not
  be written (cdev.c knew minors 10-16 only), which sent their jobs into a
  restart storm that took the supervisor down; fixed, and stat reports them as
  Linux's tty7..12.
- Erasing text at a prompt did nothing: erase_one sent a bare backspace, which
  only moves; it and the echo path now send back-blank-back through vt_feed.
- A white cursor block was left at the end of every line of a verbose boot:
  a line feed at the bottom scrolls the pixels, cursor included, and the
  cursor was erased after the scroll. It comes off first now. A blank cell is
  also marked dirty, which it was not.
- ldlsd died with a general protection fault on real hardware in its variadic
  log function: Linux module code calls with an 8-byte-aligned stack and an
  unset %al, and the prologue's movaps faults. LDLS guest code is built with
  -mstackrealign.
- make iso took over a minute for any change, because staging r8169 rewrote
  the same files and every mtime moved; it now replaces files only if their
  contents differ (about 10 s).
- The loader's splash is redrawn in the boot manager's style, and the boot
  manager shows its cursor whenever a pointer device exists.
- xf86-input-libinput is out of the image again: with it Xorg took both
  keyboard and pointer from libinput and, on the user's laptop, neither worked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 17:43:44 +01:00
Nexoniarz
b3785343f8 Boot manager: drop the window lights, keep the title bar
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 16:54:39 +01:00
Nexoniarz
b411836478 libinput, seatd and libseat; sockets from accept() can pass descriptors
Alpine's libinput, libseat, seatd, xf86-input-libinput and usbutils join the
image, and seatd runs from schinit. libinput lists the keyboard and pointer as
an ordinary user, Xorg now drives both through libinput, and a program linked
to libseat running as schos opens seat0 through seatd, is handed
/dev/input/event0 and reads its name through the descriptor.

Two kernel bugs stood in the way, and both would have stopped a Wayland
compositor too. The socket connect() queues for a listener was allocated with
its domain at zero, so sendmsg refused SCM_RIGHTS on the accepted end: a server
could receive descriptors and never send one. And sendmsg treated a control
buffer longer than its messages as an error, where Linux takes a following
header too short to be one as the end; seatd passes a zeroed CMSG_SPACE array
with one descriptor at the front.

Found with /proc/strace and a temporary kinfo in sendmsg's EINVAL sites.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 16:51:39 +01:00
Nexoniarz
8d836c66b2 PCI and USB devices in the /sys/devices tree, with uevents and USB hot-plug
PCI devices live at /sys/devices/pci0000:00/<addr> and USB devices under the
xHCI controller that carries their bus, below that bus's root hub usbN;
/sys/bus/pci/devices and /sys/bus/usb/devices are symlinks into it, and every
device has its subsystem and driver links, which is how udev learns SUBSYSTEM
and DRIVER. Writing to a device's uevent now announces it with MODALIAS,
PCI_ID, PRODUCT and the rest, so udevadm trigger covers them, and the USB core
sends add when a device is attached and remove as it goes. The root hub is
announced when the controller comes up, before anything is plugged in.

Checked under QEMU: lspci and udevadm info through the links, udevadm monitor
showing the kernel and udev add event for device_add usb-kbd/usb-mouse and
the remove event for device_del, and a triggered add for usb1 and 1-5.

Known limits: the bus-to-controller mapping is a convention (bus N is the Nth
xHCI in PCI order); PCI bridges are not parents; USB interfaces and
/dev/bus/usb nodes are absent; busybox's lsusb reads /dev/bus/usb and prints
nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 15:51:28 +01:00
Nexoniarz
dee3affa3b Boot manager: Niveus window, keys and wells, Instrument Sans, pointer, other systems, working power entries
The F8 menu is redrawn in the Niveus language: a window with three lights,
bevelled keys with a lit edge, wells for panels, anti-aliased corners and a
soft shadow, set in Instrument Sans rasterised into include/gfx/nvfont.h
(tools/python/mknvfont.py, make nvfont). Scale doubles on screens 1400 px tall
or more. Lists taller than the screen scroll.

The keyboard still drives everything, and a mouse, touchpad or tablet does
too: both UEFI pointer protocols are polled, hover moves the highlight, a
click activates. The cursor stays hidden until the pointer has really done
something, since the firmware installs a pointer protocol with or without a
mouse. The 'keep this resolution?' countdown is one ten-second budget however
many times the pointer moves.

Loaders found at well-known paths on any volume (Windows, Ubuntu, Debian,
Fedora, Arch, openSUSE, systemd-boot, rEFInd, other BOOTX64.EFI) are listed
and chain-loaded with LoadImage/StartImage. 'Reboot to firmware' used to halt;
it now sets OsIndications and restarts, and Restart and Shut Down call
ResetSystem.

Checked under QEMU+OVMF: menu, resolution list (incl. 2560x1600 at double
scale), system information, shut down, firmware setup, chain-loading a copy of
the loader from a second disk, and a full boot of the ISO to the login prompt.
Pointer hover and click were checked only with synthetic events: in this setup
a USB mouse or tablet delivered no event to the loader (one USB I/O handle
appeared but no pointer protocol came from it), and why was not established.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 15:31:39 +01:00
Nexoniarz
020aa6113c Users and permissions: accounts, login, enforced file permissions, setuid, su, passwd, doas
The kernel now checks owner/group/other on open, create, unlink, rename, exec
and path search; honours set-user-id and set-group-id; restricts kill, mount,
reboot and mknod; keeps supplementary groups; and implements chown. devtmpfs
nodes carry owners and modes that chmod/chown keep. Terminals run login, the
image has root and schos, X starts as the ordinary user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 13:16:07 +01:00
Nexoniarz
b3f72cd95e eudev starts at boot; Xorg finds its input devices through it
udevd and a one-shot trigger run from schinit, 10-schos.conf no longer lists
event0/event1, and the docs say what udev covers and what it does not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 11:44:21 +01:00
Nexoniarz
5d4d318ebf udev works: /sys device tree, uevent netlink, datagram sockets with per-message credentials
Kernel devices (input, mem, tty, graphics, block) get /sys/devices/virtual
entries with dev/uevent/subsystem, /sys/class and /sys/dev links, input
capabilities, and writable uevent files that announce on the netlink socket.
eudev now runs, processes a full trigger and settles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 11:39:13 +01:00
Nexoniarz
a9f8cb7e0f AF_UNIX SEQPACKET/DGRAM, writable /dev overlay, uevent netlink socket; eudev packages
udevd now starts, reads its rules and listens. Still to come: the sysfs
device tree and writable uevent files so a trigger has something to replay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 09:51:41 +01:00
Nexoniarz
ed81f9a07a Schcom is gone; the desktop path is Xorg now, Wayland groundwork next
SchOS will not write its own compositor. projects/schcom, its test and its make targets are deleted. roadmap.md section 5 is
now the road to Wayland (udev/eudev, a /sys/devices tree and uevent netlink, libinput and libseat, a DRM device via the GPU
plan) and 5b is the boot manager rework (Niveus/skeuomorphic UEFI GUI). XLibre, Xorg or a Wayland compositor from Alpine,
whichever suits; Xorg for now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 09:35:54 +01:00
Nexoniarz
6e78bb0d5e X runs without -dumbSched; the ISO ships r8169 and the licences
Tried in QEMU on a built ISO: startx brings up Window Maker, xclock and xeyes connect and disconnect, an xterm runs
and exits, Xorg stays up -- the pthread_join crash is gone with threads. -dumbSched is removed from .xserverrc.
make iso now stages Linux's r8169 set first when this machine has the modules (ldls-r8169-if-present), and
sysroot-install puts LICENSE, LICENSE.MIT and NOTICE in /usr/share/licenses/schos.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 09:20:22 +01:00
Nexoniarz
1334f446cc config/drivers.conf: which drivers are compiled in
y or n per optional driver (virtio-gpu, stdvga, e1000, rtl8139, native r8169, AC'97, AHCI, NVMe), all y except the native
r8169, overridable with make DRV_X=n. Only drivers nothing else calls can be optional; the registry #ifdefs them and the
Makefile leaves their objects out. Built with every one off (links, none of their symbols present), with r8169 on, and
by default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:59:55 +01:00
Nexoniarz
f94d19ae47 README, site: numbers re-measured
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:23:45 +01:00
Nexoniarz
59aff881ac docs: 1.0 is reached; Schcom stays; Linux's r8169 set ships
1.0: the acceptance test drops its reboot clause (a live system is the finish line) and passes. Schcom is kept with a
job: the installer's GUI first, SchDE on top later (roadmap section 5). The old 'LDLS ships no Linux code' policy is
gone from ldls/devices/licensing/Makefile/.gitignore/ldlsd; NOTICE records the r8169 modules (GPL-2.0-only) and the
Realtek firmware. The download button points at the v0.1.0 release asset; the stale 64 MB ISO under docs/Website is
removed. mkaroot.sh's -dumbSched comment no longer claims SchOS has no threads.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:23:20 +01:00
Nexoniarz
42008c26c0 docs: packages.md counts what world.lock actually holds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:08:25 +01:00
Nexoniarz
cb3d28e3d6 docs: README, plan, guides and the site describe what is built now
README: rewritten around what runs (Linux software, Xorg, apk, Linux's drivers), the honest gaps, measured numbers.
plan/: 1.0.md is the finish line, where each clause of the acceptance test stands, and the register (one failing
clause: a root that survives a reboot); built plans (foundations, hybrid, alpine, xorg) and the decided-against one
(wayland) are gone, their content lives in architecture/; roadmap.md is only what is still open.
guides: building/layout/testing/adding-a-driver (Linux code may be used). history: four entries (ls -lR, TLB
shootdowns, MDIO 0x02460246, netlink MSG_TRUNC). Website: cards, stack, download page, docs index.
Code comments that pointed at deleted plan docs point at the architecture docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:08:05 +01:00
Nexoniarz
2556a955b3 docs: architecture describes what is built now
kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys,
/dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship),
devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel),
graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and
desktop.md (Xorg on the framebuffer).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 07:57:25 +01:00
Nexoniarz
655c7d8d01 docs: Finch's main is the Finch-written compiler
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-26 00:38:48 +01:00
Nexoniarz
5843503033 docs: Finch is written in Finch now
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 21:55:59 +01:00
Nexoniarz
d7ad3d09f7 docs: Finch builds itself; what is left before main switches
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 19:49:21 +01:00
Nexoniarz
7d2eca8271 Faster ISO builds (69 s to 9 s) and a faster write.sh check
make iso recompiled the kernel and every command each time: iso and
ramroot wiped build/kernel, build/lib and build/cmd to make sure nothing
was compiled for the wrong RAMROOT setting. Only kernel/proc/proc.c and
kernel/dev/ramdisk.c read RAMDISK_ROOT, so only their objects go now.

Even then a no-op make iso took 69 s, because sysroot.stamp lists every
staged file with its mtime and two things re-copied files on every run:
the plain cp in etc-install (now cp -p) and fastfetch's install (now
install -C). A changed stamp rebuilds the Alpine root, the root image
and its zstd -19 compression. A no-op, and a one-file kernel change,
now take about 9 s.

write.sh read the stick back with a buffered cmp, slowly and possibly from
the page cache. It now reads 4 MB at a time with iflag=direct, which
checks the stick itself; --no-verify skips the check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 19:28:03 +01:00
Nexoniarz
afb2e084af docs: Finch's self-hosting gap, as it stands
Sized numbers, methods, defer, errors as values and maps are now in the
self-hosted compiler; C import is the one piece left.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 18:55:31 +01:00
Nexoniarz
9c729edac1 Cheaper cursor, a fortify panic that names its function; Finch's gap
The cursor now costs nothing for a byte inside an escape sequence and
one cell fewer for a plain glyph: the grid is the truth, so the old
cursor cell is repainted after the byte, and not at all when a glyph
has just been drawn over it.

__fortify_panic says "r8169: rtl_open.cold+0x..." through ldlsd's module
list, so a photograph of the screen is enough, and exits instead of
spinning on a core and holding /dev/netdev.

docs/plan/finch.md: only integrating Finch is later; finishing it is
not. The self-hosting gap measured against Finch's own bootstrap test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 18:31:40 +01:00
Nexoniarz
2651ff2169 docs: Finch, the author's language, for SchOS's own programs
A plan, deferred behind the 1.0 freeze like everything else here: what
Finch is today (measured from its repository: LLVM, self-hosting boot/
compiler, C import, targets), what SchOS would need, and the open
question of what "finishing" it means.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 18:26:05 +01:00
Nexoniarz
96389ada8f A text cursor in the tty; LDLS sets net_device.priv_len
The virtual terminals never drew a cursor. Each now paints a steady
block (the cell with its colours swapped), erased before and redrawn
after every output byte, redrawn by a repaint, and hidden by
ESC [ ? 25 l as curses programs ask.

r8169 under LDLS on the TUF A15 got through probe and died in ndo_open
on __fortify_panic. netdev_priv() is `dev->priv` now, a flexible array
counted by net_device.priv_len, and rtl_open's memset of its ring
bookkeeping is bounds-checked against it. The allocator left priv_len
zero, so the driver saw an area of no bytes. It is set from BTF when the
kernel has the member. __fortify_panic also says which check, how many
bytes it had and wanted, and where from.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 18:25:30 +01:00
Nexoniarz
a9d2e14004 net: an interface that appears after boot starts the stack
With the native r8169 out of the registry, the TUF A15 boots with no
network device; netinit() saw none and left the stack off, and
net_attach_late() then refused every interface that came later -- its
comment assumed netinit() was still to run. Linux's r8169 under LDLS
got as far as registering ldls0 and was turned away. A late interface
now starts the stack, whose first act is to attach every device there
is; net_eth_attach() refuses a device it already has, for the window
in which both could offer it.

LDLS says which refusal it got: EBUSY is another process, anything else
is the kernel.

Tested in QEMU with -nic none and netecho: no interface at boot, then
ldls0 with a DHCP lease and ping answered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 15:38:06 +01:00
Nexoniarz
d437e42b49 LDLS: every MDIO read was RFLAGS; ACPI: an Embedded Controller
On the TUF A15 Linux's r8169 found its PHY and read its ID as 0x02460246.
mdio-bus keeps the register it has just read in r11 across
preempt_enable(), which on x86 is `decl %gs:__preempt_count; je` to a
call that preserves every register. LDLS gave __preempt_count zeroes, so
the count reached zero and the call was made -- to a logging stub,
whose syscall left RFLAGS (0x246) in r11. __preempt_count now starts at
PREEMPT_ENABLED (x86 keeps NEED_RESCHED inverted), and preempt_schedule
and its notrace twin are a bare ret.

The same laptop reported its battery full and on AC, unplugged. The EC
owns both, and with no EmbeddedControl handler _REG never told the
firmware the EC was usable, so _BST and _PSR answered their fallbacks.
The EC is found by PNP0C09 or the ECDT and served by the ACPI 12.3
byte protocol, installed before _INI. SCI queries are not handled yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 15:11:36 +01:00
Nexoniarz
3676036762 Netlink datagrams can be measured, credentials name their sender
Epiphany's main thread spun at 100% on GLib's network monitor. GLib
sizes each netlink datagram with recvmsg(MSG_PEEK|MSG_TRUNC) and no
buffer; that answered 0, GLib read 0 bytes, the answer stayed queued and
poll() said readable again, forever. recvmsg on netlink now peeks
without consuming and with MSG_TRUNC returns what is waiting. A drained
netlink socket also no longer reports hangup -- it has no peer, and
sock_hup took that for a closed one.

SCM_CREDENTIALS under SO_PASSCRED named the process that created the far
end, not the one that wrote to it. For a socketpair those differ: WebKit
makes the pair, hands one end to a child, and learns the child's pid
from the credentials on its first byte. Each end now records its last
writer.

LDLS: napi_consume_skb frees the skb. r8169 frees every sent frame with
it, and as a stub it leaked one per frame until the DMA heap ran out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 14:58:14 +01:00
Nexoniarz
4ad49ed5f2 Linux's own r8169 through LDLS, deep idle, and the GPU plan
The RTL8168 is now driven by Linux's r8169, unmodified, in a process.
`make ldls-r8169` stages it from the running kernel together with libphy,
mdio-bus, mdio_devres and the Realtek PHY driver, the kernel's BTF and
the RTL PHY firmware, and etc/schinit/ldls-r8169.job starts it at boot
with its log in /var/log/ldls-r8169.log. The native driver is out of the
registry; its code, now with Linux's 8168H setup, stays as the fallback.

LDLS grows what that needed:
- ldlsd loads a comma-separated module set in dependency order, each
  linking against the ones before it, with BTF read before any
  init_module runs;
- a driver core (bus match, driver probe), devres, kernel printf, real
  udelay/msleep/ktime and 1000 Hz jiffies, phylib's bitmaps and
  link_mode_params, firmware from /lib/firmware, and the symbols whose
  stub zero was a lie (list validity, try_module_get, dev_err_probe,
  eth_platform_get_mac_address);
- pdev->bus is a real pci_bus, and cmovcc from per-CPU memory is
  rewritten like the other %gs forms.
In QEMU all five modules load, link and initialise, and r8169 registers
and finds no device. Probe and traffic are untested until real hardware.

The kernel turns ASPM off on Realtek NIC links at boot, as r8169 does
through pci_disable_link_state, which a process cannot.

Idle CPUs enter ACPI _CST's C2 (CC6 on Zen) by port read or MWAIT when
the APIC timer keeps running, instead of only ever halting at C1.

docs/plan/gpu.md: amdgpu and nouveau through LDLS, with measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 14:26:16 +01:00
Nexoniarz
438e7af141 Keyboard lights, TLB shootdowns, battery in sysfs, /proc/pid/maps
Caps, Num and Scroll Lock now light their LEDs on PS/2 and USB keyboards,
share one state between keyboards, and stop toggling on key repeat. X sets
them through evdev writes while it holds the keyboard.

Unmapping memory in a process with threads on other CPUs now flushes their
TLBs with an IPI. Without it a freed page stayed reachable through stale
translations, which corrupted Epiphany's heap on SMP.

ACPI batteries and AC adapters appear under /sys/class/power_supply.
/proc/<pid>/maps lists file mappings, and a process killed by a fault
names the file and offset it died in. r8169 gets a 256-entry RX ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 13:43:27 +01:00
Nexoniarz
73dcea7496 Netlink receives say they came from the kernel
recvfrom and recvmsg on a netlink socket reported no sender address. GLib's
network monitor converts the sender with g_socket_address_to_native to
check it is the kernel (port id 0) before believing a message, and with
nothing there it failed an assertion -- Epiphany, and anything else
watching the network through GIO, aborted at start. Every message on a
netlink socket here does come from the kernel, so that is what is reported:
a sockaddr_nl with port id 0 and no groups, as getsockname already says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 12:46:51 +01:00
Nexoniarz
4743df8c4a epoll: edge-triggered descriptors lost their edges on more than one CPU
EPOLLET reported a descriptor again only after a scan had seen it not
ready. Xorg watches its clients edge-triggered; when it read part of a
client's requests and more arrived before its next scan, the socket never
looked not-ready, so it was never reported again -- X waiting on the
client, the client waiting on X. One CPU almost never hit it; on a 16-thread
laptop the desktop froze within seconds: Window Maker's menu stuck, xterm
never mapped, text gone, and Caps Lock dead because X owns the LED.

Now a still-ready descriptor is reported again whenever anything has
signalled readiness since its last report (poll_generation()), which may be
spurious, as EPOLLET permits, and is never missed. Reproduced and verified
with QEMU at -smp 8: the root menu works and xterm runs cmatrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 11:59:12 +01:00
Nexoniarz
54b25708c3 Demand paging, the RTL8168H, links that come up late, and Ctrl+Alt+F1..F12
- Anonymous memory can be reserved without being backed: large mappings,
  MAP_NORESERVE and PROT_NONE are recorded and each page appears, zeroed,
  on first touch -- from trap() for the program, from uva2ka() for the
  kernel on its behalf. WebKit reserves 4 GB at start and touches a sliver;
  backing it all failed with ENOMEM and Epiphany aborted.
- r8169: the 8168G generation and later (the laptop's 8168H, XID 541) are
  taken from the firmware's out-of-band mode, get their PLL powered, the
  receive gate opened, and their PHY reached through the OCP window. The
  PHY's power-down bit is cleared; the old code kept it while restarting
  autonegotiation. Untested on the hardware so far.
- The network thread follows each device's link into lwIP. The link was
  read once at attach, 0.02 s into boot; real autonegotiation takes
  seconds, so on hardware the interface stayed down for good.
- Twelve terminals, switched with Ctrl+Alt+F1..F12 by default: the keys X
  uses, so the console and the desktop agree. AltGr+F-key is gone from
  the defaults (chvt -c altgr+F brings it back).
- SO_PASSCRED on Unix sockets, with SCM_CREDENTIALS delivered by recvmsg.
  WebKit sets it on its IPC sockets and aborted when refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 11:27:48 +01:00
Nexoniarz
3113eff7fe inotify, faccessat2, fsync for Linux programs, and D-Bus credentials
What installing and starting Epiphany ran into:

- inotify: inotify_init/init1, add_watch, rm_watch, as a new kind of event
  descriptor. The filesystem reports creates, deletes, renames (with a
  cookie pairing MOVED_FROM and MOVED_TO), writes, close-after-write,
  truncation and attribute changes, to watches on the inode and on its
  directory with the name. ONESHOT, MASK_ADD, MASK_CREATE, ONLYDIR,
  DONT_FOLLOW, IGNORED on removal, Q_OVERFLOW, and Linux's coalescing of
  repeated events. The hooks cost one load when nothing is watched. Without
  it GLib found no file monitor and GNOME programs aborted.
- FIONREAD on inotify descriptors and pipes (inotifywait sizes its buffer
  by it).
- fsync and fdatasync had no Linux numbers since the first commit: GLib's
  atomic writes, gtk-update-icon-cache and dbus-uuidgen all got ENOSYS.
- faccessat2, the flags-taking faccessat musl uses for any flags.
- sendmsg accepts SCM_CREDENTIALS when they are the sender's own, as Linux
  checks; GDBus sends them in every handshake and was refused with EINVAL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 06:40:28 +01:00
Nexoniarz
c77841ce3d Sound through ALSA, a 2D game, and a stack walk that cannot panic
SDL 3 has no OSS backend and SchOS has no ALSA kernel side, so sound now
goes SDL -> alsa-lib -> alsa-plugins' OSS plugin -> /dev/dsp. That path
found two bugs underneath it:

- ac97: resetting the PCM-out box also clears the descriptor list address,
  and nothing set it again. Every stream after the first stop played a
  "list" at physical address 0 -- silence -- and ALSA resets before each
  stream. If the engine halts at the end of the queue, the run bit is set
  again.
- /dev/dsp: SETFRAGMENT, GETFMTS, GETCAPS, SETTRIGGER, GETOPTR and GETODELAY
  for the plugin's ring-and-pointer model, and poll(), writable once a whole
  fragment fits (ALSA spun at 76% CPU otherwise). speaker-test now plays in
  real time at 0% CPU, verified in a recording of QEMU's AC'97.
- /etc/asound.conf: the default device is a plug to the OSS device at 16-bit
  stereo 48 kHz, what every SchOS audio driver plays.

Also:

- getstackpcs: each frame link must go up the same stack by less than
  KSTACKSIZE. The chain ends at the syscall entry with the user's rbp,
  which lbreakouthd uses as a register (-1024); the walk dereferenced it
  under the syscall tracer and the kernel panicked inside acquire().
- fb: FBIOGET_VSCREENINFO reports nominal timings, as Linux's efifb does,
  instead of zeros, which Xorg's fbdev turned into a 0 Hz RandR mode.
- procdump (Ctrl-P) shows where runnable threads are in user space.
- World: lbreakouthd instead of lbreakout2 (its stk.c passes vsnprintf a
  size of 511 plus the prefix length, and Alpine's fortified headers trap on
  that), plus curl, alsa-lib and alsa-plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 05:39:16 +01:00
Nexoniarz
393bf674f5 NetSurf: what GTK's images and libcurl's resolver needed
NetSurf (GTK 3) now starts under X and loads pages over HTTPS. Every icon
GTK draws goes gdk-pixbuf -> glycin -> a loader process -> a sealed memfd
-> GMappedFile, and each hop found something missing:

- mkaroot runs Alpine's package triggers in the root under bubblewrap.
  --no-scripts had skipped them, so there was no MIME cache, pixbuf loader
  list, compiled GSettings schemas, GIO module cache, font cache or busybox
  applet links.
- /usr/bin/bwrap is a stand-in that parses bubblewrap's options and runs
  the program with no sandbox: glycin starts its loaders only through bwrap,
  and real bwrap needs namespaces. schos-base replaces bubblewrap for it.
- ioctl: FIOCLEX, FIONCLEX, FIONBIO and FIOASYNC on any descriptor (Rust's
  std uses them; pipes and sockets answered ENOTTY).
- capget/capset: root has every capability, others none.
- socketpair accepts SOCK_DGRAM and SOCK_SEQPACKET, carried as a stream.
- waitpid and setpgid match children against the thread group leader, so a
  thread can wait for the process it spawned (ECHILD before).
- memfd: fstat reports the ftruncate length, not the page-rounded one; file
  seals (F_ADD_SEALS, F_GET_SEALS, MFD_ALLOW_SEALING) are recorded and
  enforced on later writes, truncation and writable mappings; MFD_CLOEXEC
  is honoured; read-only MAP_PRIVATE maps are allowed.
- recvfrom honours O_NONBLOCK and MSG_DONTWAIT. musl's resolver reads its
  socket until EAGAIN and blocked for ever after the first DNS answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 04:10:26 +01:00
Nexoniarz
f4d7f9100d Threads
A thread is a struct proc that shares three counted objects with others of
its group: the address space (struct mm: page tables, heap top, VMAs and a
lock for changing them), the descriptor table, and the signal dispositions.
clone() shares what its flags say and fork() copies.

- clone(): pthread_create's shape (VM, FILES, SIGHAND, THREAD, SETTLS,
  PARENT_SETTID, CHILD_CLEARTID) and posix_spawn's (VM, VFORK, a stack).
- futex: WAIT, WAIT_BITSET with timeouts, WAKE, REQUEUE, CMP_REQUEUE, keyed
  by the word's kernel address so shared pages meet. set_tid_address keeps
  the word that exit zeroes and wakes, for pthread_join.
- exit ends a thread; exit_group, a fatal signal, or the leader exiting ends
  them all. reapd frees exited threads; the leader is reaped by its parent
  once the rest are gone. getpid is the group, gettid the thread.
- Stops and resumes reach every thread; other process signals the leader.
- exec kills the other threads and unshares the dispositions.
- No TLB shootdown, so pages unmapped from a shared address space are freed
  only once every CPU has reloaded CR3 or is idle.
- procfs: /proc lists processes, task/ lists threads, comm, Threads:, Tgid.
- The console honours VMIN 0 / VTIME (btop's input drain hung on it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 03:03:50 +01:00
Nexoniarz
2372806862 Job control, function keys, and real sleeps
- SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU stop, SIGCONT resumes; the parent gets
  SIGCHLD and wait4 reports WUNTRACED/WCONTINUED; ps shows T. Ctrl-Z works,
  and mc no longer hangs waiting for its subshell to stop.
- Blocking reads (console, pty, pipe) and sleeps take a pending stop and
  carry on after SIGCONT; a caught signal without SA_RESTART ends them
  with EINTR.
- nanosleep reads tv_nsec (sleep 0.5 used to return at once), writes the
  remainder, and clock_nanosleep understands TIMER_ABSTIME.
- rt_sigaction uses the k_sigaction layout for Linux-ABI processes: the old
  action's restorer was written 112 bytes past the caller's struct. Flags
  are kept; SIGKILL and SIGSTOP cannot be caught.
- F1..F12 reach programs, as the Linux console sends them (PS/2, USB, I2C).
- TIOCLINUX subcode 6 answers "no modifiers" instead of echoing the 6 back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 02:37:45 +01:00
Nexoniarz
fd9d392416 Pseudo-terminals, and what tmux and htop needed around them
- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline,
  window sizes, job control and hangup (kernel/dev/pty.c).
- The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL,
  DECSC/DECRC; swallows charset designations and private CSI sequences;
  and LF only returns the carriage under ONLCR.
- setsid() drops the controlling terminal, so a pane's pty becomes its ctty.
- pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe).
- File modes are stored and enforced: chmod, fchmod(at), umask, creation
  modes; /tmp is 1777.
- /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag.
- SO_PEERCRED, IOV_MAX 1024.
- /proc/strace: name a program and its syscalls go to the kernel log.
- World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 02:11:10 +01:00
Nexoniarz
c65bfa3fa3 boot: driver chatter goes to the log; the console clears its margins
ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are
kinfo now: in dmesg and on a verbose boot, off the screen of a working
machine. Failures stay cprintf.

vt_repaint also paints the strip right of the last column and below the
last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:53:46 +02:00
Nexoniarz
f6c4b6377f GMMOS: USB keyboards on xHCI
The third micro driver: xhci.c with everything but finding a keyboard and
reading it taken out. It does not take over the live driver's state --
the kernel may have died inside that driver -- so each controller is
halted, reset and enumerated from nothing. Of the live driver it uses
the register mapping and the scratchpad pages the controller was lent
(DCBAA entry 0, read before the reset).

Root ports, and USB 2 hubs up to two deep (power, reset, route string,
transaction translator for a slow device behind a high-speed hub). A
boot-protocol keyboard interface gets Set Configuration, Set Protocol
boot, Set Idle, one interrupt IN endpoint and a Normal TRB, re-queued
after every report. No interrupts, no hot-plug, nothing that is not a
keyboard or a hub. A static 256 KB pool instead of the DMA allocator.

Checked under QEMU with the PS/2 controller switched off (i8042=off): a
USB keyboard on a root port, and one behind a hub, both type into the
GMMOS terminal. QEMU's hub is full speed, so the transaction-translator
path is not exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:23:14 +02:00
Nexoniarz
c6d90e69fa GMMOS: I2C keyboards, and every keyboard at once
The second micro driver: i2c_hid.c with everything but reading a
keyboard's input reports taken out. No ACPI -- AML cannot run on a dead
kernel -- so the live driver hands over what it found at boot
(i2c_hid_keyboards): the DesignWare controller, the address, the report
size, the AMD GPIO pin if there is one, and the parsed report descriptor.
No power commands, no reset, a read-only transfer.

usb_hid.c's report-protocol rewrite is split out as hid_boot_report(),
which does nothing but rewrite, and kernel/gmmos/hidkeys.c turns boot
reports into keys for this and the USB micro drivers to come.

GMMOS now probes every micro driver and polls all that answer.

Not run: QEMU has no DesignWare I2C, and the laptops at hand have PS/2
keyboards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:15:50 +02:00
Nexoniarz
cb9b06d46f GMMOS: the Guru Meditation Micro OS, alive under the PSOGM
A panic no longer ends in a picture. The report is drawn as before, and
then GMMOS runs under it: the real kernel stays dead -- the other
processors are halted, and nothing of it is called that locks, sleeps,
allocates or waits for an interrupt -- and GMMOS keeps its own clock
(the TSC), its own keyboard (micro drivers, polled), and the screen.

On the report: a live status line counting down the three-minute
power-off, which any key cancels, and the keys.

  Space  the last 300 lines of this boot's log as QR codes, a page per
         symbol, numbered; + and - trade module size for capacity
  L      the log on screen, scrolled
  T      a terminal: why, log, grep, ps, cpus, pci, mem (page tables
         walked first, so a bad address cannot fault), uptime, restart,
         poweroff
  R R    restart; P P power off -- twice, so a stray key does neither

The first micro driver is PS/2, polled: kbd.c with everything but the
keys taken out. The probe asks the keyboard to echo, so a controller
with nothing behind it is not offered as one. Polling also sidesteps the
IRQ 1 polarity quirk, which is about interrupt routing.

A panic inside GMMOS (or a second one in the report) is reported and
then only blinks; it does not re-enter GMMOS.

Checked under QEMU: every screen; two QR pages read off a screenshot
with zbar decode to the log text exactly; restart reboots; left alone it
still powers off at three minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 01:11:04 +02:00
Nexoniarz
38c89b1a9a panic stops every other processor
A panic used to stop only the processor it happened on. The rest carried
on with a dead kernel: on the ASUS laptop the report flashed up and was
drawn over, and the fans stayed loud until the power button was held.

Now the panicking processor sends every other one an NMI, which arrives
even with interrupts off, and trap() halts a processor that takes one
while panic_stop is set -- first thing, before the canary check, since
that stack may be what broke. NMI gets its own per-CPU stack (IST 1):
for a few instructions at the top of syscall_entry the live stack is the
user's, at ring 0, and a fault pushing the frame there would be a double
fault on the same stack and a reset instead of a report.

A second processor that panics while one is already reporting halts
instead of drawing over it; the owner's own nested panic still gets
through.

Checked under QEMU with -smp 4: after a sysrq crash CPUs 1-3 sit in hlt
with IF clear and CPU 0 is in the report's blink loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:50:50 +02:00
Nexoniarz
d8e8ae604f qr: every version, 1 to 40
PSOGM mode will put the kernel log on screen as QR codes, a page per
symbol, and version 6 held 134 bytes. Version 40 at level L holds 2,953.

What grew: blocks of two lengths and the interleave that steps around
them, the alignment-pattern grid, the version-information blocks from
version 7, the 16-bit character count from version 10. The per-version
table is down to the two columns that are genuinely transcription (EC
codewords per block, block count); total codewords come from the
geometry.

Checked against segno module for module at every version (exact-fill
payloads), and 120 symbols across all 40 versions decode with zbar.
OpenCV's detector cannot find symbols above about version 24 at all,
segno's included, so qrcheck reports those as the detector's limit.

The crash card stays a version-6 symbol (QR_CARD_BYTES), so a phone still
reads it from across the desk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:43:36 +02:00
Nexoniarz
5ea03c4cf5 i2c-hid: the touchpad crash was a printf format, not the driver
The PSOGM on the ASUS TUF A15 was a page fault in cprintf, from probe(),
in the touchpad driver's own success message. cprintf had no field widths:
"%04x" printed literally and consumed no argument, so the "%s" after it
was handed the product ID as a string pointer. "%lx" misprinted the same
way -- "0x4275838976x" for 0xFEDC2000 -- on the line before.

The driver's formats are fixed, and cprintf now steps over flags and
widths and takes l/ll/z as 64-bit, so a directive it does not render can
no longer shift every argument after it. Until now any %ld or %lu printed
the number followed by a stray letter.

The touchpad on/off entry in the boot menu has served its purpose and is
gone; the verbose step trace stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:28:23 +02:00
Nexoniarz
b15fb2fdaa i2c-hid on a stack of its own; PSOGM powers off after three minutes
The touchpad driver crashed the ASUS TUF A15 at boot. Its thread runs the
firmware's AML -- _DSM, _PS0, _CRS -- through uACPI at -O0, on an 8 KB
kernel stack, and kernel stacks are one array: running off the end lands
in the next thread's stack, and the machine dies a moment later in some
other code. The thread now runs on a 64 KB stack of its own, through
SCFF's call_on_stack().

And a crashed laptop sat with its fans up until somebody held the power
button, since the other processors keep running. On an ordinary boot the
PSOGM now says it will power off, and does, after three minutes: SLP_TYP
from \_S5, read at boot, written straight to PM1 control with no AML, which
could sleep or wait on a lock a dead processor holds. A verbose boot keeps
the screen up for ever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:21:39 +02:00
Nexoniarz
c79b1d6d6d boot menu: turn the touchpad driver off, and trace its steps
The ASUS TUF A15 freezes at the default resolution shortly after the
loader hands over -- screen dark, Caps Lock dead -- and it is the only
machine here with an I2C-HID touchpad, which the driver added in the same
release now probes at boot. "Touchpad driver: on/off" in the F8 menu
answers whether it is the cause on the machine itself, through a new
SCHBOOT_OFF_FLAGS byte the kernel checks before starting the driver.

A verbose boot now prints each step of the probe before taking it, so if it
is the cause, the last line on the screen says which step.

Also: S in the menu wrapped at five entries whatever the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 23:33:25 +02:00
Nexoniarz
d2c70c9129 display: confirm a resolution change, and size the console font to the screen
On the Ryzen 4600G desktop and the ASUS TUF A15, AMD's GOP "sets" the
smaller modes without leaving the panel's native timing and reports the
mode's own row length. Every check the loader made passed, and the screen
showed three or four squashed copies side by side (1920/640, 2560/640).
Nothing the firmware reports can tell such a mode from a real one.

So a mode change now asks "Keep this resolution?" in the new mode and keeps
it only on Enter within ten seconds. On a mode that does not really work the
question cannot be read, and the old mode comes back by itself.

And the reason to want a smaller mode was the text: the console font was a
fixed 16 px, unreadable on a 1440p monitor. It now scales to about 45 rows,
16 to 40 px per em, on whatever mode the firmware booted in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 23:11:22 +02:00
209 changed files with 29821 additions and 5027 deletions

12
.gitignore vendored
View file

@ -36,17 +36,15 @@ dist/
# Build output under projects/ -- these are programs that run on SchOS, and # Build output under projects/ -- these are programs that run on SchOS, and
# their binaries were tracked by accident. # their binaries were tracked by accident.
projects/schcom/schcom
projects/schcom/schcom-demo
# A built ISO dropped next to the site for download.html. 62 MB; not source. # A built ISO dropped next to the site for download.html. 62 MB; not source.
docs/Website/releases/ docs/Website/releases/
# Kernel modules, ever. docs/plan/ldls.md rule 1: LDLS ships the layer and # Kernel modules are build artefacts, not source: they are staged into the
# never the thing it loads, and that applies to this tree as well as to an # image by `make ldls-r8169` / `make ldls-stage` from a Linux build and ship
# image. The loader's test input is built from ldls/test/refmod.c, which is # in the ISO (docs/architecture/ldls.md rule 1), but a binary does not belong
# ours; a .ko here would be someone else's code, with its own provenance, in # in git. The loader's test input is built from ldls/test/refmod.c, which is
# a tree that is otherwise all first-party or attributed in NOTICE. # ours.
*.ko *.ko
*.ko.xz *.ko.xz

216
Makefile
View file

@ -26,7 +26,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \
pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
dbg.o netuser.o audio.o dmi.o) dbg.o netuser.o audio.o dmi.o pty.o kdev.o)
# Drivers for hardware that has to be *found* rather than assumed. Anything # Drivers for hardware that has to be *found* rather than assumed. Anything
# discovered by walking a bus belongs here; the fixed-address legacy devices # discovered by walking a bus belongs here; the fixed-address legacy devices
@ -38,17 +38,34 @@ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
# already eight files with four called usb_something, and each driver added made # already eight files with four called usb_something, and each driver added made
# the directory harder to read. The compile rule uses `mkdir -p $(dir $@)`, so # the directory harder to read. The compile rule uses `mkdir -p $(dir $@)`, so
# the depth costs nothing. # the depth costs nothing.
# Which of them are compiled in is config/drivers.conf: y or n per driver, y by
# default, overridable on the command line (make DRV_E1000=n). Only the drivers
# that nothing else calls can be left out; the rest are always linked.
-include config/drivers.conf
DRV_LIST = VGA_PCI VIRTIO_GPU E1000 RTL8139 R8169 AC97 AHCI NVME
DRV_DEFAULT_R8169 = n
$(foreach d,$(DRV_LIST),$(eval DRV_$(d) ?= $(if $(DRV_DEFAULT_$(d)),$(DRV_DEFAULT_$(d)),y)))
$(foreach d,$(DRV_LIST),$(if $(filter y n,$(DRV_$(d))),,$(error config/drivers.conf: DRV_$(d) must be y or n, not '$(DRV_$(d))')))
DRV_DEFS = $(foreach d,$(DRV_LIST),$(if $(filter y,$(DRV_$(d))),-DCONFIG_DRV_$(d)=1))
# Rewritten only when the choice changes, so registry.o is rebuilt then and not otherwise.
DRV_STAMP = $(BUILD)/kernel/drivers/.config
$(DRV_STAMP): FORCE
@mkdir -p $(dir $@)
@echo '$(DRV_DEFS)' | cmp -s - $@ || echo '$(DRV_DEFS)' > $@
$(BUILD)/kernel/drivers/registry.o: $(DRV_STAMP)
$(BUILD)/kernel/drivers/registry.o: CFLAGS += $(DRV_DEFS)
KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_pci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_fb.o KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_fb.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o KERN_DRIVERS += $(if $(filter y,$(DRV_VGA_PCI)),$(BUILD)/kernel/drivers/gpu/vga/vga_pci.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/e1000/e1000.o KERN_DRIVERS += $(if $(filter y,$(DRV_VIRTIO_GPU)),$(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/r8169/r8169.o KERN_DRIVERS += $(if $(filter y,$(DRV_E1000)),$(BUILD)/kernel/drivers/net/e1000/e1000.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o KERN_DRIVERS += $(if $(filter y,$(DRV_R8169)),$(BUILD)/kernel/drivers/net/r8169/r8169.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o KERN_DRIVERS += $(if $(filter y,$(DRV_RTL8139)),$(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o)
KERN_DRIVERS += $(if $(filter y,$(DRV_AC97)),$(BUILD)/kernel/drivers/audio/ac97.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/ahci/ahci.o KERN_DRIVERS += $(if $(filter y,$(DRV_AHCI)),$(BUILD)/kernel/drivers/storage/ahci/ahci.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/nvme/nvme.o KERN_DRIVERS += $(if $(filter y,$(DRV_NVME)),$(BUILD)/kernel/drivers/storage/nvme/nvme.o)
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
@ -115,8 +132,11 @@ KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \
decompress/zstd_decompress_block.o) decompress/zstd_decompress_block.o)
KERN_ZSTD += $(BUILD)/kernel/scff/scff.o KERN_ZSTD += $(BUILD)/kernel/scff/scff.o
# GMMOS, what runs after a panic, and its micro drivers. See include/gmmos/gmmos.h.
KERN_GMMOS = $(addprefix $(BUILD)/kernel/gmmos/, gmmos.o hidkeys.o ps2.o i2c.o xhci.o)
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \ KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
proc.o) proc.o mapname.o)
KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \ KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
syscall.o sys_proc.o sys_fs.o sys_mem.o sys_net.o \ syscall.o sys_proc.o sys_fs.o sys_mem.o sys_net.o \
@ -126,7 +146,8 @@ KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o
KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \ KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \
$(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(KERN_PROC) $(KERN_SYS) $(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(KERN_GMMOS) \
$(KERN_PROC) $(KERN_SYS)
# entry.o must be first in link order; remove it from KERNOBJS for the rule # entry.o must be first in link order; remove it from KERNOBJS for the rule
KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
@ -140,7 +161,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
# Third-party software does NOT go here — see ports/. # Third-party software does NOT go here — see ports/.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest su passwd
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@ -524,17 +545,24 @@ endif
# #
# Evaluated when the Makefile is read, so the wipe happens before anything is # Evaluated when the Makefile is read, so the wipe happens before anything is
# compiled against the wrong setting. # compiled against the wrong setting.
#
# Only the two objects that read RAMDISK_ROOT are dropped: kernel/proc/proc.c
# (ROOTDEV is used nowhere else) and kernel/dev/ramdisk.c. The whole of
# build/kernel, build/lib and build/cmd used to go, which made every `make iso`
# recompile the kernel and every command, rebuild the root image from the new
# binaries and compress it again at zstd -19 -- minutes, for a one-line change.
# If another file ever starts to use ROOTDEV or RAMDISK_ROOT, add it here.
RAMROOT_STATE := $(if $(RAMROOT),on,off) RAMROOT_STATE := $(if $(RAMROOT),on,off)
RAMROOT_STAMP := $(BUILD)/ramroot.state RAMROOT_STAMP := $(BUILD)/ramroot.state
$(shell mkdir -p $(BUILD); \ $(shell mkdir -p $(BUILD); \
if [ "$$(cat $(RAMROOT_STAMP) 2>/dev/null)" != "$(RAMROOT_STATE)" ]; then \ if [ "$$(cat $(RAMROOT_STAMP) 2>/dev/null)" != "$(RAMROOT_STATE)" ]; then \
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd; \ rm -f $(BUILD)/kernel/proc/proc.o $(BUILD)/kernel/dev/ramdisk.o; \
printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \ printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \
fi) fi)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# The root filesystem is an Alpine root that apk manages, with SchOS installed # The root filesystem is an Alpine root that apk manages, with SchOS installed
# into it as the package schos-base -- see docs/plan/alpine.md and "The # into it as the package schos-base -- see docs/architecture/packages.md and "The
# apk-managed root" below. Both images are made from it: schfs.img, which # apk-managed root" below. Both images are made from it: schfs.img, which
# `make qemu` boots, and the ramdisk inside the kernel, which `make iso` # `make qemu` boots, and the ramdisk inside the kernel, which `make iso`
# boots. sysroot/ is what schos-base is packaged from, not a root of its own. # boots. sysroot/ is what schos-base is packaged from, not a root of its own.
@ -625,7 +653,7 @@ UEFI_CFLAGS = $(UEFI_DEBUG_FLAGS) --target=x86_64-unknown-windows -ffreestanding
# Hence the check and the message. It is worth the four lines. # Hence the check and the message. It is worth the four lines.
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \ $(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \
boot/common/ui.c boot/common/ui.h \ boot/common/ui.c boot/common/ui.h \
include/gfx/logo.h include/gfx/theme.h include/gfx/font.h include/gfx/logo.h include/gfx/theme.h include/gfx/font.h include/gfx/nvfont.h
@mkdir -p $(BUILD)/uefi @mkdir -p $(BUILD)/uefi
@command -v clang >/dev/null 2>&1 || { \ @command -v clang >/dev/null 2>&1 || { \
echo ""; \ echo ""; \
@ -734,7 +762,6 @@ qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
# silently reused for the other, leaving ROOTDEV pointing at the wrong device. # silently reused for the other, leaving ROOTDEV pointing at the wrong device.
# Dropping the compiled objects first makes the switch always correct. # Dropping the compiled objects first makes the switch always correct.
ramroot: ramroot:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1 $(MAKE) RAMROOT=1
cmd: $(CMDBINS) cmd: $(CMDBINS)
@ -752,7 +779,7 @@ ports:
# #
# Staged into $(SYSROOT) so the next image build picks them up; run # Staged into $(SYSROOT) so the next image build picks them up; run
# `make projects && make` to get them onto a disk. # `make projects && make` to get them onto a disk.
PROJECTS ?= schcom PROJECTS ?=
projects: projects:
@test -d $(MUSLPREFIX)/include || { \ @test -d $(MUSLPREFIX)/include || { \
@ -777,7 +804,7 @@ projects:
# CFLAGS and make cannot see that. # CFLAGS and make cannot see that.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
iso: iso:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd @$(MAKE) --no-print-directory ldls-r8169-if-present
$(MAKE) RAMROOT=1 schos.iso $(MAKE) RAMROOT=1 schos.iso
# One El Torito entry, pointing at a FAT image holding the loader and kernel. # One El Torito entry, pointing at a FAT image holding the loader and kernel.
@ -852,13 +879,13 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
$(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP) $(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD) @mkdir -p $(BUILD)
@rm -f $@ @rm -f $@
@used=$$($(MKSCHFS) $@ --size 262144 --label schos \ @used=$$($(MKSCHFS) $@ --size 262144 --label schos --owners $(AROOT_DIR)/owners \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \ $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \
sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \ sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \
test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \ test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \
rm -f $@; \ rm -f $@; \
$(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \ $(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) --owners $(AROOT_DIR)/owners $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
# The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks, # The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks,
# each its own zstd frame, unpacked by the kernel the first time a block in it # each its own zstd frame, unpacked by the kernel the first time a block in it
@ -963,6 +990,10 @@ $(BUILD)/kernel/dev/%.o: kernel/dev/%.c
@mkdir -p $(BUILD)/kernel/dev @mkdir -p $(BUILD)/kernel/dev
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/gmmos/%.o: kernel/gmmos/%.c
@mkdir -p $(BUILD)/kernel/gmmos
$(CC) $(CFLAGS) -c $< -o $@
# `$(dir $@)` rather than a fixed path: drivers are grouped into one directory # `$(dir $@)` rather than a fixed path: drivers are grouped into one directory
# per device class (gpu/, net/, bus/, usb/), so the output directory varies with # per device class (gpu/, net/, bus/, usb/), so the output directory varies with
# the target and a hardcoded mkdir only creates the top one. # the target and a hardcoded mkdir only creates the top one.
@ -1089,22 +1120,30 @@ $(BUILD)/lib/%.o: lib/%.S
# own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and # own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and
# stub.c are freestanding and build unchanged for either side, which is the # stub.c are freestanding and build unchanged for either side, which is the
# whole point of having written them that way. # whole point of having written them that way.
# A Linux module is compiled with an 8-byte-aligned stack and, being kernel
# code, never sets %al for a variadic call -- so it arrives in this code with
# the stack off by eight and %al garbage. Any function here that takes `...`
# then opens with `movaps %xmm0, -0xb0(%rbp)`, which faults on a misaligned
# address: that was the general-protection fault in ldlsd on real hardware, in
# the log function, the moment the first driver message was printed.
# -mstackrealign makes every function re-align its own stack on entry.
LDLS_ALIGN = -mstackrealign
LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \ LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o) $(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o base.o)
$(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h $(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h
@mkdir -p $(BUILD)/ldls/guest @mkdir -p $(BUILD)/ldls/guest
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) $(LDLS_ALIGN) -c $< -o $@
# The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci # The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci
# and /dev/netdev, which exist on SchOS and nowhere else. # and /dev/netdev, which exist on SchOS and nowhere else.
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h $(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h ldls/kpi/link_modes.inc
@mkdir -p $(BUILD)/ldls/guest/kpi @mkdir -p $(BUILD)/ldls/guest/kpi
$(CC) $(CFLAGS) -I./ldls -c $< -o $@ $(CC) $(CFLAGS) $(LDLS_ALIGN) -I./ldls -c $< -o $@
$(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST) $(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST)
@mkdir -p $(BUILD)/cmd @mkdir -p $(BUILD)/cmd
$(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o $(CC) $(CFLAGS) $(LDLS_ALIGN) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ \ $(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ \
$(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB) $(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB)
$(OBJCOPY) -S $@ $(OBJCOPY) -S $@
@ -1135,6 +1174,22 @@ $(BUILD)/cmd/xready: cmd/xready/main.c $(MUSLPREFIX)/lib/libc.so $(LINUXHDR)
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/xready.o \ $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/xready.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND) -L$(MUSLPREFIX)/lib -lc $(PORTEND)
# su and passwd: musl, for getpwnam and crypt, and static -- they run setuid root,
# so there is no loader or library path to be led astray.
$(BUILD)/cmd/su: cmd/su/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/su.o
$(DYNCC) -static -no-pie -nostdlib -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/su.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(BUILD)/cmd/passwd: cmd/passwd/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/passwd.o
$(DYNCC) -static -no-pie -nostdlib -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/passwd.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(BUILD)/cmd/locktest: cmd/locktest/main.c $(MUSLPREFIX)/lib/libc.so $(BUILD)/cmd/locktest: cmd/locktest/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd @mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/locktest.o $(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/locktest.o
@ -1308,7 +1363,7 @@ crashqr:
$(PYTHON) tools/python/crashqr.py $(PYTHON) tools/python/crashqr.py
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# LDLS -- the Linux Driver Layer. docs/plan/ldls.md. # LDLS -- the Linux Driver Layer. docs/architecture/ldls.md.
# #
# ldls/ko.c is freestanding and builds on both sides: here as part of a host # ldls/ko.c is freestanding and builds on both sides: here as part of a host
# tool, and later as part of the SchOS-side host. Same discipline as SchFS, # tool, and later as part of the SchOS-side host. Same discipline as SchFS,
@ -1333,7 +1388,7 @@ $(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \
$(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \ $(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \
ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c
# The test object, compiled and never linked. Ours, because docs/plan/ldls.md # The test object, compiled and never linked. Ours, because docs/architecture/ldls.md
# rule 1 means no .ko is committed here -- see the header of refmod.c. # rule 1 means no .ko is committed here -- see the header of refmod.c.
# #
# -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is # -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is
@ -1356,13 +1411,12 @@ ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES)
# #
# make ldls-stage KO=/path/to/8139too.ko BTF=/sys/kernel/btf/vmlinux # make ldls-stage KO=/path/to/8139too.ko BTF=/sys/kernel/btf/vmlinux
# #
# This is the model docs/plan/ldls.md describes, carried out on the machine # For trying any module, not just the one that ships. Deliberately **not** a
# that is doing the testing: the user obtains a module and loads it on their # dependency of `all`, so an ordinary build never picks it up, and `.gitignore`
# own machine. Deliberately **not** a dependency of `all`, so an ordinary build # refuses `*.ko` so a binary cannot be committed by accident. An image with a
# never picks it up, and `.gitignore` refuses `*.ko` so it cannot be committed # module in it is a distribution of that module's code, under its own licence
# by accident. An image built after this has somebody else's GPL code in it and # (docs/licensing.md §6) -- fine for the ones SchOS ships (r8169, below), and
# must not be redistributed -- which is exactly what licensing.md §6 says about # a thing to check for any other.
# the combination, and why it is one command you have to type on purpose.
ldls-stage: ldls-stage:
@test -n "$(KO)" || { echo "usage: make ldls-stage KO=<module.ko> [BTF=<vmlinux-btf>]"; exit 1; } @test -n "$(KO)" || { echo "usage: make ldls-stage KO=<module.ko> [BTF=<vmlinux-btf>]"; exit 1; }
@mkdir -p $(SYSROOT)/lib/modules @mkdir -p $(SYSROOT)/lib/modules
@ -1370,6 +1424,55 @@ ldls-stage:
@test -z "$(BTF)" || cp "$(BTF)" $(SYSROOT)/lib/modules/btf @test -z "$(BTF)" || cp "$(BTF)" $(SYSROOT)/lib/modules/btf
@echo "staged into $(SYSROOT)/lib/modules -- this image is now yours alone" @echo "staged into $(SYSROOT)/lib/modules -- this image is now yours alone"
# Linux's r8169 and what it needs, from the kernel this machine runs, for
# etc/schinit/ldls-r8169.job. The module set is r8169's dependency closure less
# what LDLS answers itself (led-class, of_mdio, fwnode_mdio, fixed_phy), in load
# order; the firmware is the RTL8168/8111 PHY patches. These are the modules and
# firmware SchOS ships in its ISO (docs/licensing.md §6, NOTICE): run this before
# `make iso` for an image that has the laptop's network.
LDLS_KDIR ?= /run/current-system/kernel-modules/lib/modules/$(shell uname -r)/kernel
LDLS_FWDIR ?= /run/current-system/firmware
LDLS_R8169 = drivers/net/phy/mdio-bus drivers/net/phy/libphy \
drivers/net/phy/mdio_devres drivers/net/phy/realtek/realtek \
drivers/net/ethernet/realtek/r8169
# Every file is replaced only if its contents differ. sysroot.stamp records the
# mtime of each staged file, `make iso` runs this every time, and rewriting the
# same bytes moved every mtime -- which rebuilt the root image and compressed it
# at zstd -19 (a minute) after a one-line kernel change.
ldls-r8169:
@mkdir -p $(SYSROOT)/lib/modules $(SYSROOT)/lib/firmware/rtl_nic
@set -e; list=; for m in $(LDLS_R8169); do \
f=$$(ls $(LDLS_KDIR)/$$m.ko* | head -1); n=$$(basename $$m).ko; \
t=$(SYSROOT)/lib/modules/$$n; \
case $$f in *.xz) xz -dc $$f;; *.zst) zstd -qdc $$f;; *) cat $$f;; esac > $$t.new; \
if cmp -s $$t.new $$t; then rm -f $$t.new; else mv -f $$t.new $$t; fi; \
list=$$list$${list:+,}/lib/modules/$$n; done; \
if ! cmp -s /sys/kernel/btf/vmlinux $(SYSROOT)/lib/modules/btf; then \
cp /sys/kernel/btf/vmlinux $(SYSROOT)/lib/modules/btf; fi; \
printf '#!/bin/sh\nmkdir -p /var/log\nexec /bin/ldlsd %s /lib/modules/btf > /var/log/ldls-r8169.log 2>&1\n' \
"$$list" > $(SYSROOT)/lib/modules/ldls-r8169.sh.new; \
if cmp -s $(SYSROOT)/lib/modules/ldls-r8169.sh.new $(SYSROOT)/lib/modules/ldls-r8169.sh; then \
rm -f $(SYSROOT)/lib/modules/ldls-r8169.sh.new; \
else mv -f $(SYSROOT)/lib/modules/ldls-r8169.sh.new $(SYSROOT)/lib/modules/ldls-r8169.sh; fi
@for f in $(LDLS_FWDIR)/rtl_nic/rtl8168*.fw*; do \
n=$$(basename $$f); case $$n in \
*.zst) out=$${n%.zst}; zstd -qdc $$f > $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; \
*.xz) out=$${n%.xz}; xz -dc $$f > $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; \
*) out=$$n; cp $$f $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; esac; \
d=$(SYSROOT)/lib/firmware/rtl_nic/$$out; \
if cmp -s $$d.new $$d; then rm -f $$d.new; else mv -f $$d.new $$d; fi; done
@echo "staged Linux r8169 ($(shell uname -r)) into $(SYSROOT)"
# `make iso` runs this first: the ISO ships Linux's r8169 set (NOTICE), so stage it whenever
# this machine has the modules and the BTF to stage it from, and say so when it does not.
ldls-r8169-if-present:
@if ls $(LDLS_KDIR)/drivers/net/ethernet/realtek/r8169.ko* >/dev/null 2>&1 && [ -r /sys/kernel/btf/vmlinux ]; then \
$(MAKE) --no-print-directory ldls-r8169; \
else \
echo "iso: no Linux r8169 modules or BTF on this machine ($(LDLS_KDIR)) -- this ISO will not carry them"; \
fi
ldls-unstage: ldls-unstage:
rm -rf $(SYSROOT)/lib/modules rm -rf $(SYSROOT)/lib/modules
@echo "removed" @echo "removed"
@ -1423,19 +1526,27 @@ LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
# proves the same thing far less clearly. # proves the same thing far less clearly.
SRV_FILES = $(wildcard srv/*) SRV_FILES = $(wildcard srv/*)
# cp -p, not cp: sysroot.stamp lists every staged file with its mtime, and the
# Alpine root, the root image and its zstd -19 compression are all rebuilt when
# it changes. A plain cp gave every copy a new mtime, so every `make iso`
# rebuilt all of them -- over a minute of CPU for no change at all.
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES) etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES)
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos $(SYSROOT)/srv @mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos $(SYSROOT)/srv
@cp $(ETC_PLAIN) $(SYSROOT)/etc/ @cp -p $(ETC_PLAIN) $(SYSROOT)/etc/
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/ @cp -p $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/ @cp -p $(ETC_ZSH) $(SYSROOT)/etc/zsh/
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt @cp -p $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
@cp $(SRV_FILES) $(SYSROOT)/srv/ @cp -p $(SRV_FILES) $(SYSROOT)/srv/
@# The device-name databases, where lspci and lsusb look for them: the @# The device-name databases, where lspci and lsusb look for them: the
@# hwdata package's directory. They were at /usr/share/usb.ids, which no @# hwdata package's directory. They were at /usr/share/usb.ids, which no
@# tool searches, so lsusb printed numbers only. @# tool searches, so lsusb printed numbers only.
@mkdir -p $(SYSROOT)/usr/share/hwdata @mkdir -p $(SYSROOT)/usr/share/hwdata
@rm -f $(SYSROOT)/usr/share/usb.ids @rm -f $(SYSROOT)/usr/share/usb.ids
@for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true @for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp -p dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true
@# The licences travel with the image: SchOS's, xv6's, and NOTICE, which carries the
@# third-party ones -- including Linux's r8169 modules, whose source offer it records.
@mkdir -p $(SYSROOT)/usr/share/licenses/schos
@cp -p LICENSE LICENSE.MIT NOTICE $(SYSROOT)/usr/share/licenses/schos/
@echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)" @echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
# The mount points. Created here rather than by init, because a mkdir on a # The mount points. Created here rather than by init, because a mkdir on a
@ -1549,15 +1660,6 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
# it gets worse -- because stage2 loads the kernel by PIO one sector at a time, # it gets worse -- because stage2 loads the kernel by PIO one sector at a time,
# and under KVM every one of those port accesses is a VM exit, where under TCG # and under KVM every one of those port accesses is a VM exit, where under TCG
# it is cheap. # it is cheap.
# Schcom, end to end, checked from outside the machine.
#
# Boots, starts the compositor and a client, and then screendumps the
# framebuffer and asserts the pixels on the host. That last part is the whole
# value: the compositor saying "I composited it" proves nothing, and a
# screenshot nobody inspects proves less. Needs `make projects` first.
schcom-test: esp $(BUILD)/schfs.img
sh tools/posix/schcom_test.sh
# The firmware and the disks, in one place. Every run target below boots the # The firmware and the disks, in one place. Every run target below boots the
# same way, because there is only one way to boot now: OVMF, the staged ESP as # same way, because there is only one way to boot now: OVMF, the staged ESP as
# the first disk, the SchFS volume as the second. Keeping the argument list in # the first disk, the SchFS volume as the second. Keeping the argument list in
@ -1626,7 +1728,7 @@ SCHFS_BLOCKS ?= 262144
$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP) $(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD) @mkdir -p $(BUILD)
@rm -f $@ @rm -f $@
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE) ./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --owners $(AROOT_DIR)/owners --sysroot $(ROOT_TREE)
# --- The apk-managed root ------------------------------------------------------ # --- The apk-managed root ------------------------------------------------------
# #
@ -1693,6 +1795,13 @@ LOGO_PX = 128
logo: logo:
$(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/logo.h $(LOGO_PX) $(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/logo.h $(LOGO_PX)
# include/gfx/nvfont.h: the boot manager's typeface (Instrument Sans, from
# docs/Website/niveus/fonts), rasterised once and committed, same arrangement as
# logo.h. Needs Pillow, so it is run by hand: nix-shell -p 'python3.withPackages
# (p: [p.pillow])' --run "make nvfont"
nvfont:
$(PYTHON) tools/python/mknvfont.py docs/Website/niveus/fonts/static include/gfx/nvfont.h
# The USB vendor/product name database, from linux-usb.org. # The USB vendor/product name database, from linux-usb.org.
# #
# Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD # Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD
@ -1754,10 +1863,10 @@ qrtest: $(QRTEST)
qrcheck: $(QRTEST) qrcheck: $(QRTEST)
$(PYTHON) tools/python/qrcheck.py $(PYTHON) tools/python/qrcheck.py
.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ .PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm \
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \ schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi logo nvfont qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
.PRECIOUS: $(BUILD)/%.o .PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives, # 'clean' removes generated output only. dist/ (upstream tarballs) survives,
@ -1781,3 +1890,6 @@ distclean: clean
# build output: Alpine's usr/lib/sysctl.d is a directory, and including it # build output: Alpine's usr/lib/sysctl.d is a directory, and including it
# stopped every make with "Is a directory". # stopped every make with "Is a directory".
-include $(shell find $(BUILD) \( -path $(BUILD)/aroot -o -path $(BUILD)/iso -o -path $(BUILD)/ports \) -prune -o -type f -name '*.d' -print 2>/dev/null) -include $(shell find $(BUILD) \( -path $(BUILD)/aroot -o -path $(BUILD)/iso -o -path $(BUILD)/ports \) -prune -o -type f -name '*.d' -print 2>/dev/null)
FORCE:
.PHONY: FORCE ldls-r8169-if-present

30
NOTICE
View file

@ -191,6 +191,36 @@ the whole library on the host to write it. zstd is dual-licensed; SchOS takes
it under the BSD licence above. The glue written for SchOS (kernel/scff/) is it under the BSD licence above. The glue written for SchOS (kernel/scff/) is
GPL-2.0-or-later and is not part of zstd; zstd's own sources are unmodified. GPL-2.0-or-later and is not part of zstd; zstd's own sources are unmodified.
--------------------------------------------------------------------------------
Linux kernel drivers (r8169 and its modules) GPL-2.0-only
--------------------------------------------------------------------------------
Copyright (c) the Linux kernel contributors
The ISO carries these Linux kernel modules, unmodified, as built for the
kernel version that `make ldls-r8169` took them from (see
/var/log/ldls-r8169.log on a running system for the version):
drivers/net/phy/mdio-bus.ko drivers/net/phy/libphy.ko
drivers/net/phy/mdio_devres.ko drivers/net/phy/realtek/realtek.ko
drivers/net/ethernet/realtek/r8169.ko
They are loaded into an ordinary process by ldlsd (ldls/, cmd/ldlsd), which is
GPL-2.0-or-later and part of SchOS, and not linked into the kernel. The
licence of the modules is the GNU General Public License, version 2 only; the
corresponding source is the same release of the Linux kernel, available from
https://www.kernel.org/ . The BTF type data staged beside them
(/lib/modules/btf) is generated from that kernel build.
--------------------------------------------------------------------------------
Realtek RTL8168/8111 PHY firmware (rtl_nic/rtl8168*.fw) Realtek / linux-firmware
--------------------------------------------------------------------------------
Copyright (c) Realtek Semiconductor Corp.
Redistributed unmodified from the linux-firmware collection, under the terms
in its WHENCE file and the Realtek licence there: redistribution in binary
form is permitted provided the copyright notice is reproduced. Staged by
`make ldls-r8169` into /lib/firmware/rtl_nic and read by the PHY driver.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Intel 8254x device identifiers BSD 3-Clause Intel 8254x device identifiers BSD 3-Clause
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

196
README.md
View file

@ -4,48 +4,50 @@
# SchOS # SchOS
**A 64-bit operating system, written from the metal up.** **A 64-bit operating system, written from the metal up — and it runs Linux's software.**
SMP kernel · copy-on-write filesystem · Linux syscall ABI · Alpine packages · X11 on real hardware SMP kernel · copy-on-write filesystem · Linux syscall ABI · Alpine packages · Xorg · Linux's own drivers
</div> </div>
--- ---
SchOS began as [xv6](https://github.com/mit-pdos/xv6-public) and is now mostly SchOS is a hobby operating system built by one person. It began as
something else. The scheduler, the memory manager and the trap path have been [xv6](https://github.com/mit-pdos/xv6-public) and almost nothing of that is left:
rewritten for x86-64 and for more than one processor; the filesystem has been the scheduler, the memory manager, the trap path and every driver have been
replaced outright; the syscall table speaks Linux numbering, so real Linux rewritten for x86-64 and for more than one processor, and the filesystem is a
software runs on it unmodified — statically or dynamically linked, straight different one. The syscall table speaks Linux's numbering, so **real Linux
out of Alpine's package repositories. programs run on it unmodified** — statically or dynamically linked, straight out
of Alpine's package repositories — and a desktop (Xorg and Window Maker) and a
web browser (NetSurf) come up on the framebuffer.
It is a hobby system built by one person, and it is honest about that: what It is honest about being a hobby system: what is listed below has been *run*,
works below has been *run*, not inferred, and the gaps are listed as plainly as not inferred, and [the gaps](#not-there-yet) are listed as plainly as the
the features. features. It boots from a USB stick on three different real machines.
## In numbers ## In numbers
<!-- stats:begin --> <!-- stats:begin -->
| | | | | |
|---|---| |---|---|
| **First-party code** | 61,367 lines, plus 21,128 lines of comments (26%) in 272 files | | **First-party code** | 72,766 lines, plus 23,670 lines of comments (25%) in 307 files |
| **Kernel** (`kernel/`, `include/`) | 45,207 lines of code, 16,111 of comments | | **Kernel** (`kernel/`, `include/`) | 51,893 lines of code, 17,213 of comments |
| **Syscalls** | 208, of which 184 on Linux numbering | | **Syscalls** | 215, of which 193 on Linux numbering |
| **Drivers** | 15 in the kernel | | **Drivers** | 15 in the kernel |
| **First-party programs** | 29 in `cmd/` | | **First-party programs** | 29 in `cmd/` |
| **Alpine packages in the root** | 88, from 11 asked for in `ports/alpine/world` | | **Alpine packages in the root** | 179, from 29 asked for in `ports/alpine/world` |
| **History** | 253 commits since 2026-08-09 (76 days) | | **History** | 292 commits since 2026-08-09 (78 days) |
<details> <details>
<summary>Lines by language</summary> <summary>Lines by language</summary>
| Language | Code | Comments | | Language | Code | Comments |
|---|--:|--:| |---|--:|--:|
| C | 56,580 | 19,427 | | C | 67,615 | 21,904 |
| Assembly | 1,750 | 336 | | Assembly | 1,750 | 336 |
| PowerShell | 1,362 | 638 | | PowerShell | 1,362 | 638 |
| Python | 759 | 147 | | Python | 943 | 150 |
| Shell | 716 | 469 | | Shell | 896 | 531 |
| Perl | 129 | 45 | | Perl | 129 | 45 |
| Linker script | 71 | 66 | | Linker script | 71 | 66 |
@ -55,78 +57,74 @@ the features.
<code>ports/</code> and generated tables are not counted.</sub> <code>ports/</code> and generated tables are not counted.</sub>
<!-- stats:end --> <!-- stats:end -->
## The pieces ## What it is made of
| Layer | Name | What it is | | Layer | Name | What it is |
|---|---|---| |---|---|---|
| System | **SchOS** | The distribution — kernel, userland, tooling | | System | **SchOS** | The distribution — kernel, userland, tooling |
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS | | Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS and Linux's syscall ABI |
| Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem | | Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem |
| Image format | **SCFF** | The SchOS Compressed File Format the live root ships in | | Image format | **SCFF** | The SchOS Compressed File Format the live root ships in |
| API | **SchAPI** | System programming interface — the stable surface | | Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` |
| Compositor | **SchCom** | Display compositor *(planned)* | | Desktop | **Xorg + Window Maker** | Alpine's, on the framebuffer |
| Window manager | **SchWM** | *(planned)* | | Crash screen | **PSOGM / GMMOS** | The Purple Screen of Guru Meditation, and the micro OS that runs under it |
| Desktop | **SchDE** | *(planned)* | | API | **SchAPI** | The long-term native interface — *designed, not built* |
`uname -s` reports `SchIFT`, the way every Unix reports its kernel rather than `uname -s` reports `SchIFT`, the way every Unix reports its kernel rather than
its distribution. its distribution.
## What works ## What works
**Kernel** — Long mode, 4-level paging, preemptive multitasking. **SMP** up to **Kernel** — Long mode, 4-level paging, preemptive multitasking, a fair-share
64 processors, enumerated from the ACPI MADT. ACPI through scheduler with `nice`. **SMP** up to 64 processors, enumerated from the ACPI
[uACPI](https://github.com/uACPI/uACPI): interrupt routing from `_PRT`, the MADT. **Threads** (`clone`, `futex`, TLB shootdowns), **demand paging** for the
power button, clean power-off and reset. Memory sized from the E820/UEFI map. huge reservations browsers make, and dynamic linking. ACPI through
[uACPI](https://github.com/uACPI/uACPI): interrupt routing, the power button,
clean power-off and reset, batteries and AC through a laptop's embedded
controller, and deep idle states. Memory sized from the firmware's map, up to 256 GB.
**Filesystem** — **SchFS** is the root: a copy-on-write B-tree with a **Filesystem** — **SchFS** is the root: a copy-on-write B-tree with an uberblock
uberblock ring, so a crash leaves either the old tree or the new one and there ring, so a crash leaves either the old tree or the new one and there is nothing
is nothing to repair. The live image ships as **[SCFF](docs/architecture/scff.md)**: to repair. The live image ships as **[SCFF](docs/architecture/scff.md)**: the
the 358 MB root compresses to 102 MB with zstd and is unpacked a chunk at a root compresses with zstd to about a third of its size and is unpacked a chunk at
time, the first time something reads it. `/proc` has `/proc/<pid>/fd`, a time, the first time something reads it. `/proc`, `/sys` and `/dev` are
`/proc/self` and the rest that `ps`, `top` and Xorg read. synthesised, in Linux's layouts, because that is what the programs read.
**Processes** — fork, exec, wait, pipes, signals, sessions and job control, **Linux software** — `fork`, `clone`, `exec`, pipes, signals, sessions and job
`mmap` (anonymous and file-backed), POSIX shared memory, `memfd`, Unix domain control, `mmap`, POSIX and `memfd` shared memory, Unix sockets with descriptor and
sockets with descriptor passing (`SCM_RIGHTS`), `poll`, `epoll`, `eventfd`, credential passing, `poll`, `epoll`, `eventfd`, `timerfd`, `signalfd`, `inotify`,
`timerfd`, `signalfd`. **Dynamic linking**: `exec` loads `ld-musl`, so pseudo-terminals. The root is managed by Alpine's **apk**: `ports/alpine/world`
binaries come from Alpine as they are. says what is installed, `world.lock` pins it, and `apk add` fetches more over HTTPS.
**Packages** — the root is managed by Alpine's **apk**. `ports/alpine/world` **Desktop** — **Xorg with Window Maker**, `xterm`, the classic X clients, SDL games
says what is installed; the build resolves it once into `world.lock` and every and the NetSurf browser, on the framebuffer (`/dev/fb0`, Linux fbdev) with input
image after that is reproducible. SchOS itself is one more package in it. as Linux evdev. Twelve virtual terminals, switched with Ctrl+Alt+F1–F12, with X on
a terminal of its own. Sound through ALSA (over OSS and an AC'97 driver).
**Graphics** — **Xorg with Window Maker**, preinstalled, on the framebuffer **Drivers** — written from the specifications, and **Linux's own, unmodified,
(`/dev/fb0`, Linux fbdev). Six virtual terminals, switched with through [LDLS](docs/architecture/ldls.md)**: a `.ko` runs in an ordinary process
Ctrl+Alt+F1–F6 or Alt+F1–F6, with X on a terminal of its own; Ctrl+Alt+Backspace and reaches the hardware through `/dev/pci`, so a crashed driver costs an interface,
ends the session. Input as Linux evdev on `/dev/input/event*`. not the machine. The laptop's Realtek NIC is driven that way.
**Devices** — PCI with a bus-agnostic device model at `/proc/devices`.
| | | | | |
|---|---| |---|---|
| Storage | AHCI (SATA), NVMe, IDE, USB mass storage | | Storage | AHCI (SATA), NVMe, IDE, USB mass storage |
| Network | Intel e1000, Realtek r8169 and RTL8139 | | Network | Intel e1000, Realtek RTL8139 and r8169 (the latter also as Linux's driver in a process) |
| USB | xHCI, hubs, HID keyboards and mice | | USB | xHCI, hubs, HID keyboards and mice |
| Input | PS/2, I2C-HID laptop touchpads (DesignWare I2C, found through ACPI) | | Input | PS/2, I2C-HID laptop touchpads and keyboards (DesignWare I2C, found through ACPI) |
| Display | UEFI GOP framebuffer, virtio-gpu, Bochs/stdvga | | Display | UEFI GOP framebuffer, virtio-gpu, Bochs/stdvga |
| Audio | AC'97 | | Audio | AC'97 |
**Networking** — ARP, IPv4, ICMP, UDP, TCP and DHCP on lwIP. DNS, `wget` **Networking** — ARP, IPv4, ICMP, UDP, TCP and DHCP on lwIP; DNS, `wget` and `curl`
over **HTTPS**, `ping`, and an `httpd` serving a directory back. over **HTTPS**, `ping`, `httpd`, netlink for `getifaddrs`.
**Userland** — musl, toybox, mksh and zsh, plus whatever Alpine has: `ls`, **When it dies** — PSOGM, the Purple Screen of Guru Meditation: stop code, fault
`ps`, `top`, `awk`, `diff`, `xterm`, `fastfetch` behave the way you expect, address, call chain, and the log as QR codes. Under it runs **GMMOS**, a micro OS with
because they are the real programs. its own keyboard drivers, so a dead machine can still show its log, take a few
commands, and restart or power off.
**Text** — a TrueType rasteriser (`glyf` outlines, composite glyphs, cmap **On real hardware** — booted from a USB stick on an AMD B550 desktop (Ryzen 5
format 4). JetBrains Mono for the terminal, Roboto for the desktop to come. 4600G), an ASUS TUF A15 laptop and an Ivy Bridge HP all-in-one.
**When it dies** — PSOGM, the Purple Screen of Guru Meditation: stop code,
fault address, call chain, and a QR code containing a text copy of the whole
report.
**On real hardware** — booted on an AMD B550 desktop (Ryzen 5 4600G), an ASUS
TUF A15 laptop and an Ivy Bridge HP all-in-one, from a USB stick.
## Quick start ## Quick start
@ -134,19 +132,19 @@ TUF A15 laptop and an Ivy Bridge HP all-in-one, from a USB stick.
make # kernel + UEFI loader + root filesystem make # kernel + UEFI loader + root filesystem
make qemu # build it and boot it make qemu # build it and boot it
make iso # a bootable ISO for VMware or real hardware make iso # a bootable ISO for VMware or real hardware
./write.sh sdX # write that ISO to a USB stick (Linux; it checks what it is overwriting)
``` ```
The ISO is a hybrid image: write it to a USB stick with The ISO is a hybrid image: it boots on any UEFI machine from a stick written with
`dd if=schos.iso of=/dev/sdX bs=4M` (or Rufus/Etcher in DD mode) and boot it `write.sh`, `dd if=schos.iso of=/dev/sdX bs=4M`, or Rufus/Etcher in DD mode. **It runs
on any UEFI machine. from RAM and touches none of the machine's disks.** Log in on the console, and type
`startx` for the desktop.
Builds on **Linux, macOS, FreeBSD/OpenBSD/NetBSD and Windows** (Git Bash). The Builds on **Linux, macOS, FreeBSD/OpenBSD/NetBSD and Windows** (Git Bash). The
toolchain is clang + lld everywhere — it is a cross-compiler by construction, so toolchain is clang + lld everywhere — it is a cross-compiler by construction, so
one LLVM install targets the bare-metal kernel, the 16-bit boot code and musl one LLVM install targets the bare-metal kernel, the 16-bit boot code and musl
userland from any host. See **[docs/guides/building.md](docs/guides/building.md)** for userland from any host; `nix-shell` supplies all of it. See
prerequisites per platform, and **[docs/guides/building.md](docs/guides/building.md)** for prerequisites per platform.
**[docs/history/moving-to-linux.md](docs/history/moving-to-linux.md)** for moving a checkout
between machines.
## Testing ## Testing
@ -160,28 +158,35 @@ tools/windows/bootcheck.ps1 -Tty -Ps # terminal behaviour, process enumeration
make schfs-check # SchFS B-tree, object and image suites make schfs-check # SchFS B-tree, object and image suites
``` ```
`bootcheck` is PowerShell because `pwsh` runs on every supported host. `bootcheck` is PowerShell because `pwsh` runs on every supported host. Several
programs in `cmd/` (`socktest`, `memfdtest`, `xready`, …) exist to be run *on* the
The numbers at the top are measured, not typed: `python3 tools/python/stats.py` system and report which of their own checks failed. See
rewrites them here and on the website. **[docs/guides/testing.md](docs/guides/testing.md)** for what each proves. The numbers at
the top are measured, not typed: `python3 tools/python/stats.py` rewrites them here
and on the website.
## Layout ## Layout
``` ```
kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/ kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/ acpi/ gmmos/ scff/
fs/schfs/ the filesystem -- same source the host tools compile fs/schfs/ the filesystem -- same source the host tools compile
ldls/ the Linux driver layer: the .ko loader, BTF reader, Linux symbols
include/ kernel and userland headers, by subsystem (arch/ core/ fs/ dev/ ...) include/ kernel and userland headers, by subsystem (arch/ core/ fs/ dev/ ...)
boot/ uefi/ (PE loader) common/ (shared loader UI) boot/ uefi/ (PE loader) common/ (shared loader UI)
lib/ cmd/ first-party userland runtime and programs lib/ cmd/ first-party userland runtime and programs
ports/ third-party software built from source etc/ what the image starts with: schinit's job manifests, motd, ...
ports/ third-party software built from source; ports/alpine is the apk root
tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl
projects/ things that run ON SchOS (an installer, SchFS for Linux: both frozen)
docs/ architecture/ (what it is) plan/ (what's next) docs/ architecture/ (what it is) plan/ (what's next)
guides/ (how to work on it) history/ (why it's like this) guides/ (how to work on it) history/ (why it's like this)
Website/ (the site, and the docs reader)
``` ```
**[docs/README.md](docs/README.md)** is the index. If you are reading the tree **[docs/README.md](docs/README.md)** is the index. If you are reading the tree
for the first time, [docs/architecture/README.md](docs/architecture/README.md) for the first time, [docs/architecture/README.md](docs/architecture/README.md)
is the system layer by layer. is the system layer by layer, and [docs/plan/1.0.md](docs/plan/1.0.md) says
where it stands against its finish line.
One rule worth knowing: **the kernel and the host tools compile the same SchFS One rule worth knowing: **the kernel and the host tools compile the same SchFS
source**. `mkschfs` cannot disagree with the kernel about the on-disk format, source**. `mkschfs` cannot disagree with the kernel about the on-disk format,
@ -193,19 +198,24 @@ Another: **syscalls are declared in exactly one place**,
## Not there yet ## Not there yet
- **No threads.** `clone` makes processes, not threads (`CLONE_VM` is - **No installer, and nothing persists.** The live image runs from RAM and
refused), so multi-threaded programs run on one thread or not at all. Xorg writes nothing to the machine's disks, so what you install or save is gone at
runs with `-dumbSched` for this reason. reboot. This is the one thing between the system and its 1.0 definition.
- **No udev.** Xorg is told its input devices by a config file instead of - **No GPU acceleration.** Xorg draws in software; the plan is Linux's `amdgpu`
finding them; `/sys` and the device-event socket are what is missing. through LDLS ([docs/plan/gpu.md](docs/plan/gpu.md)). There is no IOMMU either,
so a driver in a process is isolated against its bugs, not against malice.
- **udev sees the kernel's own devices only.** eudev runs and Xorg finds its input devices
through it; PCI and USB devices are not yet in the `/sys/devices` tree.
- **WebKit browsers do not start.** NetSurf does; Epiphany's web process dies at
start-up, and it has not been run to ground.
- **USB 2.0 controllers (EHCI)** have no driver. On older machines the ports - **USB 2.0 controllers (EHCI)** have no driver. On older machines the ports
wired only to EHCI do nothing; xHCI ports work. wired only to EHCI do nothing; xHCI ports work.
- **Touchpads run in mouse mode**: pointer, click and tap, but no two-finger - **Touchpads run in mouse mode**: pointer, click and tap, but no two-finger
scroll yet. scroll yet.
- **No page cache**, so writable `MAP_SHARED` of a file is refused rather than - **No page cache**, so writable `MAP_SHARED` of a file is refused rather than
faked. SysV shared memory is missing too. faked, and `mprotect` is accepted and ignored (nothing has NX).
- **No installer.** SchOS runs live from the USB stick; nothing is written to - **No Wi-Fi, no HDA audio**, and the Realtek NIC on the laptop has reached the
the machine's disks. point of finding its PHY; whether it carries traffic there is not confirmed.
- SchFS has no Linux driver yet; the plan is on-disk format version first, then - SchFS has no Linux driver yet; the plan is on-disk format version first, then
FUSE, then a kernel module. FUSE, then a kernel module.
- CFF/OpenType outlines and TrueType hinting are deliberately out of scope. - CFF/OpenType outlines and TrueType hinting are deliberately out of scope.
@ -215,14 +225,16 @@ Another: **syscalls are declared in exactly one place**,
**GPL-2.0-or-later**, copyright Nexoniarz. See [LICENSE](LICENSE). **GPL-2.0-or-later**, copyright Nexoniarz. See [LICENSE](LICENSE).
The kernel derives from xv6, which is MIT — see [LICENSE.MIT](LICENSE.MIT). The kernel derives from xv6, which is MIT — see [LICENSE.MIT](LICENSE.MIT).
Third-party code redistributed in a built image (musl, toybox, mksh, zsh, kilo, Third-party code redistributed in a built image (musl, toybox, mksh, zstd, the
the fonts) keeps its own licence; [NOTICE](NOTICE) carries the attributions. fonts, and Alpine's packages, each under its own licence) keeps that licence;
See [docs/licensing.md](docs/licensing.md). [NOTICE](NOTICE) carries the attributions. See [docs/licensing.md](docs/licensing.md).
Drivers may be adapted from BSD sources and, since the move to the GPL, from Drivers may be adapted from BSD sources and, since the move to the GPL, from
Linux — GPL-2.0-only code combines with GPL-2.0-or-later, and the result ships Linux — GPL-2.0-only code combines with GPL-2.0-or-later, and the result ships
under GPLv2. Best of all is still the public specification — xHCI, virtio, under GPLv2. Linux's own drivers run unmodified through LDLS, and the ISO ships the
AHCI, e1000 — which carries no licence on what you write from it. Realtek `r8169` set and its firmware; their licences and source are in [NOTICE](NOTICE). Best of
all is still the public specification — xHCI, virtio, AHCI, e1000 — which carries
no licence on what you write from it.
--- ---

View file

@ -13,6 +13,7 @@
#include "../../include/gfx/theme.h" #include "../../include/gfx/theme.h"
#include "../../include/gfx/font.h" #include "../../include/gfx/font.h"
#include "../../include/gfx/logo.h" #include "../../include/gfx/logo.h"
#include "../../include/gfx/nvfont.h"
/* "Draw the glyph, leave the background alone." Not a colour anyone can mean: /* "Draw the glyph, leave the background alone." Not a colour anyone can mean:
* the palette is 24-bit, so the high byte is free for a sentinel. */ * the palette is 24-bit, so the high byte is free for a sentinel. */
@ -21,6 +22,25 @@
static struct ui_fb FB; static struct ui_fb FB;
static int ACTIVE; static int ACTIVE;
/* What the mark is blended against: the splash ground, or a window body. */
static ui_u32 LOGO_BG = SCH_BG;
/* The boot manager's scale and faces. 2 on a screen big enough that a 15 px
* face would be a speck; the faces are rasterised at both sizes, not doubled. */
static int SC = 1;
static const struct nvfont *F_BODY, *F_SMALL, *F_TITLE;
static int CUR_ON; /* the pointer is drawn right now */
static void
ui_set_scale(void)
{
SC = (FB.height >= 1400 || FB.width >= 2400) ? 2 : 1;
F_BODY = SC == 2 ? &nvfont_body2 : &nvfont_body1;
F_SMALL = SC == 2 ? &nvfont_small2 : &nvfont_small1;
F_TITLE = SC == 2 ? &nvfont_title2 : &nvfont_title1;
CUR_ON = 0; /* a new framebuffer holds no pointer */
}
/* Progress state. Kept here rather than passed around because the caller ticks /* Progress state. Kept here rather than passed around because the caller ticks
* it from inside a read loop that has no business carrying UI state. */ * it from inside a read loop that has no business carrying UI state. */
static ui_u32 PROG_TOTAL, PROG_DONE; static ui_u32 PROG_TOTAL, PROG_DONE;
@ -101,6 +121,7 @@ ui_init(const struct ui_fb *fb)
if (!FB.stride) if (!FB.stride)
FB.stride = FB.width * 4; FB.stride = FB.width * 4;
ACTIVE = 1; ACTIVE = 1;
ui_set_scale();
} }
int ui_active(void) { return ACTIVE; } int ui_active(void) { return ACTIVE; }
@ -236,7 +257,7 @@ ui_logo(int y, int w)
/* Coverage blends the shaded metal against the ground, so the mark /* Coverage blends the shaded metal against the ground, so the mark
* has soft edges and a lit face at the same time. */ * has soft edges and a lit face at the same time. */
put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15)); put(x0 + dx, y + dy, mix(LOGO_BG, shade, (int)cov, 15));
} }
} }
} }
@ -244,121 +265,555 @@ ui_logo(int y, int w)
/* Where the bar goes. Derived, not stored, so nothing can set it and then have /* Where the bar goes. Derived, not stored, so nothing can set it and then have
* the screen change underneath it. * the screen change underneath it.
* *
* The bar is exactly the mark's width and sits directly beneath it: the two are * The track is wider than the mark and sits beneath it: a mark with a recessed
* one object, and a bar wider than the mark it belongs to reads as a separate * track and one line of type, centred, on the same graphite the boot manager
* thing that happens to be nearby. * uses. Laid out from the middle outwards so it reads the same at 800x600 and
* at 4K. S() is defined further down with the rest of the Niveus code, so the
* scale is applied through SC directly.
*/ */
static void static void
bar_geometry(void) bar_geometry(void)
{ {
PROG_W = ui_mark_width(); PROG_W = 220 * SC;
PROG_H = BAR_H; PROG_H = 6 * SC;
PROG_X = ((int)FB.width - PROG_W) / 2; PROG_X = ((int)FB.width - PROG_W) / 2;
int mark_h = (LOGO_H * PROG_W) / LOGO_W; int mw = ui_mark_width() * SC;
int group_h = mark_h + MARK_GAP + PROG_H; if (mw > LOGO_W) mw = LOGO_W;
int mark_h = (LOGO_H * mw) / LOGO_W;
int gap = 34 * SC;
int group_h = mark_h + gap + PROG_H;
int top = ((int)FB.height - group_h) / 2; int top = ((int)FB.height - group_h) / 2;
PROG_Y = top + mark_h + MARK_GAP; PROG_Y = top + mark_h + gap;
} }
static ui_u32 bg_at(int y);
static void nv_backdrop(void);
static int nv_width(const struct nvfont *f, const char *s);
static void nv_text(const struct nvfont *f, int x, int base, const char *s,
ui_u32 fg, ui_u32 bgtop, ui_u32 bgbot, int y0, int h);
static void rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot,
ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh);
void void
ui_splash(const char *subtitle) ui_splash(const char *subtitle)
{ {
if (!ACTIVE) if (!ACTIVE)
return; return;
ui_clear(); nv_backdrop();
bar_geometry(); bar_geometry();
int w = PROG_W; int w = ui_mark_width() * SC;
if (w > LOGO_W) w = LOGO_W;
int mark_h = (LOGO_H * w) / LOGO_W; int mark_h = (LOGO_H * w) / LOGO_W;
int top = PROG_Y - MARK_GAP - mark_h; int top = PROG_Y - 34 * SC - mark_h;
ui_u32 saved = LOGO_BG;
LOGO_BG = bg_at(top + mark_h / 2);
ui_logo(top, w); ui_logo(top, w);
LOGO_BG = saved;
if (subtitle) if (subtitle)
ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); ui_progress_label(subtitle);
} }
/* --- panels --------------------------------------------------------------- /* --- Niveus -----------------------------------------------------------------
* *
* A panel is a plate: a slightly lighter ground than the screen, one lighter * The boot manager is drawn in the same language as the rest of the system
* row along the top and a darker border, which is the whole of the bevel. Two * (docs/Website/niveus): windows are solid, controls are
* rows of shading do more for the impression of a raised surface than any * bevelled keys with light falling across them, fields are wells cut into the
* amount of gradient inside it, and cost nothing to draw. * surface, and the ground under all of it is warm graphite rather than blue.
*
* Every shape here is a rounded rectangle with a vertical gradient, which is
* all that skeuomorphism needs at this size. Corners are anti-aliased by
* supersampling the corner pixels only, and the AA blends against a colour the
* caller *names*, never against what is in the framebuffer: reading the
* framebuffer back is uncached on real hardware and ruinously slow.
*
* The typeface is Instrument Sans, rasterised at build time (nvfont.h).
*/ */
static void
panel(int x, int y, int w, int h) #define NV_BG_TOP 0x1B1B1Eu /* the ground, light at the top */
#define NV_BG_BOT 0x0B0B0Du
#define NV_WIN_BODY 0x232326u
#define NV_WIN_EDGE 0x050507u
#define NV_TITLE_TOP 0x45444Au
#define NV_TITLE_BOT 0x2C2B30u
#define NV_HILITE 0x5E5D64u /* the lit upper edge of a raised thing */
#define NV_KEY_TOP 0x3F3E44u
#define NV_KEY_BOT 0x2D2C32u
#define NV_KEY_EDGE 0x111113u
#define NV_KEY_SHADOW 0x141416u
#define NV_WELL_TOP 0x101012u
#define NV_WELL_BOT 0x171719u
#define NV_WELL_EDGE 0x0A0A0Bu
#define NV_WELL_LIP 0x3A393Eu /* light catching the lower lip of a well */
#define NV_TXT 0xECEAE5u
#define NV_TXT2 0xA9A7A1u
#define NV_TXT3 0x74726Du
#define S(n) ((n) * SC)
#define WHITE 0xFFFFFFu
#define BLACK 0x000000u
static ui_u32
bg_at(int y)
{ {
ui_fill_rect(x, y, w, h, PANEL_BG); return mix(NV_BG_TOP, NV_BG_BOT, y, (int)FB.height > 1 ? (int)FB.height - 1 : 1);
ui_fill_rect(x, y, w, 1, PANEL_TOP); /* lit top edge */
ui_fill_rect(x, y + h - 1, w, 1, METAL_EDGE); /* shadowed bottom */
ui_fill_rect(x, y, 1, h, METAL_EDGE);
ui_fill_rect(x + w - 1, y, 1, h, METAL_EDGE);
} }
/* A heading in metal: the same shading the mark uses, applied per row of the
* glyph cell, so a title looks struck out of the same material. */
static void static void
metal_text_centered(int y, const char *s) nv_backdrop(void)
{ {
int len = slen(s); for (int y = 0; y < (int)FB.height; y++)
int x = ((int)FB.width - len * GLYPH_W) / 2; ui_fill_rect(0, y, (int)FB.width, 1, bg_at(y));
}
for (int row = 0; row < GLYPH_H; row++) { static int
ui_u32 shade = metal(row, GLYPH_H); nv_glyph_index(unsigned c)
for (int i = 0; i < len; i++) { {
ui_u8 bits = font_data[(ui_u8)s[i]][row]; if (c < NVFONT_FIRST || c > NVFONT_LAST)
for (int col = 0; col < GLYPH_W; col++) c = '?';
if (bits & (0x80 >> col)) return (int)(c - NVFONT_FIRST);
put(x + i * GLYPH_W + col, y + row, shade); }
static int
nv_width(const struct nvfont *f, const char *s)
{
int w = 0;
for (; *s && *s != '\n'; s++)
w += f->g[nv_glyph_index((ui_u8)*s)].adv;
return (w + 32) >> 6;
}
static int
nv_height(const struct nvfont *f)
{
return f->ascent + f->descent;
}
/* Draw `s` (up to a newline) with its origin on baseline `base`. The colour
* under it is a vertical gradient from bgtop at row y0 to bgbot at y0+h. */
static void
nv_text(const struct nvfont *f, int x, int base, const char *s, ui_u32 fg,
ui_u32 bgtop, ui_u32 bgbot, int y0, int h)
{
int pen = 0; /* 1/64 px: the face's own spacing */
for (; *s && *s != '\n'; s++) {
const struct nvglyph *g = &f->g[nv_glyph_index((ui_u8)*s)];
const ui_u8 *bits = f->bits + g->off;
int gx = x + ((pen + 32) >> 6);
for (int r = 0; r < g->h; r++) {
int py = base + g->oy + r;
ui_u32 bg = mix(bgtop, bgbot, py - y0, h > 1 ? h - 1 : 1);
for (int c = 0; c < g->w; c++) {
ui_u8 a = bits[r * g->w + c];
if (a)
put(gx + g->ox + c, py, mix(bg, fg, a, 255));
}
}
pen += g->adv;
}
}
/* How much of the pixel at (dx,dy) from a corner's centre lies inside a circle
* of radius r, in sixteenths. */
static int
corner_cov(int dx, int dy, int r)
{
int n = 0, R = r * 8;
for (int j = 0; j < 4; j++)
for (int i = 0; i < 4; i++) {
int sx = dx * 8 + 2 * i + 1, sy = dy * 8 + 2 * j + 1;
if (sx * sx + sy * sy <= R * R)
n++;
}
return n;
}
static void
rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot,
ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
{
if (w <= 0 || h <= 0)
return;
if (r * 2 > h) r = h / 2;
if (r * 2 > w) r = w / 2;
for (int rr = 0; rr < h; rr++) {
int yy = y + rr;
ui_u32 col = mix(top, bot, rr, h > 1 ? h - 1 : 1);
int cy, corner = 1;
if (rr < r) cy = y + r;
else if (rr >= h - r) cy = y + h - r;
else corner = 0;
if (!corner) {
ui_fill_rect(x, yy, w, 1, col);
continue;
}
ui_u32 bg = mix(bgtop, bgbot, yy - bgy, bgh > 1 ? bgh - 1 : 1);
ui_fill_rect(x + r, yy, w - 2 * r, 1, col);
for (int k = 0; k < r; k++) {
int dy = yy - cy;
int cl = corner_cov(k - r, dy, r);
int cr = corner_cov(k, dy, r);
if (cl) put(x + k, yy, mix(bg, col, cl, 16));
if (cr) put(x + w - r + k, yy, mix(bg, col, cr, 16));
} }
} }
} }
/* Panel metrics. /* A raised object: an edge, a gradient face, and a bright line just inside the
* * top where the light lands. */
* TITLE_H is the whole title band -- the padding above the heading, the static void
* heading itself, and the air beneath it before the rule. It was 34 while the bevel(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 edge,
* heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn ui_u32 hi, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
* *through* the lettering and the first row started on the heading's baseline. {
* Derived from its parts now, so it cannot drift from them again. rrect(x, y, w, h, r, edge, edge, bgtop, bgbot, bgy, bgh);
*/ rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r > SC ? r - SC : r,
#define ROW_H 24 top, bot, edge, edge, 0, 1);
#define PANEL_PAD 18 if (hi)
#define TITLE_GAP 14 /* heading to the rule */ ui_fill_rect(x + r, y + SC, w - 2 * r, SC, hi);
#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */ }
#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */
/* A recess: the same thing with the light reversed. Darker at the top where the
* rim shades it, with a pale lip along the bottom edge. */
static void
well(int x, int y, int w, int h, ui_u32 bgcol)
{
int r = S(8);
bevel(x, y, w, h, r, NV_WELL_TOP, NV_WELL_BOT, NV_WELL_EDGE, 0, bgcol, bgcol, 0, 1);
ui_fill_rect(x + r, y + h - SC, w - 2 * r, SC, NV_WELL_LIP);
}
/* The soft shadow beneath a window, as stacked translucent layers laid down
* from the outermost in, each one blended against the backdrop's own gradient. */
static void
shadow(int x, int y, int w, int h, int r)
{
int spread = S(16), dy = S(8);
for (int i = spread; i > 0; i -= SC) {
int t = spread - i;
int a = 60 * t * t / (spread * spread); /* percent black */
int ty = y - i + dy, th = h + 2 * i;
ui_u32 top = mix(bg_at(ty), BLACK, a, 100);
ui_u32 bot = mix(bg_at(ty + th), BLACK, a, 100);
rrect(x - i, ty, w + 2 * i, th, r + i, top, bot,
bg_at(ty), bg_at(ty + th), ty, th);
}
}
/* --- the pointer ----------------------------------------------------------- */
#define CUR_W 12
#define CUR_H 19
static const char *const CUR_SHAPE[CUR_H] = {
"X ",
"XX ",
"X.X ",
"X..X ",
"X...X ",
"X....X ",
"X.....X ",
"X......X ",
"X.......X ",
"X........X ",
"X.........X ",
"X......XXXXX",
"X...X..X ",
"X..X X..X ",
"X.X X..X ",
"XX X..X ",
"X X..X ",
" X..X ",
" XXX ",
};
static ui_u32 CUR_SAVE[CUR_W * 2 * CUR_H * 2];
static int CUR_X, CUR_Y, CUR_WANT;
static void static void
panel_frame(const char *title, int rows, const char *hint, cursor_hide(void)
int *out_x, int *out_y, int *out_w)
{ {
int w = (int)FB.width / 2; if (!CUR_ON || !ACTIVE)
if (w < 360) w = 360; return;
if (w > (int)FB.width - 60) w = (int)FB.width - 60; int w = CUR_W * SC, h = CUR_H * SC;
for (int r = 0; r < h; r++)
for (int c = 0; c < w; c++) {
int px = CUR_X + c, py = CUR_Y + r;
if (px >= 0 && py >= 0 && (ui_u32)px < FB.width && (ui_u32)py < FB.height)
*(ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4) =
CUR_SAVE[r * w + c];
}
CUR_ON = 0;
}
int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2; static void
int x = ((int)FB.width - w) / 2; cursor_show(void)
int y = ((int)FB.height - h) / 2; {
if (!CUR_WANT || CUR_ON || !ACTIVE)
return;
int w = CUR_W * SC, h = CUR_H * SC;
for (int r = 0; r < h; r++)
for (int c = 0; c < w; c++) {
int px = CUR_X + c, py = CUR_Y + r;
if (px < 0 || py < 0 || (ui_u32)px >= FB.width || (ui_u32)py >= FB.height)
continue;
ui_u32 *p = (ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4);
CUR_SAVE[r * w + c] = *p;
char ch = CUR_SHAPE[r / SC][c / SC];
if (ch == 'X') *p = BLACK;
else if (ch == '.') *p = WHITE;
}
CUR_ON = 1;
}
ui_clear(); void
ui_cursor(int x, int y, int show)
{
cursor_hide();
CUR_X = x; CUR_Y = y; CUR_WANT = show;
cursor_show();
}
/* The mark, small, above the plate: it is the same screen, not a new one. */ /* --- window chrome --------------------------------------------------------- */
int mw = ui_mark_width() * 2 / 3;
int mh = (LOGO_H * mw) / LOGO_W;
if (y - mh - 24 > 0)
ui_logo(y - mh - 24, mw);
panel(x, y, w, h); #define TB_H S(34)
metal_text_centered(y + PANEL_PAD, title); #define BODY_PAD S(18)
ui_fill_rect(x + PANEL_PAD, y + PANEL_PAD + GLYPH_H + TITLE_GAP / 2, #define HINT_H S(36)
w - PANEL_PAD * 2, 1, METAL_EDGE);
if (hint) static void
ui_text_centered(y + h + 16, hint, SCH_DIM); window(int x, int y, int w, int h, const char *title)
{
int r = S(12), tb = TB_H;
*out_x = x; *out_y = y + TITLE_H; *out_w = w; shadow(x, y, w, h, r);
rrect(x, y, w, h, r, NV_WIN_EDGE, NV_WIN_EDGE, bg_at(y), bg_at(y + h), y, h);
rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r - SC, NV_WIN_BODY, NV_WIN_BODY,
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
/* The titlebar is rounded on top only: draw it tall enough that its bottom
* corners fall below the line, then lay the body colour over the excess. */
rrect(x + SC, y + SC, w - 2 * SC, tb + r, r - SC, NV_TITLE_TOP, NV_TITLE_BOT,
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
ui_fill_rect(x + SC, y + SC + tb, w - 2 * SC, r, NV_WIN_BODY);
ui_fill_rect(x + SC, y + SC + tb - SC, w - 2 * SC, SC, NV_WIN_EDGE);
ui_fill_rect(x + r, y + SC, w - 2 * r, SC, NV_HILITE);
int tw = nv_width(F_BODY, title);
int base = y + SC + (tb - nv_height(F_BODY)) / 2 + F_BODY->ascent;
nv_text(F_BODY, x + (w - tw) / 2, base, title, NV_TXT2,
NV_TITLE_TOP, NV_TITLE_BOT, y + SC, tb);
}
static void
footer(int x, int y, int w, int h, const char *hint)
{
if (!hint)
return;
int fy = y + h - HINT_H;
ui_fill_rect(x + S(14), fy, w - S(28), SC, 0x1A1A1Cu);
ui_fill_rect(x + S(14), fy + SC, w - S(28), SC, 0x2E2E32u);
int tw = nv_width(F_SMALL, hint);
int base = fy + (HINT_H - nv_height(F_SMALL)) / 2 + F_SMALL->ascent + SC;
nv_text(F_SMALL, x + (w - tw) / 2, base, hint, NV_TXT3,
NV_WIN_BODY, NV_WIN_BODY, fy, HINT_H);
}
/* --- keys ------------------------------------------------------------------ */
#define MAXKEYS 40
static struct { int x, y, w, h; } KEY[MAXKEYS];
static struct {
const char *title, *hint;
const char *const *items;
int n, sel, scroll, vis, kind; /* kind: 0 a list of keys, 1 a panel */
int has_detail, kh, gap, logo;
int wx, wy, ww, wh;
int lx, ly, lw; /* the list's box */
int ypos[MAXKEYS];
} M;
static int
has_nl(const char *s)
{
for (; *s; s++)
if (*s == '\n')
return 1;
return 0;
}
static const char *
label_of(const char *s)
{
return s[0] == 1 ? s + 1 : s;
}
static const char *
detail_of(const char *s)
{
for (; *s; s++)
if (*s == '\n')
return s + 1;
return 0;
}
static void
paint_key(int i, int sel)
{
if (i < M.scroll || i >= M.scroll + M.vis || i >= MAXKEYS)
return;
int x = KEY[i].x, y = KEY[i].y, w = KEY[i].w, h = KEY[i].h, r = S(8);
const char *item = M.items[i];
const char *label = label_of(item), *detail = detail_of(item);
/* Wipe the ring/shadow a previous state left, then draw this one. */
ui_fill_rect(x - S(3), y - S(3), w + S(6), h + S(7), NV_WIN_BODY);
ui_u32 top, bot, edge, hi, fg, fg2;
if (sel) {
ui_u32 ring = mix(NV_WIN_BODY, SCH_ACCENT, 70, 100);
rrect(x - S(2), y - S(2), w + S(4), h + S(4), r + S(2), ring, ring,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
top = mix(SCH_ACCENT, WHITE, 20, 100);
bot = mix(SCH_ACCENT, BLACK, 24, 100);
edge = mix(SCH_ACCENT, BLACK, 62, 100);
hi = mix(top, WHITE, 45, 100);
fg = WHITE; fg2 = 0xE2DBFFu;
} else {
rrect(x, y + SC, w, h, r, NV_KEY_SHADOW, NV_KEY_SHADOW,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
top = NV_KEY_TOP; bot = NV_KEY_BOT; edge = NV_KEY_EDGE; hi = NV_HILITE;
fg = NV_TXT; fg2 = NV_TXT3;
}
bevel(x, y, w, h, r, top, bot, edge, hi, NV_WIN_BODY, NV_WIN_BODY, 0, 1);
int ty0 = y + SC, th = h - 2 * SC, tx = x + S(18);
if (detail) {
int lh = nv_height(F_BODY), dh = nv_height(F_SMALL);
int total = lh + S(2) + dh;
int t = y + (h - total) / 2;
nv_text(F_BODY, tx, t + F_BODY->ascent, label, fg, top, bot, ty0, th);
nv_text(F_SMALL, tx, t + lh + S(2) + F_SMALL->ascent, detail,
sel ? fg2 : NV_TXT2, top, bot, ty0, th);
} else {
int base = y + (h - nv_height(F_BODY)) / 2 + F_BODY->ascent;
nv_text(F_BODY, tx, base, label, fg, top, bot, ty0, th);
}
if (M.kind == 0 && M.n <= 9 && M.vis == M.n) {
char num[2] = { (char)('1' + i), 0 };
int nw = nv_width(F_SMALL, num);
int base = y + (h - nv_height(F_SMALL)) / 2 + F_SMALL->ascent;
nv_text(F_SMALL, x + w - S(18) - nw, base, num, sel ? fg2 : NV_TXT3,
top, bot, ty0, th);
}
}
static void
menu_layout(void)
{
int n = M.n > MAXKEYS ? MAXKEYS : M.n;
M.n = n;
M.has_detail = 0;
for (int i = 0; i < n; i++)
if (has_nl(M.items[i]))
M.has_detail = 1;
M.gap = S(8);
M.kh = M.has_detail ? S(50) : S(34);
M.ww = S(520);
if (M.ww > (int)FB.width - S(24))
M.ww = (int)FB.width - S(24);
int maxh = (int)FB.height - S(24);
int chrome = TB_H + BODY_PAD * 2 + HINT_H + 2 * SC;
int logoh = S(56) + S(16);
int full;
for (int pass = 0; pass < 2; pass++) {
int y = 0;
for (int i = 0; i < n; i++) {
if (pass == 0 && i && M.items[i][0] == 1)
y += S(14);
M.ypos[i] = y;
y += M.kh + M.gap;
}
full = n ? y - M.gap : 0;
if (pass == 0 && chrome + full <= maxh)
break;
if (pass == 0 && chrome + logoh + full <= maxh)
break;
}
M.logo = 1;
M.vis = n;
int list_h = full;
if (chrome + logoh + full > maxh)
M.logo = 0;
if (chrome + full > maxh) {
int avail = maxh - chrome;
M.vis = (avail + M.gap) / (M.kh + M.gap);
if (M.vis < 1) M.vis = 1;
if (M.vis > n) M.vis = n;
list_h = M.vis * M.kh + (M.vis - 1) * M.gap;
}
M.wh = chrome + (M.logo ? logoh : 0) + list_h;
M.wx = ((int)FB.width - M.ww) / 2;
M.wy = ((int)FB.height - M.wh) / 2;
if (M.wy < 0) M.wy = 0;
M.lx = M.wx + S(20);
M.lw = M.ww - S(40) - (M.vis < n ? S(14) : 0);
M.ly = M.wy + SC + TB_H + BODY_PAD + (M.logo ? logoh : 0);
if (M.sel < M.scroll) M.scroll = M.sel;
if (M.sel >= M.scroll + M.vis) M.scroll = M.sel - M.vis + 1;
if (M.scroll < 0) M.scroll = 0;
if (M.scroll > n - M.vis) M.scroll = n - M.vis;
if (M.scroll < 0) M.scroll = 0;
for (int i = 0; i < n; i++) {
KEY[i].x = M.lx;
KEY[i].w = M.lw;
KEY[i].h = M.kh;
KEY[i].y = M.ly + M.ypos[i] - M.ypos[M.scroll];
}
}
static void
menu_paint(void)
{
cursor_hide();
nv_backdrop();
window(M.wx, M.wy, M.ww, M.wh, M.title);
if (M.logo) {
ui_u32 saved = LOGO_BG;
LOGO_BG = NV_WIN_BODY;
ui_logo(M.wy + SC + TB_H + BODY_PAD, S(56));
LOGO_BG = saved;
}
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
paint_key(i, i == M.sel);
if (M.vis < M.n) {
int tx = M.lx + M.lw + S(8), tw = S(4);
int th = M.vis * M.kh + (M.vis - 1) * M.gap;
rrect(tx, M.ly, tw, th, tw / 2, NV_WELL_TOP, NV_WELL_BOT,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
int thumb = th * M.vis / M.n;
if (thumb < S(16)) thumb = S(16);
int ty = M.ly + (th - thumb) * M.scroll / (M.n - M.vis);
rrect(tx, ty, tw, thumb, tw / 2, NV_KEY_TOP, NV_KEY_BOT,
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
}
footer(M.wx, M.wy, M.ww, M.wh, M.hint);
cursor_show();
} }
void void
@ -367,46 +822,129 @@ ui_menu(const char *title, const char *const *items, int n, int selected,
{ {
if (!ACTIVE) if (!ACTIVE)
return; return;
M.title = title; M.items = items; M.n = n; M.sel = selected; M.hint = hint;
M.scroll = 0; M.kind = 0;
menu_layout();
menu_paint();
}
int x, y, w; void
panel_frame(title, n, hint, &x, &y, &w); ui_menu_select(int selected)
{
if (!ACTIVE || M.kind != 0 || selected < 0 || selected >= M.n || selected == M.sel)
return;
int old = M.sel;
M.sel = selected;
if (selected < M.scroll || selected >= M.scroll + M.vis) {
menu_layout();
menu_paint();
return;
}
cursor_hide();
paint_key(old, 0);
paint_key(selected, 1);
cursor_show();
}
int
ui_menu_hit(int x, int y)
{
if (!ACTIVE)
return -1;
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
if (x >= KEY[i].x && x < KEY[i].x + KEY[i].w &&
y >= KEY[i].y && y < KEY[i].y + KEY[i].h)
return i;
return -1;
}
/* A panel: label/value rows in a well, and one button underneath. With no
* values, the labels are a message and are centred. */
static void
box(const char *title, const char *const *labels, const char *const *values,
int n, const char *button, const char *hint)
{
if (!ACTIVE)
return;
int rowh = S(28), wpad = S(12);
int well_h = n * rowh + 2 * wpad;
int btn_h = S(34), btn_w = S(180);
M.title = title; M.hint = hint; M.kind = 1;
M.items = &button; M.n = 1; M.sel = 0; M.scroll = 0; M.vis = 1;
M.ww = S(520);
if (M.ww > (int)FB.width - S(24))
M.ww = (int)FB.width - S(24);
M.wh = TB_H + 2 * SC + BODY_PAD + well_h + S(18) + btn_h + BODY_PAD + HINT_H;
M.wx = ((int)FB.width - M.ww) / 2;
M.wy = ((int)FB.height - M.wh) / 2;
if (M.wy < 0) M.wy = 0;
cursor_hide();
nv_backdrop();
window(M.wx, M.wy, M.ww, M.wh, title);
int wx = M.wx + S(20), ww = M.ww - S(40);
int wy = M.wy + SC + TB_H + BODY_PAD;
well(wx, wy, ww, well_h, NV_WIN_BODY);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
int ry = y + i * ROW_H; int ry = wy + wpad + i * rowh;
int base = ry + (rowh - nv_height(F_BODY)) / 2 + F_BODY->ascent;
if (i == selected) { if (i)
/* The selected row is the only lit thing on the plate. Filling it ui_fill_rect(wx + S(14), ry, ww - S(28), SC, 0x1E1E21u);
* with the accent and putting the label in the ground colour reads if (values) {
* at a glance; an outline does not, at this size. */ nv_text(F_BODY, wx + S(16), base, labels[i], NV_TXT2,
/* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
* same below. Off-by-a-few here is what makes a highlight look int vw = nv_width(F_BODY, values[i]);
* like it is sitting on the text rather than around it. */ nv_text(F_BODY, wx + ww - S(16) - vw, base, values[i], NV_TXT,
ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, w - PANEL_PAD, ROW_H, NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
SCH_ACCENT);
ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE);
} else { } else {
ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE); int lw = nv_width(F_BODY, labels[i]);
nv_text(F_BODY, wx + (ww - lw) / 2, base, labels[i], NV_TXT,
NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
} }
} }
KEY[0].w = btn_w; KEY[0].h = btn_h;
KEY[0].x = M.wx + (M.ww - btn_w) / 2;
KEY[0].y = wy + well_h + S(18);
M.has_detail = 0;
paint_key(0, 1);
footer(M.wx, M.wy, M.ww, M.wh, hint);
cursor_show();
} }
void void
ui_panel(const char *title, const char *const *labels, ui_panel(const char *title, const char *const *labels,
const char *const *values, int n, const char *hint) const char *const *values, int n, const char *hint)
{ {
if (!ACTIVE) box(title, labels, values, n, "Back", hint);
return; }
int x, y, w; void
panel_frame(title, n, hint, &x, &y, &w); ui_notice(const char *title, const char *const *lines, int n,
const char *button, const char *hint)
{
box(title, lines, 0, n, button, hint);
}
for (int i = 0; i < n; i++) { /* The track is a recess, the fill is the accent, lit from above -- the same two
int ry = y + i * ROW_H; * materials as a well and a selected key in the boot manager. The fill is
ui_text(x + PANEL_PAD, ry, labels[i], SCH_DIM, SCH_BG_NONE); * redrawn from the left edge on every tick rather than extended, because its
* rounded end moves; the bar is small enough that this costs nothing. */
static void
progress_paint(int px)
{
int r = PROG_H / 2;
ui_u32 bg = bg_at(PROG_Y + PROG_H / 2);
/* Values right-aligned, so the column reads down rather than ragged. */ rrect(PROG_X, PROG_Y, PROG_W, PROG_H, r, 0x0A0A0Bu, 0x151517u, bg, bg, 0, 1);
int vw = slen(values[i]) * GLYPH_W; if (px >= PROG_H) {
ui_text(x + w - PANEL_PAD - vw, ry, values[i], SCH_FG, SCH_BG_NONE); ui_u32 top = mix(SCH_ACCENT, 0xFFFFFFu, 28, 100);
ui_u32 bot = mix(SCH_ACCENT, 0x000000u, 24, 100);
rrect(PROG_X + 1, PROG_Y + 1, px - 2, PROG_H - 2, r - 1, top, bot,
0x0A0A0Bu, 0x151517u, PROG_Y, PROG_H);
} }
} }
@ -422,10 +960,7 @@ ui_progress_begin(const char *label, ui_u32 total)
PROG_LAST_PX = 0; PROG_LAST_PX = 0;
PROG_ACTIVE = 1; PROG_ACTIVE = 1;
/* The unfilled track: a recess, not a colour. Drawn once -- repainting it progress_paint(0);
* on every tick is what makes a progress bar flicker. */
ui_fill_rect(PROG_X, PROG_Y, PROG_W, PROG_H, 0x1B1F28u);
ui_fill_rect(PROG_X, PROG_Y, PROG_W, 1, 0x101319u);
if (label) if (label)
ui_progress_label(label); ui_progress_label(label);
} }
@ -436,11 +971,16 @@ ui_progress_label(const char *label)
if (!ACTIVE || !label) if (!ACTIVE || !label)
return; return;
/* Clear the line first. A shorter label after a longer one would otherwise /* Repaint the backdrop under the line first. A shorter label after a longer
* leave the tail of the old one behind. */ * one would otherwise leave the tail of the old one behind. */
int y = PROG_Y + PROG_H + SUB_GAP; int h = nv_height(F_BODY) + 4 * SC;
ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG); int y = PROG_Y + PROG_H + 18 * SC;
ui_text_centered(y, label, SCH_DIM); for (int r = 0; r < h; r++)
ui_fill_rect(0, y + r, (int)FB.width, 1, bg_at(y + r));
int tw = nv_width(F_BODY, label);
nv_text(F_BODY, ((int)FB.width - tw) / 2, y + F_BODY->ascent, label, NV_TXT3,
bg_at(y), bg_at(y + h), y, h);
} }
void void
@ -454,16 +994,8 @@ ui_progress_tick(ui_u32 done)
PROG_DONE = done; PROG_DONE = done;
int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL); int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL);
/* Only the newly filled part. Filling from zero each time is O(width) per
* tick and visibly slower than the thing it is reporting on. */
if (px > PROG_LAST_PX) { if (px > PROG_LAST_PX) {
/* The fill is metal too, shaded across the bar's height, so it looks progress_paint(px);
* like the same material as the mark rather than a coloured rectangle
* that happens to be underneath it. */
for (int r = 0; r < PROG_H; r++)
ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y + r,
px - PROG_LAST_PX, 1, metal(r, PROG_H));
PROG_LAST_PX = px; PROG_LAST_PX = px;
} }
} }

View file

@ -77,6 +77,21 @@ void ui_splash(const char *subtitle);
void ui_menu(const char *title, const char *const *items, int n, int selected, void ui_menu(const char *title, const char *const *items, int n, int selected,
const char *hint); const char *hint);
/* Repaint only the keys that changed when the highlight moves (a full paint
* redraws the screen). Returns without effect on a panel. */
void ui_menu_select(int selected);
/* Which key is under a point, or -1. A panel has one key, index 0. */
int ui_menu_hit(int x, int y);
/* The pointer: drawn on top of everything, with what is under it saved and put
* back. Menus hide it while they repaint and show it again afterwards. */
void ui_cursor(int x, int y, int show);
/* A message in the same window as the panels, with one button. */
void ui_notice(const char *title, const char *const *lines, int n,
const char *button, const char *hint);
/* A panel of label/value rows -- the system information screen. */ /* A panel of label/value rows -- the system information screen. */
void ui_panel(const char *title, const char *const *labels, void ui_panel(const char *title, const char *const *labels,
const char *const *values, int n, const char *hint); const char *const *values, int n, const char *hint);

View file

@ -27,3 +27,11 @@ What this will need, in rough order:
The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the
ISO carries a single El Torito EFI entry. ISO carries a single El Torito EFI entry.
## The boot manager
`F8` (or `o` on a serial line) opens a Niveus-styled boot manager, drawn by
`../common/ui.c` in Instrument Sans (`include/gfx/nvfont.h`, regenerate with
`make nvfont`). It takes keyboard and pointer, lists other systems it finds on
the disks and can chain-load them, and has working Firmware Setup, Restart and
Shut Down. See [docs/architecture/boot.md](../../docs/architecture/boot.md#the-boot-manager).

View file

@ -153,17 +153,22 @@ typedef struct {
void *LocateDevicePath; void *LocateDevicePath;
void *InstallConfigurationTable; void *InstallConfigurationTable;
void *LoadImage; EFI_STATUS (*LoadImage)(BOOLEAN BootPolicy, EFI_HANDLE ParentImage,
void *StartImage; void *DevicePath, void *SourceBuffer,
UINTN SourceSize, EFI_HANDLE *ImageHandle);
EFI_STATUS (*StartImage)(EFI_HANDLE ImageHandle, UINTN *ExitDataSize,
CHAR16 **ExitData);
void *Exit; void *Exit;
void *UnloadImage; EFI_STATUS (*UnloadImage)(EFI_HANDLE ImageHandle);
EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey); EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey);
void *GetNextMonotonicCount; void *GetNextMonotonicCount;
EFI_STATUS (*Stall)(UINTN Microseconds); EFI_STATUS (*Stall)(UINTN Microseconds);
void *SetWatchdogTimer; void *SetWatchdogTimer;
void *ConnectController; /* Recursive = TRUE asks for every driver that will bind, down the tree. */
EFI_STATUS (*ConnectController)(EFI_HANDLE Controller, EFI_HANDLE *DriverImage,
void *RemainingDevicePath, BOOLEAN Recursive);
void *DisconnectController; void *DisconnectController;
void *OpenProtocol; void *OpenProtocol;
@ -181,6 +186,43 @@ typedef struct {
void **Interface); void **Interface);
} EFI_BOOT_SERVICES; } EFI_BOOT_SERVICES;
// --- runtime services -----------------------------------------------------
//
// Only the two things the boot manager needs: variables, to ask the firmware to
// show its own setup on the next start, and ResetSystem, to restart or power
// off. The slots before them are spelled out as padding so the offsets hold.
#define EfiResetCold 0
#define EfiResetShutdown 2
#define EFI_VARIABLE_NON_VOLATILE 0x1
#define EFI_VARIABLE_BOOTSERVICE_ACCESS 0x2
#define EFI_VARIABLE_RUNTIME_ACCESS 0x4
#define EFI_GLOBAL_VARIABLE_GUID \
{ 0x8BE4DF61, 0x93CA, 0x11D2, { 0xAA, 0x0D, 0x00, 0xE0, 0x98, 0x03, 0x2B, 0x8C } }
/* OsIndications bit 0: "boot into the firmware's own user interface". */
#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x1ULL
typedef struct {
char Hdr[24];
void *GetTime;
void *SetTime;
void *GetWakeupTime;
void *SetWakeupTime;
void *SetVirtualAddressMap;
void *ConvertPointer;
EFI_STATUS (*GetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 *Attributes,
UINTN *DataSize, void *Data);
void *GetNextVariableName;
EFI_STATUS (*SetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 Attributes,
UINTN DataSize, void *Data);
void *GetNextHighMonotonicCount;
void (*ResetSystem)(UINTN ResetType, EFI_STATUS Status, UINTN DataSize,
void *ResetData);
} EFI_RUNTIME_SERVICES;
typedef struct { typedef struct {
char Hdr[24]; char Hdr[24];
CHAR16 *FirmwareVendor; CHAR16 *FirmwareVendor;
@ -191,7 +233,7 @@ typedef struct {
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
EFI_HANDLE StandardErrorHandle; EFI_HANDLE StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
void *RuntimeServices; EFI_RUNTIME_SERVICES *RuntimeServices;
EFI_BOOT_SERVICES *BootServices; EFI_BOOT_SERVICES *BootServices;
UINTN NumberOfTableEntries; UINTN NumberOfTableEntries;
void *ConfigurationTable; void *ConfigurationTable;
@ -348,4 +390,42 @@ typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL {
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode; EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL; } EFI_GRAPHICS_OUTPUT_PROTOCOL;
// --- pointers -------------------------------------------------------------
//
// Two protocols, because firmware offers whichever the device is: a mouse or
// touchpad reports movement (SIMPLE_POINTER), a tablet -- QEMU's, and most
// touchscreens -- reports where it is (ABSOLUTE_POINTER). Both are polled.
#define EFI_SIMPLE_POINTER_PROTOCOL_GUID \
{ 0x31878C87, 0x0B75, 0x11D5, { 0x9A, 0x4F, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } }
#define EFI_ABSOLUTE_POINTER_PROTOCOL_GUID \
{ 0x8D59D32B, 0xC655, 0x4AE9, { 0x9B, 0x15, 0xF2, 0x59, 0x04, 0x99, 0x2A, 0x43 } }
#define EFI_NOT_READY ((EFI_STATUS)(0x8000000000000000ULL | 6))
typedef struct { int RelativeMovementX, RelativeMovementY, RelativeMovementZ;
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_STATE;
typedef struct { UINT64 ResolutionX, ResolutionY, ResolutionZ;
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_MODE;
typedef struct EFI_SIMPLE_POINTER_PROTOCOL {
EFI_STATUS (*Reset)(struct EFI_SIMPLE_POINTER_PROTOCOL *This, BOOLEAN Extended);
EFI_STATUS (*GetState)(struct EFI_SIMPLE_POINTER_PROTOCOL *This,
EFI_SIMPLE_POINTER_STATE *State);
void *WaitForInput;
EFI_SIMPLE_POINTER_MODE *Mode;
} EFI_SIMPLE_POINTER_PROTOCOL;
typedef struct { UINT64 CurrentX, CurrentY, CurrentZ; UINT32 ActiveButtons; }
EFI_ABSOLUTE_POINTER_STATE;
typedef struct { UINT64 AbsoluteMinX, AbsoluteMinY, AbsoluteMinZ,
AbsoluteMaxX, AbsoluteMaxY, AbsoluteMaxZ;
UINT32 Attributes; } EFI_ABSOLUTE_POINTER_MODE;
typedef struct EFI_ABSOLUTE_POINTER_PROTOCOL {
EFI_STATUS (*Reset)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This, BOOLEAN Extended);
EFI_STATUS (*GetState)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This,
EFI_ABSOLUTE_POINTER_STATE *State);
void *WaitForInput;
EFI_ABSOLUTE_POINTER_MODE *Mode;
} EFI_ABSOLUTE_POINTER_PROTOCOL;
#endif /* SCHOS_EFI_H */ #endif /* SCHOS_EFI_H */

View file

@ -81,6 +81,8 @@ struct elf64_phdr {
#define PT_LOAD 1 #define PT_LOAD 1
static EFI_SYSTEM_TABLE *ST; static EFI_SYSTEM_TABLE *ST;
static EFI_HANDLE SELF; /* this image */
static EFI_HANDLE BOOT_VOL; /* the volume it was loaded from */
static EFI_BOOT_SERVICES *BS; static EFI_BOOT_SERVICES *BS;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -183,7 +185,20 @@ serial_item(const char *const *items, int i, int sel, int numbered)
} else if (numbered) { } else if (numbered) {
serial_puts(" "); /* keep the column */ serial_puts(" "); /* keep the column */
} }
serial_puts(items[i]); /* The label, and its detail in brackets. Items carry both, with a newline
* between and a leading \001 where a group begins: the screen uses those
* for layout and a serial line has no use for either. */
const char *t = items[i];
if (t[0] == 1)
t++;
for (; *t && *t != '\n'; t++)
serial_putc(*t);
if (*t == '\n') {
serial_puts(" (");
for (t++; *t; t++)
serial_putc(*t);
serial_putc(')');
}
serial_puts("\n"); serial_puts("\n");
} }
@ -360,6 +375,7 @@ open_boot_volume(EFI_HANDLE image)
// is not a thing to do by accident. // is not a thing to do by accident.
st = BS->HandleProtocol(image, &li_guid, (void **)&li); st = BS->HandleProtocol(image, &li_guid, (void **)&li);
if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st); if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st);
BOOT_VOL = li->DeviceHandle;
self_base = (UINT64)(UINTN)li->ImageBase; self_base = (UINT64)(UINTN)li->ImageBase;
self_size = li->ImageSize; self_size = li->ImageSize;
@ -904,6 +920,237 @@ u64_to_dec(UINT64 v, char *out, int cap)
out[i] = 0; out[i] = 0;
} }
/* --- pointers, and one place to wait for input ------------------------------
*
* The boot manager is operated by keyboard, mouse, touchpad or touchscreen, and
* every screen waits through here so that all of them behave the same way.
*
* Both pointer protocols are polled rather than waited on: the loop is already
* polling the keyboard, and an event per device would mean a wait-for-any call
* that most firmware implements badly. A machine with no pointer simply never
* shows a cursor.
*/
#define MAX_PTR 4
static EFI_SIMPLE_POINTER_PROTOCOL *PTR_REL[MAX_PTR];
static EFI_ABSOLUTE_POINTER_PROTOCOL *PTR_ABS[MAX_PTR];
static int PTR_NREL, PTR_NABS;
static int PTR_X, PTR_Y, PTR_DOWN;
static int PTR_RESCANS, PTR_TICKS;
static int PTR_ACTIVE; /* a real event has arrived; until then there is no cursor */
static void u64_to_dec(UINT64 v, char *out, int cap);
#define EV_NONE 0
#define EV_KEY 1
#define EV_HOVER 2 /* the pointer moved onto a key: *item */
#define EV_CLICK 3 /* a key was clicked: *item */
#define PE_MOVED 1
#define PE_DOWN 2
#define PE_UP 4 /* wheel */
#define PE_DOWNW 8
/* Bind every driver to every controller.
*
* The firmware connects only what it needs to boot: the devices that carry a
* console, and the disks. A USB mouse or tablet is neither, so on most
* machines its driver has not been started and no pointer protocol exists yet.
* Doing it here, when the menu opens, costs a second or so on a real machine
* and nothing on a boot that never shows a menu. */
static void
connect_all(void)
{
EFI_HANDLE *h = 0;
UINTN n = 0;
if (EFI_ERROR(BS->LocateHandleBuffer(0 /* AllHandles */, 0, 0, &n, &h)) || !h)
return;
for (UINTN i = 0; i < n; i++)
BS->ConnectController(h[i], 0, 0, 1);
BS->FreePool(h);
}
static void
pointers_find(void)
{
EFI_GUID g1 = EFI_SIMPLE_POINTER_PROTOCOL_GUID;
EFI_GUID g2 = EFI_ABSOLUTE_POINTER_PROTOCOL_GUID;
EFI_HANDLE *h = 0;
UINTN n = 0;
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g1, 0, &n, &h)) && h) {
for (UINTN i = 0; i < n && PTR_NREL < MAX_PTR; i++) {
void *p = 0;
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g1, &p)) && p) {
EFI_SIMPLE_POINTER_PROTOCOL *sp = p;
int seen = 0;
for (int k = 0; k < PTR_NREL; k++)
if (PTR_REL[k] == sp) seen = 1;
if (!seen) {
sp->Reset(sp, 0);
PTR_REL[PTR_NREL++] = sp;
}
}
}
BS->FreePool(h);
}
h = 0; n = 0;
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g2, 0, &n, &h)) && h) {
for (UINTN i = 0; i < n && PTR_NABS < MAX_PTR; i++) {
void *p = 0;
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g2, &p)) && p) {
EFI_ABSOLUTE_POINTER_PROTOCOL *ap = p;
int seen = 0;
for (int k = 0; k < PTR_NABS; k++)
if (PTR_ABS[k] == ap) seen = 1;
if (!seen) {
ap->Reset(ap, 0);
PTR_ABS[PTR_NABS++] = ap;
}
}
}
BS->FreePool(h);
}
if (PTR_X == 0 && PTR_Y == 0)
PTR_X = PTR_Y = -1; /* centred on first use, when the size is known */
}
static int
pointer_poll(void)
{
int ev = 0, w = (int)ui_width(), h = (int)ui_height();
int down = PTR_DOWN;
for (int i = 0; i < PTR_NREL; i++) {
EFI_SIMPLE_POINTER_STATE st;
if (EFI_ERROR(PTR_REL[i]->GetState(PTR_REL[i], &st)))
continue;
int dx = st.RelativeMovementX, dy = st.RelativeMovementY;
/* A little acceleration: the counts are the device's, not pixels, and
* a fast flick has to be able to cross a 4K panel. */
if (dx > 6 || dx < -6) dx *= 2;
if (dy > 6 || dy < -6) dy *= 2;
if (dx || dy) {
PTR_X += dx; PTR_Y += dy;
ev |= PE_MOVED;
}
if (st.RelativeMovementZ)
ev |= st.RelativeMovementZ < 0 ? PE_UP : PE_DOWNW;
down = st.LeftButton;
}
for (int i = 0; i < PTR_NABS; i++) {
EFI_ABSOLUTE_POINTER_STATE st;
EFI_ABSOLUTE_POINTER_MODE *m = PTR_ABS[i]->Mode;
if (EFI_ERROR(PTR_ABS[i]->GetState(PTR_ABS[i], &st)) || !m)
continue;
UINT64 rx = m->AbsoluteMaxX - m->AbsoluteMinX;
UINT64 ry = m->AbsoluteMaxY - m->AbsoluteMinY;
if (rx && ry) {
PTR_X = (int)(((st.CurrentX - m->AbsoluteMinX) * (UINT64)(w - 1)) / rx);
PTR_Y = (int)(((st.CurrentY - m->AbsoluteMinY) * (UINT64)(h - 1)) / ry);
ev |= PE_MOVED;
}
down = (int)(st.ActiveButtons & 1);
}
if (PTR_X < 0) PTR_X = 0;
if (PTR_Y < 0) PTR_Y = 0;
if (PTR_X >= w) PTR_X = w - 1;
if (PTR_Y >= h) PTR_Y = h - 1;
if (down && !PTR_DOWN)
ev |= PE_DOWN;
PTR_DOWN = down;
return ev;
}
/* Wait for a key, a click on a key, or the pointer moving onto one.
*
* The cursor is shown whenever a pointer device is present: hiding it until
* something moved left a machine whose pointer sends no events to the
* loader with nothing on screen to say a pointer was even looked for.
*
* `budget` is a countdown in milliseconds that survives across calls: a
* caller that returns here after a hover continues the same ten seconds
* instead of being given a fresh ten. NULL waits for ever.
*/
static int
wait_event(EFI_INPUT_KEY *key, int *item, UINTN *budget)
{
int have = PTR_NREL + PTR_NABS;
int last = -2;
if (PTR_X < 0) {
PTR_X = (int)ui_width() / 2;
PTR_Y = (int)ui_height() / 2;
}
if (have)
PTR_ACTIVE = 1;
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, key)))
return EV_KEY;
/* USB devices appear some time after their controller is connected,
* and the menu is already on screen by then: look again, twice a
* second for the first ten. */
if (PTR_RESCANS < 20 && ++PTR_TICKS >= 250) {
PTR_TICKS = 0;
PTR_RESCANS++;
pointers_find();
have = PTR_NREL + PTR_NABS;
if (have && !PTR_ACTIVE) {
PTR_ACTIVE = 1;
ui_cursor(PTR_X, PTR_Y, 1);
}
}
if (have) {
int e = pointer_poll();
if ((e & (PE_MOVED | PE_DOWN)) && !PTR_ACTIVE)
PTR_ACTIVE = 1;
if (e & PE_MOVED || ((e & PE_DOWN) && PTR_ACTIVE))
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
if (e & (PE_UP | PE_DOWNW)) {
key->UnicodeChar = 0;
key->ScanCode = (e & PE_UP) ? 0x01 : 0x02;
return EV_KEY;
}
int hit = ui_menu_hit(PTR_X, PTR_Y);
if ((e & PE_DOWN) && hit >= 0) {
*item = hit;
return EV_CLICK;
}
if ((e & PE_MOVED) && hit >= 0 && hit != last) {
last = hit;
*item = hit;
return EV_HOVER;
}
if (hit < 0)
last = -2;
}
if (budget) {
if (*budget < 2)
return EV_NONE;
*budget -= 2;
}
BS->Stall(2000);
}
}
/* A message and a button. Any key or a click dismisses it. */
static void
notice(const char *title, const char *const *lines, int n, const char *button,
const char *hint)
{
EFI_INPUT_KEY key;
int item = 0;
ui_notice(title, lines, n, button, hint);
mirrors_show();
for (;;) {
int e = wait_event(&key, &item, 0);
if (e == EV_KEY || e == EV_CLICK)
return;
}
}
static void static void
res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap) res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
{ {
@ -916,6 +1163,8 @@ res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p; for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p;
if (i < cap - 1) out[i++] = 'x'; if (i < cap - 1) out[i++] = 'x';
for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p; for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p;
for (const char *p = "\nChange the screen size"; *p && i < cap - 1; p++)
out[i++] = *p;
out[i] = 0; out[i] = 0;
} }
@ -1038,16 +1287,16 @@ res_read_number(const char *prompt)
shown[i] = 0; shown[i] = 0;
const char *rows[1] = { shown }; const char *rows[1] = { shown };
ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept"); ui_notice("Custom resolution", rows, 1, "Accept", "digits, Enter to accept");
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key; EFI_INPUT_KEY key;
for (;;) { int item = 0, e;
if (ST->ConIn && do
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) e = wait_event(&key, &item, 0);
break; while (e == EV_HOVER);
BS->Stall(10000); if (e == EV_CLICK)
} return v;
if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n') if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n')
return v; return v;
if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9') if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9')
@ -1069,6 +1318,47 @@ res_read_number(const char *prompt)
* cannot prove a mode is right, but they can prove one is wrong, and a wrong * cannot prove a mode is right, but they can prove one is wrong, and a wrong
* one is refused here, in the menu, where there is still a screen to say so on. * one is refused here, in the menu, where there is still a screen to say so on.
*/ */
/* "Keep this resolution?", drawn in the new mode, and Enter within ten
* seconds to keep it -- the question every desktop asks after a mode change,
* for the same reason. The numbers the firmware reports for a mode are no
* proof the screen shows it: AMD's GOP on a Ryzen APU and on the ASUS TUF
* "sets" the smaller modes without leaving the panel's native timing, and
* reports a row length that is the mode's rather than the panel's. Every
* check above passes and the picture comes out as three or four squashed
* copies side by side. Nobody can read the question on a screen like that,
* so nobody presses Enter, and the old mode comes back on its own. */
static int
mode_confirmed(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
struct ui_fb uifb;
EFI_INPUT_KEY key;
const char *msg[2] = { "Keep this resolution?",
"If you can read this, press Enter" };
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
uifb.width = gop->Mode->Info->HorizontalResolution;
uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb);
ui_notice("Resolution", msg, 2, "Keep", "going back in 10 seconds");
if (ST->ConIn)
while (!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
; /* the key that chose the mode is not an answer */
UINTN left = 10000; /* one countdown, however many hovers */
for (;;) {
int item = 0;
int e = wait_event(&key, &item, &left);
if (e == EV_NONE)
break;
if (e == EV_CLICK)
return 1;
if (e == EV_KEY)
return key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n';
}
return 0;
}
static int static int
mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode) mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
{ {
@ -1087,10 +1377,11 @@ mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
got->VerticalResolution == want->VerticalResolution && got->VerticalResolution == want->VerticalResolution &&
got->PixelsPerScanLine >= got->HorizontalResolution && got->PixelsPerScanLine >= got->HorizontalResolution &&
gop->Mode->FrameBufferBase && gop->Mode->FrameBufferBase &&
gop->Mode->FrameBufferSize >= need) gop->Mode->FrameBufferSize >= need &&
mode_confirmed(gop))
return 1; return 1;
describe_gop(gop, L"schos: mode does not add up, going back:"); describe_gop(gop, L"schos: mode not kept, going back:");
gop->SetMode(gop, was); gop->SetMode(gop, was);
return 0; return 0;
} }
@ -1100,21 +1391,14 @@ static void
res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
struct ui_fb uifb; struct ui_fb uifb;
EFI_INPUT_KEY key; const char *msg[1] = { "Not kept -- back to the size before" };
const char *msg[1] = { "The display refused that size -- keeping this one" };
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
uifb.width = gop->Mode->Info->HorizontalResolution; uifb.width = gop->Mode->Info->HorizontalResolution;
uifb.height = gop->Mode->Info->VerticalResolution; uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb); ui_init(&uifb);
ui_menu("Resolution", msg, 1, 0, "any key to go back"); notice("Resolution", msg, 1, "OK", "any key to go back");
mirrors_show();
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
} }
static void static void
@ -1122,6 +1406,8 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
int sel = 0; int sel = 0;
int echoed = -1; int echoed = -1;
static const char hint[] =
"W/S or arrows to choose, Enter to apply, Esc or Q to go back";
for (;;) { for (;;) {
struct ui_fb uifb; struct ui_fb uifb;
@ -1138,41 +1424,53 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
items[i] = RES_TEXT[i]; items[i] = RES_TEXT[i];
if (sel > RES_N) sel = RES_N; if (sel > RES_N) sel = RES_N;
ui_menu("Resolution", items, RES_N + 1, sel, ui_menu("Resolution", items, RES_N + 1, sel, hint);
"W/S to choose, Enter to apply, Esc or Q to go back");
if (echoed < 0) { if (echoed < 0)
serial_menu("Resolution", items, RES_N + 1, sel, serial_menu("Resolution", items, RES_N + 1, sel, hint, 0);
"W/S to choose, Enter to apply, Esc or Q to go back", 0);
} else if (echoed != sel) {
serial_item(items, sel, sel, 0);
}
echoed = sel; echoed = sel;
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key;
for (;;) { for (;;) {
if (ST->ConIn && EFI_INPUT_KEY key;
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) int item = -1, act = 0;
break; int e = wait_event(&key, &item, 0);
BS->Stall(10000);
}
if (e == EV_HOVER || e == EV_CLICK) {
if (item != sel) {
sel = item;
ui_menu_select(sel);
serial_item(items, sel, sel, 0);
mirrors_show();
}
act = e == EV_CLICK;
} else {
CHAR16 c = key.UnicodeChar; CHAR16 c = key.UnicodeChar;
/* Q as well as Esc, for the same reason f8_window() accepts 'o' as /* Q as well as Esc, for the same reason f8_window() accepts
* well as F8: on a serial line a lone ESC byte is indistinguishable * 'o' as well as F8: on a serial line a lone ESC byte is
* from the start of an escape sequence, so the terminal driver holds * indistinguishable from the start of an escape sequence, so
* it back waiting for the rest and it never arrives as a key. Esc is * the terminal driver holds it back waiting for the rest and
* the obvious thing to press on a screen and unreachable on a cable. */ * it never arrives as a key. Esc is the obvious thing to
* press on a screen and unreachable on a cable. */
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q') if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
return; return;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W') { int to = sel;
sel = (sel + RES_N) % (RES_N + 1); continue; if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
to = (sel + RES_N) % (RES_N + 1);
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
to = (sel + 1) % (RES_N + 1);
else if (c == L'\r' || c == L'\n')
act = 1;
if (to != sel) {
sel = to;
ui_menu_select(sel);
serial_item(items, sel, sel, 0);
mirrors_show();
} }
if (key.ScanCode == 0x02 || c == L's' || c == L'S') {
sel = (sel + 1) % (RES_N + 1); continue;
} }
if (c == L'\r' || c == L'\n') { if (!act)
continue;
if (sel < RES_N) { if (sel < RES_N) {
if (!mode_set_checked(gop, RES_MODE[sel])) { if (!mode_set_checked(gop, RES_MODE[sel])) {
res_refused(gop); res_refused(gop);
@ -1206,14 +1504,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
/* Nothing matched. Saying so beats silently keeping the old mode /* Nothing matched. Saying so beats silently keeping the old mode
* and letting the user wonder whether the key registered. */ * and letting the user wonder whether the key registered. */
const char *msg[1] = { "No such mode -- keeping this one" }; const char *msg[1] = { "No such mode -- keeping this one" };
ui_menu("Resolution", msg, 1, 0, "any key to go back"); notice("Resolution", msg, 1, "OK", "any key to go back");
mirrors_show();
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
return; return;
} }
} }
@ -1469,19 +1760,205 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key; EFI_INPUT_KEY key;
int item = 0;
for (;;) { for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) int e = wait_event(&key, &item, 0);
if (e == EV_KEY || e == EV_CLICK)
return; return;
BS->Stall(10000);
} }
} }
/* --- other systems ------------------------------------------------------------
*
* A boot manager that can only boot itself is a menu with one item. Other
* operating systems on this machine leave their loader at a known path on an
* EFI system partition, and chain-loading one is the firmware's own
* LoadImage and StartImage: no ExitBootServices of ours is involved, and if the
* other loader gives control back the menu is still here.
*
* Only paths that are well known are probed. Walking every directory of every
* volume would find more and would also find things that are not an operating
* system, and a boot menu that lists a firmware updater is not an improvement.
*/
#define MAX_OTHERS 6
static const struct { const CHAR16 *path; const char *name; } KNOWN_LOADERS[] = {
{ L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi", "Windows Boot Manager" },
{ L"\\EFI\\ubuntu\\shimx64.efi", "Ubuntu" },
{ L"\\EFI\\debian\\shimx64.efi", "Debian" },
{ L"\\EFI\\fedora\\shimx64.efi", "Fedora" },
{ L"\\EFI\\arch\\grubx64.efi", "Arch Linux" },
{ L"\\EFI\\opensuse\\grubx64.efi", "openSUSE" },
{ L"\\EFI\\systemd\\systemd-bootx64.efi", "systemd-boot" },
{ L"\\EFI\\refind\\refind_x64.efi", "rEFInd" },
{ L"\\EFI\\BOOT\\BOOTX64.EFI", "Another EFI loader" },
};
static struct {
EFI_HANDLE vol;
const CHAR16 *path;
char text[96]; /* "name\ndetail", as the menu wants it */
} OTHERS[MAX_OTHERS];
static int OTHER_N;
static void
others_find(void)
{
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
EFI_HANDLE *h = 0;
UINTN n = 0;
OTHER_N = 0;
if (EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &fs_guid, 0, &n, &h)) || !h)
return;
for (UINTN i = 0; i < n && OTHER_N < MAX_OTHERS; i++) {
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
EFI_FILE_PROTOCOL *root = 0;
if (EFI_ERROR(BS->HandleProtocol(h[i], &fs_guid, (void **)&fs)) || !fs)
continue;
if (EFI_ERROR(fs->OpenVolume(fs, &root)) || !root)
continue;
for (UINTN k = 0; k < sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] &&
OTHER_N < MAX_OTHERS; k++) {
/* Our own volume is probed too -- SchOS shares the ESP with
* Windows on a laptop -- but the fallback path there is this
* loader, and offering to start ourselves again is no help. */
if (h[i] == BOOT_VOL && k == sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] - 1)
continue;
EFI_FILE_PROTOCOL *f = 0;
if (EFI_ERROR(root->Open(root, &f, (CHAR16 *)KNOWN_LOADERS[k].path,
EFI_FILE_MODE_READ, 0)) || !f)
continue;
f->Close(f);
int o = OTHER_N++;
OTHERS[o].vol = h[i];
OTHERS[o].path = KNOWN_LOADERS[k].path;
int c = 0;
for (const char *q = KNOWN_LOADERS[k].name; *q && c < 40; q++)
OTHERS[o].text[c++] = *q;
OTHERS[o].text[c++] = '\n';
for (const char *q = "Start it from "; *q; q++)
OTHERS[o].text[c++] = *q;
char pn[48];
narrow(KNOWN_LOADERS[k].path, pn, sizeof pn);
for (const char *q = pn; *q && c < (int)sizeof OTHERS[o].text - 1; q++)
OTHERS[o].text[c++] = *q;
OTHERS[o].text[c] = 0;
}
root->Close(root);
}
BS->FreePool(h);
}
/* Start another loader. The device path for LoadImage is the volume's own path
* with one more node on the end naming the file: copy the volume's nodes up to
* its terminator, append a file-path node, and terminate again. */
static void
chain_load(int o)
{
EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID;
static UINT8 dp[512];
UINT8 *vol = 0;
UINTN len = 0;
if (EFI_ERROR(BS->HandleProtocol(OTHERS[o].vol, &dp_guid, (void **)&vol)) || !vol)
return;
for (;;) {
UINTN nl = (UINTN)vol[len + 2] | ((UINTN)vol[len + 3] << 8);
if (nl < 4 || len + nl > sizeof dp - 64)
return;
if (vol[len] == 0x7F && vol[len + 1] == 0xFF) /* End of Entire Path */
break;
len += nl;
}
copy(dp, vol, len);
UINTN chars = 0;
while (OTHERS[o].path[chars])
chars++;
UINTN nl = 4 + (chars + 1) * 2;
if (len + nl + 4 > sizeof dp)
return;
dp[len] = 4; dp[len + 1] = 4; /* media, file path */
dp[len + 2] = (UINT8)nl; dp[len + 3] = (UINT8)(nl >> 8);
copy(dp + len + 4, OTHERS[o].path, (chars + 1) * 2);
len += nl;
dp[len] = 0x7F; dp[len + 1] = 0xFF; dp[len + 2] = 4; dp[len + 3] = 0;
EFI_HANDLE img = 0;
if (EFI_ERROR(BS->LoadImage(0, SELF, dp, 0, 0, &img)) || !img) {
const char *msg[1] = { "The firmware would not load it." };
notice("Start another system", msg, 1, "OK", "any key to go back");
return;
}
BS->StartImage(img, 0, 0);
BS->UnloadImage(img);
}
/* Ask the firmware to open its own setup on the next start, then restart.
*
* This used to halt with "reset to reach firmware setup", which is the screen
* of a loader that never learned the protocol. The firmware publishes a
* variable for exactly this: OsIndicationsSupported says what it can do and
* OsIndications is where the request goes. Not every firmware has it, and the
* ones that do not are told so rather than restarted for nothing. */
static void
firmware_setup(void)
{
EFI_RUNTIME_SERVICES *rt = ST->RuntimeServices;
EFI_GUID gv = EFI_GLOBAL_VARIABLE_GUID;
UINT64 supported = 0, cur = 0;
UINTN sz = sizeof supported;
UINT32 attr = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS;
if (EFI_ERROR(rt->GetVariable(L"OsIndicationsSupported", &gv, 0, &sz,
&supported)) ||
!(supported & EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) {
const char *msg[2] = { "This firmware cannot be asked to open its",
"setup. Use its own key when powering on." };
notice("Firmware Setup", msg, 2, "OK", "any key to go back");
return;
}
UINT32 a = 0;
sz = sizeof cur;
if (EFI_ERROR(rt->GetVariable(L"OsIndications", &gv, &a, &sz, &cur)))
cur = 0;
cur |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI;
if (EFI_ERROR(rt->SetVariable(L"OsIndications", &gv, attr, sizeof cur, &cur))) {
const char *msg[1] = { "The firmware refused the request." };
notice("Firmware Setup", msg, 1, "OK", "any key to go back");
return;
}
rt->ResetSystem(EfiResetCold, 0, 0, 0);
for (;;) __asm__ volatile("hlt");
}
static void
power(UINTN how)
{
ST->RuntimeServices->ResetSystem(how, 0, 0, 0);
for (;;) __asm__ volatile("hlt");
}
enum { A_BOOT, A_VERBOSE, A_OTHER, A_DISPLAY, A_INFO, A_FIRMWARE, A_RESTART,
A_SHUTDOWN, A_DEBUG };
static int static int
boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
int selected = 0; int selected = 0;
int echoed = -1; // the serial copy: header once, then moves int echoed = -1; // the serial copy: header once, then moves
char resbuf[48]; char resbuf[96];
connect_all();
pointers_find();
others_find();
for (;;) { for (;;) {
/* Re-read the framebuffer every pass: choosing a resolution moves it, /* Re-read the framebuffer every pass: choosing a resolution moves it,
@ -1496,78 +1973,97 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
res_row(gop, resbuf, sizeof resbuf); res_row(gop, resbuf, sizeof resbuf);
// Six entries in a debug build, five in a release one.
//
// The debugger entry is compiled out rather than hidden, because a // The debugger entry is compiled out rather than hidden, because a
// menu item that is present and refuses is an invitation to work out // menu item that is present and refuses is an invitation to work out
// how to make it not refuse. An ISO built for anybody else has no way // how to make it not refuse. An ISO built for anybody else has no way
// to turn a network debugger on, because the code that would set the // to turn a network debugger on, because the code that would set the
// flag is not in it. // flag is not in it.
const char *items[6]; //
int nitems = 5; // An item is a label, then a newline and a line of detail; a leading
items[0] = "Boot SchOS"; // \001 starts a new group, drawn with a gap above it. `what` says what
items[1] = "Boot SchOS (verbose)"; // each row does, because the number of rows is not fixed: other
items[2] = resbuf; // systems found on the disks are listed after the two ways of
items[3] = "System Information"; // starting this one.
items[4] = "Reboot to firmware"; const char *items[8 + MAX_OTHERS];
int what[8 + MAX_OTHERS], arg[8 + MAX_OTHERS];
int nitems = 0;
#define ITEM(text, a, x) do { items[nitems] = (text); what[nitems] = (a); arg[nitems] = (x); nitems++; } while (0)
ITEM("Boot SchOS\nStart the system", A_BOOT, 0);
ITEM("Boot SchOS (verbose)\nShow kernel messages while starting", A_VERBOSE, 0);
for (int o = 0; o < OTHER_N; o++)
ITEM(OTHERS[o].text, A_OTHER, o);
ITEM(resbuf, A_DISPLAY, 0);
ITEM("System Information\nFirmware, processor and memory", A_INFO, 0);
ITEM("\001Firmware Setup\nRestart into the UEFI settings", A_FIRMWARE, 0);
ITEM("Restart\nReboot the machine", A_RESTART, 0);
ITEM("Shut Down\nPower the machine off", A_SHUTDOWN, 0);
#ifdef SCHOS_DEBUG #ifdef SCHOS_DEBUG
items[5] = DEBUG_BOOT ? "Remote debugging: on (schdbgd, port 4444)" ITEM(DEBUG_BOOT ? "Remote debugging: on\nschdbgd, port 4444"
: "Remote debugging: off"; : "Remote debugging: off\nschdbgd, port 4444", A_DEBUG, 0);
nitems = 6;
#endif #endif
const char *hint = "W/S or arrows to choose, Enter to select - number keys jump";
if (selected >= nitems) selected = 0;
ui_menu("Boot Manager", items, nitems, selected, ui_menu("SchOS Boot Manager", items, nitems, selected, hint);
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
: "W/S or 1-5 to choose, Enter to select");
// The same menu on the serial line: the whole thing once, then one // The same menu on the serial line: the whole thing once, then one
// line per *move*. Reprinting on every keypress would be correct and // line per *move*. Reprinting on every keypress would be correct and
// unreadable -- a serial terminal has no cursor to paint over, so an // unreadable -- a serial terminal has no cursor to paint over, so an
// unrecognised key would scroll the selection past again for nothing. // unrecognised key would scroll the selection past again for nothing.
if (echoed < 0) { if (echoed < 0) {
serial_menu("Boot Manager", items, nitems, selected, serial_menu("Boot Manager", items, nitems, selected, hint, 1);
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
: "W/S or 1-5 to choose, Enter to select", 1);
} else if (echoed != selected) {
serial_item(items, selected, selected, 1);
} }
echoed = selected; echoed = selected;
mirrors_show(); mirrors_show();
EFI_INPUT_KEY key;
CHAR16 c = 0;
UINT16 scan = 0;
for (;;) { for (;;) {
if (ST->ConIn && EFI_INPUT_KEY key;
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { int item = -1, act = 0;
c = key.UnicodeChar; scan = key.ScanCode; break; int e = wait_event(&key, &item, 0);
} int to = selected;
BS->Stall(10000);
if (e == EV_HOVER || e == EV_CLICK) {
to = item;
act = e == EV_CLICK;
} else {
CHAR16 c = key.UnicodeChar;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
to = (selected + nitems - 1) % nitems;
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
to = (selected + 1) % nitems;
else if (c >= L'1' && c <= L'9' && (int)(c - L'1') < nitems)
to = (int)(c - L'1');
else if (c == L'\r' || c == L'\n')
act = 1;
} }
if (scan == 0x01) { selected = (selected + nitems - 1) % nitems; continue; } if (to != selected) {
if (scan == 0x02) { selected = (selected + 1) % nitems; continue; } selected = to;
if (c == L'w' || c == L'W') { selected = (selected + nitems - 1) % nitems; continue; } ui_menu_select(selected);
if (c == L's' || c == L'S') { selected = (selected + 1) % 5; continue; } serial_item(items, selected, selected, 1);
if (c >= L'1' && c <= L'0' + nitems) { selected = (int)(c - L'1'); continue; } echoed = selected;
mirrors_show();
}
if (!act)
continue;
if (c == L'\r' || c == L'\n') { switch (what[selected]) {
if (selected == 0) return 0; case A_BOOT: return 0;
if (selected == 1) return 1; case A_VERBOSE: return 1;
if (selected == 2) { res_choose(gop); continue; } case A_OTHER: chain_load(arg[selected]); break;
if (selected == 3) { sys_info(gop); continue; } case A_DISPLAY: res_choose(gop); break;
case A_INFO: sys_info(gop); break;
case A_FIRMWARE: firmware_setup(); break;
case A_RESTART: power(EfiResetCold); break;
case A_SHUTDOWN: power(EfiResetShutdown); break;
#ifdef SCHOS_DEBUG #ifdef SCHOS_DEBUG
// A toggle rather than an action: picking it changes what the // A toggle rather than an action: picking it changes what the
// entry says and leaves you in the menu, so the state you booted // entry says and leaves you in the menu, so the state you booted
// with is the state you can see before you boot. // with is the state you can see before you boot.
if (selected == 5) { DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; continue; } case A_DEBUG: DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; break;
#endif #endif
if (selected == 4) {
ui_clear();
ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",
0xB9BEC7u);
for (;;) __asm__ volatile("hlt");
} }
break; // repaint: the thing that ran may have changed the screen
} }
} }
} }
@ -2146,6 +2642,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{ {
ST = systab; ST = systab;
BS = systab->BootServices; BS = systab->BootServices;
SELF = image;
print(L"SchOS UEFI loader\r\n"); print(L"SchOS UEFI loader\r\n");

View file

@ -19,13 +19,14 @@
// guest cannot detect this: a key the host swallowed is indistinguishable from // guest cannot detect this: a key the host swallowed is indistinguishable from
// a key nobody pressed. So it is a setting. // a key nobody pressed. So it is a setting.
// //
// chvt -c altgr+F AltGr+F1..F6 (the default) // chvt -c ctrl+alt+F Ctrl+Alt+F1..F12 (the default)
// chvt -c ctrl+shift+F Ctrl+Shift+F1..F6 // chvt -c altgr+F AltGr+F1..F12
// chvt -c alt+1 Alt+1..6 // chvt -c ctrl+shift+F Ctrl+Shift+F1..F12
// chvt -c alt+1 Alt+1..9
// chvt -c off no chord at all // chvt -c off no chord at all
// //
// Whatever the modifiers are, they also bind minus and equals to the previous // Whatever the modifiers are, they also bind minus and equals to the previous
// and next terminal in use -- AltGr+- and AltGr+= by default. // and next terminal in use -- Ctrl+Alt+- and Ctrl+Alt+= by default.
#include "core/types.h" #include "core/types.h"
#include "uapi/user.h" #include "uapi/user.h"

View file

@ -9,10 +9,10 @@
// //
// ldlsd <module.ko> [vmlinux-btf] // ldlsd <module.ko> [vmlinux-btf]
// //
// **Neither file ships with SchOS.** Both are the user's: a module they // The module and the BTF of the kernel it was built for come from a Linux build
// obtained, and the BTF of the kernel it was built for. docs/licensing.md §6 // (`make ldls-stage`, `make ldls-r8169`). SchOS ships Linux's r8169 set in its
// is the whole argument, and `.gitignore` refusing `*.ko` is how it is kept // ISO; docs/licensing.md §6 has the licensing, and `.gitignore` refusing `*.ko`
// mechanically rather than by intention. // only keeps binaries out of git.
// //
// What it does *not* yet do is implement any of the symbols the driver asks // What it does *not* yet do is implement any of the symbols the driver asks
// for -- every one is still a stub that reports itself and returns zero, which // for -- every one is still a stub that reports itself and returns zero, which
@ -185,6 +185,32 @@ ldls_stub(long idx, unsigned long *args)
struct rctx { struct ko *k; struct stubs *s; }; struct rctx { struct ko *k; struct stubs *s; };
// Modules loaded before the one being relocated. A module set is loaded in
// dependency order -- libphy before r8169, ttm before amdgpu -- and what an
// earlier one defines is what a later one imports from it.
#define MAXMOD 32
static struct ko *g_mods[MAXMOD];
static int g_nmods;
static unsigned long long
find_global(struct ko *k, const char *want)
{
int i;
for (i = 0; i < k->nsym; i++) {
if ((k->sym[i].st_info >> 4) == 0) // STB_LOCAL: not for others
continue;
if (k->sym[i].st_shndx == KO_SHN_UNDEF ||
k->sym[i].st_shndx >= (unsigned short)k->nsh)
continue;
if (!k->sec[k->sym[i].st_shndx].alloc)
continue;
if (!streq(k->str + k->sym[i].st_name, want))
continue;
return k->sec[k->sym[i].st_shndx].addr + k->sym[i].st_value;
}
return 0;
}
// The KPI first, a logging stub second. // The KPI first, a logging stub second.
// //
// An unimplemented symbol still resolves -- to something that reports itself // An unimplemented symbol still resolves -- to something that reports itself
@ -208,6 +234,14 @@ resolve(void *ctx, const char *name, unsigned long long *out)
return 0; return 0;
} }
for (i = 0; i < g_nmods; i++) {
unsigned long long a = find_global(g_mods[i], name);
if (a) {
*out = a;
return 0;
}
}
for (i = 0; i < r->k->nundef; i++) for (i = 0; i < r->k->nundef; i++)
if (streq(r->k->undef[i], name)) { if (streq(r->k->undef[i], name)) {
*out = stub_addr(r->s, i); *out = stub_addr(r->s, i);
@ -230,7 +264,7 @@ resolve(void *ctx, const char *name, unsigned long long *out)
// bargain as the logging stubs, and the same caveat -- see the memcpy note in // bargain as the logging stubs, and the same caveat -- see the memcpy note in
// docs/architecture/ldls.md. // docs/architecture/ldls.md.
#define PERCPU_SLOTS 8 #define PERCPU_SLOTS 16
#define PERCPU_SIZE (16 * 1024) #define PERCPU_SIZE (16 * 1024)
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE]; static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
@ -260,6 +294,14 @@ percpu_for(void *ctx, const char *name)
*(unsigned long long *)percpu_mem[i] = *(unsigned long long *)percpu_mem[i] =
(unsigned long long)(void *)(percpu_mem[i] + 64); (unsigned long long)(void *)(percpu_mem[i] + 64);
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n"); puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
} else if (streq(name, "__preempt_count")) {
// x86 keeps PREEMPT_NEED_RESCHED inverted in this word, so that
// `decl %gs:__preempt_count; je resched` is one instruction. Zero
// means "count zero *and* a reschedule wanted", and every
// preempt_enable() then called preempt_schedule -- see the note on
// __SCT__preempt_schedule in ldls/kpi/base.c for what that cost.
*(unsigned int *)percpu_mem[i] = 0x80000000u;
puts_(" per-CPU "); puts_(name); puts_(" -- PREEMPT_ENABLED\n");
} else { } else {
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n"); puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
} }
@ -284,6 +326,33 @@ find_symbol(struct ko *k, const char *want)
return 0; return 0;
} }
// "r8169: rtl_open.cold+0x4f" for an address in a loaded module, so that a
// photograph of the screen is enough to find the line. Modules are named by
// the file they were loaded from, with the directory and ".ko" dropped.
static const char *g_modname[MAXMOD];
static void
where(unsigned long addr)
{
for (int m = 0; m < g_nmods; m++) {
struct ko *k = g_mods[m];
for (int i = 0; i < k->nsym; i++) {
const struct ko_sym *s = &k->sym[i];
if (s->st_shndx == KO_SHN_UNDEF || s->st_shndx >= (unsigned short)k->nsh ||
!k->sec[s->st_shndx].alloc || s->st_size == 0 || (s->st_info & 0xf) != 2)
continue;
unsigned long long a = k->sec[s->st_shndx].addr + s->st_value;
if (addr >= a && addr < a + s->st_size) {
puts_(g_modname[m]); puts_(": ");
puts_(k->str + s->st_name); puts_("+");
puthex(addr - a);
return;
}
}
}
puthex(addr);
}
// --- reading and writing a struct at an offset BTF gave us -------------------- // --- reading and writing a struct at an offset BTF gave us --------------------
static unsigned int static unsigned int
@ -430,6 +499,9 @@ kpi_layout_from_btf(struct btf *b)
GET(nd, "dev_addr", kpi_l.nd_dev_addr); GET(nd, "dev_addr", kpi_l.nd_dev_addr);
GET(nd, "perm_addr", kpi_l.nd_perm_addr); GET(nd, "perm_addr", kpi_l.nd_perm_addr);
GET(nd, "addr_len", kpi_l.nd_addr_len); GET(nd, "addr_len", kpi_l.nd_addr_len);
// Optional: older kernels do not have it, and 0 is how that is said.
if (btf_member_off(b, nd, "priv_len", &off, &size) == 0)
kpi_l.nd_priv_len = off;
GET(nd, "mc", kpi_l.nd_mc); GET(nd, "mc", kpi_l.nd_mc);
GET(nd, "uc", kpi_l.nd_uc); GET(nd, "uc", kpi_l.nd_uc);
GET(nd, "_tx", kpi_l.nd_tx); GET(nd, "_tx", kpi_l.nd_tx);
@ -445,8 +517,42 @@ kpi_layout_from_btf(struct btf *b)
GET(pd, "dev", kpi_l.pd_dev); GET(pd, "dev", kpi_l.pd_dev);
GET(dv, "kobj", kpi_l.dev_kobj); GET(dv, "kobj", kpi_l.dev_kobj);
GET(ko, "name", kpi_l.kobj_name); GET(ko, "name", kpi_l.kobj_name);
kpi_l.dev_kobj_ok = 1;
} }
} }
// The driver core and struct firmware. Optional: a driver that never
// registers a bus or loads firmware does not need them, so a BTF without
// one of these turns the feature off rather than refusing to run.
{
int dv = btf_find(b, "device", BTF_KIND_STRUCT);
int dd = btf_find(b, "device_driver", BTF_KIND_STRUCT);
int bt = btf_find(b, "bus_type", BTF_KIND_STRUCT);
int fw = btf_find(b, "firmware", BTF_KIND_STRUCT);
int ok = dv && dd && bt;
#define OPT(id, name, field) \
do { if (btf_member_off(b, id, name, &off, &size) < 0) ok = 0; \
else field = off; } while (0)
if (ok) {
OPT(dv, "bus", kpi_l.dev_bus);
OPT(dv, "driver", kpi_l.dev_driver);
OPT(dd, "bus", kpi_l.drv_bus);
OPT(dd, "probe", kpi_l.drv_probe);
OPT(dd, "name", kpi_l.drv_name);
OPT(bt, "match", kpi_l.bus_match);
OPT(bt, "probe", kpi_l.bus_probe);
}
kpi_l.core_ok = ok;
if (fw) {
ok = 1;
OPT(fw, "size", kpi_l.fw_size);
OPT(fw, "data", kpi_l.fw_data);
kpi_l.fw_size_of = btf_type_size(b, fw);
kpi_l.fw_ok = ok;
}
#undef OPT
puts_(" kpi driver core "); puts_(kpi_l.core_ok ? "on" : "off");
puts_(", firmware "); puts_(kpi_l.fw_ok ? "on" : "off"); puts_("\n");
}
{ {
int nq = btf_find(b, "netdev_queue", BTF_KIND_STRUCT); int nq = btf_find(b, "netdev_queue", BTF_KIND_STRUCT);
if (!nq) { puts_("ldlsd: no struct netdev_queue\n"); return -1; } if (!nq) { puts_("ldlsd: no struct netdev_queue\n"); return -1; }
@ -682,8 +788,11 @@ drive_probe(const char *btfpath)
{ {
struct pciuser_info in; struct pciuser_info in;
// No device: not a failure worth restarting for. schinit's
// KeepAlive would otherwise start this again every few seconds on
// every machine without the NIC. See SCHINIT_EXIT_NORESTART.
if (claim_for(idtab, id_size, id_vendor, id_device, &in) < 0) if (claim_for(idtab, id_size, id_vendor, id_device, &in) < 0)
return; exit_group(78);
pdev = (unsigned long)getmem(dev_size); pdev = (unsigned long)getmem(dev_size);
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; } if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; }
@ -714,6 +823,26 @@ drive_probe(const char *btfpath)
(unsigned char)(q >> (i * 8)); (unsigned char)(q >> (i * 8));
} }
// pdev->bus. pci_domain_nr() and pci_dev_id() are inlines that
// dereference it -- r8169 names its MDIO bus with both -- and a null
// one is a fault at the first line of MDIO setup.
{
unsigned int pd_bus = lay(&b, "pci_dev", "bus");
unsigned int pb_num = lay(&b, "pci_bus", "number");
unsigned int pb_sys = lay(&b, "pci_bus", "sysdata");
unsigned int pb_size = lay(&b, "pci_bus", 0);
if (pd_bus != 0xffffffffu && pb_num != 0xffffffffu &&
pb_sys != 0xffffffffu && pb_size != 0xffffffffu) {
unsigned long bus = (unsigned long)getmem(pb_size);
unsigned long sys = (unsigned long)getmem(4096);
if (bus && sys) {
((unsigned char *)bus)[pb_num] = (unsigned char)in.bus;
bcopy_((void *)(bus + pb_sys), &sys, 8);
bcopy_((void *)(pdev + pd_bus), &bus, 8);
}
}
}
wr16(pdev, dev_vendor, in.vendor); wr16(pdev, dev_vendor, in.vendor);
wr16(pdev, dev_device, in.device); wr16(pdev, dev_device, in.device);
wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func)); wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func));
@ -795,21 +924,145 @@ drive_probe(const char *btfpath)
} }
} }
// --- main ---------------------------------------------------------------------- // Read the BTF and fill in the KPI's layouts, before any module runs.
static void
int early_layout(const char *btfpath)
main(int argc, char *argv[])
{ {
struct ko k; static struct btf b;
unsigned long long blen;
unsigned char *bimg = slurp(btfpath, &blen);
long nt;
unsigned int *boff;
if (!bimg) return;
nt = btf_count(bimg, blen);
if (nt < 0) return;
boff = getmem(((unsigned long long)nt + 2) * 4);
if (!boff || btf_open(&b, bimg, blen, boff, (unsigned int)nt + 2) < 0)
return;
{
extern unsigned int kpi_netdev_size;
int nd = btf_find(&b, "net_device", BTF_KIND_STRUCT);
kpi_netdev_size = nd ? btf_type_size(&b, nd) : 0;
}
kpi_layout_from_btf(&b);
}
// One module: read, laid out, relocated, its %gs references fixed, and its
// init_module called. Kept loaded for the ones after it to link against.
static void
load_module(const char *path)
{
struct ko *k;
struct ko_section *sec; struct ko_section *sec;
struct stubs stubs; struct stubs *stubs;
struct rctx rc; struct rctx rc;
unsigned char *img, *mem, *code; unsigned char *img, *mem, *code;
unsigned long long len, span, entry; unsigned long long len, span, entry;
int nsec, i; int nsec, i;
k = getmem(sizeof(*k));
stubs = getmem(sizeof(*stubs));
if (!k || !stubs) { puts_("ldlsd: out of memory\n"); exit(); }
img = slurp(path, &len);
if (!img) { puts_("ldlsd: cannot read "); puts_(path); puts_("\n"); exit(); }
nsec = ko_sections_needed(img, len);
if (nsec < 0) {
puts_("ldlsd: that is not an ELF64 relocatable object\n");
exit();
}
sec = getmem((unsigned long long)(nsec + 1) * sizeof(struct ko_section));
if (!sec) { puts_("ldlsd: out of memory\n"); exit(); }
if (ko_open(k, img, len, sec, nsec) < 0) {
puts_("ldlsd: "); puts_(ko_err(k)); puts_("\n");
exit();
}
puts_(path); puts_("\n");
puts_(" module ");
puts_(ko_name(k) ? ko_name(k) : "(not a kernel module)"); puts_("\n");
puts_(" license ");
puts_(ko_modinfo(k, "license") ? ko_modinfo(k, "license")
: "(none declared)"); puts_("\n");
puts_(" wants "); putn(k->nundef); puts_(" symbols\n");
span = ko_span(k);
mem = getmem(span ? span : 4096);
code = getmem((unsigned long long)(k->nundef + 1) * STUB_SIZE);
if (!mem || !code) { puts_("ldlsd: out of memory\n"); exit(); }
stub_build_from(stubs, code, k->nundef, g_count);
for (i = 0; i < k->nundef; i++) {
g_names[g_count + i] = k->undef[i];
if (stub_is_bare(k->undef[i]))
stub_make_bare(code + (unsigned long long)i * STUB_SIZE);
}
g_count += k->nundef;
k_nundef_global = g_count;
if (ko_layout(k, mem, span, (unsigned long long)mem) < 0) {
puts_("ldlsd: layout: "); puts_(ko_err(k)); puts_("\n");
exit();
}
rc.k = k; rc.s = stubs;
if (ko_relocate(k, resolve, &rc) < 0) {
puts_("ldlsd: relocate: "); puts_(ko_err(k)); puts_("\n");
exit();
}
// The stack canary is fetched through %gs, which belongs to the kernel
// here. See ko_defuse_gs().
{
int g = ko_defuse_gs(k, "__ref_stack_chk_guard");
if (g < 0) g = 0;
if (g == 0) g = ko_defuse_gs(k, "__stack_chk_guard");
if (g > 0) { puts_(" defused "); putn(g); puts_(" %gs stack-guard load(s)\n"); }
}
// Whatever %gs references are left name real per-CPU variables, and those
// cannot be defused -- the driver dereferences what it reads.
{
int p = ko_fixup_percpu(k, percpu_for, 0);
if (p < 0) {
puts_("ldlsd: per-CPU: "); puts_(ko_err(k)); puts_("\n");
exit();
}
if (p > 0) { puts_(" rehomed "); putn(p); puts_(" %gs per-CPU reference(s)\n"); }
}
puts_(" loaded "); putn(span); puts_(" bytes at "); puthex((unsigned long long)mem);
puts_("\n");
entry = find_symbol(k, "init_module");
if (g_nmods < MAXMOD) {
const char *b = path, *q;
for (q = path; *q; q++)
if (*q == '/') b = q + 1;
g_modname[g_nmods] = b;
g_mods[g_nmods++] = k;
kpi_where = where;
}
if (!entry) { puts_(" no init_module -- nothing to call\n"); return; }
puts_("\n calling init_module\n\n");
{
int (*init)(void) = (int (*)(void))(void *)entry;
int r = init();
puts_("\n init_module returned "); putn(r);
puts_(" after "); putn(g_calls); puts_(" stub call(s)\n");
}
}
// --- main ----------------------------------------------------------------------
int
main(int argc, char *argv[])
{
if (argc < 2) { if (argc < 2) {
puts_("usage: ldlsd <module.ko> [vmlinux-btf] [bus:slot.func]\n"); puts_("usage: ldlsd <module.ko>[,<module.ko>...] [vmlinux-btf] [bus:slot.func]\n");
puts_(" neither file ships with SchOS; both are yours.\n"); puts_(" neither file ships with SchOS; both are yours.\n");
exit(); exit();
} }
@ -837,85 +1090,32 @@ main(int argc, char *argv[])
puthexn(sel_slot, 2); puts_("."); puthexn(sel_func, 1); puts_("\n"); puthexn(sel_slot, 2); puts_("."); puthexn(sel_func, 1); puts_("\n");
} }
img = slurp(argv[1], &len); // argv[1] is one module, or a set in dependency order separated by
if (!img) { puts_("ldlsd: cannot read "); puts_(argv[1]); puts_("\n"); exit(); } // commas: libphy.ko,realtek.ko,r8169.ko. Each is loaded, relocated
// against the KPI and the ones before it, and initialised before the next.
nsec = ko_sections_needed(img, len); {
if (nsec < 0) { static char list[4096];
puts_("ldlsd: that is not an ELF64 relocatable object\n"); char *p = list, *name;
exit(); int n = 0;
} while (argv[1][n] && n < (int)sizeof(list) - 1) { list[n] = argv[1][n]; n++; }
sec = getmem((unsigned long long)(nsec + 1) * sizeof(struct ko_section)); list[n] = 0;
if (!sec) { puts_("ldlsd: out of memory\n"); exit(); } // The layouts first: init_module is where a module registers its
// drivers, and the driver core needs struct offsets to do that.
if (ko_open(&k, img, len, sec, nsec) < 0) { if (argc > 2)
puts_("ldlsd: "); puts_(ko_err(&k)); puts_("\n"); early_layout(argv[2]);
exit(); g_names = getmem(sizeof(char *) * (unsigned long long)MAXMOD * KO_MAXUNDEF);
} if (!g_names) { puts_("ldlsd: out of memory\n"); exit(); }
puts_(argv[1]); puts_("\n");
puts_(" module ");
puts_(ko_name(&k) ? ko_name(&k) : "(not a kernel module)"); puts_("\n");
puts_(" license ");
puts_(ko_modinfo(&k, "license") ? ko_modinfo(&k, "license")
: "(none declared)"); puts_("\n");
puts_(" wants "); putn(k.nundef); puts_(" symbols\n");
span = ko_span(&k);
mem = getmem(span ? span : 4096);
code = getmem((unsigned long long)(k.nundef + 1) * STUB_SIZE);
if (!mem || !code) { puts_("ldlsd: out of memory\n"); exit(); }
stub_build(&stubs, code, k.nundef);
g_names = k.undef;
g_count = k.nundef;
k_nundef_global = k.nundef;
kpi.netfd = -1; kpi.netfd = -1;
for (i = 0; i < k.nundef; i++) for (;;) {
if (stub_is_bare(k.undef[i])) name = p;
stub_make_bare(code + (unsigned long long)i * STUB_SIZE); while (*p && *p != ',') p++;
int last = *p == 0;
if (ko_layout(&k, mem, span, (unsigned long long)mem) < 0) { *p = 0;
puts_("ldlsd: layout: "); puts_(ko_err(&k)); puts_("\n"); if (*name)
exit(); load_module(name);
if (last) break;
p++;
} }
rc.k = &k; rc.s = &stubs;
if (ko_relocate(&k, resolve, &rc) < 0) {
puts_("ldlsd: relocate: "); puts_(ko_err(&k)); puts_("\n");
exit();
}
// The stack canary is fetched through %gs, which belongs to the kernel
// here. See ko_defuse_gs().
{
int g = ko_defuse_gs(&k, "__ref_stack_chk_guard");
if (g < 0) g = 0;
if (g == 0) g = ko_defuse_gs(&k, "__stack_chk_guard");
if (g > 0) { puts_(" defused "); putn(g); puts_(" %gs stack-guard load(s)\n"); }
}
// Whatever %gs references are left name real per-CPU variables, and those
// cannot be defused -- the driver dereferences what it reads.
{
int p = ko_fixup_percpu(&k, percpu_for, 0);
if (p < 0) {
puts_("ldlsd: per-CPU: "); puts_(ko_err(&k)); puts_("\n");
exit();
}
if (p > 0) { puts_(" rehomed "); putn(p); puts_(" %gs per-CPU reference(s)\n"); }
}
puts_(" loaded "); putn(span); puts_(" bytes at "); puthex((unsigned long long)mem);
puts_("\n");
entry = find_symbol(&k, "init_module");
if (!entry) { puts_(" no init_module -- nothing to call\n"); exit(); }
puts_("\n calling init_module\n\n");
{
int (*init)(void) = (int (*)(void))(void *)entry;
int r = init();
puts_("\n init_module returned "); putn(r);
puts_(" after "); putn(g_calls); puts_(" stub call(s)\n");
} }
if (argc > 2 && g_pci_driver) if (argc > 2 && g_pci_driver)

View file

@ -2,7 +2,7 @@
// //
// memfdtest -- two processes map the same memfd, one writes, the other sees it. // memfdtest -- two processes map the same memfd, one writes, the other sees it.
// //
// That sentence is docs/plan/foundations.md's own acceptance criterion for // That sentence is docs/architecture/kernel.md's own acceptance criterion for
// item 2, and it is the check a permissive implementation cannot pass: an // item 2, and it is the check a permissive implementation cannot pass: an
// mmap that quietly returns private zeroed pages satisfies every return code // mmap that quietly returns private zeroed pages satisfies every return code
// here and fails the one assertion that matters, because the reader is a // here and fails the one assertion that matters, because the reader is a

178
cmd/passwd/main.c Normal file
View file

@ -0,0 +1,178 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// passwd [USER]
//
// Change a password. Setuid root, like su and for the same reason a program of
// its own: it has to write /etc/shadow, which no one else can.
//
// Anyone may change their own password after proving they know the old one;
// root may change anyone's without it. The new one is stored as a SHA-512
// crypt hash with a fresh random salt, and the file is replaced by rename, so
// a crash leaves the old shadow or the new one and never half of each.
#define _GNU_SOURCE
#include <crypt.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
static int
read_password(const char *prompt, char *buf, size_t n)
{
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
struct termios t, old;
size_t i = 0;
char c;
if (fd < 0)
return -1;
if (tcgetattr(fd, &old) == 0) {
t = old;
t.c_lflag &= ~(ECHO | ECHONL);
tcsetattr(fd, TCSAFLUSH, &t);
}
if (write(fd, prompt, strlen(prompt)) < 0) {}
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
if (i + 1 < n)
buf[i++] = c;
buf[i] = 0;
tcsetattr(fd, TCSAFLUSH, &old);
if (write(fd, "\n", 1) < 0) {}
close(fd);
return 0;
}
static void
make_salt(char *out)
{
static const char a[] =
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
unsigned char r[16];
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0 || read(fd, r, sizeof r) != (ssize_t)sizeof r) {
fprintf(stderr, "passwd: no random numbers\n");
exit(1);
}
close(fd);
strcpy(out, "$6$");
for (int i = 0; i < 16; i++)
out[3 + i] = a[r[i] & 63];
out[19] = '$';
out[20] = 0;
}
int
main(int argc, char **argv)
{
uid_t uid = getuid();
struct passwd *me = getpwuid(uid), *pw;
// getpwuid and getpwnam share one static buffer: keep the name we were
// given, not a pointer into it.
char *user = argc > 1 ? argv[1] : (me ? strdup(me->pw_name) : 0);
char oldp[256], p1[256], p2[256], salt[24], *hash;
FILE *in, *out;
char line[1024], tmp[] = "/etc/shadow.new";
long days = (long)(time(0) / 86400);
int found = 0;
if (!user || !(pw = getpwnam(user))) {
fprintf(stderr, "passwd: unknown user\n");
return 1;
}
{
static struct passwd copy;
copy = *pw;
copy.pw_name = strdup(pw->pw_name);
pw = &copy;
}
if (geteuid() != 0) {
fprintf(stderr, "passwd: must be setuid root\n");
return 1;
}
if (uid != 0 && pw->pw_uid != uid) {
fprintf(stderr, "passwd: you may only change your own password\n");
return 1;
}
if (uid != 0) {
struct spwd *sp = getspnam(pw->pw_name);
const char *cur = sp ? sp->sp_pwdp : "";
if (cur[0]) {
if (read_password("Current password: ", oldp, sizeof oldp) < 0)
return 1;
char *e = crypt(oldp, cur);
memset(oldp, 0, sizeof oldp);
if (!e || strcmp(e, cur) != 0) {
sleep(2);
fprintf(stderr, "passwd: incorrect password\n");
return 1;
}
}
}
if (read_password("New password: ", p1, sizeof p1) < 0 ||
read_password("Retype new password: ", p2, sizeof p2) < 0)
return 1;
if (strcmp(p1, p2) != 0) {
fprintf(stderr, "passwd: the passwords do not match\n");
return 1;
}
make_salt(salt);
// An empty password stays empty: crypt of "" is a hash, and the user asked
// for no password at all.
hash = p1[0] ? crypt(p1, salt) : "";
if (!hash) {
fprintf(stderr, "passwd: cannot hash\n");
return 1;
}
if (!(in = fopen("/etc/shadow", "r"))) {
fprintf(stderr, "passwd: /etc/shadow: %s\n", strerror(errno));
return 1;
}
umask(0077);
if (!(out = fopen(tmp, "w"))) {
fprintf(stderr, "passwd: %s: %s\n", tmp, strerror(errno));
return 1;
}
while (fgets(line, sizeof line, in)) {
size_t n = strlen(pw->pw_name);
if (strncmp(line, pw->pw_name, n) == 0 && line[n] == ':') {
// name:hash:lastchg:min:max:warn:inactive:expire:reserved
char *f = strchr(line + n + 1, ':');
fprintf(out, "%s:%s:%ld%s", pw->pw_name, hash, days,
f && (f = strchr(f + 1, ':')) ? f : ":0:::::\n");
found = 1;
} else {
fputs(line, out);
}
}
fclose(in);
if (!found)
fprintf(out, "%s:%s:%ld:0:::::\n", pw->pw_name, hash, days);
memset(p1, 0, sizeof p1);
memset(p2, 0, sizeof p2);
struct group *g = getgrnam("shadow");
fchmod(fileno(out), 0640);
if (g)
fchown(fileno(out), 0, g->gr_gid);
if (fclose(out) != 0 || rename(tmp, "/etc/shadow") < 0) {
fprintf(stderr, "passwd: could not write /etc/shadow: %s\n", strerror(errno));
unlink(tmp);
return 1;
}
printf("password updated\n");
return 0;
}

View file

@ -2,7 +2,7 @@
// //
// pcitest -- prove that a driver could live out here. // pcitest -- prove that a driver could live out here.
// //
// docs/plan/hybrid.md mechanisms 2 and 4, exercised end to end from a process: // docs/architecture/devices.md mechanisms 2 and 4, exercised end to end from a process:
// enumerate the bus, claim a device, map its registers, read them, and get // enumerate the bus, claim a device, map its registers, read them, and get
// memory the device could be pointed at. // memory the device could be pointed at.
// //

View file

@ -6,7 +6,7 @@
// worth recording because it is not "ours is better". toybox's ping reads its // worth recording because it is not "ours is better". toybox's ping reads its
// replies with recvmsg(2) and asks for the TTL through an IP_RECVTTL control // replies with recvmsg(2) and asks for the TTL through an IP_RECVTTL control
// message, and SchOS has neither: no recvmsg, no ancillary data. Both are owed // message, and SchOS has neither: no recvmsg, no ancillary data. Both are owed
// -- recvmsg is item 1 of docs/plan/foundations.md, where it is wanted for // -- recvmsg is item 1 of docs/architecture/kernel.md, where it is wanted for
// SCM_RIGHTS -- and neither should be rushed to make one applet run. // SCM_RIGHTS -- and neither should be rushed to make one applet run.
// //
// So this uses sendto/recvfrom, which the kernel does have, and proves the ICMP // So this uses sendto/recvfrom, which the kernel does have, and proves the ICMP

View file

@ -2,7 +2,7 @@
// //
// schmode -- change the display mode. // schmode -- change the display mode.
// //
// docs/plan/hybrid.md milestone H3. The interesting thing about this program // docs/architecture/devices.md milestone H3. The interesting thing about this program
// is how little it is: it opens a socket to `vgpud` and writes a line. No // is how little it is: it opens a socket to `vgpud` and writes a line. No
// syscall it makes has heard of a GPU, no kernel code runs that knows what a // syscall it makes has heard of a GPU, no kernel code runs that knows what a
// scanout is, and if the driver on the other end gets the sequence wrong the // scanout is, and if the driver on the other end gets the sequence wrong the

View file

@ -447,7 +447,7 @@ strneq_(const char *a, const char *b, int n)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 8. SCM_RIGHTS: a descriptor crossing between processes. // 8. SCM_RIGHTS: a descriptor crossing between processes.
// //
// docs/plan/foundations.md item 1, docs/plan/hybrid.md mechanism 1. The thing // docs/architecture/kernel.md item 1, docs/architecture/devices.md mechanism 1. The thing
// being proved is not that recvmsg returns a number -- it is that the number // being proved is not that recvmsg returns a number -- it is that the number
// names the *same open file* the other process had, which is why every check // names the *same open file* the other process had, which is why every check
// below moves data through the descriptor rather than inspecting it. // below moves data through the descriptor rather than inspecting it.
@ -763,7 +763,7 @@ test_fdpass_nodata(void)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 15. A bound socket is a file. // 15. A bound socket is a file.
// //
// docs/plan/foundations.md item 3, in its own words: bind() a socket, then // docs/architecture/kernel.md item 3, in its own words: bind() a socket, then
// stat() and `ls -l` the path and see an `s`, then unlink() it. // stat() and `ls -l` the path and see an `s`, then unlink() it.
// //
// The reason it matters is not tidiness. A Wayland client finds its compositor // The reason it matters is not tidiness. A Wayland client finds its compositor

221
cmd/su/main.c Normal file
View file

@ -0,0 +1,221 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// su [-|-l] [-p] [-c COMMAND] [USER]
//
// Run a shell, or one command, as another user -- root by default. Setuid root,
// and a program of its own rather than a toybox applet: toybox is one binary
// under every name, and a setuid copy of it can be asked for any of its other
// applets by naming them, all as root.
//
// The rules are the ones su has always had, kept short:
//
// - root may become anyone, with no password;
// - anyone else is asked for the target's password, and gets three tries;
// - becoming root takes membership in `wheel` first, whatever the password
// is -- an account with no password is otherwise a door for every user.
//
// -, -l a login shell: a clean environment, the target's home, argv[0] "-sh"
// -p keep the environment (HOME, SHELL, USER and LOGNAME are still set
// unless the target is root)
// -c CMD run CMD with the shell's -c rather than an interactive shell
#define _GNU_SOURCE
#include <crypt.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
static int
in_wheel(uid_t uid, gid_t gid)
{
struct group *g = getgrnam("wheel");
struct passwd *pw = getpwuid(uid);
if (!g)
return 0;
if (gid == g->gr_gid)
return 1;
for (char **m = g->gr_mem; pw && m && *m; m++)
if (strcmp(*m, pw->pw_name) == 0)
return 1;
return 0;
}
// A line from the terminal with echo off. The password is read from the
// controlling terminal, not stdin, so a script cannot feed it.
static int
read_password(const char *prompt, char *buf, size_t n)
{
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
struct termios t, old;
size_t i = 0;
char c;
if (fd < 0)
return -1;
if (tcgetattr(fd, &old) == 0) {
t = old;
t.c_lflag &= ~(ECHO | ECHONL);
tcsetattr(fd, TCSAFLUSH, &t);
}
if (write(fd, prompt, strlen(prompt)) < 0) {}
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
if (i + 1 < n)
buf[i++] = c;
buf[i] = 0;
tcsetattr(fd, TCSAFLUSH, &old);
if (write(fd, "\n", 1) < 0) {}
close(fd);
return 0;
}
// Does `pw` accept this password? An empty hash accepts anything, a locked one
// ("*", "!...") nothing.
static int
password_ok(const struct passwd *pw, const char *pass)
{
const char *hash = pw->pw_passwd;
struct spwd *sp;
char *enc;
if (hash[0] == 'x' && hash[1] == 0) {
sp = getspnam(pw->pw_name);
if (!sp)
return 0;
hash = sp->sp_pwdp;
}
if (hash[0] == 0)
return 1;
if (hash[0] == '*' || hash[0] == '!')
return 0;
enc = crypt(pass, hash);
return enc && strcmp(enc, hash) == 0;
}
int
main(int argc, char **argv)
{
const char *target = "root", *cmd = 0;
int login = 0, keep = 0, i;
struct passwd *pw, *me;
uid_t uid = getuid();
char pass[256];
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-") || !strcmp(argv[i], "-l"))
login = 1;
else if (!strcmp(argv[i], "-p"))
keep = 1;
else if (!strcmp(argv[i], "-c") && i + 1 < argc)
cmd = argv[++i];
else if (argv[i][0] == '-' && argv[i][1]) {
fprintf(stderr, "usage: su [-|-l] [-p] [-c COMMAND] [USER]\n");
return 2;
} else
target = argv[i];
}
if (geteuid() != 0) {
fprintf(stderr, "su: must be setuid root\n");
return 1;
}
// getpwnam and getpwuid return the same static buffer, so the second call
// overwrites the first answer. Keep our own copy of the one that matters.
if (!(pw = getpwnam(target))) {
fprintf(stderr, "su: unknown user %s\n", target);
return 1;
}
{
static struct passwd copy;
copy = *pw;
copy.pw_name = strdup(pw->pw_name);
copy.pw_passwd = strdup(pw->pw_passwd);
copy.pw_dir = strdup(pw->pw_dir);
copy.pw_shell = strdup(pw->pw_shell);
pw = &copy;
}
me = getpwuid(uid);
if (uid != 0) {
int tries, ok = 0;
if (pw->pw_uid == 0 && !in_wheel(uid, getgid())) {
fprintf(stderr, "su: %s is not in the wheel group\n",
me ? me->pw_name : "you");
return 1;
}
for (tries = 0; tries < 3 && !ok; tries++) {
if (pw->pw_passwd[0] == 0 ||
(pw->pw_passwd[0] == 'x' && getspnam(pw->pw_name) &&
getspnam(pw->pw_name)->sp_pwdp[0] == 0)) {
ok = 1;
break;
}
if (read_password("Password: ", pass, sizeof pass) < 0) {
fprintf(stderr, "su: no terminal\n");
return 1;
}
ok = password_ok(pw, pass);
memset(pass, 0, sizeof pass);
if (!ok) {
sleep(2);
fprintf(stderr, "su: incorrect password\n");
}
}
if (!ok)
return 1;
}
if (initgroups(pw->pw_name, pw->pw_gid) < 0 ||
setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) {
fprintf(stderr, "su: cannot become %s: %s\n", pw->pw_name, strerror(errno));
return 1;
}
{
const char *shell = pw->pw_shell && *pw->pw_shell ? pw->pw_shell : "/bin/sh";
char *sh_argv[5];
int n = 0;
char arg0[256];
if (login) {
clearenv();
setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1);
if (getenv("TERM") == 0)
setenv("TERM", "linux", 1);
}
if (login || !keep || pw->pw_uid != 0) {
setenv("HOME", pw->pw_dir, 1);
setenv("SHELL", shell, 1);
setenv("USER", pw->pw_name, 1);
setenv("LOGNAME", pw->pw_name, 1);
}
if (login && chdir(pw->pw_dir) < 0)
chdir("/");
if (login) {
const char *base = strrchr(shell, '/');
snprintf(arg0, sizeof arg0, "-%s", base ? base + 1 : shell);
sh_argv[n++] = arg0;
} else {
const char *base = strrchr(shell, '/');
sh_argv[n++] = (char *)(base ? base + 1 : shell);
}
if (cmd) {
sh_argv[n++] = "-c";
sh_argv[n++] = (char *)cmd;
}
sh_argv[n] = 0;
execv(shell, sh_argv);
fprintf(stderr, "su: %s: %s\n", shell, strerror(errno));
}
return 1;
}

View file

@ -2,7 +2,7 @@
// //
// vgpud -- the virtio-gpu driver, outside the kernel. // vgpud -- the virtio-gpu driver, outside the kernel.
// //
// docs/plan/hybrid.md's first eviction. The same device the kernel drives in // docs/architecture/devices.md's first eviction. The same device the kernel drives in
// kernel/drivers/gpu/virtio_gpu/, driven instead by a process that asks the // kernel/drivers/gpu/virtio_gpu/, driven instead by a process that asks the
// kernel for four things and nothing else: // kernel for four things and nothing else:
// //
@ -648,7 +648,7 @@ present(void)
// //
// This used to be a test card: three colour bands, four gradients, a // This used to be a test card: three colour bands, four gradients, a
// checkerboard, and this instance's pid in binary along the bottom edge. That // checkerboard, and this instance's pid in binary along the bottom edge. That
// was scaffolding for hybrid.md's H2 and H3 -- a screenshot had to answer "did // was scaffolding for the out-of-kernel driver work -- a screenshot had to answer "did
// a *userspace process* put this here", and "did the driver come back" needed // a *userspace process* put this here", and "did the driver come back" needed
// a second run to look different from a first that never stopped. Both // a second run to look different from a first that never stopped. Both
// milestones are closed, and what was left was a garish test pattern on a // milestones are closed, and what was left was a garish test pattern on a
@ -866,7 +866,7 @@ register_panic_fb(void)
// --- the modeset -------------------------------------------------------------- // --- the modeset --------------------------------------------------------------
// //
// docs/plan/hybrid.md milestone H3: *"a modeset API, which is the thing an // docs/architecture/devices.md milestone H3: *"a modeset API, which is the thing an
// out-of-kernel GPU driver makes affordable and an in-kernel one never got."* // out-of-kernel GPU driver makes affordable and an in-kernel one never got."*
// //
// The claim is worth testing rather than repeating, so here is the whole of // The claim is worth testing rather than repeating, so here is the whole of
@ -944,7 +944,7 @@ set_mode(int w, int h)
// //
// The name is a *name*, not a path. AF_UNIX sockets in this kernel live in a // The name is a *name*, not a path. AF_UNIX sockets in this kernel live in a
// table keyed by string and never touch the filesystem -- that is item 3 of // table keyed by string and never touch the filesystem -- that is item 3 of
// docs/plan/foundations.md and it is not built. Nothing here depends on which // docs/architecture/kernel.md and it is not built. Nothing here depends on which
// it is, and the day it becomes a real path this keeps working. // it is, and the day it becomes a real path this keeps working.
#define VGPUD_SOCKET "/run/vgpud" #define VGPUD_SOCKET "/run/vgpud"

45
config/drivers.conf Normal file
View file

@ -0,0 +1,45 @@
# Which drivers are compiled into the kernel.
#
# y compiled in, and enabled: it is offered every device it can drive
# n not compiled in: the code is not linked and the kernel never knows it
#
# Everything is y unless it says otherwise. Change a line, or override one for
# a single build:
#
# make DRV_E1000=n DRV_NVME=n
#
# A changed value rebuilds only kernel/drivers/registry.o and the link.
#
# These are the drivers that can be left out cleanly: nothing else in the kernel
# calls them, they are only offered a device by the registry
# (kernel/drivers/registry.c). The others cannot be, because the rest of the
# kernel calls into them directly, and are not listed:
#
# vga_fb the boot framebuffer: the console and the crash screen draw on it
# xhci, usb core, usb hid / storage / hub
# the keyboard and the root disk can arrive over USB, bio.c and
# the hotplug thread call in, and so does the I2C-HID touchpad
# PS/2, IDE, the 16550 UART
# fixed-address legacy devices (kernel/dev/)
#
# Linux's own drivers, run in a process by ldlsd, are not here: they are files in
# the image (make ldls-r8169), not code in the kernel.
# --- display --------------------------------------------------------------
DRV_VGA_PCI = y # the stdvga/Bochs card (the framebuffer driver stays regardless)
DRV_VIRTIO_GPU = y # QEMU's virtio-gpu: mode setting and an explicit flush
# --- network ----------------------------------------------------------------
DRV_E1000 = y # Intel 8254x and 8257x
DRV_RTL8139 = y # Realtek 8139 (the one Realtek part QEMU emulates)
# The native RTL8168/8111 driver. Off by default because the laptop's NIC is
# driven by Linux's r8169 under LDLS (etc/schinit/ldls-r8169.job), and a driver
# here would claim the device first. Turn it on for a machine without that job.
DRV_R8169 = n
# --- storage ----------------------------------------------------------------
DRV_AHCI = y # SATA
DRV_NVME = y # NVMe SSDs
# --- audio ------------------------------------------------------------------
DRV_AC97 = y # Intel AC'97 (QEMU's and old boards')

View file

@ -72,29 +72,36 @@ good chance it is in there.
| | | | | |
|---|---| |---|---|
| [README](architecture/README.md) | The system, layer by layer. Read first. | | [README](architecture/README.md) | The system, layer by layer. Read first. |
| [boot.md](architecture/boot.md) | Firmware to the first userspace process. | | [boot.md](architecture/boot.md) | Firmware to the first userspace process, and where the loader puts the kernel. |
| [kernel.md](architecture/kernel.md) | SchIFT: memory, processes, scheduling, traps, signals, SMP. | | [kernel.md](architecture/kernel.md) | SchIFT: memory, processes and threads, traps, signals, SMP. |
| [scheduler.md](architecture/scheduler.md) | Fair share by virtual runtime, slices, and `nice`. |
| [syscalls.md](architecture/syscalls.md) | The call table, and the Linux personality that lets real software run. | | [syscalls.md](architecture/syscalls.md) | The call table, and the Linux personality that lets real software run. |
| [filesystem.md](architecture/filesystem.md) | The VFS, the buffer cache, procfs, mounts. | | [filesystem.md](architecture/filesystem.md) | The VFS, the buffer cache, procfs, sysfs, devtmpfs, mounts. |
| [schfs.md](architecture/schfs.md) | SchFS: the copy-on-write root filesystem. | | [schfs.md](architecture/schfs.md) | SchFS: the copy-on-write root filesystem. |
| [devices.md](architecture/devices.md) | The device model, driver binding, the class vtables. | | [scff.md](architecture/scff.md) | SCFF: the compressed image the live root ships as. |
| [fat.md](architecture/fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
| [devices.md](architecture/devices.md) | The device model, driver binding, the class vtables, and drivers that live in a process. |
| [ldls.md](architecture/ldls.md) | Running Linux's drivers: a `.ko` in a process, reaching hardware through `/dev/pci`. |
| [usb.md](architecture/usb.md) | xHCI, the USB core, and the class drivers. | | [usb.md](architecture/usb.md) | xHCI, the USB core, and the class drivers. |
| [graphics.md](architecture/graphics.md) | Framebuffer, text rendering, console, and the crash screen. | | [graphics.md](architecture/graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
| [terminals.md](architecture/terminals.md) | The six virtual terminals, and how one is switched to. | | [terminals.md](architecture/terminals.md) | The twelve virtual terminals, ptys, and handing one to a display server. |
| [networking.md](architecture/networking.md) | Drivers, lwIP, sockets — Unix and INET. | | [audio.md](architecture/audio.md) | `/dev/dsp`, AudioEngine, AC'97, and ALSA over OSS. |
| [networking.md](architecture/networking.md) | NIC drivers, lwIP, sockets — Unix, INET and netlink. |
| [init.md](architecture/init.md) | `schinit`, PID 1 as a role, and the job manifests. | | [init.md](architecture/init.md) | `schinit`, PID 1 as a role, and the job manifests. |
| [packages.md](architecture/packages.md) | The apk root: Alpine's packages, and SchOS as one of them. |
| [desktop.md](architecture/desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |
| [debugging.md](architecture/debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. |
## plan/ — what SchOS will be ## plan/ — what SchOS will be
| | | | | |
|---|---| |---|---|
| [README](plan/README.md) | The three current decisions and the order they happen in. | | [README](plan/README.md) | What is here, what used to be, and the order. |
| [1.0.md](plan/1.0.md) | **The finish line, and the freeze.** What 1.0 contains, what is deferred, and the register of every half-built thing in the tree. | | [1.0.md](plan/1.0.md) | **The finish line, and the freeze.** What 1.0 contains, where it stands, and the register of every half-built thing in the tree. |
| [foundations.md](plan/foundations.md) | The kernel work everything else is blocked on. | | [roadmap.md](plan/roadmap.md) | Everything else scoped and still open. |
| [wayland.md](plan/wayland.md) | The GUI stack, and what gets deleted to make room. | | [gpu.md](plan/gpu.md) | Linux's `amdgpu` through LDLS, and `/dev/dri` served by a process. |
| [hybrid.md](plan/hybrid.md) | Moving drivers out of the kernel. | | [finch.md](plan/finch.md) | The author's language for SchOS's own programs. |
| [schapi.md](plan/schapi.md) | The long-term API layering, and the compatibility promise. | | [schapi.md](plan/schapi.md) | The long-term API layering, and the compatibility promise. |
| [roadmap.md](plan/roadmap.md) | Everything else still open. |
## guides/ — how to work on it ## guides/ — how to work on it

View file

@ -30,7 +30,7 @@ is a token. A hex value in that file means the system is missing something, and
the answer is to add it to Niveus rather than to work around it here. the answer is to add it to Niveus rather than to work around it here.
What that replaced was a 21 KB `style.css` that transcribed What that replaced was a 21 KB `style.css` that transcribed
[`include/gfx/theme.h`](../../include/theme.h) by hand — palette, type scale, [`include/gfx/theme.h`](../../include/gfx/theme.h) by hand — palette, type scale,
shadows, the lot. The intent was right and the mechanism was a copy: two files shadows, the lot. The intent was right and the mechanism was a copy: two files
that had to be kept in step by remembering to. Now the whole identity is one that had to be kept in step by remembering to. Now the whole identity is one
declaration, in the `<head>` of each page: declaration, in the `<head>` of each page:
@ -70,22 +70,16 @@ rm -rf docs/Website/niveus && cp -r docs/niveus docs/Website/niveus
## The download page ## The download page
`download.html` links to `releases/schos.iso`, a path relative to this `download.html` links to the ISO attached to the release on the project's Forgejo
directory. **Nothing puts a file there** — the ISO is 62 MB and does not belong (`https://pandor.pl/git/nexoniarz/SchOS/releases/download/v0.1.0/schos-v0.1.0.iso`). The ISO is
in the repository — so the link is dead until a build is dropped next to the hundreds of megabytes and does not belong in the repository, so nothing is stored under
site: `docs/Website/`; publishing a release is `tools/posix/release.sh vX.Y.Z`, and **when the tag
changes, change the link and the version beside it** (only the newest release keeps its ISO).
```sh The page is written so that nothing on it is a promise the tree cannot keep. In
make iso particular the support table lists the three machines SchOS has booted on and nothing else,
mkdir -p docs/Website/releases && cp schos.iso docs/Website/releases/ because that is the entire bare-metal record and a compatibility list would be fiction. When
``` that changes, change the table; do not soften the wording.
The page is written so that this is the only thing that needs doing, and so
that nothing on it is a promise the tree cannot keep. In particular the support
table says **one real machine, under test** rather than listing hardware,
because one Chromebox is the entire bare-metal record and a compatibility list
would be fiction. When that changes, change the table; do not soften the
wording.
The `dd` command is split across two lines on purpose. It was one long line The `dd` command is split across two lines on purpose. It was one long line
first, and the card clipped it at `conv=` — a destructive command that can first, and the card clipped it at `conv=` — a destructive command that can

View file

@ -30,14 +30,19 @@
["architecture/syscalls.md", "Syscalls"], ["architecture/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"], ["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"], ["architecture/schfs.md", "SchFS"],
["architecture/scff.md", "SCFF"],
["architecture/fat.md", "FAT"], ["architecture/fat.md", "FAT"],
["architecture/terminals.md", "Virtual terminals"], ["architecture/terminals.md", "Virtual terminals"],
["architecture/devices.md", "Device model"], ["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"], ["architecture/usb.md", "USB"],
["architecture/udev.md", "udev"],
["architecture/users.md", "Users and permissions"],
["architecture/graphics.md", "Graphics"], ["architecture/graphics.md", "Graphics"],
["architecture/audio.md", "Audio"], ["architecture/audio.md", "Audio"],
["architecture/networking.md", "Networking"], ["architecture/networking.md", "Networking"],
["architecture/init.md", "init"], ["architecture/init.md", "init"],
["architecture/packages.md", "Packages (apk)"],
["architecture/desktop.md", "The desktop (Xorg)"],
["architecture/ldls.md", "Linux drivers (LDLS)"], ["architecture/ldls.md", "Linux drivers (LDLS)"],
["architecture/debugging.md", "Debugging"] ["architecture/debugging.md", "Debugging"]
]}, ]},
@ -51,9 +56,8 @@
{ group: "Plan", items: [ { group: "Plan", items: [
["plan/README.md", "What plan/ is for"], ["plan/README.md", "What plan/ is for"],
["plan/roadmap.md", "Roadmap"], ["plan/roadmap.md", "Roadmap"],
["plan/foundations.md", "Foundations"], ["plan/gpu.md", "The GPU"],
["plan/wayland.md", "Wayland"], ["plan/finch.md", "Finch"],
["plan/hybrid.md", "Hybrid kernel"],
["plan/schapi.md", "SchAPI"] ["plan/schapi.md", "SchAPI"]
]}, ]},
{ group: "History", items: [ { group: "History", items: [

View file

@ -66,33 +66,34 @@
<p class="nv-prose"> <p class="nv-prose">
SchOS ships as one bootable ISO. It boots on UEFI, runs entirely from SchOS ships as one bootable ISO. It boots on UEFI, runs entirely from
RAM, and touches nothing on the machine unless you tell it to — there is RAM, and touches nothing on the machine unless you tell it to — there is
no installer yet, and that is the point of trying it this way. no installer yet, and that is the point of trying it this way. It comes up with
Xorg, Window Maker and Alpine's package manager.
</p> </p>
<div class="nv-cluster"> <div class="nv-cluster">
<a class="nv-button nv-button--primary nv-button--lg" href="releases/schos.iso" download>Download the ISO</a> <a class="nv-button nv-button--primary nv-button--lg" href="https://pandor.pl/git/nexoniarz/SchOS/releases/download/v0.1.0/schos-v0.1.0.iso" download>Download the ISO</a>
<a class="nv-button nv-button--lg" href="#build">Build it yourself</a> <a class="nv-button nv-button--lg" href="#build">Build it yourself</a>
</div> </div>
<p class="nv-caption"> <p class="nv-caption">
<code class="nv-code">schos.iso</code> · about 62 MB · UEFI x86-64 · <code class="nv-code">schos.iso</code> · v0.1.0 · a few hundred MB · UEFI x86-64 ·
nothing is written to your disks nothing is written to your disks
</p> </p>
</div> </div>
<!-- The support table. Deliberately not a compatibility list: one Chromebox <!-- The support table. Deliberately not a compatibility list: three machines
is the entire bare-metal record, and a longer table would be fiction. --> are the entire bare-metal record, and a longer table would be fiction. -->
<article class="nv-card nv-card--elevated"> <article class="nv-card nv-card--elevated">
<div class="nv-card__header"> <div class="nv-card__header">
<div> <div>
<p class="nv-overline nv-text-tertiary">Before you do</p> <p class="nv-overline nv-text-tertiary">Before you do</p>
<h2 class="nv-card__title">This has booted on exactly one real machine</h2> <h2 class="nv-card__title">This has booted on three real machines</h2>
</div> </div>
</div> </div>
<div class="nv-card__body nv-text-secondary"> <div class="nv-card__body nv-text-secondary">
Everything below QEMU is honest reporting rather than a compatibility Everything below QEMU is honest reporting rather than a compatibility
list. SchOS drives real hardware, and it has been run on a very small list. SchOS drives real hardware, and it has been run on a very small
number of it. number of it — from a USB stick, each time.
</div> </div>
<div class="nv-card__body"> <div class="nv-card__body">
<div class="nv-table-frame"> <div class="nv-table-frame">
@ -103,8 +104,16 @@
<td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td> <td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
</tr> </tr>
<tr> <tr>
<td>Asus Chromebox <span class="nv-text-tertiary">Guado CN62</span></td> <td>AMD B550 desktop <span class="nv-text-tertiary">Ryzen 5 4600G</span></td>
<td class="nv-table__actions"><span class="nv-badge nv-badge--warning">under test</span></td> <td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
</tr>
<tr>
<td>ASUS TUF A15 laptop <span class="nv-text-tertiary">Ryzen, RTL8168H</span></td>
<td class="nv-table__actions"><span class="nv-badge nv-badge--warning">boots, network under test</span></td>
</tr>
<tr>
<td>HP all-in-one <span class="nv-text-tertiary">Ivy Bridge, 2012</span></td>
<td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
</tr> </tr>
<tr> <tr>
<td>Anything else</td> <td>Anything else</td>
@ -231,11 +240,10 @@
<span class="nv-overline nv-text-tertiary">Step three</span> <span class="nv-overline nv-text-tertiary">Step three</span>
<h3 class="nv-steps__title">A shell</h3> <h3 class="nv-steps__title">A shell</h3>
<p class="nv-steps__text"> <p class="nv-steps__text">
<code class="nv-code">schinit</code> starts one on the console and a <code class="nv-code">schinit</code> starts one on the console and one on
second on <code class="nv-code">tty2</code>. each of the next five terminals. <code class="nv-code">zsh</code> is the login shell,
<code class="nv-code">mksh</code> is <code class="nv-code">/bin/sh</code>, <code class="nv-code">/bin/sh</code> is busybox's, and <code class="nv-code">mksh</code> is
with <code class="nv-code">zsh</code> beside it and the real toybox the static one recovery uses. Type <code class="nv-code">startx</code> for the desktop.
userland behind both.
</p> </p>
</div> </div>
</li> </li>
@ -243,9 +251,9 @@
<span class="nv-steps__marker">4</span> <span class="nv-steps__marker">4</span>
<div class="nv-steps__body"> <div class="nv-steps__body">
<span class="nv-overline nv-text-tertiary">Step four</span> <span class="nv-overline nv-text-tertiary">Step four</span>
<h3 class="nv-steps__title">Six terminals</h3> <h3 class="nv-steps__title">Twelve terminals</h3>
<p class="nv-steps__text"> <p class="nv-steps__text">
<code class="nv-code">AltGr+F1</code>…<code class="nv-code">F6</code> <code class="nv-code">Ctrl+Alt+F1</code>…<code class="nv-code">F12</code>
switches between them — the one chord a Linux host does not take for switches between them — the one chord a Linux host does not take for
itself — or <code class="nv-code">chvt N</code> from a shell. itself — or <code class="nv-code">chvt N</code> from a shell.
</p> </p>

View file

@ -347,7 +347,7 @@
</div> </div>
<div class="nv-card__body nv-text-secondary"> <div class="nv-card__body nv-text-secondary">
Six of them, each with its own cell grid, scrollback and foreground Six of them, each with its own cell grid, scrollback and foreground
job — switched with <code class="nv-code">AltGr+F1</code>…<code class="nv-code">F6</code>, job — switched with <code class="nv-code">Ctrl+Alt+F1</code>…<code class="nv-code">F12</code>,
which is the one chord a Linux host does not take for itself. The console which is the one chord a Linux host does not take for itself. The console
draws to whichever framebuffer is present, from the kernel's first line. draws to whichever framebuffer is present, from the kernel's first line.
</div> </div>
@ -398,13 +398,13 @@
<article class="nv-card"> <article class="nv-card">
<div class="nv-card__header"> <div class="nv-card__header">
<h3 class="nv-card__title"><span class="nv-tile nv-tile--sm" style="--nv-tile-tint: var(--nv-color-warning)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="13" height="10" rx="1.5"/><rect x="8" y="10" width="13" height="10" rx="1.5"/></svg></span>Schcom</h3> <h3 class="nv-card__title"><span class="nv-tile nv-tile--sm" ><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="13" height="10" rx="1.5"/><rect x="8" y="10" width="13" height="10" rx="1.5"/></svg></span>The desktop</h3>
<span class="nv-badge nv-badge--warning">in progress</span> <span class="nv-badge nv-badge--success">runs</span>
</div> </div>
<div class="nv-card__body nv-text-secondary"> <div class="nv-card__body nv-text-secondary">
The compositor that will own the display. v0 runs, and descriptor Xorg on the framebuffer with Window Maker, <code class="nv-code">xterm</code> and a few X clients;
passing — the one thing a Wayland client cannot do without — now NetSurf for the web. No acceleration yet — the GPU is a plan, and the plan is Linux's own
exists. What a buffer still needs is a shareable memory object. <code class="nv-code">amdgpu</code> through LDLS.
</div> </div>
</article> </article>
@ -415,7 +415,7 @@
</div> </div>
<div class="nv-card__body nv-text-secondary"> <div class="nv-card__body nv-text-secondary">
The native programming interface — specified, not built, and deliberately The native programming interface — specified, not built, and deliberately
so. It should be designed <i>from</i> the compositor's experience rather so. It should be designed <i>from</i> the desktop's experience rather
than before it. than before it.
</div> </div>
</article> </article>
@ -549,19 +549,19 @@
<dl class="nv-stat-group"> <dl class="nv-stat-group">
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">lines of code in <code class="nv-code">kernel/</code> and <code class="nv-code">include/</code></dt> <dt class="nv-stat__label">lines of code in <code class="nv-code">kernel/</code> and <code class="nv-code">include/</code></dt>
<dd class="nv-stat__value">45,207</dd> <dd class="nv-stat__value">51,893</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">lines of comments alongside them</dt> <dt class="nv-stat__label">lines of comments alongside them</dt>
<dd class="nv-stat__value">16,111</dd> <dd class="nv-stat__value">17,213</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">first-party lines in all</dt> <dt class="nv-stat__label">first-party lines in all</dt>
<dd class="nv-stat__value">61,367</dd> <dd class="nv-stat__value">72,766</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">syscalls, Linux numbering</dt> <dt class="nv-stat__label">syscalls, Linux numbering</dt>
<dd class="nv-stat__value">184</dd> <dd class="nv-stat__value">193</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">drivers in the kernel</dt> <dt class="nv-stat__label">drivers in the kernel</dt>
@ -569,15 +569,15 @@
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">Alpine packages in the live root</dt> <dt class="nv-stat__label">Alpine packages in the live root</dt>
<dd class="nv-stat__value">88</dd> <dd class="nv-stat__value">179</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">commits</dt> <dt class="nv-stat__label">commits</dt>
<dd class="nv-stat__value">253</dd> <dd class="nv-stat__value">292</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">days since the first one</dt> <dt class="nv-stat__label">days since the first one</dt>
<dd class="nv-stat__value">76</dd> <dd class="nv-stat__value">78</dd>
</div> </div>
<div class="nv-stat"> <div class="nv-stat">
<dt class="nv-stat__label">patches carried against fastfetch</dt> <dt class="nv-stat__label">patches carried against fastfetch</dt>

View file

@ -16,11 +16,10 @@ its distribution.
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS | | Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS |
| Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem | | Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem |
| API | **SchAPI** | The native programming interface — *designed, not built* | | API | **SchAPI** | The native programming interface — *designed, not built* |
| Compositor | **Schcom** | Owns the display; v0 runs | | Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` |
Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`, AltGr+F1..F6 Twelve virtual terminals sit under all of it -- `/dev/tty1`..`tty12`, Ctrl+Alt+F1..F12
or `chvt N` -- or `chvt N` -- each with its own screen, line discipline and foreground process group. See
each with its own screen, line discipline and foreground process group. See
[terminals.md](terminals.md). [terminals.md](terminals.md).
--- ---
@ -29,9 +28,11 @@ each with its own screen, line discipline and foreground process group. See
``` ```
┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ toybox kilo mksh zsh musl first-party cmd/ │ userland │ Xorg + Window Maker · Alpine's packages (apk) │ desktop, userland
│ toybox mksh musl first-party cmd/ │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ vgpud — a driver that is an ordinary process │ │ vgpud, ldlsd — drivers that are ordinary │
│ processes (and Linux's r8169 inside ldlsd) │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ schinit — PID 1, job manifests, supervision │ │ schinit — PID 1, job manifests, supervision │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
@ -45,7 +46,8 @@ each with its own screen, line discipline and foreground process group. See
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ /dev/pci — MMIO, DMA and IRQs to a process │ ← the eviction seam │ /dev/pci — MMIO, DMA and IRQs to a process │ ← the eviction seam
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ drivers: xHCI e1000 virtio-gpu VBE IDE PS/2│ │ drivers: xHCI AHCI NVMe e1000 8139 r8169 AC97 │
│ virtio-gpu VBE IDE PS/2 I2C-HID │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ scheduler │ paging │ traps │ IRQ │ SMP │ DMA │ SchIFT core │ scheduler │ paging │ traps │ IRQ │ SMP │ DMA │ SchIFT core
└─────────────────────────────────────────────────┘ └─────────────────────────────────────────────────┘
@ -59,7 +61,8 @@ never names its consumer and a consumer never names a driver. And at
**`/dev/pci`**, a driver stops having to be in the kernel at all: a process is **`/dev/pci`**, a driver stops having to be in the kernel at all: a process is
handed a device's registers, DMA memory and interrupts, and the in-kernel driver handed a device's registers, DMA memory and interrupts, and the in-kernel driver
that held it releases it cleanly. `vgpud` is the virtio-gpu driver living out that held it releases it cleanly. `vgpud` is the virtio-gpu driver living out
there today. See [../plan/hybrid.md](../plan/hybrid.md). there today, and `ldlsd` runs Linux's own drivers the same way. See
[devices.md](devices.md) and [ldls.md](ldls.md).
--- ---
@ -91,9 +94,14 @@ Read in this order if you are new:
| [fat.md](fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. | | [fat.md](fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
| [devices.md](devices.md) | Buses, classes, binding, and the vtables consumers talk to. | | [devices.md](devices.md) | Buses, classes, binding, and the vtables consumers talk to. |
| [usb.md](usb.md) | xHCI, the USB core, and the three class drivers. | | [usb.md](usb.md) | xHCI, the USB core, and the three class drivers. |
| [graphics.md](graphics.md) | Framebuffer, text rendering, the console, and PSOGM. | | [users.md](users.md) | Accounts, login, and the permissions the kernel enforces. |
| [terminals.md](terminals.md) | The six virtual terminals, and the chord that switches them. | | [udev.md](udev.md) | eudev on SchOS: the device table, the `/sys` tree, uevents, datagram sockets. |
| [networking.md](networking.md) | NIC drivers, lwIP, and both socket families. | | [graphics.md](graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
| [desktop.md](desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |
| [packages.md](packages.md) | The apk root: Alpine's packages, and SchOS as one of them. |
| [scff.md](scff.md) | The compressed image the live root ships as. |
| [terminals.md](terminals.md) | The twelve virtual terminals, ptys, and handing one to a display server. |
| [networking.md](networking.md) | NIC drivers, lwIP, and the socket families. |
| [audio.md](audio.md) | `/dev/dsp`, AudioEngine, and the AC'97 driver under it. | | [audio.md](audio.md) | `/dev/dsp`, AudioEngine, and the AC'97 driver under it. |
| [init.md](init.md) | `schinit`, PID 1 as a role, and the job manifests. | | [init.md](init.md) | `schinit`, PID 1 as a role, and the job manifests. |
| [debugging.md](debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. | | [debugging.md](debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. |

View file

@ -40,7 +40,7 @@ until the speaker has caught up*.
**It is one stream.** Mixing several programs into one device is a real feature **It is one stream.** Mixing several programs into one device is a real feature
with a real design behind it, and building it before anything had played a with a real design behind it, and building it before anything had played a
single sound would be exactly the mistake [../plan/1.0.md](../plan/1.0.md) single sound would be exactly the mistake [../plan/1.0.md](../plan/1.0.md)
keeps a register for: work that exists and is connected to nothing. The second keeps a register against: work that exists and is connected to nothing. The second
program that wants to play at the same time is what should cause it to be program that wants to play at the same time is what should cause it to be
written. written.
@ -84,6 +84,25 @@ Both register files are **I/O ports**, not MMIO — which is why nothing in the
driver maps anything. `bar[n].is_io` is what says so, and a driver that driver maps anything. `bar[n].is_io` is what says so, and a driver that
`ioremap`ed these would be mapping a port number as though it were an address. `ioremap`ed these would be mapping a port number as though it were an address.
### Through ALSA, for programs that expect it
SDL 3 has no OSS backend and SchOS has no ALSA *kernel* side, so sound from SDL games
and anything else that speaks ALSA goes **SDL → alsa-lib → alsa-plugins' OSS plugin →
`/dev/dsp`**. `alsa-lib` and `alsa-plugins` are in the Alpine `world`, and
`/etc/asound.conf` (from `schos-base`) makes the default device a `plug` onto the OSS
device at 16-bit stereo 48 kHz, which is what every SchOS audio driver plays.
Running that path found two bugs underneath it:
- **AC'97:** resetting the PCM-out box also clears the descriptor-list address and nothing
set it again, so every stream after the first stop played a "list" at physical address 0
— silence — and ALSA resets before each stream. If the engine halts at the end of the
queue, the run bit is set again.
- **`/dev/dsp` was missing the ioctls the plugin's ring-and-pointer model uses:**
`SETFRAGMENT`, `GETFMTS`, `GETCAPS`, `SETTRIGGER`, `GETOPTR`, `GETODELAY`, and `poll()`,
writable once a whole fragment fits (ALSA spun at 76% CPU otherwise). `speaker-test` now
plays in real time at 0% CPU, verified in a recording of QEMU's AC'97.
## How it is tested ## How it is tested
QEMU's `wav` audio backend writes what the guest plays to a file, so the test QEMU's `wav` audio backend writes what the guest plays to a file, so the test

View file

@ -44,7 +44,7 @@ because there are none.** Everything the jump needs is computed before.
### The hand-off block ### The hand-off block
One page of low memory at `0x30000`, defined once in One page of low memory at `0x30000`, defined once in
[`include/arch/schboot.h`](../../include/schboot.h) and included by both the loader [`include/arch/schboot.h`](../../include/arch/schboot.h) and included by both the loader
and `entry.S` so the two cannot disagree. and `entry.S` so the two cannot disagree.
| Offset | What | | Offset | What |
@ -131,6 +131,42 @@ when the early console is set up. So a low framebuffer is attached exactly as
before, and a high one is attached a few dozen instructions later, immediately before, and a high one is attached a few dozen instructions later, immediately
after `kvmalloc()` — still thousands of lines before anything else happens. after `kvmalloc()` — still thousands of lines before anything else happens.
### Where the kernel goes, and how it gets there
The kernel is linked at physical **16 MB** (`KERNPHYS` in `memlayout.h`, repeated in
`kernel.ld`, which must agree) and is about 34 MB with the ramdisk inside it. From the
old 1 MB it ran across 8 MB, where OVMF keeps ACPI NVS memory that outlives the firmware,
so it was overwritten on every boot, after `ExitBootServices`. Firmware leaves its
leftovers low; above 16 MB is where a machine is most reliably empty. (The 15 MB below
the kernel is not given to the page allocator yet.)
**Every free page of that range is claimed first.** A segment lying wholly on claimed
pages is read straight home; one that is not is read into a buffer elsewhere and
**copied home after `ExitBootServices`**, when boot-services memory is ours. An HP
all-in-one from 2012 used to freeze at the start of the loading bar because the loader
said "could not reserve" and read the kernel there anyway, over the firmware's own memory
with the firmware still running on it. Memory under the kernel that outlives the firmware
(ACPI NVS, runtime services) is named, and so is the loader or its stack sitting in the
range. Segments are read a megabyte at a time, so the bar moves through the ramdisk and
shows where a machine stopped.
The copy and the jump themselves run from **one page below the kernel's lowest address**,
allocated as loader code before the firmware exits: a short position-independent routine,
the copy list and a stack, switching to the loader's page tables and its own stack before
it copies anything. Doing the copy from code the firmware had placed wherever it liked put
some of it inside the kernel's range on that machine: a triple fault and a reset. The
routine is `sysv_abi`, because UEFI code is the Microsoft convention and the first try loaded
the framebuffer address into `rsp`.
**No 1 GB pages, no problem.** Intel's desktop CPUs before Haswell have none, and there the
PS bit in a PDPT entry is reserved: the first access after the switch is a page fault that
cannot be handled either. Both the loader and the kernel check CPUID `0x80000001` EDX bit 26;
without it the low 4 GB is four page directories of 2 MB pages (placed below the kernel, with
the staged segments kept below 4 GB), and the kernel gives each gigabyte of its direct map
and of `ioremap()` a page directory of its own. Neither KVM nor QEMU enforces the reserved
bit, so the fault could not be reproduced here; with the fallback forced on, the kernel
boots at 2 and 8 GB and both CPUs start.
### The loader jumped out from under itself ### The loader jumped out from under itself
A machine with a lot of RAM froze at "Starting the kernel" — the splash still A machine with a lot of RAM froze at "Starting the kernel" — the splash still
@ -294,9 +330,64 @@ Dropping back to protected mode to re-enter through the 32-bit multiboot path
is a mode switch whose failure mode is a triple fault with no console. The is a mode switch whose failure mode is a triple fault with no console. The
32-bit entry is still present in `entry.S` and now has no caller. 32-bit entry is still present in `entry.S` and now has no caller.
### The boot manager
F8 (or `o` on a serial line) opens the boot manager. It used to be a text list
on a dark plate; it is now drawn in the Niveus language the site is drawn in
(`docs/Website/niveus`): a window with a title bar, keys with a lit upper edge
and a shadow beneath, panels cut into the surface as wells, a graphite ground
that darkens towards the bottom, and the mark above the list.
What makes that possible in a loader is that nothing in it is large:
- **Shapes** are rounded rectangles with a vertical gradient. Corners are
anti-aliased by supersampling only the corner pixels, against a colour the
caller names. Nothing reads the framebuffer back, because that memory is
uncached on real hardware and a read per pixel is the difference between a
menu that appears and one that crawls. (The one exception is the pointer,
which saves sixteen by nineteen pixels.)
- **Type** is Instrument Sans, the face Niveus ships. A TrueType rasteriser
does not belong in a loader, so `tools/python/mknvfont.py` renders it once
into `include/gfx/nvfont.h` as 8-bit coverage bitmaps with fractional
advances, at the size for ordinary screens and at twice that for screens 1400
pixels tall or more. Rasterising at the real size is what keeps it crisp;
doubling pixels does not.
- **Layout** is computed from the item list, not drawn from fixed positions: a
list taller than the screen drops the mark, then scrolls with a bar.
An entry is a label, a newline and a line of detail. The entries are:
| Entry | Does |
|---|---|
| Boot SchOS / (verbose) | the two ways of starting this system |
| other systems | one entry per loader found at a well-known path on any volume: Windows Boot Manager, Ubuntu, Debian, Fedora, Arch, openSUSE, systemd-boot, rEFInd, and any other `\EFI\BOOT\BOOTX64.EFI` that is not this one. Chosen, they are started with the firmware's own `LoadImage`/`StartImage`, and if the other loader returns, the menu is still here |
| Resolution | the list of modes the firmware offers, or a typed size, with the "keep this?" confirmation |
| System Information | firmware, UEFI revision, processor, memory, display, ACPI |
| Firmware Setup | sets `OsIndications` and restarts into the firmware's own settings; on firmware that does not support it, says so instead of restarting for nothing |
| Restart, Shut Down | `ResetSystem` |
| Remote debugging | `make DEBUG=1` builds only |
Keyboard, mouse, touchpad and touchscreen all drive it: both UEFI pointer
protocols are polled (a mouse reports movement, a tablet reports position),
hover moves the highlight, a click activates, and the wheel scrolls. All input
goes through one function, `wait_event()`, so every screen behaves the same way.
The cursor stays hidden until the pointer has actually done something, because
the firmware's console layer installs a pointer protocol whether or not a mouse
is attached. USB devices appear some time after their controller is connected
and the menu is already up by then, so the loader connects every controller when
the menu opens and looks for pointers again twice a second for ten seconds.
The "keep this resolution?" confirmation counts ten seconds down once, however
many times the pointer moves over the button: a person with a garbled screen
waving a mouse around must not be able to keep postponing the revert.
**Not verified:** hover and click were exercised with synthetic events. Under
QEMU and OVMF a USB mouse or tablet never delivered an event to the loader, so
the pointer path has not been seen working on a device -- real hardware first.
### The menu is on the serial line too ### The menu is on the serial line too
The three F8 screens paint into the framebuffer, which is the right place for The screens paint into the framebuffer, which is the right place for
them on a machine with a screen and nowhere at all on one driven over a serial them on a machine with a screen and nowhere at all on one driven over a serial
cable — which is how this loader is usually driven. `f8_window()` already went cable — which is how this loader is usually driven. `f8_window()` already went
out of its way to support that, accepting `o` as well as F8 because *"a serial out of its way to support that, accepting `o` as well as F8 because *"a serial
@ -315,7 +406,7 @@ its text across the menu that was just painted there.
Two details that are not cosmetic: Two details that are not cosmetic:
- **The item number is shown only where it is a key you can press.** The boot - **The item number is shown only where it is a key you can press.** The boot
menu takes 1–5. The resolution list takes only W/S and Enter and is longer menu takes 1–9. The resolution list takes only W/S and Enter and is longer
than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a
character nobody can type, offering to select something. character nobody can type, offering to select something.
- **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is - **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is
@ -610,7 +701,9 @@ in the one process that runs first.
## What it does not do ## What it does not do
- **No BIOS path.** A machine without UEFI does not boot SchOS. - **No BIOS path.** A machine without UEFI does not boot SchOS. The ISO carries a real EFI
system partition (MBR type `0xEF`, FAT16 by cluster count, the type old firmware reads best),
so a stick written with `dd` or `write.sh` boots on strict firmware as well as on lenient.
- **The quiet boot hides a slow one.** With nothing printing between the - **The quiet boot hides a slow one.** With nothing printing between the
loader and the greeting, a boot that stalls for ten seconds looks the same as loader and the greeting, a boot that stalls for ten seconds looks the same as
one that does not. F8 is the answer and it has to be pressed *before* the one that does not. F8 is the answer and it has to be pressed *before* the
@ -621,9 +714,11 @@ in the one process that runs first.
with its partitions, but nothing mounts it. See [usb.md](usb.md). with its partitions, but nothing mounts it. See [usb.md](usb.md).
- **NVMe and AHCI poll for completion**, one command at a time per disk. That - **NVMe and AHCI poll for completion**, one command at a time per disk. That
is enough to boot and run from them, not to be fast. is enough to boot and run from them, not to be fast.
- **`make ramroot` is the only way to boot with no data disk.** It links a - **The live ISO is a ramdisk root.** `make iso` builds the root as a SchFS image,
filesystem image into the kernel binary, which costs boot time and carries a compresses it ([scff.md](scff.md)) and links it into the kernel; nothing is written to
licensing constraint — see [../licensing.md](../licensing.md) §4. the machine's disks, and everything written to the root is gone at reboot. There is
no installer, and no kernel that finds a root on a disk by SchFS's GPT type as its
*default*. `make ramroot` is the same without compression.
- **The 32-bit multiboot entry is dead code.** It is left in place deliberately: - **The 32-bit multiboot entry is dead code.** It is left in place deliberately:
deleting kernel entry code is its own piece of work with its own way of deleting kernel entry code is its own piece of work with its own way of
failing silently. failing silently.

View file

@ -29,10 +29,10 @@ asked for one. That is the right default and it has one bad consequence —
to enable it.** A laptop with no serial port that stops before it can draw has to enable it.** A laptop with no serial port that stops before it can draw has
nowhere to say why, and `schinitd enable` needs a prompt that does not exist. nowhere to say why, and `schinitd enable` needs a prompt that does not exist.
So a build made with `make DEBUG=1` has a sixth entry in the F8 menu: So a build made with `make DEBUG=1` has one more entry at the end of the F8 menu:
``` ```
> 6. Remote debugging: off > Remote debugging: off
``` ```
Picking it toggles the entry, booting carries the choice through Picking it toggles the entry, booting carries the choice through
@ -179,6 +179,10 @@ part of the boot is blank for the part that is hardest to debug any other way.
--- ---
A panic is not the end of the log. GMMOS ([graphics.md](graphics.md)) keeps running
under the report, can show this log on screen or as QR codes a phone can read, and
has a small terminal for looking around a machine whose kernel is dead.
## Asking a question ## Asking a question
`/dev/dbg` is a conversation: write a line, read the answer. `/dev/dbg` is a conversation: write a line, read the answer.
@ -493,10 +497,16 @@ something that reports nonsense.
- **One conversation at a time.** The answer buffer is global, so two programs - **One conversation at a time.** The answer buffer is global, so two programs
asking at once get one answer each in some order — never half of the other's, asking at once get one answer each in some order — never half of the other's,
but not two independent sessions either. but not two independent sessions either.
- **No userspace symbolization.** The kernel names kernel symbols. A fault in a - **No userspace symbolization.** The kernel names kernel symbols. A process
process still reports a bare address, and resolving it is a host-side job. killed by a fault is told which *file and offset* it died in (from
- **The stack walk stops where the frame pointer stops.** `getcallerpcs()` walks `/proc/<pid>/maps`' records), which is enough to resolve with the file's own
`rbp` chains, so a trap frame in the middle of a chain ends the walk. symbols on the host; a bare address is all there is for anonymous memory.
- **The stack walk stops where the frame pointer stops, and it will not follow a
bad one.** `getstackpcs()` walks `rbp` chains, and each link must go up the same
stack by less than `KSTACKSIZE`: a chain can end at the syscall entry with a *user*
`rbp` that a program uses as an ordinary register (`-1024` in one game), and
dereferencing that under the syscall tracer once panicked the kernel inside
`acquire()`. A trap frame in the middle of a chain ends the walk.
- **`sysrq g` costs you log history.** Its output goes through `cprintf`, so - **`sysrq g` costs you log history.** Its output goes through `cprintf`, so
sixteen kilobytes of views push the same amount out of a 128 KB ring. sixteen kilobytes of views push the same amount out of a 128 KB ring.
- **No tracing.** No static tracepoints, no event ring, no counters. "What - **No tracing.** No static tracepoints, no event ring, no counters. "What

View file

@ -0,0 +1,79 @@
# The desktop: Xorg on the framebuffer
The desktop is X11, on the framebuffer: Alpine's `xorg-server` with the `fbdev` video
driver and the `evdev` input driver, Window Maker as the window manager, and `xterm`.
`startx` runs `wmaker`. Nothing here is written by SchOS except the kernel interfaces it
stands on and four small files in `tools/posix/mkaroot.sh`.
**Xorg is the desktop for now, not for ever.** SchOS will not write its own compositor (the one it had, Schcom, was deleted).
What comes next is whichever of XLibre (the Xorg fork), Xorg or a Wayland compositor from Alpine suits the machine; the groundwork
Wayland needs — udev, `/sys/devices`, libinput, a DRM device — is in [../plan/roadmap.md](../plan/roadmap.md) §5.
## Why X and not Wayland
Every Wayland compositor worth running — labwc, and everything built on wlroots — needs
DRM/KMS, libinput, udev and libseat. DRM/KMS is the largest of them by far: a kernel graphics
subsystem, and in practice a great deal taken from Linux. Xorg runs on a plain framebuffer
through `fbdev`, which SchOS already had. That is the shorter road to a desktop by a long way,
and it brings the window managers with it. udev, libinput and libseat are now in the image
([udev.md](udev.md)); DRM/KMS is the part still missing, and it is the GPU plan
([../plan/gpu.md](../plan/gpu.md)).
## What it stands on
| | |
|---|---|
| **fbdev** | `/dev/fb0` speaks Linux's fbdev ([graphics.md](graphics.md#devfb0-and-how-xorg-draws)) |
| **evdev** | `/dev/input/event0` (every keyboard) and `event1` (every mouse and touchpad) with Linux's queries, LED writes and key/button/relative-axis bits |
| **VTs** | `/dev/tty0`, `VT_SETMODE(VT_PROCESS)`, `KD_GRAPHICS`, the release/acquire handshake ([terminals.md](terminals.md)) |
| **dlopen** | Xorg loads its drivers as modules, so `exec` does dynamic linking ([kernel.md](kernel.md)) |
| **sysfs** | `/sys/bus/pci/devices/*/{boot_vga,enable,graphics/fb0}`, which libpciaccess reads |
| **ptys** | `xterm` runs its shell on a pseudo-terminal |
| **Unix sockets** | the X11 socket, abstract names, `SO_PEERCRED`, `flock` |
| **headers** | `<linux/*.h>` in the build sysroot, as `ports/linux-headers` |
`cmd/xready` checks every one of the kernel calls Xorg makes before it draws anything — the
evdev queries, the fbdev calls, the console calls, and the `/sys` files — one at a time, on a
terminal of its own, and puts everything back. The calls were written against what the
programs are documented to do; each step past it found one that was not, which is what running
the real program is for.
## The four files
- **`/root/.xserverrc`** starts `X` on the VT `startx` was started from (startx works that
out only when `uname -s` says Linux, and SchOS's says SchIFT). It used to add **`-dumbSched`**,
Xorg's switch for reading input on the main loop, because Xorg normally reads input on a
thread of its own and, when SchOS had no threads, the server crashed the first time a client
disconnected, in `pthread_join` on a thread that was never made. Threads exist now and the switch
is gone: under QEMU, X starts with Window Maker, `xclock` and `xeyes` connect and disconnect, an
`xterm` runs and exits, and the server stays up.
- **`/root/.xinitrc`** is `exec wmaker`. Without it xinit falls back to twm and an xterm, which
are not in the image.
- **`/etc/X11/xorg.conf.d/10-schos.conf`** holds what the defaults do not give: Ctrl+Alt+Backspace
enabled, the keyboard options, and the framebuffer driver. Input devices are *found*, not
named: Xorg asks libudev, which reads what `udevd` wrote to `/run/udev` ([udev.md](udev.md)).
- **`/etc/asound.conf`** is for sound, not X ([audio.md](audio.md)).
## Using it
Twelve virtual terminals, switched with Ctrl+Alt+F1–F12 — on the console and under X
alike, because X owns the keyboard while it runs and switches only with that chord. `startx` on
a terminal runs the desktop there; Ctrl+Alt+Backspace ends the session and gives the terminal
back. Window Maker, `xterm`, `xeyes`, `xclock`, `xcalc`, DejaVu fonts; SDL 2 games
(`lbreakouthd`) and NetSurf, a GTK 3 browser, run in it.
Touchpads that sit on an I2C bus (most laptops) are driven by `kernel/acpi/i2c_hid.c`, found
through ACPI, and reach X as a mouse: pointer, click and tap.
## What it does not do
- **No acceleration.** Software rendering to the framebuffer: no DRM, no glamor, no DRI, no
OpenGL beyond what software gives. GPU drivers are a plan ([../plan/gpu.md](../plan/gpu.md)).
- **udev covers the kernel's own devices only.** Input, terminals, memory devices, the
framebuffer and disks are announced; PCI and USB devices are in `/sys/bus/*/devices` but are
not yet in the `/sys/devices` tree or on the uevent socket ([udev.md](udev.md)).
- **Touchpads are mice**: no two-finger scroll, no gestures, no palm rejection.
- **No resolution change from inside X.** The mode is the console's; `schmode` changes the
`vgpud` head only, and the loader's menu is where a real panel's mode is chosen.
- **Runs as an ordinary user** where there is one: `startx` works by group permission alone
([users.md](users.md)); the image has only root; X itself is still a single seat with no logind or seatd.

View file

@ -4,7 +4,61 @@ How hardware gets found, claimed and used, and what the rest of the kernel sees
of it. of it.
Writing a driver is [../guides/adding-a-driver.md](../guides/adding-a-driver.md). Writing a driver is [../guides/adding-a-driver.md](../guides/adding-a-driver.md).
Moving drivers out of the kernel entirely is [../plan/hybrid.md](../plan/hybrid.md). Drivers that live in a process rather than in the kernel are described below
(*Drivers that are not in the kernel*), and the foreign ones in [ldls.md](ldls.md).
---
## Which drivers are compiled in
`config/drivers.conf` lists the drivers that can be left out of a kernel, `y` (compiled in and offered every device it can
drive) or `n` (not linked at all), all `y` unless a line says otherwise — the one exception is the native `r8169`, `n`
because Linux's runs in its place under LDLS. Override one for a build with `make DRV_E1000=n DRV_NVME=n`. A changed
choice rebuilds only `registry.o` and the link. The list is short on purpose: a driver is optional only if nothing else in
the kernel calls it (display: `vga_pci`, `virtio_gpu`; network: `e1000`, `rtl8139`, `r8169`; storage: `ahci`, `nvme`;
audio: `ac97`). `vga_fb`, the USB stack and the legacy PS/2, IDE and UART devices are always there, because the console, the
keyboard and the root disk can depend on them. A kernel with every optional driver off builds and links; whether such a
kernel is useful on any particular machine is the person who turned them off to say.
## What stays in the kernel, and what may leave
"Hybrid" is used to mean everything from "a monolith with a nice device model" to
"a microkernel that gave up", so the line is worth drawing exactly.
**Stays in, permanently:** scheduling, address spaces, the trap and syscall path;
IPC and shared memory; the VFS and the **root** filesystem; IRQ demultiplexing; and
the console and PSOGM. That last one is not a size compromise but an invariant: the
crash screen must work when everything else is dead, so it cannot depend on anything
that could *be* dead. A crash screen served by a userspace process is absent in
exactly the case it exists for.
**May leave, and should:** GPU drivers, non-root filesystems, NICs after the first,
and anything written by someone else.
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
> replaceable, or plausibly written by someone else goes out.**
>
> **The kernel keeps a minimal driver for everything on the path to a shell, and
> userspace owns the good one.** Break that and the compositor cannot start because
> the display driver is a process that needs a compositor.
### USB stays in the kernel, and why
It was on the list to leave, and the premise was wrong. USB is not a peripheral; it
is the bus peripherals arrive on, structurally the same kind of thing as PCI, and
nobody has proposed evicting PCI. The keyboard and the root disk that arrive over it
are on the path to a shell, so the stack that finds them is part of that path. Even
with a minimal in-kernel fallback written, the good driver and the minimal one would
be the same driver, and an eviction that leaves an identical copy behind has moved
nothing. A second, quieter difference: `/dev/pci` hands over one PCI *function*, one
owner and nothing shared, while a USB device sits behind a controller other devices
also sit behind — "the xHCI controller" includes the keyboard. What the eviction was
for, third-party drivers living outside the kernel, survives and belongs to
[ldls.md](ldls.md).
The root filesystem stays in the kernel for the same reason (SchFS cannot be
restarted, and a bug in it is fatal), and so does the first NIC, because the network
thread and lwIP are in the kernel.
--- ---
@ -148,7 +202,7 @@ bitmap, and loses them when it releases or exits. Granting it is not optional
a driver's `outl` is an instruction compiled into it, not a call anything can a driver's `outl` is an instruction compiled into it, not a call anything can
intercept — and granting it *coarsely* is not acceptable, because `IOPL=3` is intercept — and granting it *coarsely* is not acceptable, because `IOPL=3` is
one flag that hands over the interrupt controller and the disk along with the one flag that hands over the interrupt controller and the disk along with the
NIC. See [../../include/ioperm.h](../../include/ioperm.h). NIC. See [../../include/arch/ioperm.h](../../include/arch/ioperm.h).
### Handing a device over ### Handing a device over
@ -184,8 +238,47 @@ client — and that is worth a sentence here because it is the thing the device
model makes possible rather than the thing it does. A modeset is five commands model makes possible rather than the thing it does. A modeset is five commands
to the device. In the kernel those five commands are also a reason to be to the device. In the kernel those five commands are also a reason to be
nervous, because a wrong one takes the machine; in a process the worst case is nervous, because a wrong one takes the machine; in a process the worst case is
that `schinit` starts another one. See that `schinit` starts another one. That is why the in-kernel driver never grew
[../plan/hybrid.md](../plan/hybrid.md), "A modeset, from a process". one — not difficulty, exposure.
```
# schmode
1280x800 current
1280x800 maximum, framebuffer 4000 KB
# schmode 1024 768
1024x768 ok
```
The scanout really resizes, and nothing in the kernel changed for it: `schmode`
writes a line to a socket and `vgpud` sends five commands to the device. The
framebuffer is allocated **once, at the largest mode**, and every smaller mode
reuses it, because `PCIIOC_DMA` has no free — a driver's blocks come back when it
exits and not before — so a modeset that allocated would spend the arena and the
failure would look like a GPU bug rather than like counting. A larger mode is
refused, and says why. The control socket speaks lines of text and has no buffer
submission, damage or vblank, which a compositor would need and nothing has asked
for.
### A panic has to reach the screen
Handing a device away costs something that was not on the list. The kernel panics,
paints the report into the console framebuffer, and halts — and the display it gave
to a process keeps showing whatever that process last put there. On a machine with
two heads, one says the kernel is dead and the other shows a shell prompt. There is
no scheduling after a panic, so the process holding the device is never going to run
again, and the kernel let go of the device deliberately.
So the driver says in advance where it is scanning out, with `PCIIOC_PANICFB`
(`include/dev/pciuser.h`), and the kernel: (1) paints the report into that memory;
(2) asks the *kernel* driver the device was taken from to put it on screen —
`panic_present` in `struct pci_driver`; (3) which resets the device and rebuilds one
queue from **static** memory — no allocation, no locks, no sleeping, because a
corrupted heap is one of the ways a machine gets here. The registration is checked
like the promise it is: the range must lie inside a block that process got from
`PCIIOC_DMA` on that device, or it would be a way to nominate any physical address
to be overwritten at panic. It costs 20 KB of static memory in the virtio-gpu driver,
paid whether or not the machine ever panics; allocating it on demand would be
cheaper and would not work.
### Taking it back ### Taking it back
@ -222,8 +315,9 @@ enforces it.
Coming back is a job, not a kernel feature. `etc/schinit/vgpud.job` carries Coming back is a job, not a kernel feature. `etc/schinit/vgpud.job` carries
`KeepAlive = yes`, so killing the driver is the entire test: `schinitd` starts `KeepAlive = yes`, so killing the driver is the entire test: `schinitd` starts
another one, it claims the device, and the pid of whichever process drew the another one, it claims the device, and the pid of whichever process drew the
frame is legible in the frame itself. See frame is legible in the frame itself: read off the scanout across four generations of
[../plan/hybrid.md](../plan/hybrid.md), "The driver came back". the driver it said 4, 8, 10, 12, and the DMA arena handed back the same addresses
every time.
### What this is not ### What this is not
@ -231,8 +325,7 @@ frame is legible in the frame itself. See
buffer can point the device at any physical address. This isolates a driver buffer can point the device at any physical address. This isolates a driver
against its own *bugs* and not against its own *intent* — a crashed driver that against its own *bugs* and not against its own *intent* — a crashed driver that
restarts instead of taking the machine down is the entire benefit, and it is restarts instead of taking the machine down is the entire benefit, and it is
worth having on those terms. [../plan/hybrid.md](../plan/hybrid.md) says so at worth having on those terms. VT-d/AMD-Vi is its own project.
length.
**One device per process.** A driver drives one thing; if you need two, run two. **One device per process.** A driver drives one thing; if you need two, run two.
@ -240,29 +333,22 @@ length.
## Linux drivers ## Linux drivers
**Decided 2026-08-08.** Linux drivers are not shipped with SchOS and never [ldls.md](ldls.md) is how SchOS uses them: a Linux `.ko` runs in a process and
will be. The user may install one on their own machine, through a device reaches the hardware through `/dev/pci`. The licensing position, stated exactly
manager or equivalent, and SchOS will load it. because it is easy to get wrong in both directions:
The licensing reasoning matters and is worth stating exactly, because it is - **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so shipping a
easy to get wrong in both directions: GPL-2.0-only driver in an image is *lawful* — see [../licensing.md](../licensing.md) §6 —
and the owner has decided it is wanted (code and firmware blobs). The ISO ships Linux's
r8169 set. The repository holds no binary: `.gitignore` refuses `*.ko`, and
`make ldls-r8169` stages the modules from a Linux build, where the matching BTF is.
- **Implementing a loader is not copying.** Writing support for a module format and
a driver ABI from published documentation produces our own work; the layouts come
from the kernel's BTF at load time on the user's machine, so the source holds none
of Linux's.
- **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so The loader is ours and GPL-2.0-or-later; what it loads is Linux's, unmodified, with its own
shipping a GPL-2.0-only driver in an image is now *possible* — see licence and source recorded in `NOTICE`.
[../licensing.md](../licensing.md) §6 — but LDLS still does not, by policy. A
user obtaining a driver themselves and loading it on their own machine is
their act, not ours, and is not a distribution by us.
- **Implementing a loader is not copying.** Writing support for a module format
and a driver ABI from published documentation produces our own work. It is a
clean-room implementation, and it stays one only if it is written from
documentation rather than transcribed from Linux source. Reading Linux to
understand *hardware* is fine; translating its code is not, whatever the
identifiers say.
So the rule for this subsystem is the same as everywhere else in the tree: the
loader is ours and GPL-2.0-or-later, what it loads is the user's business, and
no third-party module enters an image we build. `RAMDISK_EXCLUDE` and `NOTPORTS` are the
existing mechanisms for keeping that true mechanically rather than by intention.
--- ---
@ -371,12 +457,13 @@ the console's minors to the console and the rest to their own functions. But
- **No hot-plug.** Devices are registered at boot and never removed; the - **No hot-plug.** Devices are registered at boot and never removed; the
`in_use` flag is the hook a hot-unplug path would need, and nothing sets it `in_use` flag is the hook a hot-unplug path would need, and nothing sets it
for that purpose. for that purpose.
- **`NDEVICE` is 32**, compile-time, like every other table in the kernel. - **`NDEVICE` is 128**, compile-time, like every other table in the kernel.
- **In-kernel drivers are fixed at link time.** There is no module loader, and - **In-kernel drivers are fixed at link time.** There is no module loader, and
nothing can be added to the kernel after the build. A driver *outside* the nothing can be added to the kernel after the build. A driver *outside* the
kernel is an ordinary program and can be started whenever — but it reaches its kernel is an ordinary program and can be started whenever — it reaches its
device through `/dev/pci` and publishes nothing to the class vtables, so the device through `/dev/pci`, and a NIC driver can publish itself to the network
console cannot be handed to one yet. class through `/dev/netdev` (LDLS does); a display driver publishes nothing to
`display_ops`, so the console cannot be handed to one.
- **The kernel does not rebind its own driver.** `PCIIOC_RELEASE`, or the process - **The kernel does not rebind its own driver.** `PCIIOC_RELEASE`, or the process
exiting, leaves the interrupt line masked and the in-kernel driver unbound — exiting, leaves the interrupt line masked and the in-kernel driver unbound —
the console stays wherever the release put it. A *supervisor* can start a the console stays wherever the release put it. A *supervisor* can start a

View file

@ -199,11 +199,14 @@ provides — reads and writes never reach it, because `fileread`/`filewrite` see
It is read-only: `dirlink` and `ialloc` answer `EROFS` rather than pretending. It is read-only: `dirlink` and `ialloc` answer `EROFS` rather than pretending.
A node exists because the kernel has the device, or it does not exist. A node exists because the kernel has the device, or it does not exist.
### What is there ### What is there in `/dev`
`console`, `null`, `keyboard`, `zero`, `full`, `random`, `urandom`, `tty`, `console`, `null`, `zero`, `full`, `random`, `urandom`, `tty`, `tty0` and
`stdin`, `stdout`, `stderr`, `fb0`, `mouse`, `hda`, `hdb`, and `input/event0` `tty1`..`tty12`, `stdin`, `stdout`, `stderr`, `keyboard`, `mouse`, `fb0`, `dsp`,
and `input/event1` in one subdirectory. `ptmx` and a `pts/` directory of pseudo-terminal slaves, `hda`, `hdb`,
`input/event0`.., and the SchOS-specific `pci` (a device's registers, DMA and
interrupts, for a driver in a process), `netdev` (a network interface for a
process), `dbg` and `dbgro` (the kernel debugger's views).
Every disk and partition the block layer registered is there too, under the Every disk and partition the block layer registered is there too, under the
name it was given (`sata0`, `sata0p2`, `nvme0p1`, `usb0p1`): major 8, minor its name it was given (`sata0`, `sata0p2`, `nvme0p1`, `usb0p1`): major 8, minor its
@ -218,11 +221,18 @@ nothing behind it fails with `ENXIO`, which is a better answer than a missing
path — and with no node at all, `echo x > /dev/hdb` silently creates an ordinary path — and with no node at all, `echo x > /dev/hdb` silently creates an ordinary
file of that name and appears to work. file of that name and appears to work.
### What is there ### What is there in `/proc`
Per-process directories, plus the global files: `meminfo`, `uptime`, `cpuinfo`, `/proc/self`, and one directory per process holding `stat`, `status`, `cmdline`,
`version`, `stat`, `loadavg`, `mounts`, `filesystems`, `schfs`, `pci`, `statm`, `comm`, `schedstat`, `maps`, `exe` (a symlink), `fd/` (a symlink per open
`interrupts`, `devices`, `netif`, and `sysrq`. descriptor) and `task/` (one directory per thread). `maps` lists the file
mappings, and a process killed by a fault is told which file and offset it died
in.
The global files: `meminfo`, `uptime`, `cpuinfo`, `version`, `stat`, `loadavg`,
`mounts`, `filesystems`, `partitions`, `schfs`, `pci`, `interrupts`, `devices`,
`netif`, `resolv.conf`, `slab`, `fsstat`, `kmsg`, `strace`, `bootmode`,
`bootdebug`, `quiet`, `verbose`, `schinit` and `sysrq`.
**/proc is read-only except `sysrq`**, which is the one deliberate write target, **/proc is read-only except `sysrq`**, which is the one deliberate write target,
and the code says so in one place. Marking every procfs inode read-only once and the code says so in one place. Marking every procfs inode read-only once
@ -242,13 +252,15 @@ mounted by init with `mount("sysfs", "/sys", "sysfs", 0, 0)`.
| path | contents | | path | contents |
|------|----------| |------|----------|
| `/sys/bus/pci/devices/0000:BB:DD.F/` | `vendor device class revision subsystem_vendor subsystem_device irq resource config uevent` | | `/sys/devices/pci0000:00/0000:BB:DD.F/` (and `/sys/bus/pci/devices/` links to it) | `vendor device class revision subsystem_vendor subsystem_device irq resource config uevent subsystem driver` |
| `/sys/bus/usb/devices/B-P[.P...]/` | `idVendor idProduct bcdDevice busnum devnum speed bDeviceClass bDeviceSubClass bDeviceProtocol bNumConfigurations uevent` | | `/sys/devices/pci0000:00/<xHCI>/usbB/B-P[.P...]/` (and `/sys/bus/usb/devices/` links) | `idVendor idProduct bcdDevice busnum devnum speed bDeviceClass bDeviceSubClass bDeviceProtocol bNumConfigurations uevent subsystem driver` |
| `/sys/firmware/acpi/tables/` | each ACPI table, raw; a repeated signature is numbered from 1 (`SSDT1`, `SSDT2`) | | `/sys/firmware/acpi/tables/` | each ACPI table, raw; a repeated signature is numbered from 1 (`SSDT1`, `SSDT2`) |
| `/sys/firmware/dmi/tables/` | `smbios_entry_point` and `DMI`, raw | | `/sys/firmware/dmi/tables/` | `smbios_entry_point` and `DMI`, raw |
| `/sys/firmware/memmap/N/` | `start end type`, the firmware's map as the loader handed it over | | `/sys/firmware/memmap/N/` | `start end type`, the firmware's map as the loader handed it over |
| `/sys/firmware/efi/` | present when UEFI booted the kernel; empty | | `/sys/firmware/efi/` | present when UEFI booted the kernel; empty |
| `/sys/class/dmi/id/` | `sys_vendor product_name product_version board_name bios_version`, and the rest Linux has, except serials and the UUID | | `/sys/class/dmi/id/` | `sys_vendor product_name product_version board_name bios_version`, and the rest Linux has, except serials and the UUID |
| `/sys/class/power_supply/` | `BAT0` (`capacity`, `status`, `energy_*`/`charge_*`, `voltage_now`, `cycle_count`, ...) and `AC` (`online`), from ACPI — the battery and the adapter are read through the laptop's embedded controller |
| `/sys/bus/pci/devices/…/` | also `boot_vga`, `enable` and `graphics/fb0`, which libpciaccess (and so Xorg) reads |
**Linux's layout because everything that reads hardware reads it from there.** **Linux's layout because everything that reads hardware reads it from there.**
toybox's `lspci` and `lsusb` walk `/sys/bus/*/devices` and parse each `uevent` toybox's `lspci` and `lsusb` walk `/sys/bus/*/devices` and parse each `uevent`
@ -263,9 +275,18 @@ at the moment it is read, so a stick shows up in `/sys/bus/usb/devices` as soon
as it is attached. Text files report size 0, as `/proc`'s do; binary ones (the as it is attached. Text files report size 0, as `/proc`'s do; binary ones (the
tables, `config`) report their real length, since tools copy them by it. tables, `config`) report their real length, since tools copy them by it.
What is missing, compared with Linux: `/sys/devices` and its symlinks (the The kernel's own devices (input, tty, mem, graphics, block) have the Linux tree:
`bus/*/devices` entries are directories rather than links into it), `/sys/devices/virtual/<class>/<name>` with `dev`, `uevent` and a `subsystem`
`/sys/class` beyond `dmi`, `/sys/block`, and anything writable. PCI `config` is link, `/sys/class/<class>/<name>` and `/sys/dev/{char,block}/MAJ:MIN` as symlinks
into it, and input capabilities; `uevent` is writable, and a write is announced
on the uevent netlink socket ([udev.md](udev.md)).
PCI and USB devices are in that tree too, under `/sys/devices/pci0000:00`, with
the `bus/*/devices` entries as links into it and `add`/`remove` events for USB
hot-plug ([udev.md](udev.md)).
What is missing, compared with Linux: PCI bridges as parents, USB interfaces,
`/sys/class` entries for most devices, `/sys/block`, and most of what is writable. PCI `config` is
256 bytes, because config space is read through port `0xCF8`, which reaches 256 bytes, because config space is read through port `0xCF8`, which reaches
no further. no further.
@ -376,8 +397,6 @@ through.
- **No page cache.** `mmap` of a file reads the whole thing in at map time, and - **No page cache.** `mmap` of a file reads the whole thing in at map time, and
writable `MAP_SHARED` of a file is refused rather than faked. This is the writable `MAP_SHARED` of a file is refused rather than faked. This is the
single largest gap in the layer. single largest gap in the layer.
- **No symlinks in procfs**, which is why `/proc/<pid>/exe` is a regular file
and `ps -o comm` prints `[init]`.
- **`rename()` does not move directories**, and will not replace one. `EISDIR` - **`rename()` does not move directories**, and will not replace one. `EISDIR`
either way. either way.
- **No named pipes.** `mkfifo()` fails with `EPERM`, Linux's answer for a - **No named pipes.** `mkfifo()` fails with `EPERM`, Linux's answer for a
@ -385,19 +404,13 @@ through.
device, because `mknod()` read a Linux program's `(mode, dev)` as the native device, because `mknod()` read a Linux program's `(mode, dev)` as the native
`(major, minor)`: `ls -l` showed `crw-rw-rw- 438, 0` where a `p` belongs. The `(major, minor)`: `ls -l` showed `crw-rw-rw- 438, 0` where a `p` belongs. The
zsh port was configured with `zsh_cv_sys_fifo=yes` on the strength of that. zsh port was configured with `zsh_cv_sys_fifo=yes` on the strength of that.
- **No `/dev/fd`, and no `/proc/self`.** A process cannot reach its own open - **No `/sys/devices` and no uevents**, so nothing can be told that a device
descriptors by path. `<(cmd)` needs one or the other — zsh now knows it has appeared; Xorg is given its input devices by a config file instead.
neither and says so.
- **No `AF_UNIX` socket inodes.** Socket names live in a table in
`kernel/net/unix.c`, so `bind()` creates nothing that can be `stat()`ed or
`unlink()`ed.
- **No unified inode cache across filesystems** beyond the single `itable`. - **No unified inode cache across filesystems** beyond the single `itable`.
- **A known, un-root-caused hang:** heavy `ls -lR` use across many commands in
one long shell session can eventually make a *later*, unrelated directory
hang; the identical command succeeds on a fresh boot. It is not inode or file
table exhaustion — both fail cleanly and neither fired. It smells like a lock
or refcount leak in the `*at()` paths or in toybox's own fd handling. The next
step is instrumenting `itable`/`ftable` occupancy across a long session to see
what is climbing.
- **`sys_delete`/`sys_show`/`sys_showstat`** are orphaned entries from the old - **`sys_delete`/`sys_show`/`sys_showstat`** are orphaned entries from the old
v1 filesystem's recycle bin. Nothing reaches them and they die with `fs.c`. v1 filesystem's recycle bin. Nothing reaches them and they die with `fs.c`.
- **One long-standing hang was never found.** Heavy `ls -lR` use across many
commands in one long shell session once made a *later*, unrelated directory
hang; the identical command succeeded on a fresh boot. It was instrumented and
hunted and could not be reproduced, which is recorded in
[../history/kernel.md](../history/kernel.md) rather than forgotten.

View file

@ -1,8 +1,8 @@
# Graphics, text and the console # Graphics, text and the console
Four things share this area and they are deliberately separate: the kernel's own Five things share this area and they are deliberately separate: the kernel's own
drawing surface, the compositor's display interface, the text-rendering layer, drawing surface, the `display` class a display server could use, the text-rendering
and the crash screen. layer, the crash screen — and `/dev/fb0`, which is how Xorg reaches the screen.
--- ---
@ -11,13 +11,13 @@ and the crash screen.
This distinction causes confusion every time somebody meets it, so it is the This distinction causes confusion every time somebody meets it, so it is the
first thing here. first thing here.
| | [`gfx.h`](../../include/gfx.h) | [`display.h`](../../include/display.h) | | | [`gfx.h`](../../include/gfx/gfx.h) | [`display.h`](../../include/gfx/display.h) |
|---|---|---| |---|---|---|
| **What** | The kernel's own drawing surface | The compositor's interface | | **What** | The kernel's own drawing surface | A frame interface for a display server |
| **About** | Pixels and text | *Frames* | | **About** | Pixels and text | *Frames* |
| **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` | | **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` |
| **Buffering** | None. Direct writes to the scanout. | Double-buffered | | **Buffering** | None. Direct writes to the scanout. | Double-buffered |
| **Users** | The console and PSOGM | Schcom | | **Users** | The console, PSOGM, GMMOS | None today — Xorg draws through `/dev/fb0` |
| **Must work when** | **Nothing else in the system does** | Userspace is healthy | | **Must work when** | **Nothing else in the system does** | Userspace is healthy |
That last row is the reason they are not one interface. The console and the That last row is the reason they are not one interface. The console and the
@ -67,7 +67,7 @@ and `vgpud` mirroring it see what is on the screen.
`struct display_ops`, found with `device_find_class(DEVCLASS_DISPLAY)`. `struct display_ops`, found with `device_find_class(DEVCLASS_DISPLAY)`.
`acquire()` allocates the back buffer **on first call, never at boot** — it is `acquire()` allocates the back buffer **on first call, never at boot** — it is
around two megabytes at 800×600, and a machine whose compositor never starts around two megabytes at 800×600, and a machine whose display server never starts
should not pay that. A driver that cannot double-buffer returns 0, and the should not pay that. A driver that cannot double-buffer returns 0, and the
caller draws directly and accepts tearing rather than not drawing at all. caller draws directly and accepts tearing rather than not drawing at all.
`wait_vblank` is optional for the same reason. `wait_vblank` is optional for the same reason.
@ -133,7 +133,7 @@ A machine with both a stdvga and a virtio-gpu has two displays, and `vgpud` —
the out-of-kernel driver, `cmd/vgpud/main.c` — owns the second one. For a long the out-of-kernel driver, `cmd/vgpud/main.c` — owns the second one. For a long
time it drew a test card there: colour bands, gradients, a checkerboard, and time it drew a test card there: colour bands, gradients, a checkerboard, and
its own pid in binary along the bottom. That was scaffolding for its own pid in binary along the bottom. That was scaffolding for
[../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3, where a screenshot had to the out-of-kernel driver work ([devices.md](devices.md)), where a screenshot had to
answer *did a userspace process put this here* and *did the driver come back*. answer *did a userspace process put this here* and *did the driver come back*.
Both of those are closed, so the test card became the Shift mark. Which was Both of those are closed, so the test card became the Shift mark. Which was
@ -223,9 +223,8 @@ shows the top-left corner, which is where a terminal puts everything worth
seeing. Scaling is the alternative and it is the wrong one: text resampled to a seeing. Scaling is the alternative and it is the wrong one: text resampled to a
non-integer ratio is text you cannot read. non-integer ratio is text you cannot read.
**This is not the endpoint.** A compositor (Gate 3) will want this head for A display server that wants the head for itself can have it; until one does, the
itself, and when there is one, the mirror goes away. Until then the honest honest thing for a working display to show is the system running on it.
thing for a working display to show is the system running on it.
### The F8 resolution is the head's resolution ### The F8 resolution is the head's resolution
@ -263,9 +262,26 @@ not apply: two `vgpud`s cannot coexist, because the first holds the PCI claim
and the second is refused the device long before it reaches `bind()`. The claim and the second is refused the device long before it reaches `bind()`. The claim
is the mutual exclusion; the socket node never was. is the mutual exclusion; the socket node never was.
## /dev/fb0, and how Xorg draws
Xorg runs on the framebuffer through its `fbdev` driver, so `/dev/fb0` speaks Linux's
fbdev: `FBIOGET_FSCREENINFO`, `FBIOGET_VSCREENINFO`, `FBIOPUT_VSCREENINFO` (accepted
when it asks for the mode already in force), `FBIOPAN_DISPLAY`, `FBIOBLANK`,
`FBIOPUTCMAP`, and `mmap` of the physical framebuffer. `FBIOGET_VSCREENINFO` reports
nominal timings, as Linux's `efifb` does, instead of zeros — a zero pixel clock made
fbdev compute a 0 Hz RandR mode. Pixel layout comes from the same table the kernel
draws with, so a colour a program builds is the colour the kernel would.
Once a program maps the framebuffer the kernel has no way to know when it draws, so
`fb_mapped()` marks the screen as no longer the text console's and `fbclose()` puts it
back. A terminal switched to `KD_GRAPHICS` is not repainted ([terminals.md](terminals.md)),
and the `VT_PROCESS` handshake tells the server before it loses the screen. `cmd/xready`
checks every one of these calls before Xorg is started; the picture itself is the proof
beyond that.
## Text ## Text
[`include/gfx/text.h`](../../include/text.h) + `kernel/dev/text.c`. [`include/gfx/text.h`](../../include/gfx/text.h) + `kernel/dev/text.c`.
**The font was never the problem — the character-cell grid was.** The console **The font was never the problem — the character-cell grid was.** The console
draws text by indexing `font_data[256][16]` and stepping the cursor eight draws text by indexing `font_data[256][16]` and stepping the cursor eight
@ -537,24 +553,63 @@ channel that works when the machine will not come back at all.
machine whose network stack is dead**, and a phone camera is an out-of-band machine whose network stack is dead**, and a phone camera is an out-of-band
channel needing no driver on our side. channel needing no driver on our side.
It encodes the **report itself**, not a URL — it reaches nothing. Deliberately It encodes the **report itself**, not a URL — it reaches nothing. Byte mode only
small in three ways: byte mode only, EC level L only (the screen is a clean, and EC level L only (the screen is a clean, evenly lit, high-contrast source; the
evenly lit, high-contrast source; the damage QR's stronger levels protect damage QR's stronger levels protect against does not apply), but **every version,
against does not apply), and versions 1–6 only, stopping short of version 7's 1 to 40**: version 40 at level L holds 2,953 bytes, which is what lets GMMOS put the
extra version-information blocks and version 10's wider count field. kernel log on screen as a series of codes. Checked against the `segno` library module
for module at every version.
134 bytes is a large crash report. If a payload ever needs more, the honest fix It draws on the report unconditionally rather than on a keypress, which is better:
is to shorten the report. it costs nothing, and requiring input would mean the one case where input is least
trustworthy is also the case where the report is hardest to retrieve. Verified by
`make crashshot && make crashqr`, which decodes the symbol out of the captured
framebuffer.
It draws unconditionally rather than on a keypress, which is better: it costs ### Every processor stops
nothing, and requiring input would mean the one case where input is least
trustworthy is also the case where the report is hardest to retrieve. Verified A panic used to stop only the processor it happened on; the rest carried on with a
by `make crashshot && make crashqr`, which decodes the symbol out of the dead kernel, drew over the report, and kept the fans loud until somebody held the power
captured framebuffer. button. The panicking processor now sends every other one an **NMI**, which arrives even
with interrupts off, and `trap()` halts a processor that takes one while `panic_stop` is
set — first thing, before the canary check, because that stack may be what broke. NMI has
its own per-CPU stack (IST 1): for a few instructions at the top of `syscall_entry` the
live stack is the user's, at ring 0, and a fault pushing the frame there would be a double
fault on the same stack and a reset instead of a report. A second processor that panics
while one is reporting halts.
## GMMOS — the Guru Meditation Micro OS
The report used to be the last thing the kernel did: a picture, blinking keyboard lights,
and a machine that needed its power button. GMMOS keeps the report and goes on living
under it (`kernel/gmmos/`, about 2,000 lines). The real kernel stays dead — nothing of it is
called again that takes a lock, sleeps, allocates or waits for an interrupt. GMMOS has its
own: the framebuffer, a clock read from the TSC, and keyboards it polls itself through
**micro drivers**, cut-down copies of the real ones that do only what a keyboard needs.
| Key | |
|---|---|
| (nothing) | a status line counts down **three minutes** to power-off; any key cancels it |
| **Space** | the last 300 lines of this boot's log as **QR codes**, a page per symbol, numbered so a phone can take them in order; `+` / `-` trade module size for capacity |
| **L** | the log on screen, scrolled with the arrows |
| **T** | a terminal: `why`, `log`, `grep`, `ps`, `cpus`, `pci`, `mem` (page tables walked first, so a bad address cannot fault), `uptime`, `restart`, `poweroff` |
| **R R** / **P P** | restart / power off — twice, so a stray key does neither |
The micro drivers are **PS/2**, **I2C-HID** (the laptop's own keyboard, over a DesignWare
controller — the live driver hands over what it found at boot, because AML cannot run on a
dead kernel) and **xHCI** (USB keyboards: every controller is halted, reset and enumerated
from nothing, root ports and USB 2 hubs up to two deep, a boot-protocol keyboard, no
interrupts, a static 256 KB pool). GMMOS probes all of them and polls every one that answers.
Every buffer is static. A panic inside GMMOS, or a second one in the report, is reported
and then only blinks; GMMOS does not re-enter itself. Checked under QEMU: every screen, two
QR pages read off a screenshot with `zbar` and decoded to the log text exactly, restart
reboots, and left alone it powers off at three minutes. What has been seen on real hardware
is the report and the keyboards the laptops have; the xHCI path is the least exercised.
## One palette ## One palette
[`include/gfx/theme.h`](../../include/theme.h). There were three unrelated designs [`include/gfx/theme.h`](../../include/gfx/theme.h). There were three unrelated designs
and the seams showed: the splash was deep purple with gold text deliberately and the seams showed: the splash was deep purple with gold text deliberately
matching the *panic* screen, the console was cream with near-black text, and so matching the *panic* screen, the console was cream with near-black text, and so
a healthy boot was dressed as a crash and then abruptly became an amber a healthy boot was dressed as a crash and then abruptly became an amber
@ -571,16 +626,20 @@ makes it mean something.
## What it does not do ## What it does not do
- **The console still has its cell grid.** Cells are now a Unicode codepoint - **The console still has its cell grid.** Cells are a Unicode codepoint drawn by the
drawn by the TrueType rasteriser rather than a CP437 byte drawn from a bitmap, TrueType rasteriser, and the cell size follows the font's measured advance (the font
and the cell size follows the font's measured advance — but it is still a scales to about 45 rows, 16 to 40 px per em, on whatever mode the firmware booted
fixed grid, and the text layer's proportional path is unused by it. in) — but it is a fixed grid, and the text layer's proportional path is unused by it.
- **No mode setting.** Resolution is whatever the loader asked for. There is no - **Mode setting is the loader's and `vgpud`'s.** On real hardware resolution is whatever
`/dev/dri`-style device and no way to change it at runtime. the loader chose from its menu — which now asks "Keep this resolution?" and falls back
- **No double buffering in practice.** `display_ops.acquire` is specified and by itself, because AMD's GOP "sets" smaller modes without leaving the panel's native
the compositor needs it; virtio-gpu binds but exposes no modeset API, and timing and the screen showed three or four squashed copies side by side. There is no
`/dev/dri`, no KMS, and Xorg runs at the mode the console is in.
- **No double buffering in practice.** `display_ops.acquire` is specified and nothing
uses it; virtio-gpu exposes a modeset API on `vgpud`'s socket and nothing more, and
`wait_vblank` is unimplemented everywhere. `wait_vblank` is unimplemented everywhere.
- **No hardware acceleration of any kind**, and none is planned before 2D blit. - **No hardware acceleration of any kind**: Xorg draws in software to the framebuffer
(no glamor, no DRI), and GPU drivers are a plan, not a feature ([../plan/gpu.md](../plan/gpu.md)).
- **No CFF/Type 2 charstrings**, so OTF fonts do not render — that is a separate - **No CFF/Type 2 charstrings**, so OTF fonts do not render — that is a separate
`font_ops` provider and a bytecode interpreter, not an extension of the `glyf` `font_ops` provider and a bytecode interpreter, not an extension of the `glyf`
path. TrueType hinting is deliberately out of scope. path. TrueType hinting is deliberately out of scope.
@ -593,6 +652,6 @@ makes it mean something.
- **No composite point-matching.** Components positioned by matching a point in - **No composite point-matching.** Components positioned by matching a point in
the base rather than by an offset are placed at the origin. Fonts that use it the base rather than by an offset are placed at the origin. Fonts that use it
are rare and none in the tree do. are rare and none in the tree do.
- **PSOGM renders and stops.** The interactive crash mode described in - **GMMOS cannot look at a live process by name.** The "interactive crash mode" is built as far
[../plan/roadmap.md](../plan/roadmap.md) — walk the stack, read another as a dead machine allows — log, a terminal, `ps`, `cpus`, `pci`, memory by physical walk — but
process's memory, dump page tables — is designed and not built. the scheduler is gone, so there is no running a debugger against a process.

View file

@ -11,18 +11,27 @@ driver or a protocol stack: memory, processes, scheduling, traps and signals.
One page table per process; the kernel is mapped into all of them at One page table per process; the kernel is mapped into all of them at
`KERNBASE = 0xFFFF800000000000`, so a trap does not switch `CR3`. `KERNBASE = 0xFFFF800000000000`, so a trap does not switch `CR3`.
`kvmalloc()` maps physical 0–4 GB as four 1 GB pages, with the top one marked `kvmalloc()` maps as many 1 GB pages of physical memory as the machine has RAM
uncached — that is where PCI BARs and the framebuffer live. This is why 3 GB is (up to `PHYSTOP_MAX`, 256 GB), always at least the low four. The 3–4 GB slot stays
the most memory the kernel can treat as RAM, and it is a deliberate simplicity: uncached because that is where the PCI hole is; everything above 4 GB is RAM, so
a direct map with a fixed offset makes `P2V`/`V2P` arithmetic rather than a those are cached, and a device BAR up there still gets its own uncached window
lookup. from `ioremap()`, which takes the entries the direct map did not. On a CPU
without 1 GB pages (Intel's desktop parts before Haswell), each gigabyte is a
page directory of 2 MB pages instead — the same reach, one extra page of table
per gigabyte. A direct map with a fixed offset makes `P2V`/`V2P` arithmetic
rather than a lookup.
`kalloc()` hands out one 4 KB page at a time from a free list. **There is no How much RAM there is comes from the firmware's E820/UEFI map, as a *list* of
slab or pool allocator**, and the consequence is visible throughout the tree: usable regions, not one run ending at a top address (OVMF drops an ACPI NVS
every subsystem that needs many small objects has invented its own fixed-size region at 8 MB, which used to hide everything above it). The kernel itself is
table — `NPROC` processes, `NSOCK` sockets, `NDEVICE` devices, `NSHM` segments. linked at 16 MB, clear of what firmware keeps low. `kalloc` holds never-used
That is why the limits are compile-time constants rather than a design decision memory as ranges, so a large machine costs nothing until its memory is used.
about each one.
`kalloc()` hands out one 4 KB page at a time from a free list. Objects smaller
than a page come from the slab caches below; objects larger than one do not have
a home yet, so the biggest tables (`NPROC` processes, `NSOCK` sockets, `NDEVICE`
devices, `NSHM` segments) are still fixed-size arrays, which is why their limits
are compile-time constants rather than a design decision about each one.
The free list carries a canary, checked at the top of both `kalloc()` and The free list carries a canary, checked at the top of both `kalloc()` and
`kfree()`, so a wild write into it panics at the point it is touched rather than `kfree()`, so a wild write into it panics at the point it is touched rather than
@ -150,8 +159,8 @@ pages, reference counted, mapped through a VMA with `no_free` set so that
`memfd_create(2)` is the same machinery reached by **descriptor** instead of by `memfd_create(2)` is the same machinery reached by **descriptor** instead of by
key. That difference is the whole point and the reason it exists on Linux at key. That difference is the whole point and the reason it exists on Linux at
all: two processes share memory by passing the fd over a socket, so neither has all: two processes share memory by passing the fd over a socket, so neither has
to agree on a path or race to invent a key. It is what a Wayland buffer crosses to agree on a path or race to invent a key. It is how a display server and its client
on, and it is why descriptor passing (`SCM_RIGHTS`) had to be built first. share a buffer, and it is why descriptor passing (`SCM_RIGHTS`) had to be built first.
| | | | | |
|---|---| |---|---|
@ -171,10 +180,46 @@ version of something this kernel cannot do:
mapper. Here a freed page would simply stay mapped to memory somebody else mapper. Here a freed page would simply stay mapped to memory somebody else
now owns. now owns.
Writable `MAP_SHARED` of a **real file** is still `ENODEV`. That needs a page Writable `MAP_SHARED` of a **real file** is still refused. That needs a page
cache, it is a separate project, and nothing in 1.0 asks for it — an anonymous cache, it is a separate project, and nothing that runs today asks for it — an
object has no such contract to break, which is exactly why `memfd` is the anonymous object has no such contract to break, which is exactly why `memfd` is
narrow answer. the narrow answer.
### Demand paging
Anonymous memory can be recorded without being backed: the page appears,
zeroed, the first time it is touched — by the program, through `trap()`, or by
the kernel on its behalf, through `uva2ka()`. `mmap` does this for mappings of
1 MB or more, for `MAP_NORESERVE`, and for `PROT_NONE`. The reason is WebKit,
which reserves 4 GB of address space at start for its JIT and heaps and touches
a sliver of it; backing all of it at `mmap` time failed with `ENOMEM` and the
browser aborted. Go, Java and Firefox reserve the same way.
`vm_fault()` takes a spinlock and nothing that sleeps, so `uva2ka()` may call it
from anywhere; the lock keeps two threads of one process from both mapping the
same page. `fork` and `munmap` skip the holes, so a page nobody touched is one
they never see, and a child that touches it reads zeroes — what it would have
read in the parent.
File-backed mappings are **not** lazy: `mmap` of a file still reads the range in
at map time.
### TLB shootdowns
Unmapping from an address space that threads on other processors are running in
interrupts those processors and waits until each has reloaded `CR3`
(`tlb_shootdown()` in `vm.c`; the vector is one of the MSI lines, which `trap()`
already dispatches with an EOI). Without it, when one thread unmapped a range and
`mmap` handed the same addresses straight back, a thread on another CPU went on
reading and writing the *old* pages through its TLB: stores vanished and loads
were stale. musl's allocator noticed first — a group's metadata no longer
pointing back at it — and Epiphany, forty threads on four processors, died at a
different address on every run.
Only one shootdown runs at a time, and it is taken without disabling interrupts:
two processors each waiting with interrupts off for the other to answer would
wait for ever. On a single processor, or if no vector could be allocated, it
does nothing (and says so at boot).
### What the fixed tables actually cost ### What the fixed tables actually cost
@ -184,31 +229,69 @@ partly true:
| | size | table | | | | size | table | |
|---|---|---|---| |---|---|---|---|
| `struct proc` | 11744 | `NPROC` 128 | larger than a page; 1 KB of it is FPU state | | `struct proc` | 1984 | `NPROC` 128 | 254 KB in all; the address space, descriptors and signal handlers live in their own objects now, shared by threads |
| `struct socket` | 4936 | `NSOCK` 128 | larger than a page | | `struct mm` | 8232 | one per process, 128 | 1 MB in all, almost all of it the 256-entry VMA table |
| `struct socket` | about 4900 | `NSOCK` 128 | larger than a page |
| `struct device` | 120 | `NDEVICE` 128 | 15 KB, never hit | | `struct device` | 120 | `NDEVICE` 128 | 15 KB, never hit |
| `struct buf` | 200 | `NBUF` 256 to start | grows from a slab, data from `kalloc`, up to RAM/32 | | `struct buf` | 200 | `NBUF` 256 to start | grows from a slab, data from `kalloc`, up to RAM/32 |
| `struct inode` | 296 | none | grows from a slab; `NINODE` 200 is only how many are made up front | | `struct inode` | about 460 | none | grows from a slab; `NINODE` 200 is only how many are made up front |
| `struct file` | 64 | none | grows from a slab; `NFILE` 512 is only the static pool | | `struct file` | about 88 | none | grows from a slab; `NFILE` 512 is only the static pool |
The two largest cannot go in a page-backed slab at all. Freeing them needs a (Sizes from `nm -S` on a build of the tree, not from a header comment.)
contiguous multi-page allocator, which is a separate thing and is not written.
The buffer header shrank to 200 bytes when its 4 KB of data moved out to a page `struct proc` shrank by a factor of six when threads arrived, because the page
of its own, which is what let the cache grow at all. tables, VMAs, descriptor table and signal dispositions moved out of it into
objects a thread group shares. `struct socket` is still too large for a
page-backed slab; freeing it needs a contiguous multi-page allocator, which is a
separate thing and is not written. The buffer header shrank to 200 bytes when its
4 KB of data moved out to a page of its own, which is what let the cache grow at
all.
## Processes ## Processes
`struct proc` in [`include/core/proc.h`](../../include/proc.h). A fixed table of `struct proc` in [`include/core/proc.h`](../../include/core/proc.h). A fixed table of
`NPROC` entries, allocated by linear scan. `NPROC` entries, allocated by linear scan. A thread is a `struct proc` too.
**The field order at the top of the struct is fixed by assembly.** `trapasm.S` **The field order at the top of the struct is fixed by assembly.** `trapasm.S`
hard-codes `fs_base` at offset `0x18`, and there is a `_Static_assert` in hard-codes `fs_base` at offset `0x18`, and there is a `_Static_assert` in
`proc.c` that fails the build if it moves. Anything new goes at the end. `proc.c` that fails the build if it moves. Anything new goes at the end.
Each process carries: page directory and size, kernel stack, open file table, Each process carries: a kernel stack, cwd inode *and* cwd path string, signal
cwd inode *and* cwd path string, signal handlers and masks, VMA table for masks, POSIX identity (pgid, sid, and all six of real/effective/saved uid and
`mmap`, POSIX identity (pgid, sid, and all six of real/effective/saved uid and gid), the Linux-ABI syscall map, and its x87/SSE registers (saved on a switch;
gid), and the Linux-ABI syscall map. the kernel itself stays off them). Three things it *points to* rather than
owns, because a thread group shares them:
| | |
|---|---|
| `struct mm` | the address space: page tables, heap top, the VMA table, which file each span came from, and a lock so one thread at a time changes the mappings |
| `struct files` | the descriptor table and its `FD_CLOEXEC` flags |
| `struct sighand` | signal dispositions — what `sigaction()` sets |
Each is reference counted. `clone()` shares what its flags say and `fork()`
copies all three.
### Threads
`clone()` takes `pthread_create`'s flags (`CLONE_VM`, `FILES`, `SIGHAND`,
`THREAD`, `SETTLS`, `PARENT_SETTID`, `CHILD_CLEARTID`) and `posix_spawn`'s
(`VM`, `VFORK`, a stack). What follows from that:
- **futex** — `WAIT`, `WAIT_BITSET` with timeouts, `WAKE`, `REQUEUE`,
`CMP_REQUEUE`, keyed by the word's *kernel* address so processes that map the
same page meet. `set_tid_address` keeps the word that exit zeroes and wakes,
which is what `pthread_join` waits on.
- **Exit.** `exit` ends the calling thread; `exit_group`, a fatal signal, or the
leader exiting ends them all. `reapd` frees exited threads, and the leader is
reaped by its parent once the rest are gone. `getpid` is the group, `gettid`
the thread.
- **Signals.** Stops and resumes reach every thread; other process-directed
signals go to the leader.
- **`exec`** kills the other threads and unshares the dispositions.
- **`/proc`** lists processes, `task/` lists threads; `comm`, `Threads:` and
`Tgid` are there.
The first program to need all of this was GTK's, then WebKit's.
### Slot reuse ### Slot reuse
@ -232,12 +315,15 @@ exactly as a syscall would. The network thread is the current user.
## Scheduling ## Scheduling
Round-robin over the process table, one scheduler per CPU, each with its own Fair share by virtual runtime, one scheduler per CPU, each with its own context
context. `swtch()` saves callee-saved registers and switches stacks; `sched()` — the rule, the guards and `nice` are in [scheduler.md](scheduler.md).
returns to the scheduler and asserts four invariants on the way — holding `swtch()` saves callee-saved registers and switches stacks; `sched()` returns to
`ptable.lock`, `ncli == 1`, not `RUNNING`, interrupts off. the scheduler and asserts four invariants on the way — holding `ptable.lock`,
`ncli == 1`, not `RUNNING`, interrupts off.
When a full pass finds nothing runnable the CPU halts rather than spinning. When nothing is runnable the CPU goes idle rather than spinning: into ACPI's
`_CST` C2 state (CC6 on Zen), by port read or `MWAIT`, where the APIC timer keeps
running through it, and otherwise a plain `hlt`.
**Locking is a single `ptable.lock` over the whole table.** Not per-process: a **Locking is a single `ptable.lock` over the whole table.** Not per-process: a
scheduling operation touches two processes (the one leaving and the one scheduling operation touches two processes (the one leaving and the one
@ -309,9 +395,23 @@ mksh's job control could not make progress until they existed.
## What userspace gets ## What userspace gets
`fork`, `exec`, `wait`/`waitpid`/`wait4` with `WNOHANG` and real pid selection, `fork`, `clone` (threads), `exec`, `wait`/`waitpid`/`wait4` with `WNOHANG` and
pipes, signals, `mmap` (anonymous and file-backed), POSIX shared memory, Unix real pid selection, pipes, signals, `mmap` (anonymous and file-backed), POSIX
domain sockets, `poll`/`select`/`epoll`, `eventfd`, `timerfd`, `signalfd`. shared memory, `memfd`, Unix domain sockets, `poll`/`select`/`epoll`, `eventfd`,
`timerfd`, `signalfd`, `inotify`, `futex`.
### Dynamic linking
`exec` handles `ET_EXEC` and `ET_DYN`. A dynamically linked program names an
interpreter in `PT_INTERP` — on musl, `/lib/ld-musl-x86_64.so.1`, which is libc
under another name — and the kernel's whole part is to load that as well and
start it instead of the program. The interpreter finds the program through the
auxiliary vector (`AT_PHDR`, `AT_ENTRY`), maps libc and whatever else it needs
with `mmap()`, relocates, and jumps to `AT_ENTRY` itself. A position-independent
program goes where a fixed one would (`0x400000`); the interpreter goes at the
start of the `mmap` area (`0x40000000`) and `mmap` carries on above it, so the
two never meet. This is what lets Alpine's binaries run unmodified, and
`dlopen` works, so Xorg loads its drivers as modules.
`mmap` uses a bump allocator (`proc->mmap_next`, starting at `0x40000000`) so `mmap` uses a bump allocator (`proc->mmap_next`, starting at `0x40000000`) so
every mapping gets its own disjoint, exactly-sized range. It previously assigned every mapping gets its own disjoint, exactly-sized range. It previously assigned
@ -329,24 +429,29 @@ allocation over 8 MB mapped straight into a neighbouring live mapping.
loop reports 97%, `sleep 2` reports 0% — but kernel time needs TSC deltas loop reports 97%, `sleep 2` reports 0% — but kernel time needs TSC deltas
closed and reopened around every `sched()`, which is scheduler accounting closed and reopened around every `sched()`, which is scheduler accounting
rather than sampling, and is not written. rather than sampling, and is not written.
- **`struct socket` is too big for a slab**, and every subsystem's fixed-size
- **No slab allocator.** One page at a time, and every subsystem's fixed-size table (`NPROC`, `NSOCK`, `NDEVICE`, `NSHM`) is still a compile-time constant.
table is a consequence. This is the single change that would most improve the Slabs exist for the objects that fit in a page.
rest of the kernel. - **No page cache.** `mmap` of a file *reads the range in* at map time, and
- **No demand paging and no page cache.** `mmap` of a file *reads the whole writable `MAP_SHARED` of a file is refused outright rather than faked. Demand
thing in* at map time. Writable `MAP_SHARED` of a file is therefore refused paging covers anonymous memory only.
outright rather than faked — see [../plan/foundations.md](../plan/foundations.md). - **Root is `euid == 0` and nothing finer.** No capabilities; file access, `kill`, `exec`
- **No dynamic linking.** `exec` handles `ET_EXEC` only: no `PT_INTERP`, no and the privileged calls are enforced as [users.md](users.md) says.
`ET_DYN`, no shared libraries, no `dlopen`. - **`mprotect` is accepted and not enforced** (`sys_mprotect` returns 0). RELRO is not read-only,
- **No threads.** `clone` exists but does not share an address space; a `PROT_NONE` guard does not fault, and nothing has `NX` (`EFER.NXE` is never set), so every
`gettid` returns the pid. mapping is executable.
- **Permissions are tracked but not enforced.** uid/gid are maintained honestly - **`poll`, `select` and `epoll` wait a tick at a time** (10 ms at `HZ=100`) unless something
so `getuid()`/`setuid()` stop lying, and nothing consults them on access. that can make a descriptor ready calls `poll_kick()` — which the pipe, socket and event paths
do. busybox's `wget` polls before every read and downloaded at 85 KB/s, a segment per tick,
until the network path started kicking.
- **No swap**, and no memory pressure handling of any kind. `kalloc` failing is - **No swap**, and no memory pressure handling of any kind. `kalloc` failing is
reported up as `-ENOMEM` and that is the whole policy. reported up as `-ENOMEM` and that is the whole policy.
- **Nested signal delivery is not supported.** `in_signal` guards against - **Nested signal delivery is not supported.** `in_signal` guards against
re-entering a handler while one is running. re-entering a handler while one is running.
- **Scheduling is fair-share on one global run queue.** Priorities and - **One global run queue.** Fair share and `nice` exist; per-CPU run queues, work
fairness accounting exist now ([scheduler.md](scheduler.md)); per-CPU run stealing and CPU affinity do not, and `sched_setaffinity` is accepted and
queues, work stealing and CPU affinity do not, despite `sched_setaffinity` ignored.
existing. - **No IOMMU**, so DMA-capable devices (and the driver processes that program
them) are trusted.
- **256 GB is the most RAM** the kernel uses; the rest is ignored, with a line
saying so.

View file

@ -6,7 +6,11 @@ drivers, and LDLS is what does that.
It works, and on more than one driver. `8139too` and `e1000` — unmodified GPL It works, and on more than one driver. `8139too` and `e1000` — unmodified GPL
kernel modules — load into a SchOS process, drive their hardware through kernel modules — load into a SchOS process, drive their hardware through
`/dev/pci`, and carry packets: `/dev/pci`, and carry packets (both under QEMU). A third, Linux's `r8169`, is
the NIC driver on the ASUS TUF A15 laptop, and it is the first that needs a
*set* of modules and not one; it is described under *A set of modules*. It has
registered its interface and read its PHY on that laptop; whether it carries
traffic there has not been confirmed yet.
``` ```
# ldlsd /lib/modules/8139too.ko /lib/modules/btf & # ldlsd /lib/modules/8139too.ko /lib/modules/btf &
@ -46,18 +50,25 @@ Every one was a call that succeeded, returned zero, and did nothing.
Five, and everything else is an implementation of them. Five, and everything else is an implementation of them.
### 1. Nothing GPL ever enters an image we build ### 1. Linux's drivers ship in the image
The driver is **the user's**: obtained by them, loaded on their machine. SchOS SchOS is GPL-2.0-or-later, and GPL-2.0-only code combines with it, so there is no
ships the layer and never the thing it loads. `.gitignore` refuses `*.ko`, licensing reason to keep Linux's drivers out of an image. They are in: the **r8169 set**
`make ldls-stage` is the one command that puts a module into a local image and (`mdio-bus`, `libphy`, `mdio_devres`, the Realtek PHY driver and `r8169`, unmodified) and
is deliberately not part of `all`, and `make ldls-unstage` takes it out again. the RTL8168 PHY firmware are what the ISO carries for the laptop's network, with their
licence and corresponding source recorded in `NOTICE`
([../licensing.md](../licensing.md) §6).
[../licensing.md](../licensing.md) §6 is the whole argument. The short form is (An earlier version of this rule said the opposite — that LDLS would ship the layer and never
that the combination happens in a user process on the user's machine, so no the thing it loads. That was written when SchOS was Apache-2.0 and the combination could not
combined work is distributed by anyone. Since SchOS moved to GPL-2.0-or-later be distributed; it stopped being true with the move to the GPL, and the owner has said Linux
that is no longer the only way it *could* be done — it is the way it is done, code and blobs may be shipped.)
because a module we never ship is a module we never have to answer for.
What is left is mechanical. The repository holds no `*.ko` — a binary does not belong in git, so
`.gitignore` refuses them — and `make ldls-r8169` (before `make iso`) stages the set from a
Linux build, together with its BTF, because a module, its dependencies and the BTF that
describes them have to come from the same build. `make ldls-stage KO=…` does the same for any
other module, which is then a distribution of *that* module's code, under its own licence.
### 2. It runs in a process, never in the kernel ### 2. It runs in a process, never in the kernel
@ -186,6 +197,55 @@ the object has to live where a signed 32-bit absolute address reaches. A
process's mmap area starts at `0x40000000`, which is inside that window; on the process's mmap area starts at `0x40000000`, which is inside that window; on the
host harness `MAP_32BIT` reaches it from the other end. host harness `MAP_32BIT` reaches it from the other end.
### A set of modules — what r8169 needed
Linux's `r8169` is not one module. It needs `mdio-bus`, `libphy`, `mdio_devres`,
the Realtek PHY driver, the NIC itself, the RTL PHY firmware, and the BTF of the
kernel they came from. `make ldls-r8169` stages all of that from the running
kernel; `etc/schinit/ldls-r8169.job` starts `ldlsd` on it at boot, with its output
in `/var/log/ldls-r8169.log`. The job has `KeepAlive = no`: a driver that crashes
on real hardware would otherwise be restarted into the same crash every few seconds.
What that added to `ldlsd`:
- **A comma-separated module set**, loaded in dependency order, each linking
against the ones before it, with the BTF read before any `init_module` runs.
A library module (`libphy`) just exports symbols to the next; the PCI `probe`
runs once, at the end, for whichever module registered a `pci_driver`.
- **A driver core**: bus match, driver probe, so the PHY driver binds to the PHY
the way it does in Linux; **devres**; a kernel `printf`; real `udelay`/`msleep`/
`ktime`; and **1000 Hz `jiffies`**, because the module's `HZ` is compiled in and
phylib re-arms its link poll `HZ` jiffies out.
- **Firmware** read from `/lib/firmware`.
- **phylib's data**, the `link_mode_params` table and its bitmaps.
- **The symbols whose stub-zero was a lie**: list validity, `try_module_get`,
`dev_err_probe`, `eth_platform_get_mac_address`, `napi_consume_skb` (as a stub it
leaked one buffer per transmitted frame until the DMA heap ran out), and the
rest of the table at the end of this document.
- `pdev->bus` is a real `pci_bus`; `net_device.priv_len` is set from BTF (the
driver's `memset` of its ring bookkeeping is bounds-checked against it, and
`__fortify_panic` fired on a length of zero); and `cmovcc` from per-CPU memory is
rewritten like the other `%gs` forms.
- **`__preempt_count` starts at `PREEMPT_ENABLED`.** mdio-bus keeps the register
it has just read in `r11` across `preempt_enable()`, which on x86 is
`decl %gs:__preempt_count; je`, to a call that preserves every register. With the
count at zero the call was made, into a logging stub whose syscall left `RFLAGS`
(0x246) in `r11` — so every MDIO read returned `0x02460246`. `preempt_schedule` is
now a bare `ret`.
- `__fortify_panic` names the module and function (`r8169: rtl_open.cold+0x…`) so
a photograph of the screen is enough, and exits instead of spinning on a core and
holding `/dev/netdev`.
The kernel did two things for it. It turns **ASPM off on Realtek NIC links** at
boot, as `r8169` does through `pci_disable_link_state`, which a process cannot;
and a network interface that **appears after boot starts the stack** — with the
native driver out of the registry the laptop boots with no interface at all, and
`ldls0` arrives later. `ldlsd` says which refusal it got when attaching: `EBUSY` is
another process, anything else is the kernel.
The native `drivers/net/r8169/` stays in the tree, with Linux's 8168H setup, as
the fallback; it is out of the registry.
### The KPI — `ldls/kpi/` ### The KPI — `ldls/kpi/`
The Linux symbols, implemented. Register access to a mapped BAR; the PCI calls The Linux symbols, implemented. Register access to a mapped BAR; the PCI calls
@ -232,8 +292,8 @@ and setting both to zero makes the whole thing reversible with no lookup table.
### The back end — `/dev/pci` ### The back end — `/dev/pci`
Already built, for `vgpud`, before any of this existed. Already built, for `vgpud`, before any of this existed. The design insisted the
[hybrid.md](../plan/hybrid.md) insisted the interface be about *PCI devices* interface be about *PCI devices*
rather than about a driver model, on the argument that an interface whose first rather than about a driver model, on the argument that an interface whose first
consumers are native drivers is a general mechanism rather than a hole shaped consumers are native drivers is a general mechanism rather than a hole shaped
like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and
@ -241,10 +301,9 @@ like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and
### Giving a device up — `release` ### Giving a device up — `release`
A driver in the kernel now has a `release` hook, and `/dev/pci` calls it before A driver in the kernel has a `release` hook, and `/dev/pci` calls it before
handing its device to a process. `docs/plan/hybrid.md` had said the honest handing its device to a process. The honest answer to "may I have the one you are
answer to "may I have the one you are using" is *no* until a driver could be using" was *no* until a driver could be told to let go; e1000 is the first that can.
told to let go; e1000 is the first that can.
Letting go is three things, and the third was missed at first: Letting go is three things, and the third was missed at first:
@ -276,7 +335,7 @@ one flag and hands over all 65536 ports, including the interrupt controller,
the timer and the disk; the TSS I/O permission bitmap is per port. A foreign the timer and the disk; the TSS I/O permission bitmap is per port. A foreign
driver already has DMA with no IOMMU, and the answer to "this is not a security driver already has DMA with no IOMMU, and the answer to "this is not a security
boundary" is not to move it further — so it is the bitmap, at 8 KB per CPU of boundary" is not to move it further — so it is the bitmap, at 8 KB per CPU of
resident memory. See [../../include/ioperm.h](../../include/ioperm.h). resident memory. See [../../include/arch/ioperm.h](../../include/arch/ioperm.h).
Writing it found a latent hole. The TSS's I/O map base is at offset 102 and the Writing it found a latent hole. The TSS's I/O map base is at offset 102 and the
old code wrote offset 64; the field it meant to set stayed 0, and a base of 0 old code wrote offset 64; the field it meant to set stayed 0, and a base of 0
@ -336,17 +395,17 @@ point of having built it against our own:
## What it does not do ## What it does not do
- **Two drivers, one kernel version, one machine.** `8139too` and `e1000` on - **Three drivers, one kernel version.** `8139too` and `e1000` on 6.18.46
6.18.46 against QEMU. The second driver was worth more than the first: it against QEMU, and `r8169` on that version against QEMU (where it finds no
found the per-CPU problem, the retpoline problem, the port-I/O problem, the device) and on one laptop (where it gets as far as the PHY). The second driver
interrupt storm and a seven-year-old hole in the TSS. Nothing here has been was worth more than the first: it found the per-CPU problem, the retpoline
tried on a second kernel or on real silicon, and "LDLS runs Linux drivers" is problem, the port-I/O problem, the interrupt storm and a seven-year-old hole in
still a claim about a category from a sample of two. the TSS. Nothing here has been tried on a second kernel, and "LDLS runs Linux
drivers" is still a claim about a category from a sample of three.
- **No interrupts**, per the polling note above. - **No interrupts**, per the polling note above.
- **`jiffies` is not `HZ`.** It comes from a 100 Hz `uptime()`, which is a - **`jiffies` assumes `HZ` is 1000**, which is what the kernels LDLS loads from
plausible `HZ` and not necessarily the one the module was built for. A driver are built with (`ldls_load.hz` can say otherwise). It comes from the
measuring a timeout against it gets a longer timeout, which is the safe monotonic clock in milliseconds.
direction and is still wrong.
- **One work item at a time, on the main loop.** A driver whose deferred work - **One work item at a time, on the main loop.** A driver whose deferred work
blocks stops everything, including the transmit path it is probably waiting blocks stops everything, including the transmit path it is probably waiting
for. for.
@ -363,14 +422,21 @@ point of having built it against our own:
process, which is a leak in the strict sense and the right trade for a process, which is a leak in the strict sense and the right trade for a
program that will be killed to restart it. program that will be killed to restart it.
- **BTF has to come from somewhere.** SchOS has no `/sys/kernel/btf/vmlinux`, - **BTF has to come from somewhere.** SchOS has no `/sys/kernel/btf/vmlinux`,
so the user brings it with the module, from the kernel the module was built so `make ldls-stage` / `ldls-r8169` copy it from the Linux kernel the module
for. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither. came from. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither.
- **There is address-space groundwork nothing uses.** `kernel/arch/x86_64/vm.c`
maps the kernel image a second time at `LDLS_KALIAS`, in the top 2 GB where a
`-mcmodel=kernel` module can reach, with a `vmemmap` and a module area beside it
— at every boot — and `include/ldls/ldls.h` describes a `/dev/ldls` control
channel to load modules into the kernel. That was the start of an in-kernel
loader, contradicting rule 2. Nothing calls either, and it should be finished
as a decision or deleted.
- **No config-space writes.** `/dev/pci` has none, deliberately — a write is - **No config-space writes.** `/dev/pci` has none, deliberately — a write is
how a BAR gets moved out from under the kernel — so a driver that needs one how a BAR gets moved out from under the kernel — so a driver that needs one
is refused loudly rather than quietly succeeded. is refused loudly rather than quietly succeeded.
- **No module-to-module dependencies, no firmware loading, no sysfs, no - **No sysfs and no unload.** Modules depend on each other and firmware is read
unload.** `request_firmware` fails, `module_exit` is never called, and the from `/lib/firmware`, but `module_exit` is never called, and the way to stop a
way to stop a driver is to kill the process. driver is to kill the process.
- **Some symbols are still logging stubs.** Deliberate, per rule 5 — and the - **Some symbols are still logging stubs.** Deliberate, per rule 5 — and the
single most expensive thing in this layer. **A stub that returns zero is not single most expensive thing in this layer. **A stub that returns zero is not
safe when zero is a meaningful answer**, and the list of ways that bit is safe when zero is a meaningful answer**, and the list of ways that bit is

View file

@ -1,7 +1,9 @@
# Networking # Networking
ARP, IPv4, ICMP, UDP, TCP and DHCP, over a driver-agnostic interface class, ARP, IPv4, ICMP, UDP, TCP and DHCP, over a driver-agnostic interface class,
reached through the same socket syscalls as Unix domain sockets. reached through the same socket syscalls as Unix domain sockets — plus DNS
(resolved in the process, by musl), HTTPS (`wget`, Alpine's TLS libraries) and a
small netlink responder for `getifaddrs`.
Verified end to end rather than by inspection: `bootcheck -Net` starts a Verified end to end rather than by inspection: `bootcheck -Net` starts a
listener and a UDP responder on the host, tells the guest to talk to them, and listener and a UDP responder on the host, tells the guest to talk to them, and
@ -25,7 +27,9 @@ that completes a handshake and drops every byte.
└────────────────┬───────────────────┘ └────────────────┬───────────────────┘
│ struct netdev_ops │ struct netdev_ops
┌────────────────┴───────────────────┐ ┌────────────────┴───────────────────┐
│ e1000, and any future NIC driver │ │ e1000 rtl8139 r8169 — or a │ in the kernel
│ Linux driver in a process, which │ (LDLS, via /dev/netdev)
│ is a netdev like any other │
└────────────────────────────────────┘ └────────────────────────────────────┘
``` ```
@ -86,50 +90,55 @@ covers two decades of silicon — and why the driver prints the chip's XID on
probe, since the PCI device id tells you almost nothing about which part probe, since the PCI device id tells you almost nothing about which part
arrived. arrived.
### Why it is native, when the plan said LDLS ### Native first, then Linux's own
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md). Measuring The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md), and
it is what changed the answer: the module has **183 undefined symbols, 130 of measuring it changed that: the module has **183 undefined symbols, 130 of them not
them not implemented**, and 35 of those 130 are **phylib** — implemented**, and 35 of those 130 are **phylib** — `phy_connect_direct`,
`phy_connect_direct`, `phy_start`, `mdiobus_read`, `genphy_soft_reset`. `phy_start`, `mdiobus_read`, `genphy_soft_reset`. phylib is a subsystem with a link
state machine, not a set of leaf calls, and LDLS's usual answer for an
unimplemented symbol — a stub that reports itself and returns zero — is precisely
wrong for it: `phy_start` returning zero means the link never comes up and nothing
says why. So a **native driver** came first: it reaches the PHY through the
chip's own MII window (`PHYAR`, one register that is a whole MII bus) and skips
the subsystem, in the shape of the e1000 beside it.
phylib is a subsystem with a link state machine, not a set of leaf calls, and It has since been done the other way as well. LDLS grew a driver core, devres,
LDLS's usual answer for an unimplemented symbol — a stub that reports itself firmware loading and phylib's tables ([ldls.md](ldls.md), *A set of modules*),
and returns zero — is precisely wrong for it. `phy_start` returning zero means and the ASUS TUF A15's RTL8168H is now driven by **Linux's r8169, unmodified, in
the link never comes up and there is nothing on screen to say why. The 8139too a process**: `make ldls-r8169` stages it with libphy, mdio-bus and the PHY driver,
that ldls.md uses as its model has no phylib dependency, which is why that one and `etc/schinit/ldls-r8169.job` starts it at boot. The native driver is **out of
was tractable. the registry** — it would claim the device first — and stays in the tree, with
Linux's 8168H setup, as a fallback.
A native driver reaches the PHY through the chip's own MII window (`PHYAR`, On that laptop the interface is `ldls0`, and it appears *after* boot, which is why
one register that is a whole MII bus) and skips the subsystem entirely. It is `net_attach_late()` starts the stack when the first interface arrives instead of
less work than the port, it carries no GPL entanglement, it needs no firmware turning it away.
blob, and it is not coupled to one Linux version.
The register layout is a *fact about the hardware*, taken the way
[ldls.md](ldls.md) §3 says facts may be taken. The expression is this tree's:
a descriptor-ring driver in the same shape as the e1000 beside it.
### The Chromebox has one of these ### The Chromebox has one of these
The Asus Chromebox CN62 "Guado" turns out to carry an RTL8111 rather than the The Asus Chromebox CN62 "Guado" carries an RTL8111 rather than the Intel part its
Intel part its spec sheet suggests, so this driver covers it as well as the spec sheet suggests, so the family match (`10EC:8168`) covers it as well as the
laptop that prompted it — which is the argument for the family match in one laptop — which is the argument for matching a family in one line rather than a
line rather than a device id per board. device id per board.
### It has not run on hardware ### It has not been shown to work on hardware
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139` **Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139` and
and nothing else from Realtek, and the 8139 is a different programming model — nothing else from Realtek, and the 8139 is a different programming model — a
a circular receive buffer rather than descriptors — so it is deliberately *not* circular receive buffer rather than descriptors — so it is deliberately *not* in
in the match table. Claiming it would break a NIC that works. the match table. Claiming it would break a NIC that works.
What has been verified: the driver builds, its presence changes nothing on a What has been verified: both drivers build, their presence changes nothing on a
machine without the device, and with an `rtl8139` on the bus it correctly machine without the device, and Linux's r8169 under LDLS loads, links and
leaves it unclaimed (`10ec:8139 network controller [-]`). initialises in QEMU, registering and finding no device. On the laptop it reached
probe and read the PHY (after a bug in LDLS that made every MDIO read return
`0x02460246` was fixed); the native 8168H bring-up — the PLL, the receive gate, the
PHY through the OCP window — has not run on a part.
What has not: every register write, on a real part. What has not: traffic on real hardware, from either.
So the probe says everything a first bug report would need, with `cprintf` The native probe says everything a first bug report would need, with `cprintf`
rather than `kinfo` so it reaches the screen on a machine with no serial port: rather than `kinfo` so it reaches the screen on a machine with no serial port:
``` ```
@ -141,6 +150,9 @@ register 1, where bit 2 is link and bit 5 is autonegotiation complete. Those
four facts separate "the chip is not there", "the chip is there and the PHY four facts separate "the chip is not there", "the chip is there and the PHY
sees nothing", and "everything is up and the rings are wrong". sees nothing", and "everything is up and the rings are wrong".
The kernel turns **ASPM off** on Realtek NIC links at boot, as Linux's driver
does through `pci_disable_link_state` — which a process under LDLS cannot do.
### Two constants that were wrong first ### Two constants that were wrong first
Recorded because both would have worked, quietly: Recorded because both would have worked, quietly:
@ -156,7 +168,7 @@ Recorded because both would have worked, quietly:
## The netdev class ## The netdev class
A driver publishes a [`struct netdev_ops`](../../include/netdev.h) and is then A driver publishes a [`struct netdev_ops`](../../include/dev/netdev.h) and is then
finished with the network stack forever: it never mentions lwIP, and the stack finished with the network stack forever: it never mentions lwIP, and the stack
never mentions it. never mentions it.
@ -262,6 +274,12 @@ to. Measured across three pings: `rx_frames` and `wake_rx` both went 2 → 6.
table, and it returns immediately: everything happens in that thread, which table, and it returns immediately: everything happens in that thread, which
cannot run until the scheduler is entered. cannot run until the scheduler is entered.
**The thread follows each device's link into lwIP.** The link used to be read once
at attach, 0.02 s into boot; real autonegotiation takes seconds, so on hardware
the interface stayed down for good. It also starts the stack when an interface
appears after boot (a driver in a process, a USB NIC), and attaches every device
there is as its first act; `net_eth_attach()` refuses one it already has.
DHCP obtains an address at boot. `/proc/netif` reports each interface's address, DHCP obtains an address at boot. `/proc/netif` reports each interface's address,
netmask, gateway, MAC and frame counts, taken at the lwIP seam. netmask, gateway, MAC and frame counts, taken at the lwIP seam.
@ -326,19 +344,20 @@ The creation lives in `sys_bind()` rather than here, because that is the layer
with both: `kernel/net/unix.c` knows about sockets and has no business calling with both: `kernel/net/unix.c` knows about sockets and has no business calling
`create()`. `create()`.
**`flock()` is still missing.** libwayland locks `wayland-0.lock` beside the `flock()` and `fcntl` record locks exist (`kernel/fs/lock.c`), so the lock files
socket so that two compositors cannot claim one display. The lock file can be next to a socket work the way they do on Linux. Names in
created and opened now that paths are real; the advisory lock itself is a the **abstract namespace** (a `sun_path` starting with a NUL) name no file; they
separate mechanism with no other caller yet. work, and `getsockname` reports them unnamed. `SO_PASSCRED` is honoured, and
`SO_PEERCRED` answers (tmux asks).
### Descriptor passing ### Descriptor passing
`sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry `sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry
that a pipe cannot: a reference to an object the kernel already knows, rather that a pipe cannot: a reference to an object the kernel already knows, rather
than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor
and has no integer-shaped alternative, and and has no integer-shaped alternative, and the same mechanism is how a driver
[hybrid.md](../plan/hybrid.md)'s mechanism 1 is how a driver process is handed process is handed the device it is allowed to touch ([devices.md](devices.md)).
the device it is allowed to touch. Both wanted the same primitive. Both wanted the same primitive.
**A batch of descriptors is pinned to the stream position of the first byte of **A batch of descriptors is pinned to the stream position of the first byte of
the message that carried it.** That single rule is the whole delivery model, and the message that carried it.** That single rule is the whole delivery model, and
@ -374,9 +393,9 @@ descriptors to a position that never arrives.
**There is no garbage collector.** An AF_UNIX socket descriptor, sent over that **There is no garbage collector.** An AF_UNIX socket descriptor, sent over that
same socket and never received, holds a reference to the socket that holds the same socket and never received, holds a reference to the socket that holds the
reference to it, and neither close can reach zero. Linux carries several hundred reference to it, and neither close can reach zero. Linux carries several hundred
lines to break these cycles. Nothing on either plan passes a socket over a lines to break these cycles. Nothing passes a socket over a
socket — Wayland passes memory, the driver work passes devices — so this is a socket — display servers pass memory, the driver work passes devices — so this is
known and bounded leak (thirty-two sockets) rather than a silent one. a known and bounded leak (thirty-two sockets) rather than a silent one.
### AF_INET ### AF_INET
@ -593,59 +612,51 @@ information was there and the code did not ask.
## What it does not do ## What it does not do
- **`ping` is a first-party program**, `cmd/ping`, not toybox's. `recvmsg` now - **`ping` is a first-party program**, `cmd/ping`, not toybox's or busybox's. Ours
exists, so half of what stopped toybox's is gone; what remains is has no `-I`, no flood mode and no TTL display (`IP_RECVTTL` is an `IPPROTO_IP`
`IP_RECVTTL`, an `IPPROTO_IP` control message that would have to be generated control message that would have to be generated on the receive path), and takes a
on the receive path rather than merely carried. Ours has no `-I`, no flood dotted quad or `localhost` — it does not link musl's resolver. Busybox's `ping`,
mode and no TTL display. from the Alpine root, works over a `SOCK_RAW` ICMP socket.
- **`SCM_RIGHTS` is the only control message.** `SCM_CREDENTIALS` is refused - **Only `SCM_RIGHTS` and `SCM_CREDENTIALS` travel in a control message.** Anything
with `EINVAL` rather than ignored: a caller told its credentials were passed else is refused rather than ignored.
would have the receiver read a uid out of uninitialised memory and act on it.
- **A message may carry eight descriptors, and a socket eight such messages.** - **A message may carry eight descriptors, and a socket eight such messages.**
Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds
a reference to each one until it is received; a sender eight unreceived a reference to each one until it is received; a sender eight unreceived
messages ahead of its receiver is not rescued by a ninth slot. messages ahead of its receiver is not rescued by a ninth slot.
- **`ping` still takes a dotted quad**, even though names now work. It is a - **Nothing re-runs `resolvconf`.** The nameserver is written once at boot
first-party program that does not link musl's resolver, and `localhost` is (`etc/schinit/resolvconf.job`). A lease that changes the DNS server, or a
special-cased in it rather than resolved. Every *ported* program resolves `/dev/pci` handover that brings up a new interface, leaves a stale
names properly; ours is the one that does not. `/etc/resolv.conf` until somebody runs it again.
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot. A - **lwIP's own DNS client is unused.** Resolution happens entirely in the process
lease that changes the DNS server, or a `/dev/pci` handover that brings up a (musl); nothing in the kernel can resolve a name.
new interface, leaves a stale `/etc/resolv.conf` until somebody runs it again. - **`httpd` serves HTTP/1.0** with `Connection: close`, one request per connection,
- **lwIP's own DNS client is still unused.** It is compiled in and only and has no timeouts: a client that connects and says nothing ties up a child
`dns_getserver()` is ever called; resolution happens entirely in the process. process until it goes away, and `SO_RCVTIMEO` does not exist here.
Nothing in the kernel can resolve a name, which matters the day something in
the kernel wants to.
- **`httpd` does not serve HTTP/1.1.** Every response is HTTP/1.0 with
`Connection: close`, because the directory listing is generated straight into
the socket and so has no `Content-Length` to give. One request per connection
is the cost.
- **A queued-but-never-accepted connection is closed without clearing its - **A queued-but-never-accepted connection is closed without clearing its
callbacks.** `inet_close()` clears the argument and closes, which is enough callbacks.** `inet_close()` clears the argument and closes, which is enough
because the pcb is freed — but it is a narrower guarantee than the one the because the pcb is freed, but it is a narrower guarantee than the main path
main path makes, and it relies on `tcp_close()` succeeding. makes and relies on `tcp_close()` succeeding.
- **`httpd` has no timeouts.** A client that connects and says nothing ties up - **No routing table**, and no `/proc/net/route`. With two interfaces up, the first
a child process until it goes away; `SO_RCVTIMEO` does not exist here (see is the default route and the second is reachable only by an address on its own
below), so there is nothing to set. subnet. That is a rule rather than an accident, and it is not routing.
- **No routing table**, and no `/proc/net/route` to report one. With two - **Netlink is a responder**: `RTM_GETLINK` and `RTM_GETADDR` dumps (what
interfaces up, the first is the default route and the second is reachable only `getifaddrs` needs), `MSG_PEEK|MSG_TRUNC` sizing (GLib's network monitor measures
by an address on its own subnet. That is a rule rather than an accident, and it each datagram that way and spun at 100% when it answered 0), and nothing else. No
is not routing. The missing *file* is the more immediately visible half: it is `RTM_GETROUTE`, no event stream, no multicast groups; a program that expects to
how a program asks which interface carries the default route, and without it *subscribe* to link changes gets a socket that answers dumps and never speaks
anything that filters on "the default one" sees nothing. again.
- **Netlink serves two message types.** No `RTM_GETROUTE`, no `RTM_NEWROUTE`, no - **`setsockopt` applies what it understands** (`SO_REUSEADDR`, `SO_KEEPALIVE`,
event stream, no multicast groups. A program that expects to *subscribe* to `SO_BROADCAST`, `TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` report the
link changes gets a socket that answers dumps and never speaks again. real size; `SO_PASSCRED` on Unix sockets) and refuses the rest with `ENOPROTOOPT`.
- **No `getsockname`/`getpeername`**, and `accept()` does not report the peer's Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
address for an AF_INET socket. A server that wants to log who connected cannot. - **`SOCK_DGRAM` and `SOCK_SEQPACKET` over AF_UNIX** keep message boundaries (each message carries a
- **`setsockopt` covers six options**, and refuses the rest with `ENOPROTOOPT` 16-byte header in the same ring) but have no unconnected form: `socketpair` and listeners work,
rather than pretending: `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_BROADCAST` and `sendto` a path does not.
`TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` are accepted and - **Netlink `NETLINK_KOBJECT_UEVENT`** is the one multicast family: sockets bound to group 1 hear the
report the real size on read. Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`. kernel's device events, group 2 what udevd re-announces, and a datagram can be addressed to a port id.
- **`SOCK_DGRAM` over AF_UNIX does not exist** — stream only. - **No IPv6**, no firewall or packet filter of any kind, and a TCP stack that has
- **AF_UNIX names are not filesystem paths**, so nothing can `stat()` or been used for downloads at tens of megabits and no more than that.
`unlink()` a socket. - **Three drivers in the kernel, one Linux driver outside it**, and the class is
- **One NIC driver exists.** The class is proven with two e1000 cards, not with proven with two e1000 cards and an LDLS handover, not on a wide range of
two different drivers. hardware. The Realtek parts have never carried a packet on real silicon from this
- **No IPv6**, no DNS resolver in the kernel (lwIP's is compiled but unused by tree.
anything), and no firewall or packet filter of any kind.

View file

@ -0,0 +1,102 @@
# Packages: an apk root
Alpine is musl with a different init, and SchOS is musl with a different kernel.
Alpine's binaries run on SchOS unmodified — its dynamic linker, its busybox, and
`apk` itself, fetching over TLS through SchOS's network stack. So instead of
porting programs one at a time, SchOS installs them.
## The model
The root filesystem is an apk-managed root:
| Part | Where it comes from |
|---|---|
| musl, busybox, apk-tools, alpine-keys, CA certificates | Alpine's minirootfs (`ports/alpine`) |
| what `ports/alpine/world` lists (zsh, Xorg, Window Maker, htop, NetSurf …) | Alpine's packages, pinned in `world.lock` |
| schinit, its jobs, SchOS's tools, toybox, mksh, configuration | **schos-base**, an apk package the build makes from `sysroot/` |
`apk add` installs anything from Alpine's main and community repositories.
`apk upgrade` updates Alpine's packages, and schos-base from the SchOS repository
at `/var/lib/schos/repo`.
PID 1 is still schinit. Alpine's OpenRC is not installed, and packages that ship
OpenRC services install them where nothing reads them.
## Who owns what
Where Alpine and SchOS both have a program, Alpine's wins, and SchOS keeps only
what is its own (`tools/posix/mkaroot.sh` says so in its header):
- `/bin/sh`, `cat`, `sed`, `find` and the rest are busybox. Alpine's package scripts
are written against busybox.
- zsh is Alpine's. SchOS keeps its `zshrc` and `zshenv` (`replaces=zsh`).
- `/etc/os-release` and `/etc/motd` are SchOS's (`replaces=`).
- `passwd`, `group` and `profile` are Alpine's. schos-base's post-install sets root's
shell to zsh, and SchOS's profile is `/etc/profile.d/schos.sh`.
- `pci.ids` and `usb.ids` are Alpine's (`hwdata-pci`, `hwdata-usb`): Xorg depends on
`hwdata-pci`, and two owners of one file is an error apk will not install past.
- mksh is `/bin/mksh`. schinit's recovery shell looks for it first because it is static:
a broken musl cannot take recovery down with it.
## Building
make qemu boots build/schfs.img, the root as a 1 GB SchFS disk
make iso the same root as the live system's compressed ramdisk
make alpine-lock re-resolve world against Alpine (needs the network)
It is the only root; `sysroot/` is what schos-base is packaged from. Only
`alpine-lock` asks Alpine what is current. A normal build installs exactly what
`world.lock` names — files in `dist/alpine/`, fetched once and checked against the
lock's sha256, and against Alpine's signatures by apk — and never asks what is newer.
`world.lock` resolves the thirty-odd names in `world` into about 180 packages, every
dependency included, so every image after the first is reproducible.
schos-base and its repository index are signed with a key made on the first build
(`build/apk-keys/`, never committed). The root trusts it beside Alpine's keys. The
install runs as an ordinary user with `--no-scripts`, because apk's scripts want a
chroot; `mkaroot.sh` does by hand the one change schos-base's post-install would
have made.
## What it took from the kernel
All of it found by running apk, and none of it specific to apk:
- **TCP receive window:** it reopened before the reader drained, so lwIP refused data
and the sender waited on retransmission timers (5 KB/s). It reopens on read now, with a
32-segment window; downloads run at tens of megabits.
- **`mmap` address reuse:** freed ranges were never reused, so musl's allocator used up the
256 mapping records with 780 MB of RAM free.
- **`unlinkat` / `renameat`** returned a bare `-1`, read as `EPERM`, where apk expects
`ENOENT`; `renameat` could not replace its target.
- **`#!` scripts in `execve`**, and `ENOEXEC`: apk runs triggers directly.
- **`chroot`**, and mounting devtmpfs and proc more than once.
- Later, as bigger programs arrived: dynamic linking, threads and `futex`, demand paging,
pseudo-terminals, `inotify`, `flock`, Unix-socket credentials. Each is in
[kernel.md](kernel.md) or [syscalls.md](syscalls.md) because a package asked for it.
## The live system
The live ISO runs the apk root from RAM. The ramdisk is the compressed image
([scff.md](scff.md)); at boot the volume grows to half of the free memory (4 GB of root on
an 8 GB machine), backed by pages as they are written. Xorg and its dependencies are in the
image already; anything else installs into that space in about a minute, over the network,
and **is gone at reboot**.
## What it does not do
- **No installer.** Nothing is written to the machine's disks, so packages do not survive
a reboot. Keeping them needs an installer that writes the root to a SATA or NVMe disk,
and a kernel that finds its root there by the SchFS GPT type rather than as IDE disk 1.
- **Not every Alpine package runs.** The ones in `world` were chosen *because* they run:
terminal programs that draw and poll (htop, btop, tmux, mc), X clients, SDL 2 games,
NetSurf on GTK 3. Others hit whatever the kernel does not do yet; Epiphany (WebKit) is
one — its web-process child dies at start-up, and it has not been run to ground.
A package is one `apk add` away, and finding what it needs is how most of the kernel's
Linux surface got written.
- **Fortified libc is strict, and Alpine's packages are not always correct.** `lbreakout2`
hands `vsnprintf` a size larger than its buffer and traps on Alpine too; `world` carries
`lbreakouthd` instead.
- **apk's scripts and triggers do not run at build time** (`--no-scripts`), so a package
whose script matters has to have it done by hand in `mkaroot.sh`.
- **OpenRC services do nothing.** Anything that expects an init script to start it has to
be given a schinit job ([init.md](init.md)).

View file

@ -102,9 +102,8 @@ tracked. Two rules, with the two errnos POSIX gives them:
alone. Without it nice is not a courtesy but a way for any process to alone. Without it nice is not a courtesy but a way for any process to
outrank the rest of the system by asking. outrank the rest of the system by asking.
Both are inert today, because everything runs as root. They are written now Both bite now that there are users ([users.md](users.md)); root is exempt from
because the alternative is growing a priority knob and a permission model in both.
separate commits, and the window between the two is exactly the bug.
## Deliberately not a red-black tree ## Deliberately not a red-black tree

View file

@ -74,19 +74,19 @@ machine off, and Linux number 48 is the socket call. Two syscalls cannot share a
## What is implemented ## What is implemented
Roughly 190 native numbers covering: About 215 native numbers (193 of them with a Linux number) covering:
| Area | | | Area | |
|---|---| |---|---|
| Process | fork, exec, exit, wait/waitpid/wait4/waitid, clone, kill, sessions, process groups | | Process | fork, exec, exit, exit_group, wait/waitpid/wait4/waitid, clone (processes and threads), futex, kill, tgkill, sessions, process groups |
| Identity | the full real/effective/saved set for both uid and gid, groups, umask | | Identity | the full real/effective/saved set for both uid and gid, groups, umask |
| Memory | mmap, munmap, mprotect, brk/sbrk, madvise, System V shared memory | | Memory | mmap, munmap, mremap, mprotect, brk/sbrk, madvise, System V shared memory, memfd |
| Filesystem | the ordinary set, the whole `*at()` family, statfs, xattr, mount/umount2 | | Filesystem | the ordinary set, the whole `*at()` family, statfs, xattr, mount/umount2 |
| I/O | read/write, readv/writev, pread/pwrite, sendfile, poll/select/ppoll/pselect6, epoll (create, create1, ctl, wait, pwait), ioctl, fcntl | | I/O | read/write, readv/writev, pread/pwrite, sendfile, poll/select/ppoll/pselect6, epoll (create, create1, ctl, wait, pwait), ioctl, fcntl |
| Sockets | socket, bind, listen, accept, connect, sendto, recvfrom, socketpair, setsockopt, getsockopt | | Sockets | socket, bind, listen, accept/accept4, connect, sendto/recvfrom, sendmsg/recvmsg, socketpair, getsockname/getpeername, setsockopt/getsockopt, over AF_UNIX, AF_INET and AF_NETLINK |
| Signals | rt_sigaction, rt_sigreturn, rt_sigsuspend, rt_sigpending, rt_sigtimedwait, sigaltstack | | Signals | rt_sigaction, rt_sigreturn, rt_sigsuspend, rt_sigpending, rt_sigtimedwait, sigaltstack |
| Time | gettimeofday, clock_gettime, clock_getres, nanosleep, clock_nanosleep, alarm, timers | | Time | gettimeofday, clock_gettime, clock_getres, nanosleep, clock_nanosleep, alarm, timers |
| Notification | eventfd, eventfd2, timerfd_create/settime/gettime, signalfd, signalfd4 -- see `kernel/sys/sys_event.c` | | Notification | eventfd, timerfd, signalfd, inotify -- see `kernel/sys/sys_event.c` |
### Argument fetching ### Argument fetching
@ -113,20 +113,19 @@ field by field, each through its own translation, or use `copyin`/`copyout`.
## What it does not do ## What it does not do
- **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and nothing else.** - **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and `SCM_CREDENTIALS`**, and nothing
`SCM_CREDENTIALS` is refused rather than ignored. See else. Credentials are delivered only to a socket that asked with `SO_PASSCRED`
[networking.md](networking.md#descriptor-passing). (D-Bus and WebKit do); other control messages are refused rather than ignored.
- **No `memfd_create`.** See [networking.md](networking.md#descriptor-passing).
- **No `getsockname` or `getpeername`**, and `accept()` does not report an - **`setsockopt` applies the options it understands** — they reach lwIP's pcb or
AF_INET peer's address. the Unix socket — and answers `ENOPROTOOPT` for everything else, never a
- **`setsockopt` handles six options** and answers `ENOPROTOOPT` for everything success that did nothing. The options that take a `struct` (`SO_RCVTIMEO`,
else. `SO_RCVTIMEO`/`SO_SNDTIMEO` are among the refused. `SO_SNDTIMEO`) are among the refused.
- **`clone` does not share an address space**, so it is `fork` with extra steps.
- **`sched_setaffinity` is accepted and ignored.** - **`sched_setaffinity` is accepted and ignored.**
- **`getrusage` does not distinguish `RUSAGE_CHILDREN`** from `RUSAGE_SELF`, and - **The ABI is chosen by link address.** Anything linked at `0x100000` or above
fills in only the calling process's own tick count. — musl's programs, and every dynamically linked one, whose interpreter is
- **The ABI is chosen by link address.** It works, and it is a heuristic rather placed at `0x40000000` — gets the Linux map; a first-party program linked at
than a fact about the binary. `0x4000` gets the native one. It works, and it is a heuristic rather than a
- **`/proc/<pid>/exe` is a regular file, not a symlink**, so `readlink()` on it fact about the binary.
fails and `ps -o comm` prints `[init]` rather than `init`. Default `ps` and - **`ptrace` and `seccomp`** are not implemented; a program that needs them
`top` output is unaffected. gets `ENOSYS`.

View file

@ -1,12 +1,13 @@
<!-- SPDX-License-Identifier: GPL-2.0-or-later --> <!-- SPDX-License-Identifier: GPL-2.0-or-later -->
# Virtual terminals # Virtual terminals
Six of them, `/dev/tty1` through `/dev/tty6`. Twelve of them, `/dev/tty1` through `/dev/tty12`, one per function key.
| | | | | |
|---|---| |---|---|
| **AltGr+F1 .. F6** | go to that terminal | | **Ctrl+Alt+F1 .. F12** | go to that terminal — on the console and under X alike |
| **AltGr+-** / **AltGr+=** | previous / next terminal *in use* | | **Alt+F1 .. F12** | the same, on the text console only (X keeps plain Alt) |
| **Ctrl+Alt+-** / **Ctrl+Alt+=** | previous / next terminal *in use* |
| `chvt N` | the same, from a script or a serial console | | `chvt N` | the same, from a script or a serial console |
The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own
@ -14,8 +15,9 @@ cursor, its own line discipline, its own foreground process group and its own
session. Two shells on two terminals are two unrelated processes that happen to session. Two shells on two terminals are two unrelated processes that happen to
share a keyboard and a screen. share a keyboard and a screen.
A seventh is deliberately not bound. That is where a display server goes, A display server takes one of them with `VT_SETMODE` and does its own
and it will claim switching with `VT_SETMODE` rather than a chord. switching; X switches with Ctrl+Alt+F-key, which is why that is the default
here too.
--- ---
@ -93,7 +95,9 @@ nothing to choose between. It records `proc->ctty` now, which is what
|---|---|---| |---|---|---|
| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch | | `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch |
| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` | | `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` |
| `/dev/tty1`..`tty6` | 11-16 | that terminal, whether or not it is on screen | | `/dev/tty0` | | the active terminal, which is what Xorg and `openvt` ask for |
| `/dev/tty1`..`tty12` | 11-22 | that terminal, whether or not it is on screen |
| `/dev/ptmx`, `/dev/pts/N` | | pseudo-terminals, below |
`/dev/tty` was an alias for `/dev/console` with the same major and minor. They `/dev/tty` was an alias for `/dev/console` with the same major and minor. They
are different questions and now have different answers. are different questions and now have different answers.
@ -188,13 +192,13 @@ every candidate.
|---|---| |---|---|
| **The host takes it, and the guest never sees it** | | | **The host takes it, and the guest never sees it** | |
| `Alt+F4`, `Alt+F2` | window manager | | `Alt+F4`, `Alt+F2` | window manager |
| `Ctrl+Alt+F1..F6` | display manager — and it matches **loosely**, so adding Shift does not get past it | | `Ctrl+Alt+F1..F12` | display manager — and it matches **loosely**, so adding Shift does not get past it |
| `Ctrl+Alt+`digit | QEMU's own console tabs | | `Ctrl+Alt+`digit | QEMU's own console tabs |
| **Applications want it, and lose it permanently if the kernel eats it** | | | **Applications want it, and lose it permanently if the kernel eats it** | |
| `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor | | `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor |
| `Ctrl+`letter | control characters | | `Ctrl+`letter | control characters |
| `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` | | `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` |
| `F1..F6` alone | help, and whatever a full-screen program wants | | `F1..F12` alone | help, and whatever a full-screen program wants |
| **The keyboard layout wants it** | | | **The keyboard layout wants it** | |
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt | | `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt |
@ -202,32 +206,33 @@ So the kernel ships a default and lets it be moved:
```sh ```sh
chvt # report the active terminal and the current chord chvt # report the active terminal and the current chord
chvt -c altgr+F # AltGr+F1..F6 (the default) chvt -c ctrl+alt+F # Ctrl+Alt+F1..F12 (the default)
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F6 chvt -c altgr+F # AltGr+F1..F12 (the default until 2026-10)
chvt -c alt+1 # Alt+1..6 chvt -c ctrl+shift+F # Ctrl+Shift+F1..F12
chvt -c alt+1 # Alt+1..9
chvt -c off # no chord; chvt N is then the only way chvt -c off # no chord; chvt N is then the only way
``` ```
`off` is a legitimate setting, not a failure mode. On a machine whose host `off` is a legitimate setting, not a failure mode. On a machine whose host
grabs everything worth binding, `chvt N` from a shell is the honest answer. grabs everything worth binding, `chvt N` from a shell is the honest answer.
The default is **AltGr+F1..F6**, and the `AltGr+`letter row above needs reading The default is **Ctrl+Alt+F1..F12**. It was AltGr+F-key for a long time —
carefully: what a Polish layout wants is AltGr with a **letter**. AltGr with a AltGr with a **function key** produces nothing on any layout, no window manager
**function key** produces nothing on any layout, no window manager binds it, binds it, and no application asks for it — but X owns the keyboard while it
and no application asks for it. It is the one combination all three parties runs and switches only with Ctrl+Alt+F-key, so the console and the desktop
leave alone. answered different keys for the same thing. One chord everywhere wins. Inside
a VM whose host grabs Ctrl+Alt+F-key, `chvt -c altgr+F` brings the old one
back.
That distinction is only expressible because Right Alt is tracked as its own That distinction is only expressible because Right Alt is tracked as its own
modifier bit. It used to share `ALT`, which is harmless for typing — neither is modifier bit. It used to share `ALT`, which is harmless for typing — neither is
used to look up a character — and made this impossible to say. used to look up a character — and made this impossible to say.
Verified rather than argued: with the default set, `AltGr+F2` switches, AltGr+letter still reaches the shell untouched, so ą is unaffected either way.
`AltGr+F1` returns to a pixel-identical tty1, and `AltGr+a` still reaches the
shell, so ą is untouched.
Whatever the modifiers are, they also bind **minus** and **equals** to the Whatever the modifiers are, they also bind **minus** and **equals** to the
previous and next terminal. Those step only through terminals that are *in previous and next terminal. Those step only through terminals that are *in
use* — the same `opened` count `VT_GETSTATE` reports — because six terminals use* — the same `opened` count `VT_GETSTATE` reports — because twelve terminals
with two shells on them means four blank screens in between, and cycling past with two shells on them means four blank screens in between, and cycling past
them is worse than not having the key. them is worse than not having the key.
@ -267,12 +272,10 @@ Three layers, in increasing order of how much a program is claiming:
`VT_SETMODE` used to be accepted and ignored — a program could ask for the `VT_SETMODE` used to be accepted and ignored — a program could ask for the
chord, be told yes, and still not get it. It works now. chord, be told yes, and still not get it. It works now.
What `VT_PROCESS` does **not** do yet is the release handshake: Linux signals `VT_PROCESS` also does the release handshake Linux does: a switch away from a terminal in
the owning process on every switch request and blocks until it answers that mode signals the owning process (`relsig`) and waits for `VT_RELDISP`, which is how a
`VT_RELDISP`, which is how a display server releases the GPU before the screen display server stops drawing before the screen changes hands; switching back sends `acqsig`.
changes hands. Nothing here has anything to release, so the kernel simply See *Handing a terminal to a program*, below.
stops switching rather than negotiating. `struct vt_mode` already carries
`relsig`/`acqsig` for when Schcom needs it.
Input goes to whichever terminal is on screen. That is what makes a terminal a Input goes to whichever terminal is on screen. That is what makes a terminal a
terminal: the keyboard follows the eyes. terminal: the keyboard follows the eyes.
@ -365,34 +368,56 @@ and erase characters — were added at the same time, because `TERM=linux`
declares them and a terminal that drops them silently is the same class of bug declares them and a terminal that drops them silently is the same class of bug
waiting for a different editor. waiting for a different editor.
## Handing a terminal to a program
A display server takes a terminal with `VT_SETMODE(VT_PROCESS)` and `KDSETMODE(KD_GRAPHICS)`,
and from then on switching away from it is a **request**: the kernel sends the owner
`relsig` and waits for `VT_RELDISP`, so the server stops drawing and only then says
yes; switching back sends `acqsig` and the server redraws. A terminal in graphics mode
is not repainted by the kernel. If the owner dies, the terminal goes back to the kernel
whole. This is how Xorg runs on a terminal of its own, with Ctrl+Alt+F-key switching
between it and the text consoles and Ctrl+Alt+Backspace ending the session.
## Pseudo-terminals
`/dev/ptmx` makes a pair on every open and hands back the master; the slave is
`/dev/pts/N`, which the program on the other side gets as its stdin, stdout and
controlling terminal. Between them is a line discipline, the console's in miniature:
what the master writes is the slave's input (canonical editing, echo, `^C` as `SIGINT`
to the foreground group) and what the slave writes is read back by the master with NL
turned into CR NL. xterm, tmux, `script` and `sshd` all need exactly this. The pairs
share one lock; `kernel/dev/pty.c`.
## The text cursor
A steady inverted block — the cell with its colours swapped relative to what it would
show, so a cursor on reverse-video text is the plain text and every cursor on screen
is visible against its cell. `ESC[?25l` hides it, `ESC[?25h` shows it. It is drawn into
the grid's repaint and does not blink.
## What it does not do ## What it does not do
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does - **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
nothing because there is nothing above the top row to show. nothing because there is nothing above the top row to show.
- **No reflow on resize.** The geometry is read once at `vt_init()`. Nothing - **No reflow on resize.** The geometry is read once at `vt_init()`; the console's
resizes a framebuffer console today; a compositor will, and that is when this font is sized to the screen at that point, and a resolution change from `schmode`
has to grow. repaints it, but text is not re-wrapped.
- **No `VT_RELDISP` handshake.** `VT_PROCESS` stops the kernel switching, but - **No terminal past 12.** Linux allows 63. Twelve is one per function key.
the kernel does not signal the owning process and wait for an answer. A
display server that needs to release the GPU *before* the screen changes
hands will need that, and `struct vt_mode` already has the fields.
- **No terminal past 6.** Linux allows 63. Six is what the chords reach and
what the grids are sized for.
- **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped - **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped
and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the
fix, and costs BSS. fix, and costs BSS.
- **No UTF-8 the font lacks.** The console draws through the TrueType - **No UTF-8 the font lacks.** The console draws through the TrueType
rasteriser now, so the limit is the font's `cmap` rather than a 256-entry rasteriser, so the limit is the font's `cmap` — Latin, Latin Extended, Greek,
table — Latin, Latin Extended, Greek, Cyrillic and the common punctuation and Cyrillic and the common punctuation and maths symbols all render. Anything
maths symbols all render. Anything genuinely absent still draws as `?`, which genuinely absent still draws as `?`, which is deliberate: drawing a
is deliberate: drawing a *different* character would be worse. *different* character would be worse.
- **No shaping, and no wide characters.** One codepoint is one cell of one - **No shaping, and no wide characters.** One codepoint is one cell of one
advance. Combining marks land on top of their own cell rather than the advance. Combining marks land on top of their own cell rather than the
preceding one, and CJK draws at half the width it wants. Both need a preceding one, and CJK draws at half the width it wants. Both need a
width table and a cell that can span two columns. width table and a cell that can span two columns.
- **No dead keys**, so French and Spanish layouts are absent rather than - **No dead keys**, so French and Spanish layouts are absent rather than
broken. See above. broken.
- **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does - **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does
not move anybody's fingers. not move anybody's fingers.
- **Only three layouts.** Adding one is a table in `keymap.c` and a line in the - **Only three layouts** (US, Polish programmers', German). Adding one is a table in `keymap.c` and a line in the
catalogue; the mechanism is not the limit, the typing is. catalogue; the mechanism is not the limit, the typing is.

138
docs/architecture/udev.md Normal file
View file

@ -0,0 +1,138 @@
# udev
Alpine's `eudev` runs on SchOS as an ordinary process, started by `schinit`
(`etc/schinit/udevd.job`, with `udev-trigger.job` replaying the devices that
were there first). It is what lets Xorg *find* its keyboard and mouse instead of
being told, and the road to a Wayland compositor starts here: libinput and
libseat both ask udev what exists.
The kernel's half is three pieces, all small, none of them specific to udev.
## The device table: `kernel/dev/kdev.c`
The devices the kernel implements itself — input event nodes, the memory
devices, terminals, the framebuffer, the disks — are listed once, with their
class, name, `/dev` name and parent. Everything below is generated from it.
The device numbers are the ones `stat()` reports for the node, not SchOS's
internal ones: `/dev/tty1` is `4:1` to a Linux program (`linux_tty_rdev` in
`kernel/sys/sys_fs.c`), and udev compares what it reads in `/sys` with what
`stat()` says about the node. `dev_stat_numbers()` is the one mapping both use.
## `/sys`: `kernel/fs/sysfs.c`
```
/sys/devices/pci0000:00/0000:00:1f.2/ vendor device class ... uevent subsystem -> ../../../bus/pci driver
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3 idVendor idProduct ... uevent subsystem -> ../../../../../bus/usb
/sys/bus/pci/devices/0000:00:1f.2 -> ../../../devices/pci0000:00/0000:00:1f.2
/sys/bus/usb/devices/1-3 -> ../../../devices/pci0000:00/0000:00:14.0/usb1/1-3
/sys/devices/virtual/input/input0/ name capabilities/{ev,key,rel,abs} properties
/sys/devices/virtual/input/input0/event0/ dev uevent subsystem -> ../../../../../class/input
/sys/class/input/event0 -> ../../devices/virtual/input/input0/event0
/sys/dev/char/13:64 ... -> ../../devices/virtual/...
```
Symlinks are real (`T_SYMLINK` inodes whose target is generated). `uevent` is
readable (`DEVPATH=`, `SUBSYSTEM=`, `MAJOR=`, `MINOR=`, `DEVNAME=`) and
writable: `echo add > uevent` — which is what `udevadm trigger` does — makes the
kernel announce the device again. PCI and USB devices announce the same way:
`MODALIAS`, `PCI_ID`, `PCI_CLASS`, `PCI_SLOT_NAME` for a PCI device, `PRODUCT`,
`TYPE`, `BUSNUM`, `DEVNUM` for a USB one.
**PCI devices** are nested under the PCI-to-PCI bridge they sit behind: a
device's parent is the bridge whose secondary..subordinate bus range holds its
bus (the innermost if several do), read from the bridge's config space, so an
xHCI behind a root port is `pci0000:00/0000:00:04.0/0000:01:00.0`. **USB
devices** sit under the xHCI controller that carries their bus, below that
bus's root hub `usbN`; the controller tells the USB core its own PCI address
when it comes up, so the link is recorded, not guessed. A hub's children are
its siblings, not its children.
**USB interfaces** (`1-5:1.0`) are directories below their device, with
`bInterfaceClass`, `bInterfaceNumber`, a `driver` link when one is bound, and a
`uevent` carrying `DEVTYPE=usb_interface`, `INTERFACE=` and a `MODALIAS=usb:...`
that names the interface's class. They are announced after the drivers have
been offered the device, so the event says which interface was taken, and
removed before the device. Devices also carry what `lsusb -t` and libusb read:
`version`, `bMaxPacketSize0`, `bNumInterfaces`, `bmAttributes`, `bMaxPower`.
**Hot-plug is real**: the USB core announces `add` when a device is attached
and `remove` as it detaches, and `usbN` is announced when its controller comes
up. With `udevadm monitor`, plugging in a mouse prints the kernel event and
then the same event after udev's rules have run. PCI has no hot-plug here.
The input capability files are what `input_id` reads to decide
`ID_INPUT_KEYBOARD` or `ID_INPUT_MOUSE`. SchOS has two input devices whatever
the machine — every keyboard on `event0`, every mouse on `event1`
(`kernel/dev/input.c`) — so each has a fixed, plausible capability set.
## `/dev`: `kernel/fs/devfs.c`
devtmpfs is no longer read-only: udev creates `/dev/char/MAJ:MIN`, `/dev/fd`,
`/dev/input/by-*` and the like. A small in-memory overlay (192 entries) holds
directories and symlinks, listed after the kernel's own nodes in whichever
directory they are in. Device nodes themselves are still the kernel's: `mknod`
and regular files in `/dev` are refused.
## The sockets: `kernel/net/unix.c`
- **`AF_UNIX` `SOCK_DGRAM` and `SOCK_SEQPACKET`** keep message boundaries.
udevd's control socket is `SEQPACKET`, and its manager and workers talk over
a `DGRAM` socketpair. A message carries its sender's credentials with it
(several workers share one end), a zero-length datagram is a datagram, and a
datagram socket never reads end-of-stream.
- **`AF_NETLINK` / `NETLINK_KOBJECT_UEVENT`** sockets bind to multicast groups:
group 1 is the kernel's events, group 2 is what udevd re-announces once its
rules have run. A datagram can also be addressed to a port id — udevd hands
an event to an idle worker that way, and the worker believes it only from the
manager's port. The kernel's messages arrive as credentials with uid 0, pid 0.
A queue that is full drops the message, as Linux does, and `udevadm trigger`
is how a reader recovers.
## libinput and libseat
`libinput` (Alpine) reads the evdev nodes udev tagged and `xf86-input-libinput`
gives Xorg the same library; `libinput list-devices` as an ordinary user shows
`SchOS keyboard` and `SchOS pointer`. Xorg's `40-libinput.conf` now claims both,
and the log says `Using input driver 'libinput'`.
`seatd` (job `etc/schinit/seatd.job`, socket `/run/seatd.sock`, group `video`)
and `libseat` let a program that is not root take devices from the system. The
client asks seatd for `seat0`; seatd takes the VT (`VT_SETMODE`, `KDSETMODE`,
`KDSKBMODE`, all there already), opens the device as root and passes the
descriptor back over the socket with `SCM_RIGHTS`. Checked as `schos` against
`/dev/input/event0` and `event1` with a small program linked to libseat, reading
the device's name through the descriptor it was handed.
Getting there found two kernel bugs, both in descriptor passing:
- **A socket returned by `accept()` was not an `AF_UNIX` socket as far as
`sendmsg` was concerned.** The end `connect()` queues for the listener was
allocated with its domain left at zero, and `sendmsg` refuses `SCM_RIGHTS` on
anything else. So a *server* could receive descriptors and never send one:
seatd, and equally a Wayland compositor handing a client a buffer.
- **A control buffer longer than its messages was an error.** seatd passes a
zeroed array with one descriptor at the front; Linux treats a following header
too short to be one as the end. `sendmsg` now does too.
## What is not there
- `/dev/bus/usb` nodes (usbfs). `lsusb -t` works because it reads sysfs; plain
`lsusb` goes through libusb, which also refuses to start while `uname -r`
says `0.1.0` (it wants Linux 2.6.32 or newer) and wants usbfs to open devices.
- A hub's port count (`maxchild` reads 0), and string descriptors
(`manufacturer`, `product`, `serial`).
- A USB keyboard works because every keyboard is `event0`; udev knows the
interface arrived but the input device does not hang below it.
- `hwdb` (`/etc/udev/hwdb.bin`): `udevadm hwdb --update` has not been run in the
image, so `60-input-id` logs "returned non-zero" for the hwdb import. The
properties that matter come from `input_id`.
- Block device partitions, `by-id` / `by-uuid` links, and a `/sys/block`.
- A compositor. libinput and libseat are in place, DRM is not; see [../plan/roadmap.md](../plan/roadmap.md).
## Testing
Boot the ISO and run `udevd --debug &`, then `udevadm trigger; udevadm settle`.
`/run/udev/data/c7:0` should contain
`ID_INPUT_KEYBOARD=1`, and `udevadm info /dev/input/event0` should print the
device. Under X, `/var/log/Xorg.0.log` says `config/udev: Adding input device`.

View file

@ -21,13 +21,14 @@ The specification is public and the controller is not complicated.
> BSD is much more valuable as the reference you check yourself against than as > BSD is much more valuable as the reference you check yourself against than as
> the code you paste. > the code you paste.
Licensing points the same way: the BSD stacks are BSD-2/3-clause and compatible, Licensing does not decide it: the BSD stacks are BSD-2/3-clause and compatible, and
Linux's drivers are GPL-2.0-only and are not — see since SchOS became GPL-2.0-or-later Linux's GPL-2.0-only drivers are compatible too
[../licensing.md](../licensing.md). — see [../licensing.md](../licensing.md). The argument against porting is the
framework underneath, not the licence.
## What the spec gives you for free ## What the spec gives you for free
Everything in [`include/dev/usb.h`](../../include/usb.h) comes from USB 2.0 Everything in [`include/dev/usb.h`](../../include/dev/usb.h) comes from USB 2.0
chapter 9, which every device implements regardless of speed, class or vendor. chapter 9, which every device implements regardless of speed, class or vendor.
**That is what makes enumeration possible at all:** before you know anything **That is what makes enumeration possible at all:** before you know anything
@ -129,7 +130,8 @@ working one.
filesystem's next read, by the rule in `blkdev_rw()`: a filesystem that went on filesystem's next read, by the rule in `blkdev_rw()`: a filesystem that went on
with missing blocks would be guessing. with missing blocks would be guessing.
- **HID is keyboards and pointers.** Consumer-control interfaces (volume keys), - **HID is keyboards and pointers.** Consumer-control interfaces (volume keys),
game pads and the rest are declined. Keyboard LEDs are not driven. game pads and the rest are declined. Caps, Num and Scroll Lock light their LEDs
on USB and PS/2 keyboards, one state shared between keyboards.
- **USB 3 hubs** are not driven: the hub driver speaks the USB 2 hub protocol. - **USB 3 hubs** are not driven: the hub driver speaks the USB 2 hub protocol.
- **No isochronous transfers**, so no audio and no cameras. - **No isochronous transfers**, so no audio and no cameras.
- **No USB 3 SuperSpeed-specific features** beyond what enumeration needs — the - **No USB 3 SuperSpeed-specific features** beyond what enumeration needs — the

View file

@ -0,0 +1,79 @@
# Users and permissions
SchOS has accounts, a login prompt, and file permissions that the kernel
enforces. Until now every process was root and `uid` was bookkeeping; this is
the part that makes it a multi-user system.
## What you meet
- **`login` on all twelve terminals, tty1–tty12.** `etc/schos-getty` takes the
terminal back from whoever had it and runs `login`, which asks who is there
and starts their shell; when the shell exits `schinit` starts the prompt
again. Alt+F1…F12 (or Ctrl+Alt+F1…F12) switches between them. (There is no
longer a login-free root shell on tty6.)
- **One account in the image: `root`**, with an empty password, so `login` lets
it in without asking — it is a live image. `passwd` sets one. More users are
an `adduser` away; `wheel` is empty, so `su` and `doas` have no one to serve
until then.
- **`su` and `passwd`** (`cmd/su`, `cmd/passwd`) are setuid root and small
programs of their own, not toybox applets: toybox is one binary under every
name, and a setuid copy of it can be asked for any applet by naming it, as
root. `su` to root takes membership in `wheel`, whatever the password.
- **`doas`** (Alpine's, setuid) with `permit nopass :wheel` in `/etc/doas.conf`.
## What the kernel enforces
| | |
|---|---|
| open, create, unlink, rename, link | the usual owner/group/other bits, `w`+`x` on the directory, and the sticky bit (`/tmp`) |
| path walk | `x` on every directory searched |
| `exec` | `x` on the file; set-user-id and set-group-id make the new image run as its owner or group, and the saved ids follow |
| `chmod`, `chown`, `utimensat` | owner or root; a non-root `chown` drops the set-id bits |
| `access` / `faccessat` | by the *real* ids (`AT_EACCESS` for the effective ones) |
| `kill`, `tkill`, `tgkill` | root, or a target whose real or saved uid is one of yours. The kernel's own signals (`SIGPIPE`, the terminal's) are nobody's to refuse |
| `mount`, `umount`, `chroot`, `reboot`, `shutdown`, `setgroups`, `mknod` of a device | root |
| `setuid` family | root may set anything; anyone else only to an id they already hold |
Root reads and writes everything and executes a file only if some execute bit
is set. A process's supplementary groups (`setgroups`) are consulted along with
its effective group. Ownership is stored by SchFS (it always had the fields);
FAT volumes belong to everyone, since they cannot say otherwise, and `/proc`
and `/sys` are root's.
`inode_permission()` in `kernel/fs/fs.c` is the one check; the rest of this is
call sites.
## Devices
`/dev` is built by the kernel and each node has the owner, group and mode in the
`devnodes[]` table of `kernel/fs/devfs.c`: `null` and `zero` are 0666,
terminals `root:tty 0660`, `fb0` `root:video`, input `root:input`, raw disks
`root:disk`, `pci`, `dbg` and `netdev` root's alone. `chmod` and `chown` on a
node are kept (login claims its terminal this way, and udev sets modes) in an
override table applied whenever the inode is loaded. A new pseudo-terminal's
slave belongs to whoever opened `ptmx`.
udev would set a node it has no rule for to 0600, which made `/dev/null`
root-only, so `etc/udev-schos.rules` (installed as `90-schos.rules`) names the
kernel's devices with the answers the kernel gave.
## The image
Everything in the image is owned by root: the build host's user means nothing
inside it. `mkschfs --owners FILE` takes the exceptions, one `path uid gid
[mode]` per line (`tools/posix/mkaroot.sh` writes it: the modes of `/tmp`,
`/var/tmp` and `/root`). The setuid bits are set
on `su`, `passwd` and `doas` in the root tree directly, because `apk` run as an
ordinary user may have dropped them.
## Not done
- **No PAM and no `/etc/securetty`.** `login` is busybox's and checks
`/etc/shadow` itself.
- **No capabilities.** Root is `euid == 0`, all of it.
- **No per-user `/run`, no logind.** X as a non-root user works (`startx` as `schos`, under
QEMU: Window Maker up) by group permission alone — video, input and tty — and
`/etc/X11/Xwrapper.config` lets anyone start it, because the wrapper's "console users" test
looks for a Linux VT on a standard descriptor that xinit's server child does not keep.
- **`/proc` and `/sys` hide nothing from other users.**
- **`mprotect` is still not enforced** ([kernel.md](kernel.md)).

View file

@ -192,7 +192,7 @@ If either is edited, edit both.
|---|---| |---|---|
| Loader splash | Shift, from `include/gfx/logo.h`, shaded per row by `metal()` in `boot/common/ui.c` — silver, not accent. | | Loader splash | Shift, from `include/gfx/logo.h`, shaded per row by `metal()` in `boot/common/ui.c` — silver, not accent. |
| Kernel console / early boot | Shift, same mask, composited flat in a theme colour by `gfx_draw_logo`. | | Kernel console / early boot | Shift, same mask, composited flat in a theme colour by `gfx_draw_logo`. |
| A display with no compositor | Shift again, drawn by `vgpud` on `SCH_BG`. It replaced a test card — colour bands, gradients, a checkerboard and the driver's pid in binary — which was scaffolding for [../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3 and outlived them. The mark answers the same question a black screen cannot, *something is driving this device*, and answers it as the system rather than as a test rig. | | A display with no compositor | Shift again, drawn by `vgpud` on `SCH_BG`. It replaced a test card — colour bands, gradients, a checkerboard and the driver's pid in binary — which was scaffolding for [../architecture/devices.md](../architecture/devices.md)'s driver work and outlived them. The mark answers the same question a black screen cannot, *something is driving this device*, and answers it as the system rather than as a test rig. |
| PSOGM | Shift, the same mask again, by `gfx_draw_logo()` from `kernel/core/panic.c`. At its native 128 px — there is no resampler in the crash path, and the mark was only ever drawn at one size there anyway. | | PSOGM | Shift, the same mask again, by `gfx_draw_logo()` from `kernel/core/panic.c`. At its native 128 px — there is no resampler in the crash path, and the mark was only ever drawn at one size there anyway. |
| Serial crash report | `:(`, as text. The mark cannot be drawn in ASCII, and a mood marker in a log is not a mark. | | Serial crash report | `:(`, as text. The mark cannot be drawn in ASCII, and a mood marker in a log is not a mark. |
| SchFS | **Nowhere yet.** `uberblock.svg` has no consumer; it is waiting for `projects/schfs-linux/` to have a README worth heading. | | SchFS | **Nowhere yet.** `uberblock.svg` has no consumer; it is waiting for `projects/schfs-linux/` to have a README worth heading. |

View file

@ -12,12 +12,20 @@ virtio, NVMe and the Intel datasheets are public documents with no licence on
what you write from them. `kernel/drivers/bus/xhci/xhci.c` came from the spec, what you write from them. `kernel/drivers/bus/xhci/xhci.c` came from the spec,
and so did `net/e1000/` and `gpu/virtio_gpu/`. and so did `net/e1000/` and `gpu/virtio_gpu/`.
**BSD: yes. Linux: no.** This is a licence incompatibility rather than a **Three sources, in the order to try them:**
preference, and it decides the whole strategy — see
[../licensing.md](../licensing.md). BSD-2 and BSD-3 code can be used directly 1. **A native driver, written from the specification.** The default, and what every driver in the
with a `NOTICE` entry; GPL-2.0-only code can never be, at any distance. Reading kernel is.
Linux to understand *hardware* is fine and normal; translating a Linux driver 2. **Linux's own driver, unmodified, through LDLS** ([ldls.md](../architecture/ldls.md)), when the
function by function produces a derivative work whatever the identifiers say. device is one Linux supports and SchOS cannot afford to write a driver for — a NIC with a PHY
behind it, a GPU, a Wi-Fi card. It runs in a process, so a crash costs the interface and not the
machine. This is how the laptop's Realtek NIC is driven.
3. **BSD or Linux code taken into the tree.** Licensing no longer forbids either: SchOS is
GPL-2.0-or-later, so GPL-2.0-only code combines with it, and BSD-2/3 code can be used directly with
a `NOTICE` entry — see [../licensing.md](../licensing.md). The reason to prefer 1 and 2 is
maintenance, not law: a driver copied in is a driver that has to be tracked.
Reading another kernel to understand *hardware* is fine and normal.
One caveat about "port it from BSD" as a plan, learned by looking: FreeBSD's 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`, `sys/dev/usb` is roughly 100k lines welded to newbus, devclass, `bus_dma`,
@ -45,22 +53,29 @@ ours.
kernel/drivers/ kernel/drivers/
registry.c registry.c
gpu/vga/ gpu/virtio_gpu/ gpu/vga/ gpu/virtio_gpu/
net/e1000/ net/e1000/ net/rtl8139/ net/r8169/
storage/ahci/ storage/nvme/
audio/ac97.c
bus/xhci/ bus/xhci/
usb/core/ usb/hid/ usb/storage/ usb/hub/ usb/core/ usb/hid/ usb/storage/ usb/hub/
2. Declare it in `kernel/drivers/registry.c` and add it to the array. 2. Declare it in `kernel/drivers/registry.c` and add it to the array.
3. Add the object to `KERN_DRIVERS` in the `Makefile`. 3. Add the object to `KERN_DRIVERS` in the `Makefile`.
3½. If nothing else in the kernel calls it, make it optional: a `DRV_<NAME>` line in
`config/drivers.conf`, the name in `DRV_LIST` in the Makefile, the object behind
`$(filter y,$(DRV_<NAME>))`, and an `#ifdef CONFIG_DRV_<NAME>` around its line in `registry.c`.
A driver other code calls (`vga_fb`, the USB stack) cannot be optional.
4. Fill in the **class vtable** for what it is, and point `.ops` at it. That is 4. Fill in the **class vtable** for what it is, and point `.ops` at it. That is
the whole of what the rest of the kernel sees: the whole of what the rest of the kernel sees:
| class | vtable | consumer | | class | vtable | consumer |
|---|---|---| |---|---|---|
| `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../../include/display.h)) | the compositor | | `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../../include/gfx/display.h)) | none today — Xorg draws through `/dev/fb0` |
| `DEVCLASS_NET` | `struct netdev_ops` ([netdev.h](../../include/netdev.h)) | the TCP/IP stack | | `DEVCLASS_NET` | `struct netdev_ops` ([netdev.h](../../include/dev/netdev.h)) | the TCP/IP stack |
| `DEVCLASS_AUDIO` | `struct audio_ops` | `/dev/dsp` |
A display driver that owns a linear framebuffer also publishes it with A display driver that owns a linear framebuffer also publishes it with
`gfx_attach_scanout()` ([gfx.h](../../include/gfx.h)). That is what the text `gfx_attach_scanout()` ([gfx.h](../../include/gfx/gfx.h)). That is what the text
console and the crash screen draw on, and it is separate from the display console and the crash screen draw on, and it is separate from the display
class on purpose: the class is about *frames* — acquire, present, vblank — class on purpose: the class is about *frames* — acquire, present, vblank —
and the crash screen has to work when nothing else in the system does. and the crash screen has to work when nothing else in the system does.
@ -102,7 +117,7 @@ That is data; the logic stays ours.
- [ ] One directory under `kernel/drivers/<class>/<name>/` - [ ] One directory under `kernel/drivers/<class>/<name>/`
- [ ] Exports a `struct pci_driver` or `struct usb_driver` - [ ] Exports a `struct pci_driver` or `struct usb_driver`
- [ ] Declared in `kernel/drivers/registry.c` **and** added to its array - [ ] Declared in `kernel/drivers/registry.c` **and** added to its array
- [ ] Object added to `KERN_DRIVERS` in the `Makefile` - [ ] Object added to `KERN_DRIVERS` in the `Makefile`, and an option in `config/drivers.conf` if it can be left out
- [ ] Class vtable filled in and `.ops` pointed at it - [ ] Class vtable filled in and `.ops` pointed at it
- [ ] `.attached` implemented if it can drive more than one card - [ ] `.attached` implemented if it can drive more than one card
- [ ] `devclass` set, if it is a USB driver - [ ] `devclass` set, if it is a USB driver

View file

@ -8,8 +8,18 @@ change that only works on one of them is a bug.
```sh ```sh
make # kernel + UEFI loader + root filesystem make # kernel + UEFI loader + root filesystem
make qemu # build and boot it make qemu # build and boot it
make iso # a bootable ISO (the live system) for VMware or real hardware
./write.sh sdX # write that ISO to a USB stick (Linux; it checks what it is writing over)
``` ```
The first build downloads what it needs (musl, toybox, Alpine's packages -- a few
hundred megabytes, kept in `dist/`) and compiles Xorg's dependencies not at all: the
packages are Alpine's binaries, pinned in `ports/alpine/world.lock`. See
[packages](../architecture/packages.md).
Which drivers go into the kernel is `config/drivers.conf` (all of the optional ones by default);
`make DRV_E1000=n` leaves one out for a build. See [devices](../architecture/devices.md).
## Prerequisites ## Prerequisites
| Need | Why | Windows | Linux | macOS | BSD | | Need | Why | Windows | Linux | macOS | BSD |
@ -17,7 +27,7 @@ make qemu # build and boot it
| `clang`, `ld.lld`, `llvm-objcopy`, `llvm-objdump` | cross-compiles the kernel, the 16/32-bit boot code, and musl userland | LLVM installer | `clang lld llvm` | `brew install llvm` | `pkg install llvm` | | `clang`, `ld.lld`, `llvm-objcopy`, `llvm-objdump` | cross-compiles the kernel, the 16/32-bit boot code, and musl userland | LLVM installer | `clang lld llvm` | `brew install llvm` | `pkg install llvm` |
| `make` | — | Git Bash | `make` | Xcode CLT | `gmake` | | `make` | — | Git Bash | `make` | Xcode CLT | `gmake` |
| `perl` | generates the interrupt vector table | Git Bash | usually present | present | `pkg install perl5` | | `perl` | generates the interrupt vector table | Git Bash | usually present | present | `pkg install perl5` |
| `python3` | boot logo, crash-screen QR | python.org (`python`) | `python3` | `brew install python` | `pkg install python3` | | `python3` | boot logo, crash-screen QR, the numbers in the README | python.org (`python`) | `python3` | `brew install python` | `pkg install python3` |
| `qemu-system-x86_64` | running and testing | `scoop install qemu` | `qemu-system-x86` | `brew install qemu` | `pkg install qemu` | | `qemu-system-x86_64` | running and testing | `scoop install qemu` | `qemu-system-x86` | `brew install qemu` | `pkg install qemu` |
| `curl`, `tar` | fetching ports | Git Bash | present | present | present | | `curl`, `tar` | fetching ports | Git Bash | present | present | present |
| `xorriso` **or** `mkisofs` | `make iso` only | `scoop install cdrtools` | `xorriso` | `brew install xorriso` | `pkg install xorriso` | | `xorriso` **or** `mkisofs` | `make iso` only | `scoop install cdrtools` | `xorriso` | `brew install xorriso` | `pkg install xorriso` |
@ -81,10 +91,11 @@ produce.
## Ports ## Ports
`musl`, `toybox` and `mksh` are built from source into `sysroot/`. **This does `musl`, `toybox`, `mksh`, lwIP, uACPI, zstd, ncurses and the rest of `ports/` are built from source
not work on a Windows host** — `make` prints `ports: skipped` and uses whatever into `sysroot/`; Alpine's packages are unpacked into the root from `dist/alpine/`. **This does not work
is already staged in `sysroot/`. Building them needs a Linux host (or any other on a Windows host** -- `make` prints `ports: skipped` and uses whatever is already staged in
Unix); do it once and the staged result serves the Windows builds. `sysroot/`. Building them needs a Linux host (or any other Unix); do it once and the staged result
serves the Windows builds.
## Where the tools live ## Where the tools live

View file

@ -14,47 +14,46 @@ deliberately kept out of `clean`).
schos/ schos/
├─ boot/ uefi/ the PE/COFF loader (splash, F8 menu, kernel load), ├─ boot/ uefi/ the PE/COFF loader (splash, F8 menu, kernel load),
│ common/ the UI it shares. UEFI only -- there is no BIOS path. │ common/ the UI it shares. UEFI only -- there is no BIOS path.
├─ kernel/ The kernel, by subsystem: arch/ core/ dev/ fs/ proc/ sys/ ├─ kernel/ The kernel, by subsystem: arch/ core/ dev/ drivers/ fs/ net/
│ proc/ sys/ acpi/ (uACPI glue, the EC, I2C-HID) gmmos/ (the
│ micro OS under the crash screen) scff/ (the decompressor).
│ drivers/ is the in-kernel drivers, by class: audio bus gpu
│ net storage usb.
├─ ldls/ The Linux driver layer: the .ko loader, the BTF reader, the
│ Linux symbols reimplemented (kpi/), and host-side test tools.
│ The process that runs it is cmd/ldlsd.
├─ include/ Headers shared by kernel, userland, and tools/mkfs, by ├─ include/ Headers shared by kernel, userland, and tools/mkfs, by
│ subsystem -- see "include/" below │ subsystem -- see "include/" below
├─ lib/ First-party userland runtime: _start glue, syscall stubs, ├─ lib/ First-party userland runtime: _start glue, syscall stubs,
│ the initcode blob embedded in the kernel, user.ld │ the initcode blob embedded in the kernel, user.ld
├─ cmd/ First-party programs — one directory per program: ├─ cmd/ First-party programs — one directory per program:
│ cmd/<name>/main.c → sysroot/bin/<name> │ cmd/<name>/main.c → sysroot/bin/<name>
│ schinit and schinitd are PID 1 and its supervisor; vgpud is │ schinit and schinitd are PID 1 and its supervisor; vgpud and
│ the virtio-gpu driver, which runs out here rather than in │ ldlsd are drivers that run out here rather than in the
│ the kernel; socktest/shmtest/nettest/pcitest are suites. │ kernel; socktest/shmtest/nettest/pcitest/xready are suites.
├─ ports/ Third-party software — one directory per port. ├─ etc/ The configuration the image starts with: schinit's job
│ Contains ONLY a Makefile and optional patches/. │ manifests (etc/schinit/*.job), motd, os-release, the zsh files.
├─ projects/ Things that run ON SchOS rather than things that ARE it: ├─ ports/ Third-party software -- one directory per port, containing
│ the compositor, window manager, desktop, and installer. │ ONLY a Makefile and optional patches/. musl, toybox, mksh,
│ lwIP, uACPI, zstd, ncurses, the fonts, fastfetch, kilo; and
│ alpine/, which is the apk root (world, world.lock).
├─ projects/ Things that run ON SchOS rather than things that ARE it.
│ Not part of the kernel build. See projects/README.md. │ Not part of the kernel build. See projects/README.md.
├─ tools/ Host-side helpers, SOURCE ONLY: mkfs.c, the SchFS test ├─ tools/ Host-side helpers, SOURCE ONLY: mkfs.c, mkschfs.c, mkscff.c,
│ suites, sign.pl, vectors.pl, and the PowerShell harnesses. │ the SchFS test suites, vectors.pl, and the harnesses, grouped
│ Their binaries build into build/tools/ -- this directory │ by what runs them (windows/ .ps1, posix/ .sh, python/ .py).
│ used to hold both, which is the one rule this layout has. │ Their binaries build into build/tools/. tools/windows/loc.ps1
│ tools/windows/loc.ps1 counts how much of the tree is first-party. │ and tools/python/stats.py count how much of the tree is
├─ sysroot/ Staging for the disk image. etc/ is hand-maintained; │ first-party.
│ bin/ lib/ sbin/ are populated by `make sysroot-install`. ├─ sysroot/ Staging for the disk image, GENERATED by the build -- edit
├─ dist/ Upstream tarball cache. Survives `make clean`. │ tools/posix/mkaroot.sh and etc/, not this.
├─ Refrence/ A FreeBSD checkout (Refrence/freebsd-src), kept as reference ├─ dist/ Upstream tarball and package cache. Survives `make clean`.
│ material and a parts bin. Never built, never in an image.
│ Read it when a specification is ambiguous; copy from it only
│ after the per-file licence check in docs/licensing.md.
│ A full clone is 2.1 GB and a previous NetBSD one was deleted
│ for that reason. Only sys/dev, sys/net and sys/netinet have
│ ever been read, so a sparse checkout is ~100 MB:
│ git clone --filter=blob:none --sparse <url> Refrence/freebsd-src
│ git -C Refrence/freebsd-src sparse-checkout set │ sys/dev sys/net sys/netinet
├─ src-trunk/ A NetBSD checkout, when present -- reference material and a
│ parts bin. NOT built, NOT installed, must never reach an
│ image. Currently absent; it was deleted for disk space and
│ can be re-fetched sparsely. Copying from it is governed by
│ the per-file licence check in docs/licensing.md §3 --
│ NetBSD is not uniformly licensed.
├─ shell.nix NixOS development shell: the whole toolchain in one file ├─ shell.nix NixOS development shell: the whole toolchain in one file
├─ write.sh Writes schos.iso to a USB stick, safely (Linux).
├─ run.ps1 The Windows-side QEMU launcher.
├─ docs/ architecture/ what it is plan/ what's next ├─ docs/ architecture/ what it is plan/ what's next
│ guides/ how to work history/ why it's like this │ guides/ how to work history/ why it's like this
│ Website/ the site, and the docs reader that renders this
└─ build/ ALL generated output (see below) └─ build/ ALL generated output (see below)
``` ```
@ -92,7 +91,10 @@ build/
├─ kernel/ object files, the linked kernel, kernel.asm ├─ kernel/ object files, the linked kernel, kernel.asm
├─ lib/ libc glue objects, initcode blob ├─ lib/ libc glue objects, initcode blob
├─ cmd/ linked first-party programs ├─ cmd/ linked first-party programs
├─ fs.img the filesystem image ├─ aroot/ the apk root the image is made from
├─ schfs.img the root as a disk image (make qemu)
├─ fs-ram.img the same root as the live ramdisk, fs-ram.scff compressed
├─ iso/ … what make iso assembles
└─ ports/<name>/ └─ ports/<name>/
├─ src/ extracted upstream tarball ├─ src/ extracted upstream tarball
├─ obj/ out-of-tree build directory ├─ obj/ out-of-tree build directory
@ -130,57 +132,44 @@ Local modifications to upstream go in `ports/foo/patches/*.patch` and are
applied after extraction — never by editing files under `build/`, which are applied after extraction — never by editing files under `build/`, which are
wiped on every clean. wiped on every clean.
## Ramdisk root ## The live root
```sh ```sh
make # normal: root on the IDE disk (build/fs.img as hdb) make # normal: the root is a SchFS disk image, build/schfs.img
make ramroot # root from a SCHFS image linked into the kernel make iso # the same root as a compressed ramdisk linked into the kernel
make ramroot # the ramdisk, uncompressed
``` ```
`make ramroot` builds `build/fs-ram.img` (default 6000 blocks, override with `make iso` builds the root as a SchFS image, compresses it
`RAMDISK_BLOCKS=`) and links it into the kernel, so the system boots to a shell ([SCFF](../architecture/scff.md)) and links it into the kernel, so the system boots
with **no data disk attached at all**. That is what makes destructive work on to a shell with **no data disk attached at all** -- which is what the USB stick is.
the real disk survivable — reformatting it, or bringing up a new on-disk format It is also what makes destructive work on a real disk survivable. Nothing written
— and it is the first piece of the installer. to a live root survives a reboot.
Use `make ramroot`, not `make RAMROOT=1`: the flag changes `CFLAGS`, which make Use the targets, not `make RAMROOT=1`: the flag changes `CFLAGS`, which make cannot
cannot see, so objects compiled for one setting would be silently reused for see, so objects compiled for one setting would be silently reused for the other and
the other and `ROOTDEV` would point at the wrong device. The target drops the `ROOTDEV` would point at the wrong device. The targets drop the compiled objects
compiled objects first. Switching back is the same — plain `make` after a first. Going back to the disk root is the same -- run `make clean` or remove
`ramroot` build reuses stale objects, so run `rm -rf build/kernel build/lib `build/kernel build/lib build/cmd` first.
build/cmd` first (or just `make clean`).
The image costs boot time: the loader reads the kernel through the firmware's
file protocol, so a 6 MB ramroot kernel is visibly slower to start than the
350 KB default.
**What may go in it is a licensing question, not only a size one.** The image is **What may go in it is a licensing question, not only a size one.** The image is
linked into the kernel binary, so anything in `sysroot/` when it is built becomes linked into the kernel binary, so anything in `sysroot/` when it is built becomes
part of a distributable artifact. A file staged there under a licence that part of a distributable artifact. A file staged there under a licence that is not
is not ours to redistribute does not merely ride along — it makes the kernel ours to redistribute does not merely ride along -- it makes the kernel itself
itself undistributable. `RAMDISK_EXCLUDE` in the Makefile is the list of undistributable. `RAMDISK_EXCLUDE` in the Makefile is the list of paths kept out,
paths kept out, and enforcing it in the build rather than relying on someone and enforcing it in the build rather than relying on someone remembering to unstage
remembering to unstage something first is deliberate. It has been needed once something first is deliberate. (Linux modules for LDLS are staged by `make
already: a 33 MB test payload landed in `sysroot/` and both overflowed the image ldls-stage`/`ldls-r8169` on request, and `.gitignore` refuses `*.ko`: binaries stay out of git. The
and would have gone into the kernel binary. r8169 set is meant to be staged before `make iso`, since the ISO ships it.)
Three knobs, all in the Makefile, none of them `FSSIZE`: The knobs: `RAMDISK_BLOCKS` (image size), `RAMDISK_NINODES` (the inode table is
*preallocated*: the default of 12000 once cost a quarter of a small image to
describe about 260 files), `RAMDISK_EXCLUDE`. If `mkfs` reports the image is full,
read its message: it prints the metadata breakdown and names the right knob.
| | | The ISO build is fast on purpose (a few seconds when little changed): it copies with
|---|---| `cp -p` and `install -C` so unchanged files keep their time stamps, and toggling the
| `RAMDISK_BLOCKS` | image size, 6000 (× 1 KiB `BSIZE`) | ramdisk does not wipe the tree.
| `RAMDISK_NINODES` | inode table size, 2000 |
| `RAMDISK_EXCLUDE` | sysroot-relative paths to omit, with their subtrees |
`RAMDISK_NINODES` is worth knowing about because the inode table is
*preallocated*: mkfs's default of 12000 inodes cost 1501 of the 6000 blocks — a
quarter of the image — to describe about 260 files. At 2000 it costs 251, and
metadata overall fell from 1596 blocks to 345.
If `mkfs` reports the image is full, read its message: it now prints the
metadata breakdown and names the right knob for whichever image overflowed. It
used to say only "increase FSSIZE in param.h", which is a constant that has
nothing to do with the ramdisk root and sends you to the wrong file.
## Two things that will bite you ## Two things that will bite you

View file

@ -12,9 +12,9 @@ reporting that it started.
| | What it runs | What it proves | | | What it runs | What it proves |
|---|---|---| |---|---|---|
| `make schfs-check` | Host-native unit suites | The filesystem's logic, in about a second | | `make schfs-check`, `make kmem-test`, `ldls-inspect`/`ldls-run` | Host-native unit suites and the Linux-module loader run on the build machine | The filesystem's logic, the slab allocator's bookkeeping, and a `.ko`'s layout, in about a second |
| `tools/windows/bootcheck.ps1` | A real boot, driven over the serial console | That the system answers correctly | | `tools/windows/bootcheck.ps1` | A real boot, driven over the serial console | That the system answers correctly |
| `make crashqr`, `make schcom-test` | A boot, then pixel assertions from the host | That the *screen* is right | | `make crashqr` | A boot, then pixel assertions from the host | That the *screen* is right |
The levels exist because each catches what the one below cannot. A unit test The levels exist because each catches what the one below cannot. A unit test
cannot tell you the kernel fails to boot; a boot test cannot tell you the screen cannot tell you the kernel fails to boot; a boot test cannot tell you the screen
@ -77,21 +77,31 @@ dropped every byte would pass any check made from one side alone.
Under QEMU's user-mode networking the host is `10.0.2.2` seen from inside, and Under QEMU's user-mode networking the host is `10.0.2.2` seen from inside, and
SLIRP connects to the host's loopback — so the listener binds there. SLIRP connects to the host's loopback — so the listener binds there.
## Test programs that run on SchOS
Several first-party programs in `cmd/` exist to be run **on** the system and print which of their checks
failed, because only the system can say whether its own interfaces behave. `bootcheck` runs the ones it
can; the rest are run by hand or by a script:
| | Checks |
|---|---|
| `socktest`, `nettest`, `netecho` | Unix and INET sockets, `SCM_RIGHTS` and credentials, `setsockopt`'s refusals |
| `shmtest`, `memfdtest` | System V shared memory; `memfd` mapped by two processes, and one sent over a socket |
| `pcitest` | `/dev/pci`: claim, map, DMA, interrupts, release |
| `evtest`, `locktest`, `sigtest` | the event descriptors; `flock` and record locks; signals |
| `dynhello` | dynamic linking: TLS, the heap, a forked child that still has its libraries |
| `xready` | every kernel call Xorg makes, before Xorg is started — the evdev queries, the fbdev ioctls, the console calls, the `/sys` files |
| `tools/python/ldls_check.py` | what LDLS needs from a kernel's BTF |
## Pixel assertions ## Pixel assertions
```sh ```sh
make crashshot && make crashqr # crash the kernel, decode the QR out of the framebuffer make crashshot && make crashqr # crash the kernel, decode the QR out of the framebuffer
make schcom-test # boot, run the compositor and a client, check the pixels
``` ```
`tools/windows/splashshot.ps1` counts colours. **That exists because a log-only `tools/windows/splashshot.ps1` counts colours. **That exists because a log-only
harness once passed on a blank screen.** harness once passed on a blank screen.**
`schcom-test` asserts **from the host** — the compositor's own report that it
composited something proves nothing. It runs two overlapping clients and checks
that the newer one wins in the overlap, because a compositor that cannot order
two surfaces cannot host a window manager.
Other capture tools, all in `tools/windows/`: `dispshot`, `fbshot`, Other capture tools, all in `tools/windows/`: `dispshot`, `fbshot`,
`splashshot`, `textshot`, `ttyshot`. `splashshot`, `textshot`, `ttyshot`.
@ -148,10 +158,12 @@ leak.
- **No automated test runs on real hardware.** Everything above is QEMU. - **No automated test runs on real hardware.** Everything above is QEMU.
- **SMP is exercised but not stressed** — boots on 1, 2 and 4 processors are - **SMP is exercised but not stressed** — boots on 1, 2 and 4 processors are
checked; nothing hunts for races. checked; nothing hunts for races.
- **Nothing asserts the desktop by pixel.** `xready` checks the calls Xorg makes; whether Window Maker
and an `xterm` appear on a real screen is looked at in screenshots, not asserted.
- **No fuzzing anywhere**, and no `fsx`/`xfstests` against SchFS. Getting SchFS - **No fuzzing anywhere**, and no `fsx`/`xfstests` against SchFS. Getting SchFS
under those is the main argument for the FUSE implementation in under those is the main argument for the FUSE implementation in
[../plan/roadmap.md](../plan/roadmap.md). [../plan/roadmap.md](../plan/roadmap.md).
- **The SchFS round-trip test needs symlinks and hard links**, so it skips on a - **The SchFS round-trip test needs symlinks and hard links**, so it skips on a
Git Bash host and only runs on Unix. Git Bash host and only runs on Unix.
- **`make ports` does not work on Windows.** musl, toybox and mksh must be built - **`make ports` does not work on Windows.** musl, toybox, mksh and the rest must be built
once on a Unix host and staged. once on a Unix host and staged.

View file

@ -5,7 +5,7 @@ and the few write-ups that have nowhere better to live.
| | | | | |
|---|---| |---|---|
| [kernel.md](kernel.md) | The expensive ones. Boot, SMP, signals, ABI assumptions, `/proc`, input. | | [kernel.md](kernel.md) | The expensive ones. Boot, SMP, signals, ABI assumptions, `/proc`, input, TLBs, a stub that returned a flag word. |
| [schfs.md](schfs.md) | How the filesystem was built, and what each phase cost. | | [schfs.md](schfs.md) | How the filesystem was built, and what each phase cost. |
| [moving-to-linux.md](moving-to-linux.md) | The Windows-to-Fedora move. | | [moving-to-linux.md](moving-to-linux.md) | The Windows-to-Fedora move. |

View file

@ -298,17 +298,8 @@ exported entry points, `nameifrom()`/`nameiparentfrom()`. `sys_fs.c` gained
Every `*at()` syscall now uses these instead of its own inline Every `*at()` syscall now uses these instead of its own inline
absolute-path/`AT_FDCWD`-only check. absolute-path/`AT_FDCWD`-only check.
**Known follow-on issue, not yet root-caused**: heavy `ls`/`ls -lR` use **Known follow-on issue** (heavy `ls -lR` in one long shell session eventually hanging a
across many commands in one long-running shell session can eventually make *later* directory): hunted, and not reproducible — see section 24.
a *later*, unrelated directory hang (reproduced with `ls -lR /var` after
~10 prior commands in the same session; the identical command succeeds
immediately on a fresh boot). Not an inode-table or file-table exhaustion
— both `iget()` and `filealloc()` fail cleanly (panic / return 0
respectively) when full, and neither fired. Smells like a lock or
reference-count leak somewhere in the *at() paths above, or in toybox's
own fd handling, that only manifests after enough accumulated opens/closes.
Next step: instrument `itable`/`ftable` occupancy counts and watch them
across a long `ls`-heavy session to see what's actually climbing.
## 6. Boot speed ## 6. Boot speed
@ -1255,3 +1246,86 @@ is at least as long as the width, and that the aperture holds that many rows.
If not, it goes back to the previous mode and says so in the menu. This proves If not, it goes back to the previous mode and says so in the menu. This proves
nothing about a mode that passes, but it stops a lying one from reaching the nothing about a mode that passes, but it stops a lying one from reaching the
kernel. kernel.
---
## 24. The `ls -lR` hang that was hunted and not found
**Symptom.** After about ten `ls`-heavy commands in one long shell session, a later and
unrelated `ls -lR /var` would hang; the same command succeeded at once on a fresh boot. It
was not inode-table or file-table exhaustion (both fail cleanly), and the first hypothesis
was a lock or reference-count leak in the `*at()` paths.
**What was done.** `/proc/fsstat` was given the occupancy counters the hypothesis asked
for (inode, file and buffer peaks, the SchFS volume lock and transaction owner), and the
workload was made heavier than the report: ten rounds of `ls -lR /bin` interleaved with
writes and `cd` — 33 execs and 13,378 tree descents. Nothing climbed:
inode_peak 9 / 200 file_peak 9 / 512
buf_pinned_peak 1 / 256 schfs0 locked 0 tx_owner_pid 0 tx_depth 0
**Conclusion.** The hypothesis is eliminated, and the bug is very likely gone — fixed in passing
by the `*at()` dirfd rewrite, the `getdents` cookie rewrite, or SchFS becoming the root, all of
which landed after it was reported. It was closed rather than chased. The instrumentation
stayed: if it happens again, `/proc/fsstat` names the table, or shows `locked 1` with
`tx_owner_pid 0`, which is an orphaned volume lock.
Two real defects turned up on the way and were fixed, neither proven to be the cause:
`holdingsleep()` never compared the owning pid, which disarmed all seven `panic()` assertions
built on it; and SchFS's `tx_mine()` compared a bare `struct proc *`, which a recycled ptable
slot makes ambiguous.
**The transferable part.** A hang you cannot reproduce still earns its instrumentation. The
absence of a result *is* the result, and the right place to leave it is where the next person
with the same symptom will look.
## 25. Epiphany's heap was corrupted by a page nobody had unmapped yet
**Symptom.** Epiphany (WebKit: forty threads on four processors) died at a different address
on every run; musl's allocator was the first to notice, a group's metadata no longer pointing
back at its group.
**Diagnosis.** Nothing was freed twice. One thread unmapped a range, `mmap` handed the same
addresses straight back, and a thread on *another* processor went on reading and writing the
**old** pages through its TLB: stores vanished, loads were stale. Deferring the free of the
unmapped pages had already kept a stale translation from reaching a page someone else owned;
it did not keep one address space *coherent*.
**Fix.** Unmapping from a shared address space interrupts the processors running it and waits
until each has reloaded `CR3` (`tlb_shootdown()`, `kernel/arch/x86_64/vm.c`).
**The transferable part.** Threads turned an invisible assumption — one CPU's TLB is the only
one that matters — into the loudest bug in the tree, in the one program that used the most
threads. Memory corruption that moves every run is a coherence bug until proven otherwise.
## 26. Every MDIO read returned `0x02460246`
**Symptom.** Linux's `r8169` under LDLS, on the laptop, read its PHY's ID as `0x02460246`.
**Diagnosis.** `0x246` is a value of `RFLAGS`, not a register. `mdio-bus` keeps the word it
has just read in `r11` across `preempt_enable()`, which on x86 is `decl %gs:__preempt_count; je`
to a call that preserves every register. LDLS had given `__preempt_count` zeroes, so the count
reached zero after the decrement and the call was made — into a logging stub whose `syscall`
instruction leaves `RFLAGS` in `r11`.
**Fix.** `__preempt_count` starts at `PREEMPT_ENABLED` (x86 keeps `NEED_RESCHED` inverted), and
`preempt_schedule` and its notrace twin are a bare `ret`.
**The transferable part.** A stub that returns zero is not safe when zero changes which path
runs — the same family as the five in [../architecture/ldls.md](../architecture/ldls.md). The
tell here was a value that was a plausible *flag word* rather than a plausible *register*.
## 27. GLib's network monitor spun at 100%
**Symptom.** Epiphany's main thread consumed a full core in `poll()` returning at once.
**Diagnosis.** GLib sizes each netlink datagram with `recvmsg(MSG_PEEK|MSG_TRUNC)` and no buffer.
The kernel answered 0, GLib read 0 bytes, the datagram stayed queued, and `poll()` said readable
again, for ever. A drained netlink socket also reported hangup, because `sock_hup` took "no peer"
for "closed".
**Fix.** `recvmsg` on netlink peeks without consuming and with `MSG_TRUNC` returns what is waiting.
**The transferable part.** A kernel that accepts a flag and does nothing with it turns a
well-formed program into a busy loop. Do it, or refuse it.

View file

@ -267,42 +267,33 @@ copied file is a file to be understood, attributed and maintained forever.
--- ---
## 6. LDLS, and the distribution boundary ## 6. LDLS, and shipping Linux's drivers
[architecture/ldls.md](architecture/ldls.md) runs Linux drivers. This section [architecture/ldls.md](architecture/ldls.md) runs Linux drivers. This section used to be about a
was written when SchOS was Apache-2.0 and the boundary below was a legal distribution boundary: written when SchOS was Apache-2.0, it said a GPL-2.0-only `.ko` could not be
necessity. Under GPL-2.0-or-later it is a **project policy** instead: shipping shipped and so SchOS would ship the layer and never the modules. **That no longer holds.** SchOS is
a GPL-2.0-only `.ko` with SchOS would now be lawful, provided the source goes GPL-2.0-or-later, GPL-2.0-only code combines with it, and the owner has decided that Linux code and
with it. It is still not done, because a module we never ship is a module we firmware blobs are shipped. The ISO carries Linux's **r8169 set** — `mdio-bus`, `libphy`,
never have to vouch for, track upstream, or provide source for. `mdio_devres`, the Realtek PHY driver and `r8169`, unmodified — and the RTL8168 PHY firmware.
### What shipping them asks of us
- **The licence stays with the code.** The modules are GPL-2.0-only, the firmware is Realtek's, redistributed
under linux-firmware's terms. `NOTICE` lists both, with where they come from.
- **The source goes with the binary** (GPLv2 §3). The modules are stock builds of the Linux kernel version
that `make ldls-r8169` took them from, so the corresponding source is that release of Linux, unmodified.
- **Nothing combines at link time.** The modules are loaded into an ordinary process by our loader; our code
is GPL-2.0-or-later, theirs GPL-2.0-only, and both are GPLv2-compatible whichever way one reads "combined".
- **No binary in git.** The repository stays source-only: `.gitignore` refuses `*.ko`, and `make ldls-r8169`
stages the set from a Linux build into the image.
### LDLS is ours ### LDLS is ours
Every file under `ldls/` is ours, GPL-2.0-or-later. The loader was written from Every file under `ldls/` is ours, GPL-2.0-or-later. The loader was written from the ELF-64 gABI and the
the ELF-64 gABI and the x86-64 psABI; the shim is written under the rule in x86-64 psABI; the shim is written under the rule in [architecture/ldls.md](architecture/ldls.md). Copying
[architecture/ldls.md](architecture/ldls.md). Copying Linux code into it would Linux code into it would no longer be a licence problem, but the shim's value is that it is small and
no longer be a licence problem, but the shim's value is that it is small and
understood, so the existing rule stands. understood, so the existing rule stands.
### The combination is the user's
| | licence | who distributes it |
|---|---|---|
| `ldls/` — loader, shim, host | **GPL-2.0-or-later** | us |
| a `.ko` | GPL-2.0-only, usually | the user's distribution |
| the two running together in a process | — | **nobody** |
GPLv2 §0 is explicit about where its obligations attach:
> Activities other than copying, distribution and modification are not covered
> by this License; they are outside its scope. **The act of running the Program
> is not restricted.**
So: we ship LDLS and no modules. The user obtains a module from wherever they
get their software and runs it on their own machine. `RAMDISK_EXCLUDE` and the
`*.ko` rule in `.gitignore` keep that boundary mechanical rather than
intentional.
### `EXPORT_SYMBOL_GPL` does not apply ### `EXPORT_SYMBOL_GPL` does not apply
Linux marks much of its internal API `EXPORT_SYMBOL_GPL`, and modules carry Linux marks much of its internal API `EXPORT_SYMBOL_GPL`, and modules carry
@ -322,7 +313,7 @@ show.
### BTF: the layouts are not in our source at all ### BTF: the layouts are not in our source at all
§3 of the plan has one uncomfortable row — struct layouts. A compiled driver Rule 3 of [architecture/ldls.md](architecture/ldls.md) has one uncomfortable row — struct layouts. A compiled driver
indexes fields by offset, so the shim has to reproduce them exactly, and a indexes fields by offset, so the shim has to reproduce them exactly, and a
field-for-field match *looks* like transcription even when it is not. field-for-field match *looks* like transcription even when it is not.

3705
docs/niveus/dist/niveus.css vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,17 +7,16 @@ A finish line, so that the list of open work stops being infinite.
## Why this document exists ## Why this document exists
[schapi.md](schapi.md) §3 says the stability freeze starts at 1.0 and covers [schapi.md](schapi.md) §3 says the stability freeze starts at 1.0 and covers
whatever is marked `stable` at that moment. **1.0 was never defined**, which whatever is marked `stable` at that moment. 1.0 was never defined, which means
means there has been no finish line — and with no finish line every gap is there was no finish line — and with no finish line every gap is equally mandatory,
equally mandatory, the list only grows, and the project feels larger every week the list only grows, and the project feels larger every week regardless of how much
regardless of how much gets done. gets done.
That feeling is structural, not a judgement about progress. This document fixes That feeling is structural, not a judgement about progress. This document fixes the
the structure by drawing a line and putting most of the open work on the far structure by drawing a line and putting most of the open work on the far side of it.
side of it.
> **Anything not named in this document is not part of 1.0.** It is not > **Anything not named in this document is not part of 1.0.** It is not cancelled,
> cancelled, not forgotten, and not up for reconsideration until 1.0 ships. > not forgotten, and not up for reconsideration until 1.0 ships.
--- ---
@ -26,31 +25,42 @@ side of it.
> **A machine one person can sit down at and use for an afternoon without > **A machine one person can sit down at and use for an afternoon without
> falling off an edge.** > falling off an edge.**
Concretely — boots on real hardware, roots on SchFS, runs a shell and the real Concretely — boots on real hardware, roots on SchOS's own filesystem, runs a shell
toybox userland, is on the network, and puts a windowed terminal on the screen. and a real userland, is on the network, and puts a windowed desktop on the screen.
It is deliberately a **small complete system** rather than a rich incomplete It started as a deliberately small system: *no third-party applications and no package
one. SchOS 1.0 is not Linux, does not run third-party applications, and has no manager*. That has changed on its own, and is worth recording rather than tidying away:
package manager. It is a system that works, end to end, with nothing in it that the root is an **apk root** ([packages](../architecture/packages.md)), Alpine's
lies about what it did. programs run on it unmodified, and the desktop is Xorg with Window Maker
([desktop](../architecture/desktop.md)). What did not change is the bar: nothing in the
system lies about what it did.
### The acceptance test ### The acceptance test
One unattended run, asserted from the host, the way everything else here is One unattended run, asserted from the host, the way everything else here is tested:
tested. When this passes, it is 1.0:
``` ```
boot on real hardware from a USB stick boot on real hardware from a USB stick
→ SchFS root mounts, generation counter advances → SchFS root mounts, generation counter advances
→ schinit starts the compositor → schinit starts shells on the virtual terminals
→ a terminal window appears and is asserted by pixel → X starts; a window appears
→ in it: ls, cat, a file written, a network fetch → in it: ls, cat, a file written, a network fetch over HTTPS
→ reboot
→ the file is still there and the checksum matches
``` ```
Nothing in that sentence is a feature list. Every clause is something that is **It passes. 1.0 is reached.** Where each clause stands:
currently either broken, missing, or unproven.
| Clause | State |
|---|---|
| boot from a USB stick on real hardware | **done** on an AMD B550 desktop, an ASUS TUF A15 laptop and an Ivy Bridge HP all-in-one |
| SchFS root, generation counter | **done** — the root is a SchFS image, in RAM when live |
| a terminal to type in | **done** — a shell on the first six of the twelve virtual terminals (`etc/schinit/*-shell.job`) |
| X starts, a window appears | **done** (`startx` → Window Maker + xterm); *asserted by pixel* has no automated test yet — `bootcheck` drives the serial console, and the screenshots are looked at |
| `ls`, `cat`, a file written | **done** |
| a network fetch over HTTPS | **done** (`wget`, `curl`, `apk`) |
The test used to end with *reboot — the file is still there*. That clause is **dropped**, deliberately: 1.0 is a **live**
system. The root lives in RAM and nothing written to it survives a reboot, and that is what the project is calling finished.
Persistence (an installer, or a persistent partition) is after 1.0 — see [roadmap.md](roadmap.md) §5.
--- ---
@ -59,11 +69,10 @@ currently either broken, missing, or unproven.
**No new capability is started until this register is empty.** **No new capability is started until this register is empty.**
The failure this exists to stop is not laziness and not scope creep — it is The failure this exists to stop is not laziness and not scope creep — it is
*abandonment by promotion*. A thing gets built to the point where it works, the *abandonment by promotion*. A thing gets built to the point where it works, the next
next thing becomes more interesting, and the first one is left in a corner: thing becomes more interesting, and the first one is left in a corner: present in the tree,
present in the tree, not finished, not deleted, and not visible in any list. not finished, not deleted, and not visible in any list. Enough of those and the tree stops
Enough of those and the tree stops matching its own documentation, which is the matching its own documentation.
state this register was written to end.
Two rules, and they are the same rule seen from either end: Two rules, and they are the same rule seen from either end:
@ -71,73 +80,46 @@ Two rules, and they are the same rule seen from either end:
> >
> **Adding a row requires closing one.** Two things in flight. Never three. > **Adding a row requires closing one.** Two things in flight. Never three.
A row leaves this table in one of exactly two ways. Either it is **finished** — A row leaves this table in one of exactly two ways. Either it is **finished** — and finished
and finished means a description exists in [../architecture/](../architecture/), means a description exists in [../architecture/](../architecture/), not that the code works —
not that the code works — or it is **deleted**, and deleted means gone from the or it is **deleted**, and deleted means gone from the tree, not marked for deletion. A
tree, not marked for deletion. A directory that has said "to be deleted" for directory that has said "to be deleted" for months is the corner, not the cure.
months is the corner, not the cure; `projects/schwm/` and `projects/schde/`
proved that and are now actually gone.
### Built, and not connected to anything ### Built, and not connected to anything
The galling ones. The work exists, is tested, and nothing uses it.
| | Where it stopped | | | Where it stopped |
|---|---| |---|---|
| ~~**The text layer**~~ | **Half closed, and the half that closed is the one that mattered.** The console draws real TrueType through `font_ops` now: cells hold a codepoint, the cell size comes from the font's measured advance, and `kernel/dev/ttf.c` has a caller that is not a self-test. What is still unused is `text.c`'s *proportional* path — the console remains a fixed grid. See Gate 0. | | **The in-kernel LDLS window** | `vm.c` maps the kernel image a second time in the top 2 GB at every boot, and `include/ldls/ldls.h` describes a `/dev/ldls` loader — the start of loading Linux modules *into the kernel*, which [ldls.md](../architecture/ldls.md) rule 2 says is not how it is done. Nothing calls either. **Delete it**, or decide. |
| **A font from disk** | Still true, and now deliberate rather than pending: the console typeface is linked into the kernel image, because a panic during boot has to be readable before there is a filesystem. Loading a *second* font from disk is the open part. | | **`display_ops`** | `acquire` and `wait_vblank` are in the vtable, implemented by one driver at most, with no consumer: Xorg draws through `/dev/fb0`. Kept for the GPU work ([gpu.md](gpu.md)); that is the reason it stays. |
| **Socket activation** | `schinitd` parses `[Socket name]` and remembers the path. Nothing holds the socket. **This is the entire reason for copying launchd**, and it is the half that was not built. | | **Socket activation** | `schinitd` parses `[Socket name]` and remembers the path. Nothing holds the socket. This is the entire reason for copying launchd, and it is the half that was not built. |
| **`display_ops.wait_vblank`** | In the vtable, implemented by the VBE driver, left null by virtio-gpu. A compositor that needs it cannot rely on it. | | **`sys_delete`, `sys_show`, `sys_showstat`** | Orphaned entries from the old v1 filesystem's recycle bin. They die with `fs.c`. |
### Started, and stopped part-way ### Started, and stopped part-way
| | Where it stopped | | | Where it stopped |
|---|---| |---|---|
| **`foundations.md`** | All but item 4 (`setsockopt`) built: descriptor passing, `memfd`, filesystem-named `AF_UNIX`, dynamic linking, and epoll/eventfd/timerfd/signalfd. | | **LDLS on real hardware** | `r8169` registers its interface on the laptop and reads its PHY; whether it carries traffic there is **not confirmed**. The native driver is out of the registry and stays as the fallback. |
| ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". | | **The laptop's embedded controller** | The EC is found and served (battery and AC read through it); **SCI queries are not handled**, so events from it are not seen. |
| ~~**`ldls.md`**~~ | **Closed.** L0 through L4 built: the loader handles 2,440 stock modules, and both `8139too` and Linux's `e1000` run in a SchOS process through `/dev/pci` with `ping` working over each. The e1000 run is a handover — the kernel was driving that NIC and gave it up. Described in [../architecture/ldls.md](../architecture/ldls.md). Two drivers, one kernel version, one machine. | | **Epiphany (WebKit)** | `apk add epiphany` installs it; its web-process child dies at start-up ("short read from the peer"). NetSurf runs. Not diagnosed. |
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
| ~~**`debugger.md`**~~ | **Closed.** D0 through D5 built, including the network transport: `schdbgd` bridges TCP to `/dev/dbgro`, ships disabled, binds loopback, and cannot reach a view that changes state. Described in [../architecture/debugging.md](../architecture/debugging.md). |
**The row LDLS spent, and gave back.** `hybrid.md` closed — H3 built, H4
withdrawn — and `ldls.md` took the slot it freed. It has since closed too, so
the slot is free again. Worth being honest that this is
the largest thing ever to enter this register, and that it was **explicitly not
part of 1.0**: nothing in the acceptance test needs a foreign driver. It was
taken on because the mechanisms it uses were finished and idle, and because
the alternative to it is writing a driver for every device that exists.
**The row the debugger spent, and gave back.** Adding one requires closing one,
and *A released PCI device* closed when H2 landed. It is worth saying plainly
that the debugger is not a capability of the OS the way every other row here is:
nothing in it ships to a user or appears in the acceptance test. The case for it
is that **Gate 0 is a list of bugs to find in a kernel that could not be asked
anything** — so it is the instrument the rest of this register is surveyed with,
and it earned its slot by making every other row cheaper to close.
It has closed. **Every plan in `docs/plan/` that was open is now closed**, which
means the next thing is Gate 0 itself: the survey, with the instrument built.
**Closed since this register was written.** *A released PCI device* — release is
now symmetric with claim (bus mastering cleared before the DMA is freed, a
refusal when the device is the only display), a supervised job starts the
replacement, and it is described in
[../architecture/devices.md](../architecture/devices.md) under "Taking it back".
### Named in a "what it does not do" and still true ### Named in a "what it does not do" and still true
Not corners — these were written down honestly on the day and stayed written Not corners — these were written down honestly on the day and stayed written down. They are here
down. They are here so the register is the whole picture rather than the so the register is the whole picture rather than the flattering part of it. Each is described where
flattering part of it. it lives.
| | | | | |
|---|---| |---|---|
| `/proc/<pid>/exe` is a regular file | so `readlink()` fails and `ps -o comm` prints `[init]` | | No page cache | writable `MAP_SHARED` of a file is refused — [kernel.md](../architecture/kernel.md) |
| `getrusage` | does not distinguish `RUSAGE_CHILDREN` from `RUSAGE_SELF` | | `mprotect` is a no-op, nothing has NX | [kernel.md](../architecture/kernel.md) |
| No slab allocator | every subsystem's fixed-size table is a consequence | | No routing table | first interface wins, which is a rule and not routing — [networking.md](../architecture/networking.md) |
| No `getsockname`/`getpeername` | and `accept()` does not report an AF_INET peer |
| No routing table | first interface wins, which is a rule and not routing |
| No descriptor garbage collector | a socket passed over itself and never received leaks the pair | | No descriptor garbage collector | a socket passed over itself and never received leaks the pair |
| udev sees only the kernel's own devices | PCI and USB are not in the `/sys/devices` tree — [udev.md](../architecture/udev.md) |
| No EHCI driver | USB 2.0 ports wired only to EHCI do nothing — [usb.md](../architecture/usb.md) |
| Touchpads are mice | no two-finger scroll — [desktop.md](../architecture/desktop.md) |
| `ru_stime` is zero | [kernel.md](../architecture/kernel.md) |
| No IOMMU | a driver in a process is isolated against bugs, not malice — [devices.md](../architecture/devices.md) |
| One audio chip family | AC'97 only; laptops have HDA — [audio.md](../architecture/audio.md) |
### Frozen — scoped, unstarted, and not to be started ### Frozen — scoped, unstarted, and not to be started
@ -147,431 +129,75 @@ Neither cancelled nor in progress. **Do not open these.**
|---|---| |---|---|
| `projects/schinst/` | the installer, the installation environment, `.sip` | | `projects/schinst/` | the installer, the installation environment, `.sip` |
| `projects/schfs-linux/` | SchFS as a filesystem for Linux | | `projects/schfs-linux/` | SchFS as a filesystem for Linux |
| [roadmap.md](roadmap.md) §4 | the interactive crash screen | | [gpu.md](gpu.md) | amdgpu and nouveau through LDLS — measured, with the work it needs written down |
| [roadmap.md](roadmap.md) §6 | SchFS beyond SchOS | | [finch.md](finch.md) | Finch inside SchOS (the language itself is finished and lives in its own repository) |
| [schapi.md](schapi.md) | the API layering, designed and not built | | [schapi.md](schapi.md) | the API layering, designed and not built |
Each of those is well argued and none of them is on the path to the acceptance Each of those is well argued and none of them is on the path to the acceptance test above. That is
test above. That is the whole test for whether something belongs in this section: the whole test for whether something belongs in this section: **not "is it worth doing" but "does 1.0
**not "is it worth doing" but "does 1.0 fail without it".** fail without it".**
--- ---
## The four gates ## The gates
In order. **Do not start a gate before the one before it closes**, and closing In order. **Do not start a gate before the one before it closes**, and closing means a description exists in
means a description exists in [../architecture/](../architecture/) — see [../architecture/](../architecture/).
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
### Gate 0 — Fix what is already wrong | Gate | What | State |
|---|---|---|
**This comes first, before any new capability.** The desktop work is the | **0 — Fix what is already wrong** | the SMP register corruption, `setsockopt` that lied, timestamps that read 1970, the `ls -lR` hang (hunted, not reproducible — [history](../history/kernel.md) §24), … | **closed** |
temptation and it is the wrong thing to do next: a compositor built on a kernel | **1 — The foundations** | descriptor passing, a shareable memory object (`memfd`), `AF_UNIX` in the filesystem, dynamic linking, the event descriptors | **closed** — [kernel.md](../architecture/kernel.md), [networking.md](../architecture/networking.md) |
with an unexplained hang and a socket layer that reports success for things it | **2 — Init as a role** | `schinit`, the supervisor, the heartbeat, PID 1 re-exec'd in place | **closed** — [init.md](../architecture/init.md) |
did not do will fail in ways indistinguishable from its own bugs. That is the | **3 — The desktop** | Xorg on the framebuffer, Window Maker, a terminal | **closed** — [desktop.md](../architecture/desktop.md) |
same argument that put `netdev_selftest()` before the TCP/IP stack, and it was | **4 — A root that survives a reboot** | dropped from 1.0 by decision; it is the installer, after 1.0 | **not in 1.0** |
right then.
**Things that are actively wrong:**
- [x] ~~**The `ls -lR` hang.**~~ **Not reproducible.** Instrumented and hunted;
see below. Left here rather than deleted because the absence of a result
is the result.
- [x] ~~**`setsockopt` returns 0 without setting anything.**~~ Six options now
genuinely reach lwIP's pcb and everything else is `ENOPROTOOPT`. Round-trip
checks in `cmd/socktest`, which a permissive stub cannot pass: it has to
read back a value it was set *and* refuse an unknown option.
- [x] ~~**`accept4` silently drops its flags.**~~ It is its own table entry now
rather than an alias for `accept` — an alias would have had to read a
fourth argument that `accept` never passes, inventing a `SOCK_NONBLOCK`
out of whatever the caller last left in that register.
- [x] ~~**Timestamps on everything created after boot read 1970.**~~ Both
`know()` (SchFS) and `now_sec()` (v1) returned `uptime()` under comments
saying the kernel had no wall clock. It has had one since `time_init()`
started reading the RTC; the comments outlived the condition. Two errors,
not one — `uptime()` counts 10 ms ticks and they were used as seconds, so
the clocks also ran 100× fast.
- [x] ~~**User processes are corrupted on SMP.**~~ **Found and fixed.**
`syscall_entry` parked `%rax`, `%rcx` and `%rdx` in three *globals*
across the `FS_BASE` swap, under a comment arguing that `SYSCALL`
clearing `IF` made it safe. `IF` answers for an interrupt on *this* CPU
and says nothing about a second CPU running the same three stores — and
`%rcx` is the user return address `SYSRET` uses, so the CPU that lost the
race returned its process into another process's instruction stream.
That is the whole of it: `rip 0x0` was a process handed a zero and
following it, and `0xffffffffffffffea` was `-EINVAL` in a swapped `%rax`
used as a pointer. Per-CPU words at `%gs:32..48` now. Measured at four
CPUs against the same pristine image: **10 faults in 10 boots before, 0
in 18 after.** See [../architecture/kernel.md](../architecture/kernel.md),
"Per-CPU state".
- [x] ~~**`$(command substitution)` hangs the shell.**~~ **Not a kernel bug.**
`ports/zsh/Makefile` answered configure's "does `kill(pid, 0)` return
ESRCH correctly?" with `no`, which makes zsh `#define ESRCH EINVAL`.
`waitforpid()` ends its wait on `errno != ESRCH`, so with the kernel
correctly returning ESRCH (3) and zsh comparing against EINVAL (22), the
shell waited for ever on a child it had already reaped. One word in a
cross-compile cache. Autoconf's own default for that check is `yes`;
omitting the line would have been right. See
[../history/kernel.md](../history/kernel.md), section 7.
- [x] ~~**`getrusage` does not distinguish `RUSAGE_CHILDREN`** from
`RUSAGE_SELF`.~~ It did not distinguish anything: it ignored `who` and
reported the machine's *uptime* as the caller's user time, so every
process of every age answered with the same number. Per-process CPU time
is sampled at the timer tick now, children's time is rolled up on reap,
and `wait4()`'s fourth argument — a `struct rusage *` that was read off
the stack and thrown away — is filled. That last one is why zsh's `time`
reported `16777216.01s user` for everything: 2^24 seconds is
uninitialised memory, and it never varied because it was never written.
`time` now says 97% cpu for a busy loop and 0% for `sleep 2`.
`ru_stime` is a known zero; see
[../architecture/kernel.md](../architecture/kernel.md).
- [x] ~~**`/proc/<pid>/exe` is a regular file, not a symlink.**~~
`lrwxrwxrwx ... exe -> /bin/schinit`, and `readlink` returns it. It also
needed a real `st_size`, which every other procfs file deliberately
reports as zero: `readlink(2)` bounds its copy by it, so a symlink of
size 0 resolves to the empty string — which looks like it worked and is
worse than the regular file was.
- [x] ~~**OVMF memory reporting.**~~ The region-list fix did close it. 1 GB
reports 1017 MB usable, 2 GB reports 2041 MB, which is the right shape:
the firmware keeps about 7 MB either way.
- [x] ~~**`ps` printed a header and nothing else.**~~ Found while checking the
`exe` fix. `ps` with no arguments shows only processes on *this*
terminal, and `/proc/<pid>/stat` hardcoded `tty_nr` to 0, so nothing
could ever match — `ps -e` always worked, which is what hid it. Fields
14-17 (utime/stime/cutime/cstime) were hardcoded zero too, so `TIME` was
`00:00:00` for everything however long it had run.
**Work already done and not connected:**
These are the galling ones — the replacement exists, is tested, and is unused.
- [x] ~~**Point the console at a real typeface.**~~ It draws JetBrains Mono
through `font_ops` and `ttf.c`. The blocker was not the font: `vtcell.ch`
was **one byte** holding a CP437 index, so `Ą` (U+0104) could not be
*stored*, whatever was drawing it. The cell is a `uint32` codepoint now
and the cell size comes from the font's measured advance.
- [x] ~~**Delete the cell grid.**~~ Done as far as it should be done, and the
rest of it retired — which needs explaining, because the item as written
asked for something a terminal cannot be.
`include/gfx/text.h` made the case against the grid in four charges: it
cannot do variable-width glyphs, cannot position text off a multiple of
eight, cannot anti-alias, and cannot hold a codepoint above 255. **All
four were about the drawing layer, and all four are now false** — glyphs
carry metrics, PSOGM positions text at arbitrary pixels, the TrueType
path blends 8-bit coverage, and `vtcell.ch` is a `uint32`.
What is left is a grid in `vt.c`, and that one is not in the way: it is
what a terminal *is*. VT100 escapes address columns, `TIOCGWINSZ`
reports rows and columns, and every program that draws a box assumes
both. Deleting it would not modernise the console, it would stop it
being a terminal.
So the real remainder was the duplicate: `gfx.c` walked `font_data`
itself and blitted glyphs its own way, while `text.c` did the same job
into a `struct surface` for nobody but a self-test. Two implementations
that had to agree about a cell size only one of them was told had
changed — and they did not agree, which is why every line of PSOGM once
drew the next character's bitmap underneath itself. There is one blitter
now: `gfx.c` hands `text_draw_glyph` the cell *as a surface of its own*,
so clipping a tall glyph to its line is the same bounds check that clips
to a screen. The bitmap is reached through `font_builtin()` like any
other provider, and `gfx.c` no longer includes `font.h`.
- [x] ~~**Load a font from disk**~~ and point the console at it. The built-in
one is linked in on purpose — an early panic has to be readable — so this
is about a *replaceable* font, not about having one. Drop a TrueType file
at `\schos\console.ttf` on the ESP and the console is drawn with it.
**The loader reads it, not the kernel**, and that is the whole design
decision. The kernel could open the file once the root filesystem is
mounted — but the console has been drawing since long before that, so
switching then means re-measuring the cell and re-laying out every
terminal mid-boot, which wipes the kernel log off the screen at exactly
the moment a machine being brought up for the first time needs it. The
loader allocates the pages as `EfiReservedMemoryType`, so the E820
handoff marks them reserved, the page allocator never offers them, and
the kernel uses the file *in place* — no copy, and no need for a
contiguous allocator the kernel does not have.
- [x] ~~**Interrupt-driven receive.**~~ The e1000's handler calls
`netdev_rx_signal()`, which ends the network thread's wait early; the frame
is still *pulled* by the thread, because an interrupt handler can neither
sleep on the buffer cache nor take the lwIP lock. The wait stays on the
tick so lwIP's timers keep their floor. Proven rather than assumed:
`/proc/netif` gained `wake_rx`/`wake_tick`, and across three pings
`rx_frames` and `wake_rx` both went 2 -> 6.
**Structural, because everything after this compounds on it:**
- [x] ~~**A slab or pool allocator.**~~ `kernel/core/kmem.c`. Slabs are pages
from `kalloc()` with a header at the front; free objects hold the free
list in their own first eight bytes, so an object needs no header and
freeing needs no search — the object's page base *is* its slab, and the
slab names its cache. Two interfaces: named `kmem_cache` for a subsystem
with one kind of thing, and `kmalloc`/`kmfree` over power-of-two classes
for everything else. Visible in `/proc/slab`, asserted by
`make kmem-test` on the host — the same arrangement as `schfs-objtest`,
so the bookkeeping is checked in a second rather than in a boot.
**It does not do what this row said it would, and that is worth
recording.** The claim was that every fixed table — `NPROC`, `NSOCK`,
`NDEVICE`, `NFILE`, `NINODE`, `NBUF` — exists to work around `kalloc`.
Measured: `struct proc` is 10656 bytes, `struct socket` 4928, `struct
buf` 4272. **All three are larger than a page**, so a page-backed slab
cannot hold them whatever it does; `NINODE` is a cache and is supposed to
be bounded; and `NFILE`/`NDEVICE` are 48 and 120 bytes, where the table
costs 24 KB and 3.8 KB and the ceiling has never been reached. The tables
were not all working around `kalloc`. Freeing the big ones needs a
contiguous multi-page allocator, which is a different thing and is not
written.
What it does fix is real and was measured rather than assumed. A pipe is
640 bytes and came from a whole 4096-byte page, so every `|` in a shell
command burned 3.5 KB it never touched — **six pipes per page now**. And
`procread()` built its answer in a 2048-byte array on the kernel stack,
under a comment that had warned about exactly that since it was written;
its frame is 64 bytes now instead of ~2100.
- [x] ~~**SchFS on-disk format version policy.**~~ Done. `SCHFS_VERSION` for
frame changes, plus `compat`/`ro_compat`/`incompat` feature flags in what
was `reserved0` — so existing images keep every field offset and read
unchanged. `uber_check()` now distinguishes *damaged* (step over it; that
is what the ring is for) from *too new* (stop), because falling back to an
older uberblock mounts a stale filesystem and reports success. Proven on a
forged image: the old code mounted the stale root and exited 0.
Still owed: an actual upgrade step, which is only writable once there are
two formats. See [../architecture/schfs.md](../architecture/schfs.md).
**Found while doing the above, and fixed:**
- [x] ~~Every `AF_INET` socket failed with `ENFILE` until DHCP bound.~~
`inet_new()` required an *address* before it would hand out a socket, and
reported the refusal as "file table overflow". Creating a socket is not
the same as having somewhere to send; a bind or connect with no route
fails on its own with an error that says so. `net_is_ready()` (lwIP up) is
now distinct from `net_is_up()` (DHCP bound).
- [x] ~~`socktest` asserted "AF_INET is refused, not faked".~~ True when written,
false since AF_INET landed — and **green the whole time**, because the
suite runs before DHCP binds and the `ENFILE` above made the refusal look
real. A test asserting a failure has to say *which* failure.
**The suite itself:**
Both of these were found by running `make schfs-check` rather than by reading
it, and both predate the current work — confirmed by stashing and re-running.
They are here rather than filed as nice-to-haves because a suite that is red for
known reasons stops being read, and then it is red for unknown reasons too.
- [x] ~~**`make schfs-check` fails at baseline.**~~ Fixed, and it was not only a
test bug. The allocation map was taken through a `palloc` callback and
there was **no way to give it back** — no `schfs_space_release`, and a
comment in `schfs_space_init` reading *"caller frees what was taken"* for
an API the caller did not have.
So the kernel leaked the whole map on **every unmount**, up to 2 MB a
time, and `schfs_space_init`'s own error path stranded a page it had
already taken because `npages` was only raised after *both* allocations
succeeded. The test's 518 leaked pages were the symptom that pointed at
it.
The map now carries its own `pfree` so it can only be freed by the
allocator that filled it, `schfs_vol_open`/`schfs_vol_format` release any
map the volume already holds, and unmount releases. `schfs_objtest`
asserts the balance — 520 pages taken, 520 given back — so the next leak
fails the suite instead of being absorbed by it.
- [x] ~~**31 UndefinedBehaviorSanitizer errors.**~~ Fixed, plus two the test
never reached. One root cause: a leaf's value area holds variable-length
records packed end to end, so a record begins wherever the last one ended
— a multiple of 8 only by luck — and the code read them by casting a
pointer into the block, which tells the compiler they are aligned when
they are not.
The seven record types that live at arbitrary offsets now carry
`SCHFS_PACKED`. **No on-disk layout changed**, and that was measured
before it was applied rather than assumed: every one already had explicit
`pad` fields, so sizes and member offsets are identical with and without
the annotation. Seven `_Static_assert`s now pin those sizes, because a
record that silently grows misreads every existing volume from that point
on and looks like corruption rather than like a struct that changed.
`schfs_node_hdr`, `schfs_item`, `schfs_kptr` and `schfs_uberblock` are
deliberately left alone — the node header is exactly 32 bytes, so
everything after it is already 8-aligned, and annotating them would cost
byte-wise access on the tree's hottest paths to fix a problem they do not
have.
Verified by reading an image written *before* the change: full
`schfs_fsck --check` passes on structure, checksums, key ordering, data
blocks and derived free space.
Also caught on the way: `mkschfs` passed a null pointer to `qsort` for an
empty directory. Every implementation ignores it when the count is zero;
the standard does not say they have to.
**The identity item:**
- [x] ~~**Boot from a USB disk.**~~ The root device is probed at boot instead of
compiled in: ramdisk (only in a `RAMDISK_ROOT` build), then USB, then the
IDE disks, first SchFS volume wins. Verified with **no IDE disk attached
at all** — boots, mounts the stick, writes a file, and the file is still
there on the next boot. `dmesg` says `root: USB disk (device 3)`, and a
machine with no root names every candidate it looked at instead of
panicking with "no root filesystem". See
[../architecture/boot.md](../architecture/boot.md).
#### The `ls -lR` hang, and why it is closed
Hunted with the instrumentation it asked for, under a workload heavier than the
one that originally triggered it — ten iterations of `ls -lR /bin`, interleaved
with writes and `cd`, 33 execs and 13378 tree descents. Measured across it:
```
inode_peak 9 / 200 unchanged
file_peak 9 / 512 unchanged
buf_pinned_peak 1 / 256 unchanged
schfs0 locked 0 tx_owner_pid 0 tx_depth 0
```
Nothing climbed, nothing wedged, and the repaired `holdingsleep` assertion never
fired. **That eliminates this file's own stated hypothesis** — "a lock or
reference-count leak somewhere in the `*at()` paths" — and it is very likely the
bug was fixed in passing by the `*at()` dirfd rewrite, the getdents cookie
rewrite, or SchFS becoming the root, all of which landed after it was reported.
It is closed rather than chased further, and that is a deliberate application of
the two-things-in-flight rule. The instrumentation is permanent: if it ever
happens again, `/proc/fsstat` names the table, or shows `locked 1` with
`tx_owner_pid 0` and the volume lock is orphaned. That is a much better position
than the one it was reported from.
Two real defects were found on the way and fixed, neither proven to be the
cause: `holdingsleep()` never compared the owning pid, which disarmed all seven
`panic()` assertions built on it; and SchFS's `tx_mine()` compared a bare
`struct proc *`, which a recycled ptable slot makes ambiguous.
### Gate 1 — The foundations
[foundations.md](foundations.md), items 1–4. Descriptor passing, a shareable
memory object, `AF_UNIX` in the filesystem namespace.
Nothing above needs these; everything below stops without them.
- [x] ~~**Item 1 — `sendmsg`/`recvmsg` with `SCM_RIGHTS`.**~~ Built and tested;
fourteen checks in `cmd/socktest`, each moving data through the passed
descriptor rather than inspecting it. Described in
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
This was also mechanism 1 of [hybrid.md](hybrid.md) — one primitive for
both, which is the whole reason those are one plan.
- [x] ~~**Item 2 — a shareable memory object.**~~ `memfd_create`, `ftruncate`,
`mmap(MAP_SHARED)`. Built on the `shmget` segments exactly as
[foundations.md](foundations.md) proposed — the pages, refcounts,
`no_free` VMAs and the fork/exit accounting were all already right; what
memfd adds is a segment reached by *descriptor* rather than by name or
System V key, which is the whole reason it exists on Linux.
`cmd/memfdtest`: seventeen checks, ending with a memfd sent over a
socket to a receiver that closes every inherited copy first.
- [x] ~~**Item 3 — `AF_UNIX` sockets in the filesystem namespace.**~~ `bind()`
creates a `T_SOCK` node; `connect()` resolves the path and matches the
socket bound to *that inode*, not to a string. `ls -l` shows
`srwxr-xr-x`, `stat()` reports `S_IFSOCK`, `unlink()` removes it, and a
second bind to a live path is `EADDRINUSE` *from the directory* — which
is what makes a socket file left by a crashed server refuse a rebind, as
it must. `cmd/socktest` section 15. `flock()` is still missing and is
recorded as such.
### Gate 2 — Init as a role — **done**
Milestones I0–I3, all built. PID 1 forks a supervisor and spawns jobs on its
behalf; the job table lives in `/proc/schinit`; a heartbeat runs both ways; and
`proc_reincarnate()` re-execs PID 1 in place rather than panicking. Both failure
paths are exercised by `sysrq` keys rather than reasoned about.
Before the desktop rather than after, because a compositor being brought up for
the first time crashes constantly, and something that restarts it and says why is
worth far more during that work than at the end of it.
The description is now [../architecture/init.md](../architecture/init.md); the
plan document it replaced has been deleted, per
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
### Gate 3 — The desktop
[xorg.md](xorg.md): Xorg on the framebuffer, twm and xterm, then Window
Maker. Wayland ([wayland.md](wayland.md)) is not being worked on -- what it
needs, DRM/KMS above all, is far larger than what X needs.
**W4 and W5 are not in 1.0.** Multi-window focus policy and a panel are
post-1.0. One terminal window that works is the bar.
--- ---
## Not in 1.0 ## Not in 1.0
The point of this list is that it is *written down*, so it can stop occupying The point of this list is that it is *written down*, so it can stop occupying attention. Nothing here is cancelled.
attention. Nothing here is cancelled.
| | Why it waits | | | Why it waits |
|---|---| |---|---|
| **The rest of the hybrid kernel** ([hybrid.md](hybrid.md)) | Still not a completeness requirement — but the mechanisms and the first eviction happened early, because the alternative to a driver compatibility layer is writing a driver for every device that exists, and that is not a plan. What is deferred is the foreign driver host, not the interface it needs. | | **GPU acceleration, Mesa** | [gpu.md](gpu.md). A whole project; software rendering is enough to use the machine. |
| **Mesa, SDL3** | The kernel and musl do dynamic linking now (`cmd/dynhello`), but every shipped program is still static. Mesa is the single biggest post-1.0 unlock and it is a whole project. | | **A page cache** | General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. |
| **A page cache** | `memfd` covers the desktop's needs. General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. | | **SchFS snapshots, FUSE, a Linux module, sparse files, online fsck** | The format version is the prerequisite and it exists; the rest is [roadmap.md](roadmap.md) §6. |
| **`.sip` and the installer** | 1.0 installs by writing a USB stick. That is how most systems start and it is honest. |
| **SchFS snapshots, FUSE, Linux module, sparse files, online fsck** | The format version (Gate 0) is the prerequisite for all of them, and it is the only part on a clock. |
| **The interactive crash screen** | PSOGM renders, prints a full report and emits a QR code. That is enough. |
| **CFF/OTF fonts, kerning, text layout** | TrueType and fixed advances are enough for a terminal. | | **CFF/OTF fonts, kerning, text layout** | TrueType and fixed advances are enough for a terminal. |
| **Routing, `getsockname`/`getpeername`, IPv6** | One interface, one default route, works. | | **Routing, IPv6, Wi-Fi** | One interface, one default route, works. |
| **Multiple outputs, damage tracking, subsurfaces, xkbcommon** | One screen, full repaints, one keymap. | | **HDA audio, recording, mixing** | AC'97 plays; that is sound. |
| **Audio** | Does not exist in any form and is not required to use a machine. | | **Threads' loose ends, `ptrace`, `seccomp`** | Whatever a package needs is found by running it. |
| **Panel, notifications, settings, file manager** | A terminal is a desktop. | | **SchAPI itself** | Deliberately. It should be designed from the desktop's experience, not before it. |
| **SchAPI itself** | Deliberately. It should be designed *from* the compositor's experience, not before it. |
--- ---
## The rule for this list ## The rule for this list
**Adding to Gate 0–3 requires deleting something else from it.** **Adding to the gates requires deleting something else from them.**
Not a productivity trick — a forcing function. The reason this project feels Not a productivity trick — a forcing function. The reason this project felt overwhelming is that ~20 fronts
overwhelming is that ~20 fronts have been open simultaneously, and no amount of were open simultaneously, and no amount of discipline makes that workable. The fix is structural.
discipline makes that workable. The fix is structural.
And one working limit, worth holding to literally: And one working limit, worth holding to literally:
> **Two things in flight. Never three.** > **Two things in flight. Never three.**
One in Gate 0 and one being written up. Everything else is closed — in the
document, and out of your head.
---
## How you will know it is going well ## How you will know it is going well
Not by velocity. By this: Not by velocity. By this: **every closed item leaves a description behind.** When the `ls -lR` hang was
closed, `history/kernel.md` gained an entry. When a gap closed, a "what it does not do" section shrank.
**Every closed item leaves a description behind.** When the `ls -lR` hang is Those sections shrinking is the real progress bar, and it is the only one that does not lie.
fixed, `history/kernel.md` gains an entry saying what it actually was. When the
console moves onto the text layer, `architecture/graphics.md` loses a line from
its *What it does not do* section.
Those "does not do" sections shrinking is the real progress bar, and it is the
only one that does not lie.
---
## A note worth keeping ## A note worth keeping
This document was written because the work started to feel overwhelming, and This document was written because the work started to feel overwhelming, and that is worth recording rather
that is worth recording rather than tidying away — because the feeling was than tidying away — because the feeling was about the *shape* of the work and not its quality, and the two are
about the *shape* of the work and not its quality, and the two are easy to easy to confuse when you are inside it.
confuse when you are inside it.
The evidence at the time: SMP working on real hardware after five bugs stacked The evidence at the time: SMP working on real hardware after five bugs stacked behind each other; a copy-on-write
behind each other; a copy-on-write filesystem whose crash test cuts the power one filesystem whose crash test cuts the power one write later, 119 times, and demands a clean mount after each; a
write later, 119 times, and demands a clean mount after each; a TCP/IP stack TCP/IP stack asserted from both ends; an xHCI driver written from the specification; and a documentation tree that
asserted from both ends; an xHCI driver written from the specification; and a records why things are strange so they do not get un-fixed by accident. Since then: a desktop, real Linux
documentation tree that records why things are strange so they do not get programs and Linux's own drivers, running.
un-fixed by accident.
None of that is the output of a project in trouble. It was a project with too None of that is the output of a project in trouble. It was a project with too many tabs open.
many tabs open.

View file

@ -1,143 +1,65 @@
# What happens next # What happens next
Everything in this directory is **unbuilt**. When a piece of it is finished it Everything in this directory is **unbuilt**. When a piece of it is finished it leaves — deleted from here,
leaves — deleted from here, rewritten as a description in rewritten as a description in [../architecture/](../architecture/). See
[../architecture/](../architecture/). See [the rule](../README.md#the-rule-that-keeps-this-from-rotting). [the rule](../README.md#the-rule-that-keeps-this-from-rotting).
> ## Read [1.0.md](1.0.md) first. > ## Read [1.0.md](1.0.md) first.
> >
> It draws a finish line and puts most of the work below on the far side of it. > It draws the finish line, says where the system stands against it, and holds
> **Anything not named there is not being worked on**, and the documents in this > **[the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)**
> directory describe more than 1.0 contains. > — the one place anything half-built is listed. No new capability starts until that register is
> > empty, and a row leaves it only by being finished or by being deleted from the tree.
> It also holds **[the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)**,
> which is the one place anything half-built is listed. No new capability starts
> until that register is empty, and a row leaves it only by being finished or by
> being deleted from the tree.
--- ---
## The three current decisions ## What is here
Taken together, because they turn out to need the same foundation.
| | | | | |
|---|---| |---|---|
| **[alpine.md](alpine.md)** | **Packages.** The root is managed by Alpine's apk: Alpine's binaries run unmodified, and SchOS itself is the apk package schos-base. | | **[1.0.md](1.0.md)** | **The finish line.** What 1.0 is, the acceptance test and where each clause stands, the register, and everything explicitly deferred. |
| **[xorg.md](xorg.md)** | **The desktop.** Xorg on the framebuffer with the fbdev and evdev drivers; twm, then Window Maker. Replaces wayland.md for Gate 3. | | **[roadmap.md](roadmap.md)** | Everything else that is scoped and agreed: storage and the installer, graphics, networking, the hardware that makes a laptop a laptop, SchFS beyond SchOS, the kernel's own gaps. |
| **[wayland.md](wayland.md)** | *Not being worked on; see xorg.md.* The GUI stack becomes Wayland. SchWM and SchDE are **deleted** — actually gone from the tree, not marked; Schcom survives and changes its wire format. | | **[gpu.md](gpu.md)** | Linux's own `amdgpu` through LDLS, and the `/dev/dri` a process has to serve. Measured, with the work it needs written down. |
| ~~**hybrid.md**~~ | **Built.** All four mechanisms exist, the GPU driver runs outside the kernel, it survives being killed, and the display mode can be changed from a process. H4 (USB) was *withdrawn* rather than left open — USB is the bus peripherals arrive on, not a peripheral. | | **[finch.md](finch.md)** | **Later.** The author's own LLVM language for SchOS's applications and tools in place of C. The language is finished and self-hosting in its own repository; the plan here is a target, bindings generated from `include/uapi/`, and the compiler in the image. |
| ~~init.md~~ | **Built.** PID 1 is now a role the kernel refills; the description moved to [../architecture/init.md](../architecture/init.md). | | **[schapi.md](schapi.md)** | The long-term API layering and the compatibility promise. Direction agreed, nothing built. |
And the one they all wait on: ## What used to be here, and is built
| | | Plans leave this directory when they are done. These did, and what replaced them is the description:
| Was | Now |
|---|---| |---|---|
| **[foundations.md](foundations.md)** | Descriptor passing, shared memory, sockets in the filesystem. The critical path. **Item 1 is built** — `sendmsg`/`recvmsg` with `SCM_RIGHTS`. | | Descriptor passing, shared memory, sockets in the filesystem, dynamic linking, the event descriptors (`foundations.md`) | [kernel.md](../architecture/kernel.md), [networking.md](../architecture/networking.md), [syscalls.md](../architecture/syscalls.md) |
| `init.md` | [init.md](../architecture/init.md) |
| `hybrid.md` — drivers in processes, `/dev/pci`, a modeset API, the panic path, why USB stays | [devices.md](../architecture/devices.md) |
| `ldls.md` — Linux drivers in a process | [ldls.md](../architecture/ldls.md) |
| `debugger.md` | [debugging.md](../architecture/debugging.md) |
| `alpine.md` — an apk-managed root | [packages.md](../architecture/packages.md) |
| `xorg.md` — the desktop | [desktop.md](../architecture/desktop.md) |
| The interactive crash screen (`roadmap.md` §4) | GMMOS, in [graphics.md](../architecture/graphics.md) |
And the one that says which parts of the above are actually being worked on: And one that was **decided against**: `wayland.md`. Wayland needs DRM/KMS, libinput, udev and libseat, and DRM/KMS
alone is a kernel graphics subsystem. The desktop is X. The reasoning moved to [desktop.md](../architecture/desktop.md).
| | | `SchWM`, `SchDE` and the compositor `Schcom` are gone from the tree: SchOS uses other people's window manager and desktop — Window Maker
|---|---| now; XLibre, Xorg or a Wayland compositor later — and puts its work into what they need underneath (udev, DRM).
| **[1.0.md](1.0.md)** | The finish line. Four gates, and everything else explicitly deferred. |
And the one the hybrid mechanisms were built for:
| | |
|---|---|
| ~~ldls.md~~ | **Built.** The Linux Driver Layer for SchOS: someone else's drivers, in a process, reached through `/dev/pci`. `ping` works over `8139too` and over `e1000`. The description moved to [../architecture/ldls.md](../architecture/ldls.md). |
| ~~debugger.md~~ | **Built**, all of it, including the network transport. A debugger that observes rather than halts and answers in this kernel's own vocabulary: symbols, a log that outlives the kernel, `/dev/dbg`, `whose`, hardware watchpoints, and `schdbgd`. The description moved to [../architecture/debugging.md](../architecture/debugging.md). |
Two longer-horizon documents sit behind these: [schapi.md](schapi.md), the API
layering and the compatibility promise, and [roadmap.md](roadmap.md), which holds
everything still open that is not one of the three above.
--- ---
## The order ## The order
Set by [1.0.md](1.0.md), which is the authority. In short: Set by [1.0.md](1.0.md), which is the authority. **1.0 is reached** (a live system; persistence is not part of it).
What is left before anything new starts:
### 0. Fix what is already wrong — **first** 1. **Empty the register**: decide the unused in-kernel LDLS window (delete it, or use it), and build socket activation or
remove the key.
Not a document of its own; it is Gate 0 of [1.0.md](1.0.md), assembled from the 2. **The owner's list** ([roadmap.md](roadmap.md)): the Wayland groundwork (udev first, §5), making LDLS good enough that the GPU
*What it does not do* sections in [../architecture/](../architecture/). driver can run ([gpu.md](gpu.md)), a boot manager worth looking at (§5b), then the installer, then Finch inside SchOS
([finch.md](finch.md)).
**Before any new capability**, and the reason is the one that put
`netdev_selftest()` before the TCP/IP stack: a compositor built on a kernel with
an unexplained hang and a socket layer that reports success for things it did not
do will fail in ways indistinguishable from its own bugs.
### 1. `foundations.md` — the critical path
Both the Wayland work and the hybrid work stop here. Descriptor passing and
shared memory are the substrate for handing a buffer to a compositor *and* for
handing a device to a driver process. **Doing it once for both is the entire
reason these are one plan rather than three.**
### 2. `init.md` — before the desktop, not after — **done**
A compositor being brought up for the first time crashes constantly, and
something that restarts it and says why is worth far more during that work than
at the end of it. Built; see [../architecture/init.md](../architecture/init.md).
### 3. `hybrid.md` — **moved up, and the reason is worth recording**
This document used to put the hybrid work *after 1.0*, deliberately last, on this
argument:
> designing a userspace driver interface with no driver consuming it produces an
> interface that is wrong in ways nobody can see
The argument was sound and the conclusion drawn from it was wrong. What forced
the change was the driver situation: SchOS cannot write a driver for every device
that exists, no project can, and the only route to the hardware support an
established OS has is a compatibility layer with the foreign drivers kept
*outside* the kernel. That layer is this plan's mechanisms. It is not optional and
it is not late work.
The original argument still holds, and it is answered rather than ignored:
**SchOS's own drivers are the consumers.** `vgpud` is a real driver, with real
requirements, and building the interface against it is what keeps it a general
mechanism rather than a hole shaped like something foreign. The compositor is
still a demanding consumer — it is no longer the *only* one available.
Four mechanisms, all built. One eviction done, the second withdrawn with its
reasons recorded, and the foreign driver host they were for is
[../architecture/ldls.md](../architecture/ldls.md) — built, and carrying
packets.
### 4. `wayland.md` — with the GPU driver either side of the seam
W1–W3 and a terminal. W4 and W5 are post-1.0. Item 1 of `foundations.md` —
descriptor passing — was the thing it could not start without, and it now
exists.
---
## In one sentence
> Fix what is broken, build the descriptor-passing and shared-memory substrate,
> make init survivable, move the drivers out through an interface SchOS's own
> drivers have to live with first — and put a terminal on the screen.
---
## What this plan does not address ## What this plan does not address
Stated here so it is not mistaken for covered: Stated here so it is not mistaken for covered:
- **No IOMMU**, so the hybrid work isolates against bugs and not against a - **No IOMMU**, so a driver in a process is isolated against its own bugs and not against a hostile driver.
hostile driver. - **No page cache**, so a general writable `MAP_SHARED` of a file is refused; `memfd` is the narrow answer.
- **No page cache**, so `foundations.md` delivers a `memfd` and not a general - **No installer**, so nothing a live system writes survives a reboot — by decision, for 1.0.
answer to shared file mappings. The general answer is still owed.
- **No garbage collector for descriptors in flight**, so an `AF_UNIX` socket
passed over itself and never received leaks. Bounded at thirty-two sockets, and
nothing on this plan does it.
- **No dynamic linking**, so no Mesa, no accelerated clients, and no third party
shipping a `.so` for anything.
- **A new compatibility surface** between PID 1 and PID 2, created by a plan
whose own [schapi.md](schapi.md) §3 spends a section warning about those.
- **The three open networking items** — `getsockname`, routing, and
interrupt-driven receive — are untouched by any of this and still open. They
are in [roadmap.md](roadmap.md).

View file

@ -1,80 +0,0 @@
# apk: Alpine's packages on SchOS
Alpine is musl with a different init, and SchOS is musl with a different
kernel. Alpine's binaries run on SchOS unmodified: its dynamic linker, its
busybox, and apk itself, fetching over TLS through SchOS's network stack.
So instead of porting programs one at a time, SchOS can install them.
## The model
The root filesystem is an apk-managed root:
| Part | Where it comes from |
|---|---|
| musl, busybox, apk-tools, alpine-keys, CA certificates | Alpine's minirootfs (`ports/alpine`) |
| what `ports/alpine/world` lists (zsh, terminfo, ...) | Alpine's packages, pinned in `world.lock` |
| schinit, its jobs, SchOS's tools, toybox, mksh, configuration | **schos-base**, an apk package the build makes from `sysroot/` |
`apk add` installs anything from Alpine's main and community repositories.
`apk upgrade` updates Alpine's packages, and schos-base from the SchOS
repository at `/var/lib/schos/repo`.
PID 1 is still schinit. Alpine's OpenRC is not installed, and packages that
ship OpenRC services install them where nothing reads them.
## Who owns what
Where Alpine and SchOS both have a program, Alpine's wins, and SchOS keeps
only what is its own (see `tools/posix/mkaroot.sh`):
- `/bin/sh`, `cat`, `sed`, `find` and the rest are busybox. Alpine's package
scripts are written against busybox.
- zsh is Alpine's. SchOS keeps its `zshrc` and `zshenv` (`replaces=zsh`).
- `/etc/os-release` and `/etc/motd` are SchOS's (`replaces=`).
- `passwd`, `group` and `profile` are Alpine's. schos-base's post-install
sets root's shell to zsh, and SchOS's profile is `/etc/profile.d/schos.sh`.
- mksh is `/bin/mksh`. schinit's recovery shell looks for it first because it
is static: a broken musl cannot take recovery down with it.
## Building
make qemu boots build/schfs.img, the root as a 1 GB SchFS disk
make iso the same root as the live system's ramdisk
make alpine-lock re-resolve world against Alpine (needs the network)
It is the only root. sysroot/ is what schos-base is packaged from.
Only `alpine-lock` asks Alpine what is current. A normal build installs
exactly what `world.lock` names: files in `dist/alpine/`, fetched once and
checked against the lock's sha256, and against Alpine's signatures by apk.
schos-base and its repository index are signed with a key made on the first
build (`build/apk-keys/`, never committed). The root trusts it beside
Alpine's keys.
## Kernel work this needed
All of it found by running apk:
- **TCP receive window:** it reopened before the reader drained, so lwIP
refused data and the sender waited on retransmission timers (5 KB/s). It
now reopens on read, with a 32-segment window.
- **mmap address reuse:** freed ranges were never reused, so musl's malloc
used up the 256 mapping records with 780 MB of RAM free.
- **unlinkat / renameat:** bare `-1`, read as EPERM, where apk expects ENOENT;
renameat could not replace its target, and wrote v1 dirents into SchFS.
- **`#!` scripts in execve**, and ENOEXEC: apk runs triggers directly.
- **chroot**, and mounting devtmpfs and proc more than once.
## Next
- The live ISO runs the apk root from RAM. The image in the kernel is only
what the root holds; at boot the volume grows into half of the free memory
(4 GB of root on an 8 GB machine), backed by pages as they are written.
Xorg installs into it in about a minute. All of it is gone at reboot. Keeping packages needs an installer that writes the root
to a SATA or NVMe disk, and a kernel that finds its root there by the
SchFS GPT type rather than as IDE disk 1.
- Xorg and Window Maker are in `world`, so the image starts with them:
`startx` runs `wmaker` from `/root/.xinitrc`. Mesa brings LLVM with it,
about 200 MB of the 358 MB root; the root ships compressed
([SCFF](../architecture/scff.md)), so that costs about 60 MB of ISO.

77
docs/plan/finch.md Normal file
View file

@ -0,0 +1,77 @@
# Finch, for SchOS's own programs
**Two things, and only one of them is later.**
- *Bringing Finch into SchOS* (below) is **later** — behind [1.0.md](1.0.md)'s
freeze like everything else here that is not named there.
- *Finishing Finch itself*, and making it fully self-hosting, is work on the
Finch repository and does not touch this tree. It can proceed now; see
[the gap](#the-self-hosting-gap).
Finch is the author's own language: <https://github.com/Nexoniarz/Finch> (mirrored to
`pandor.pl/git/nexoniarz/Finch`, which is read-only). The plan is to write SchOS's own
applications and tools in it, in place of C, and so leave behind what C does
badly: silent narrowing, signed/unsigned mixing, undefined behaviour on an
out-of-range index, a forgotten `free`.
## What it is today (version 2.4.0)
Measured from the repository, not from its README alone:
- **LLVM front end**, C-like syntax, no semicolons, no headers, no preprocessor.
It compiles through LLVM's `-O2` pipeline to native code; there is no
garbage collector.
- **Values, not references:** lists, text and structs copy on assignment and
are freed at the end of their block. Out-of-range indexes, division by zero
and null pointers stop with a runtime error rather than undefined behaviour.
- **Errors as values** (`int(s) or 0`, `try read_file(p)`), maps, methods,
modules.
- **C interop through libclang:** `import "foo.h"` plus `link "foo"` calls any C
library, structs by value and callbacks included. That is the way in to
everything SchOS already has — musl, the `/dev/*` interfaces, Alpine's
libraries.
- **Targets:** Linux, macOS, Windows, on x86-64 and ARM64 (`src/platform.fch`;
the System V, AAPCS64 and Microsoft x64 ABIs are in `src/gen.fch`). ARM64 and
Windows programs are run under qemu and Wine in the tests; macOS is IR-checked.
- **Language:** enums, `match`, generics, errors as values, maps, methods,
`defer`, and a standard library in Finch (`json`, `path`, `time`, `proc`).
- **Tooling:** a language server (`finch lsp`, written in Finch), VS Code and
Kate support, 107 tests.
- **Written in itself:** the whole compiler is Finch (about 8,500 lines in
`src/`, no C++) and builds itself from a committed seed to a byte-identical
result — `bootstrap/build.sh`.
## What SchOS would need to do
1. **A `schos` target.** `finch` picks a triple, a C compiler and a linker per
target. SchOS runs unmodified Alpine binaries and Alpine's musl
(see [packages](../architecture/packages.md)), so the first target is **the existing
`x86_64-unknown-linux-musl`**, and a Finch program built on the host should
run on SchOS before anything in Finch is made SchOS-specific. Test that
before writing a line of target code.
2. **The compiler on SchOS.** Needs LLVM 21 and libclang, both of which Alpine
packages. That is a question of apk and memory, not of SchOS.
3. **The Finch-written compiler on SchOS.** It emits LLVM IR as text and needs
only `clang` and libclang, no LLVM C++ libraries, so it is the compiler that
would ship in the image. It needs a seed made for the SchOS target.
4. **Bindings** for SchOS's own interfaces (`/dev/pci`, `/dev/netdev`,
schinit's job protocol, the framebuffer) — generated by the C importer from
the headers in `include/uapi/`, so they are not hand-written and cannot
drift from the kernel.
## Where self-hosting stands
The compiler is written in Finch and nothing else, and it is `main` of `github.com/Nexoniarz/Finch`
(`before-bootstrap` there is the all-C++ compiler, kept as the backup). `bootstrap/build.sh` builds it from a seed (its
own IR) and then from itself, and checks the two make identical IR. 110 tests pass, also under valgrind with no leaks.
Seeds: Linux x86-64 and ARM64; Windows x86-64 (runs under Wine, compiles itself and passes every test program); macOS
ARM64 and x86-64 (made with the 11.3 SDK, checked as IR only — nobody has run them on a Mac). The language has enums
(also with values inside), `match`, generics and a standard library; the compiler carries its runtime and std inside
itself.
## Rules
- Inference, not an instruction: the kernel stays in C. Finch is for
applications and tools; the user said "our apps or stuff like that".
- No new capability starts until [the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)
is empty; the SchOS half of this document is a plan, not an exception to that.

View file

@ -1,199 +0,0 @@
# Foundations
The kernel work that [wayland.md](wayland.md) and [hybrid.md](hybrid.md) both
stop at. **This is the critical path**, and it is the reason those two are one
plan rather than two.
The insight is small and it decides the schedule: the substrate for passing a
buffer to a compositor and the substrate for handing a device to a driver process
are the same substrate. Descriptor passing and shared memory, built once.
---
## What is actually in the tree
Measured against the source, not against a README. Each of these was checked
before anything below was planned around it:
| | |
|---|---|
| ~~`sendmsg`/`recvmsg`, and therefore **`SCM_RIGHTS`**~~ | **Built.** See item 1 below. |
| Writable `MAP_SHARED` of a file | **Refused** — `kernel/sys/sys_mem.c:131` returns `-ENODEV`. |
| `memfd_create` | Does not exist. |
| `AF_UNIX` names | In a table in `kernel/net/unix.c`, not in the filesystem. |
| `setsockopt` | `return 0;` unconditionally — `kernel/sys/sys_net.c:436`. |
| `exec` | `ET_EXEC` only. No `PT_INTERP`, no `ET_DYN`. |
> **`projects/schcom/README.md` said `SCM_RIGHTS` was done while nothing in the
> kernel implemented it** — the most load-bearing wrong line in the tree, with an
> entire GUI plan drawn on top of it. The line is now true, which is not the same
> as it having been right, and the README says so.
The `MAP_SHARED` refusal is worth defending rather than fixing carelessly. The
code returns `-ENODEV` *honestly*: the contract of a writable shared mapping is
that stores land in the file and other mappers see them, and there is no page
cache that could make that true. Quietly handing back a private copy would be the
same lie in a new place.
---
## The six items
In dependency order. The first three are what Wayland cannot start without.
All but item 4 are built.
### 1. `sendmsg` / `recvmsg` with `SCM_RIGHTS` — **done**
Not optional and not substitutable. `wl_shm_create_pool` passes a **file
descriptor**; there is no path in the Wayland protocol that passes an integer
instead.
The `handle_kind` field in `projects/schcom/proto.h` was good foresight and it
cannot help here — *our* protocol is the flexible one, and Wayland's is not.
Built as described: `struct msghdr`/`cmsghdr` in Linux's layout, ancillary data
queued alongside the byte stream in `kernel/net/unix.c`, descriptor installation
into the receiver's file table, and the three ways a message can end — received,
received without room, never received — each releasing the kernel's reference.
Fourteen checks in `cmd/socktest`, every one of them moving data through the
descriptor rather than inspecting it. Documented in
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
The delivery rule is the part worth knowing before building on it: **descriptors
are pinned to the stream position of the first byte of the message that carried
them**, and a read that has not reached that position is cut short so it stops
before it. That is what keeps "which message did these belong to" answerable.
**This was also item 1 of the hybrid work**, where it is how a driver process is
handed the device it is allowed to touch.
Two bugs fell out of building it, neither in the new code:
- `sockpair()` never set the socket's domain, so **every `socketpair()` socket
claimed domain 0**. Invisible for as long as the data paths asked only whether
a socket was `AF_INET` or `AF_NETLINK` — zero answers "neither" correctly.
`SCM_RIGHTS` asks "is this `AF_UNIX`", and zero is wrong for that one.
- `pipe_poll()` reports `POLLHUP` but not `POLLIN` for a pipe at end of file, so
a **non-blocking read of a pipe whose writers have all gone returned `EAGAIN`
forever** instead of 0. A program that polls and reads would spin waiting for
an end that had already happened. `sockreadable()` always had the equivalent
condition; the pipe path did not.
### 2. A shareable memory object — **done**
`memfd_create`, plus `mmap(MAP_SHARED)` on it genuinely working between two
processes. Described in
[../architecture/kernel.md](../architecture/kernel.md#shared-memory-objects);
asserted by `cmd/memfdtest`, seventeen checks including the one that matters —
a descriptor sent over a socket, mapped by a receiver that has closed every
inherited copy, with stores visible in both directions.
Narrower than a general page cache, and sufficient. An anonymous shared object
needs no file behind it, which is exactly why `memfd` exists on Linux at all —
and it means this can be built on the shared-memory machinery already in
`kernel/sys/sys_mem.c` (`shmget`/`shmat`/`shmdt` work, and `struct vma` already
carries `shmid` and a `no_free` flag for pages that are not ours to release).
The general answer — a page cache, and writable `MAP_SHARED` of a real file —
stays owed. It is a larger project and nothing on this plan needs it.
### 3. `AF_UNIX` sockets in the filesystem namespace — **done**
Clients find a Wayland compositor by `bind()`ing `$XDG_RUNTIME_DIR/wayland-0` as
a real path, and libwayland `flock()`s the lock file beside it. A name table can
express neither.
`bind()` creates a `T_SOCK` node and `connect()` resolves the path and matches
the socket bound to **that inode** — not to a string, which is what made two
spellings of one path miss each other. `ls -l` shows `srwxr-xr-x`, `stat()`
reports `S_IFSOCK`, and `unlink()` removes it. Asserted by `cmd/socktest`,
section 15. `flock()` is still not implemented; see below.
`kernel/net/unix.c`'s own header comment already predicted this:
> *"What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a socket
> path failing with ENOENT. Both are worth fixing when the VFS next gets opened
> up; neither blocks anything."*
They block something now. This is that moment, and it is a VFS change rather than
a socket change: a socket inode type threaded through `iget`, `dirlookup` and
`stat`, plus `flock`.
### 4. `setsockopt` that does not lie
At minimum `SO_PASSCRED` and `SO_SNDBUF`. Returning 0 for an option you ignored
is worse than `ENOPROTOOPT`, because the caller adjusts its behaviour on the
strength of the lie.
Small, and grouped here because it is in the same file as item 3 and will be
found by the same testing.
### 5. Dynamic linking — **done**
`PT_INTERP` and `ET_DYN` in `kernel/fs/exec.c`. A program that names an
interpreter gets it loaded at the bottom of the mmap area and started instead,
with `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, `AT_BASE` and `AT_ENTRY` to find the
program by; a PIE program is placed where a fixed one would be. The interpreter
is musl's own: `ports/musl` now builds `libc.so` as well, shipped as
`/lib/ld-musl-x86_64.so.1`, which is libc and loader in one file.
The kernel half that was missing besides `exec` was `mmap(MAP_FIXED)` of a
file. It handed back zeroed pages whatever the descriptor said, and kept no
record of them for `fork` — and a dynamic linker maps every library segment
after the first exactly that way.
Every port still links `-static`, on purpose: this makes the other kind
possible, it does not convert anything. `cmd/dynhello` is the test — a
program against `libc.so` and a library of its own, checking TLS, the heap and
a forked child that still has its libraries.
`mprotect` is accepted and not enforced, so RELRO is not read-only yet.
### 6. The event descriptors — **done**
`epoll`, `eventfd`, `timerfd` and `signalfd`, in `kernel/sys/sys_event.c`.
libwayland's event loop is all four in one epoll set, which is why they arrive
together; not in the original list because they were stubs that looked like
syscalls, returning `ENOSYS` from `table.inc` entries that read as done.
They wait the way `poll()` does, a tick at a time, so the latency is 10 ms.
`cmd/evtest` checks each one, then all four in one set, level- and
edge-triggered.
Building it found a signal bug: a default-fatal signal killed on arrival even
when it was blocked. A blocked signal now stays pending, which is the contract
`signalfd` is built on.
---
## Verification
Each item gets a test before the thing that needs it is built, because a
substrate that is subtly wrong fails inside the consumer and looks like the
consumer's bug.
- **Item 1:** two processes, one passes an open descriptor, the other reads
through it. Plus the leak case: a sender that exits with descriptors in flight.
- **Item 2:** two processes map the same `memfd`, one writes, the other sees it.
- **Item 3:** `bind()` a socket, then `stat()` and `ls -l` the path and see an
`s`, then `unlink()` it.
- **Items 1–3 together:** a third test that does all of it at once, because that
combination is what Wayland actually performs and the three passing separately
does not prove it.
These belong in `bootcheck` as a new switch, following the `-Sock` model.
---
## What this does not deliver
- **No page cache**, so writable `MAP_SHARED` of a real file stays refused.
- **No `dlopen`** until item 5 is done, which means no Mesa and no third-party
shared libraries.
- **`SO_*` coverage is the two options named**, not the full set.
- **No credential passing** (`SCM_CREDENTIALS`) — only descriptors. Nothing on
this plan needs it, and a compositor that wants to know who connected will.
- **No `flock()`.** libwayland locks `wayland-0.lock` beside the socket to stop
two compositors claiming one display. The socket itself is now a real path,
so the lock file can be created and opened; what is missing is the advisory
lock, which is a separate mechanism and has no other caller yet.

125
docs/plan/gpu.md Normal file
View file

@ -0,0 +1,125 @@
# The GPU, through Linux's own driver
The laptop this is for (ASUS TUF A15 FA507NU) has a Radeon 680M in the CPU and
an RTX 4050 beside it. SchOS draws everything with the CPU on an EFI
framebuffer. Rendering a browser page that way keeps every core busy, and the
machine runs hot doing it.
Writing a Radeon driver is not on the table: amdgpu is around four million lines
before its register headers. The plan is to **run Linux's amdgpu unmodified**,
with its firmware, in a process under LDLS ([../architecture/ldls.md](../architecture/ldls.md)),
and to give Mesa the `/dev/dri` it expects. Licensing is not an obstacle:
SchOS is GPL-2.0-or-later, and the user has said that Linux code and firmware
blobs may be shipped. That supersedes LDLS's rule 1.
## Why LDLS and not an in-kernel linuxkpi
FreeBSD's drm-kmod compiles Linux's DRM source into kernel modules against a
shim (linuxkpi). SchOS has already chosen the other route, and for good reasons
([devices.md](../architecture/devices.md)): GPU drivers live outside the kernel, a crashed driver
falls back to the framebuffer instead of taking the machine down, and LDLS
loads unmodified `.ko` files against the running kernel's BTF instead of
needing a source port per Linux release. vgpud already drives a GPU from a
process, and LDLS already runs 8139too and e1000 that way.
## What it costs: the numbers
Measured against Linux 6.18.54 (the host's kernel and modules), with
`modprobe --show-depends`, `nm -u` and the export tables:
| Driver | Modules in the closure | Symbols needed from the core kernel | LDLS provides today |
|---|---|---|---|
| r8169 | 12 (libphy, mdio-bus, realtek PHY, ...) | 330 | 70 |
| amdgpu | 15 (ttm, gpu-sched, drm_buddy, drm_exec, drm_display_helper, ...) | 1,166 | 76 |
Of amdgpu's 1,166 symbols, **314 are the DRM core** (`drm_*`). NixOS builds
`drm.ko` and `drm_kms_helper.ko` into the kernel, so a module set that has them
as modules is needed; see step 0. A further 45 are dma-fence, dma-buf and
dma-resv. The rest is ordinary kernel: memory, locks, workqueues, timers, PCI,
ACPI, firmware loading, i2c, backlight.
## What LDLS lacks, and must grow
LDLS today assumes one thread, no-op locks and a polled interrupt handler
(ldls.md, "What it costs"). amdgpu cannot work under that:
1. **Threads.** `kthread_run`, workqueues with real worker threads,
`gpu-sched`'s scheduler threads. Map them to pthreads in the driver process.
2. **Locks that lock.** mutex, spinlock, rwsem, `ww_mutex` (TTM and dma-resv
depend on wound/wait semantics), completions and wait queues, on futexes.
3. **Interrupts as interrupts.** A thread blocked in `read()` on `/dev/pci`
calls the handler and any threaded handler, instead of the poll loop.
4. **Timers.** `timer_list`, `hrtimer`, `delayed_work` and `jiffies`, from a
timer thread.
5. **Memory with physical addresses.** TTM allocates system pages for GTT and
maps them for the GPU. Without an IOMMU, DMA address equals physical
address, so the `PCIIOC_DMA` arena has to grow to hundreds of megabytes,
with `struct page` (vmemmap) coverage for all of it.
6. **Firmware.** `request_firmware` reads `/lib/firmware/amdgpu/*.bin`, from
Alpine's `linux-firmware-amdgpu` package.
7. **ACPI.** On an APU the VBIOS comes from the VFCT table, and amdgpu calls
ATIF/ATCS. A small `/dev/acpi` (read a table, evaluate a method through
uACPI) covers both.
Each of these is general LDLS work, not amdgpu work. Every later driver needs
the same pieces: e1000e, iwlwifi, nouveau.
## The new kernel piece: `/dev/dri` served by a process
Mesa and Xorg open `/dev/dri/card0` and `/dev/dri/renderD128`, then use
`ioctl`, `mmap` and `poll`, and pass buffers to each other as dma-buf file
descriptors. The kernel has to forward each of those to the driver process:
- **open, ioctl, poll, close** go to the server process with the client's
identity attached, the same pattern `/dev/netdev` uses for NICs, extended to
ioctls with their argument buffers.
- **mmap** is the part the kernel must do itself. The server replies with
physical ranges (VRAM through the BAR, or GTT pages from the arena), and the
kernel maps those into the client's address space.
- **dma-buf** becomes a kernel file object naming a buffer the server owns.
`SCM_RIGHTS` passes it between processes (already built; see
[networking.md](../architecture/networking.md#descriptor-passing)), and X's DRI3 needs exactly that.
- **sysfs**: `/sys/class/drm/card0`, `/sys/dev/char/226:*`, and the
vendor/device files that libdrm and Mesa read to pick a driver.
## Order
Each step ends with something you can run.
0. **One module set.** Build Linux 6.18 from source, once, with `DRM=m`,
`DRM_AMDGPU=m` and BTF on. That gives `drm.ko`, `drm_kms_helper.ko` and the
rest, plus a matching `vmlinux` BTF. `make ldls-modules` does the build, and
packaging it as an apk makes it installable.
1. **LDLS concurrency and memory** (lacks 1–5 above), proven on **r8169**.
The real Linux r8169 brings PHY tuning, the firmware patch, ASPM handling
and every RTL8168 variant. It is the laptop NIC, so this step also fixes the
network properly. Done when the laptop's ethernet runs at gigabit speed
through `ldls0`.
2. **amdgpu probes.** The driver process loads firmware, brings up the IP
blocks and takes the display from the EFI framebuffer. The kernel console
must hand over the screen and get it back if the process dies. Done when
`dmesg` shows amdgpu's "SMU is initialized" and the panel stays lit.
3. **KMS.** `/dev/dri/card0` with ioctl forwarding and the mmap path. Done
when `modetest` (Alpine `libdrm-tests`) sets a mode and draws a frame.
4. **Rendering.** `renderD128`, GEM mmap, dma-buf, syncobj. Done when
`eglinfo` names radeonsi and `glmark2-es2-drm` and `vkcube` (RADV) run.
5. **The desktop.** Xorg's modesetting driver with glamor, then Epiphany with
GL compositing. Done when the fans stay quiet while a page scrolls.
## The RTX 4050
Later, and in two parts:
- **Powered off, now.** Without a driver the dGPU sits in D0 and draws power.
Putting it in D3cold through its ACPI power resources (`_PR3` / `_OFF`) is
a few dozen lines in the kernel and helps the heat and the battery today.
- **Driven, after amdgpu.** nouveau with GSP firmware (Ada needs it), through
the same LDLS and `/dev/dri` machinery, for PRIME render offload.
## Not GPU, but also why the laptop runs hot
Until now, idle CPUs halted at C1 and never reached the deep states listed in
ACPI `_CST`. That kept every core powered at an idle shell prompt. It is fixed
alongside this plan: the scheduler's idle path now enters `_CST`'s C2 (CC6 on
Zen). P-state control (AMD CPPC) is still missing, so the cores run at the
firmware's default clock under load.

View file

@ -1,558 +0,0 @@
# The kernel becomes hybrid
"Hybrid" is used to mean everything from "a monolith with a nice device model" to
"a microkernel that gave up". **Drawing the line precisely is most of the work**,
so that is what this document is.
The device model it builds on is
[../architecture/devices.md](../architecture/devices.md). It is last on the
plan, and the reason is in [README.md](README.md#the-order-and-why-it-is-not-the-order-they-are-listed-in).
---
## The line
### Stays in the kernel, permanently
- scheduling, address spaces, the trap and syscall path
- IPC and shared memory — the mechanism everything else is built out of
- the VFS and the **root** filesystem
- IRQ demultiplexing
- the console and PSOGM
**That last one is not a size compromise, it is an invariant.** The crash screen
must work when everything else in the system is dead, which means it cannot
depend on anything that could *be* dead. A crash screen served by a userspace
process is a crash screen that is absent in exactly the case it exists for.
### May leave, and should
- GPU drivers
- USB, and every USB class driver under it
- non-root filesystems
- NICs after the first
- input transformation — layouts, gestures, remapping
### The rule that separates the columns
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
> replaceable, or plausibly written by someone else goes out.**
That is NT's compromise and macOS's, arrived at from the same pressure, and it is
the one that survives contact with a real machine.
---
## Built so far
**Mechanisms 2 and 4 exist.** `/dev/pci` (include/dev/pciuser.h, kernel/dev/pciuser.c)
enumerates the bus, hands a device to a process exclusively, maps a BAR into it
and allocates memory a device can be pointed at. `pcitest` proves it end to end:
```
# bdf vendor:device class driver
6 00:04.0 8086:100e network e1000
7 00:07.0 8086:10d3 network -
device 7: 8086:10d3 (network)
claimed
BAR0 at 0x0000000081060000, 128 KB
regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
MMIO reaches the device
DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000
```
Those register values are the e1000e's CTRL and STATUS, with full-duplex and
link-up set -- which is how you tell a working mapping from one that reads
0xFFFFFFFF off an unmapped address. Claiming a device the kernel drives is
refused, and everything a process held is released when it exits, because a
driver that crashed must not keep its hardware away from its replacement.
**Mechanism 3 exists too.** A process asks for a device's interrupts and then
reads them, uio-style:
```
interrupts routed here (line 10)
interrupt delivered, count = 1
ICR = 0x80000004
```
`0x80000004` is the e1000e's cause register with the bit the test asserted and
INT_ASSERTED set, read back by userspace as its acknowledgement.
The line is **masked by the kernel on arrival and unmasked by userspace's
write()**, and that is the entire design rather than a detail. A PCI interrupt
line is level-triggered: it stays asserted until the cause is cleared *at the
device*, which is a different register on every chip and therefore something
only the driver can do. Signalling EOI on a line that is still asserted
re-enters the handler immediately and for ever -- 72,000 times a second, when
this kernel last did it by accident (history/kernel.md §13).
So a driver that dies mid-interrupt leaves the line **masked, not unmasked**.
The instinct is the other way round -- do not leave the device dead -- and it
is wrong: unmasking an asserted line with no handler attached is a crashed
driver taking the machine down with it. Verified by killing one on purpose:
```
interrupt asserted; exiting WITHOUT acking (simulated crash)
timer: 3431 ticks taken, 3436 expected
```
Five ticks lost in seven seconds, which is the ordinary background. The next
driver to claim the device re-arms the line, so masked is safe without being
permanent -- also verified.
## The first eviction happened
`cmd/vgpud` is the virtio-gpu driver, outside the kernel. It finds the device
by vendor and device id, asks for it, and the kernel driver hands it over:
```
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
virtio-gpu: released to userspace; screen is the VBE framebuffer
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: drew a test pattern from userspace
```
Full virtio 1.0 bring-up from a process: reset, feature negotiation, virtqueue
in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D, ATTACH_BACKING,
SET_SCANOUT, and TRANSFER/FLUSH to present. The pattern it draws is on the
virtio-gpu head while the console carries on in VBE on the other one -- which
is the fallback rule working rather than being described.
**No syscall knows this is a GPU.** The kernel handed over a PCI device; the
process decided what it was. That is the property that makes the interface
general, and it is why the evictions come before anything foreign.
### What the first real driver asked for that a test program had not
- **PCI configuration space** (`PCIIOC_CFG`). A virtio device says where its
registers are in *vendor capabilities*, which live in config space -- so a
driver that cannot read it cannot find its own registers. Anything modern is
discovered the same way.
- **A way for the incumbent to let go.** `struct pci_driver` gained a
`release` hook. A driver without one keeps its device, which is always safe
and, for anything on the path to a shell, correct.
- **The outgoing driver freeing its DMA.** Releasing the device while keeping
its 4 MB framebuffer is half a handover: the incoming driver asks for one and
is told there is no room. Found by doing it.
- **A bigger arena.** 4 MB was exactly one framebuffer at this resolution, and
for the moment either side of a handover there are two. 16 MB now.
### And one bug it reproduced immediately
The first working run raised this:
```
irq 11: 77780 interrupts/second -- a device on this line is not acknowledging it.
irq 11: handler 'e1000' is attached
```
`vgpud` polls the used ring, so it registered no handler -- and a virtio device
interrupts on every completion. A level-triggered line nobody acknowledges
re-fires for ever, drowning the e1000 that shares it. The fix is one line,
`VRING_AVAIL_F_NO_INTERRUPT` before the queue is enabled, and the value of it
is that the storm detector built for history/kernel.md section 13 named the
line, the rate and the neighbour without anyone having to look.
**All four mechanisms now exist**, and with H3 done and H4 withdrawn, nothing
remains on this plan. What the mechanisms were built for next is the foreign
driver host -- [ldls.md](ldls.md).
### One thing the interface is deliberately not
It is about **PCI devices**, not about any driver model, and its first users
will be SchOS's own drivers. That ordering is the design: an interface whose
first consumers are native drivers is a general mechanism that something
foreign can use later, rather than a hole shaped like something foreign. The
difference matters technically -- a Linux-shaped interface would be wrong for
the evictions this plan actually calls for -- and it happens to be the only
version that survives a licensing conversation.
---
## The driver came back
H2 is the milestone this plan calls the one that cannot be skipped, and the
reason is that H1 proves only half of the idea. A driver that runs in a process
is interesting; a driver that runs in a process, *dies*, and is replaced by
another process that picks the device up is the thing the whole hybrid argument
rests on.
`etc/schinit/vgpud.job` makes it a supervised job -- `KeepAlive = yes` -- so
killing it is the whole test:
```
# cat /proc/schinit
1 4 11 os.schos.vgpud
# kill 4
schinitd: restarting os.schos.vgpud
vgpud: virtio-gpu at 00:03.0
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: pid 8 drew a test pattern from userspace
```
The test pattern ends in a twelve-block tally of the drawing process's pid,
because a second run that redraws an identical picture is indistinguishable
from a first run that never stopped. Read off the virtio-gpu scanout across
four generations of the driver it says 4, 8, 10, 12. The same physical address
comes back every time, which is the DMA arena reclaiming cleanly rather than
drifting.
### What it took that H1 had not needed
- **A halt the kernel can apply without knowing what the device is.** A driver
that dies leaves its device bus-mastering into memory the kernel is about to
hand to somebody else. The kernel cannot quiesce a device it does not
understand, but it can clear `PCI_CMD_BUS_MASTER`, and that is enough to stop
the writes. `pciuser_stop()` does it before the DMA is freed, not after, and
claim/release became a symmetric pair: claiming sets the bit, releasing
clears it. `pcitest` grew a `bm` column so the state is visible rather than
assumed.
- **A refusal, when there is nothing to fall back to.** `vgpu_release()` now
asks `vga_fb_restore()` whether there is a framebuffer to put the console on,
and declines to hand the device over if there is not. The rule that the
kernel keeps a minimal driver for everything on the path to a shell is only
worth writing down if something enforces it.
- **The DMA arena not being handed to two allocators at once.** This is the bug
that stopped H2 for a day, and it is worth writing out: `freevm()` frees
every mapped page below `PHYSTOP`, and the DMA arena is the top 16 MB of
exactly that range. So a DMA block mapped into a driver and left for
`freevm()` went onto the page allocator's free list when the driver died,
while `pciuser_release_all()` handed the same pages back to the arena. Both
allocators then believed they owned it. The restarted `vgpud` asked for its
4 MB framebuffer, got pages `kalloc()` had already given out -- including its
own page tables -- and `dma_alloc()`'s zeroing wiped them, so the driver died
inside the ioctl, on the return from a syscall whose stack had stopped
existing. `do_dma()` now records a VMA with `no_free` set, the same way
shared memory does, so `proc_release_shared()` unmaps the range before the
page tables are torn down.
### The clause that is not true yet
The milestone says *"the compositor reattaches"*. It does not, because there is
no compositor running: Schcom is v0 and is not started at boot. What has been
shown is the two parts that do not depend on it -- the screen falls back to
VBE, and the driver restarts and takes the device back. During the gap the VBE
head carries a live console and the virtio-gpu head still shows the dead
driver's last frame, which is exactly what a device nobody is driving does.
(That last sentence is still true of a *crashed driver*, and no longer true of
a crashed *kernel* -- see "The clause nobody wrote down" below.)
Reattachment is a Schcom milestone, not a hybrid one, and it is recorded that
way rather than counted here.
---
## A modeset, from a process
H3 is one sentence in the milestone table -- *"a modeset API, which is the
thing an out-of-kernel GPU driver makes affordable and an in-kernel one never
got"* -- and the claim in it is worth testing rather than repeating.
```
# schmode
1280x800 current
1280x800 maximum, framebuffer 4000 KB
# schmode modes
1280x800 *
1280x720
1024x768
800x600
640x480
# schmode 1024 768
1024x768 ok
```
The scanout really resizes: captured from the virtio-gpu head, the frames come
back 1280x800, then 1024x768, then 640x480, then 1280x800 again, with the
checkerboard band showing no shear at any of them.
**Nothing in the kernel changed for this.** No syscall on the path has heard of
a scanout; `schmode` writes a line to a socket and `vgpud` sends five commands
to the device. That is the whole of the milestone, and the reason it is
interesting is the comparison: the same five commands in the kernel would have
been the same five commands *and a reason to be nervous*, because getting the
sequence wrong there takes the machine instead of one process. **That is why
the in-kernel driver never grew one** -- not difficulty, exposure.
### What it costs, and what it refuses
The framebuffer is allocated **once, at the largest mode**, and every smaller
mode reuses it. `PCIIOC_DMA` has no free -- a driver's blocks come back when it
exits and not before -- so a modeset that allocated would spend sixteen slots
and then the arena, and the failure would look like a GPU bug rather than like
counting. A mode larger than the initial allocation is refused, and says why:
```
# schmode 1920 1080
refused: larger than the framebuffer, which is fixed at startup
```
That bound is 4 MB, `PCIIOC_DMA`'s cap, which at 32 bits per pixel is about
1280x800. Raising it is a one-line change to a constant and a question about
how much of a 16 MB arena one driver should be able to take, which is a better
question than it looks and is not answered here.
### The socket, and the argument it settles
`vgpud` used to end in `for(;;) sleep(100);` under a comment arguing that
building a control socket before there is a compositor to judge it is exactly
the mistake this document spends a section warning about.
That argument was right about a *compositor protocol* and wrong as a reason to
have no socket at all. A modeset API has a consumer that exists -- somebody
changing the mode and looking at the screen -- and `schmode` is small enough
that it can judge an interface this small. So the socket is there, it speaks
lines of text for the same reasons
[../architecture/debugging.md](../architecture/debugging.md) gives, and what is
deliberately **not** on it is everything a compositor would need: no buffer
submission, no damage, no vblank. Those still wait, for the original reason.
---
## The mechanisms
Four, and only the fourth is large.
### 1. A control channel — **built**
`AF_UNIX` plus `SCM_RIGHTS` — item 1 of [foundations.md](foundations.md), which
the Wayland work also needed first. A descriptor is how a driver process is
handed the device it is allowed to touch.
Built as `sendmsg`/`recvmsg` in Linux's shape, because the programs that will
use it are not ours: libwayland and toybox both call `recvmsg` with Linux's
structures and would not notice a friendlier door. Descriptors are pinned to the
stream position of the first byte of their message, and the kernel holds a
reference to each until it is received — or closes it when the socket is dropped
with the message still queued, which is the case that leaks if nobody writes it
down. See
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
**Do not build Mach ports.** The port model is worth its cost when there is a
naming and capability story to hang on it; there is not one yet, and inventing it
before the first userspace driver exists is designing against an imaginary user.
### 2. MMIO to userspace
Map a PCI BAR into a process.
`sys_mmap` already maps the framebuffer's real physical range at its real
address — the path exists. This is that path generalised, behind a check that the
caller was granted that device.
### 3. Interrupts to userspace
**The handler stays in the kernel and does one thing: write to an `eventfd`.**
The driver process reads.
`eventfd` exists already, and that is why it is the right primitive. A signal
would be delivered to a process that cannot safely be interrupted mid-transaction.
### 4. DMA to userspace
Physically contiguous memory whose physical address the process is told.
`kernel/core/dma.c` already owns an arena reserved for exactly this, before
`kalloc` gets anything. It needs a door, a per-device budget, and a way to hand
the region back.
---
## The clause nobody wrote down: a panic has to reach the screen
Handing a device away costs something that was not on the list. The kernel
panics, paints the report into the console framebuffer, and halts — and the
display it gave to a process keeps showing whatever that process last put
there. On a machine with two heads, one says the kernel is dead and the other
shows a shell prompt.
It is not a race that can be won by being careful. There is no scheduling after
a panic, so the process holding the device is never going to run again, and the
kernel let go of the device deliberately.
**So the userspace driver says in advance where it is scanning out**, with
`PCIIOC_PANICFB` (see `include/dev/pciuser.h`), and the kernel does the rest:
1. paints the report into that memory,
2. asks the *kernel* driver the device was taken from to put it on screen —
`panic_present` in `struct pci_driver`,
3. which resets the device and rebuilds one queue from **static** memory: no
allocation, no locks, no sleeping, because a corrupted heap is one of the
ways a machine gets here in the first place.
The registration is checked like the promise it is: the range must lie inside a
block that process got from `PCIIOC_DMA` on that device. Without that it would
be a way to nominate any physical address in the machine to be overwritten at
panic, which is a worse bug than the one it fixes.
What this buys is a property worth stating plainly: **a device can be evicted
to userspace without the kernel losing its ability to tell you the machine
died.** That is the difference between a driver boundary and a blind spot, and
it is one more thing the in-kernel arrangement got for free and the hybrid one
has to earn.
It costs 20 KB of static memory in the virtio-gpu driver, paid whether or not
the machine ever panics. Allocating it on demand would be cheaper and would not
work.
---
## The honest limitation
> **There is no IOMMU, so a userspace driver is isolated against its own bugs and
> not against malice.**
A process that can program a device's DMA engine can write to any physical
address, whatever its page tables say.
That is not a reason to stop — isolation against bugs is most of the value, and a
crashed driver that restarts instead of panicking the machine is the entire
point. But it must be written down, because *"userspace driver"* reads as
*"sandboxed"* and here it does not mean that yet. VT-d/AMD-Vi is its own project
and comes after.
---
## Order, and one thing not to do
### Do not build loadable kernel modules first
It is the obvious first step and it is a generation behind. It means designing a
**stable in-kernel driver ABI**, which is the most expensive promise in this
entire plan, for a mechanism Apple is actively retiring in favour of the one
above.
`../architecture/devices.md` already separates the two ideas. They are separable,
and the second one is the one to build.
### First eviction: the GPU driver
Four reasons it is the right one and not just the interesting one:
- **The boundary already exists and is already correct** — `struct display_ops`.
- **It has exactly one consumer**, the compositor, so the interface has a real
user to be judged by.
- **There is a working fallback underneath.** The VBE framebuffer stays
in-kernel, so a GPU driver that fails to start degrades to a working screen
rather than to no screen.
- It directly serves the work that will actually be happening.
### Second: USB — and why it turned out not to be
`xhci.c` is 1364 lines, self-contained, already spoken to through a bus
abstraction, and it is where third-party drivers will show up first. This is what
DriverKit was built for and the resemblance is not accidental.
#### It is not the same shape as the GPU, and that was missed
Written before the GPU eviction had been done, H4 says *"USB, same shape"*. It
is not, and the difference is one this document's own rule already decides.
The GPU could leave because **there was a working fallback underneath it**: the
VBE framebuffer stays in the kernel, so a GPU driver that fails to start
degrades to a working screen. That is one of the four reasons the GPU was
chosen to go first, listed a few lines above.
USB has no fallback, and it carries two things that are on the path to a shell:
- **The root disk.** `USBDISK_DEV` dispatches through `bio.c` to
`usb_storage_rw`, and `usbstick.img` is a real boot medium built by `make`.
- **The keyboard.** `usb_hid` feeds `input_report(INPUT_KBD, ...)`, which is how
anything without PS/2 is typed on — including the Chromebox this is meant to
boot.
So evicting `xhci.c` wholesale breaks the rule three sections down:
> **The kernel keeps a minimal driver for everything on the path to a shell, and
> userspace owns the good one.**
>
> Break this and you get the circular boot failure where the compositor cannot
> start because the display driver is a process that needs a compositor.
The USB version of that sentence is *the root filesystem cannot mount because
the storage driver is a process that lives on the root filesystem*, and it is
the same failure wearing different clothes.
There is a second difference, subtler and probably the harder one. `/dev/pci`
hands over a **PCI function** — one device, one owner, nothing shared. A USB
device is not that: it sits behind a controller that other devices also sit
behind, so the unit of ownership and the unit of hardware are no longer the
same thing. Handing a process "the xHCI controller" gives it the keyboard too;
handing it "one USB device" is an interface `/dev/pci` does not have and that
this plan has not designed.
#### Decided: USB stays in the kernel
**H4 is withdrawn.** Not deferred, not blocked on the interface above —
withdrawn, because the premise was wrong.
> **USB is not a peripheral.** It is the bus the peripherals arrive on.
Nearly every machine has a USB port in some form, down to a bad micro-B on a
board that has nothing else, and the ones that do not have one are not made
worse by the stack being present. That makes USB structurally the same kind of
thing as PCI, not the same kind of thing as a GPU — and this document has never
proposed evicting PCI.
Reading the eviction rule again with that in mind, it already said so:
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
> replaceable, or plausibly written by someone else goes out.**
USB is how things are *discovered at runtime*, which is not the same as being
one of them. The keyboard and the root disk that arrive over it are on the path
to a shell, and the stack that finds them is part of that path.
The two obstacles in the section above are real and they are why the premise
got questioned, but they are not the reason for this decision. Even with a
minimal in-kernel fallback written and a per-device ownership interface
designed, the answer would be the same: the good USB driver and the minimal one
would be the same driver, and an eviction that leaves an identical copy behind
has moved nothing.
What the eviction was *for* — third-party drivers, written by someone else,
living outside the kernel — is a real goal and it survives this. It is simply
not a USB goal; it belongs to the foreign driver host, which is
[ldls.md](ldls.md).
### The rule that keeps the boot path honest
> **The kernel keeps a minimal driver for everything on the path to a shell, and
> userspace owns the good one.**
Break this and you get the circular boot failure where the compositor cannot
start because the display driver is a process that needs a compositor.
---
## Milestones
| | | |
|---|---|---|
| **H0** | Mechanisms 2, 3 and 4, each with a test that exercises it from a userspace program against a device nothing else is using. | done |
| **H1** | A userspace virtio-gpu driver, publishing `display_ops` across the channel. Schcom talks to it and does not know the difference. | done, except that Schcom does not talk to it -- `cmd/vgpud` holds the device and draws its own frame |
| **H2** | Kill the driver process on purpose. The screen falls back to VBE, the driver restarts, the compositor reattaches. **This is the milestone that proves the whole idea**, and it is the one that cannot be skipped. | done, less the compositor -- see "The driver came back" |
| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. | done -- see "A modeset, from a process" |
| ~~**H4**~~ | ~~USB, same shape.~~ **Withdrawn** — USB is the bus peripherals arrive on, not a peripheral. See "Decided: USB stays in the kernel". | |
---
## What it will still not do
- **No isolation against a hostile driver**, per the IOMMU note above.
- **No stable driver ABI**, deliberately. Out-of-tree drivers remain a
recompile-with-the-tree affair until there is a reason to promise otherwise.
- **The root filesystem stays in the kernel**, so SchFS cannot be restarted and a
bug in it is still fatal. Moving it out needs an initramfs story that does not
exist.
- **The first NIC stays in the kernel**, because the network thread and lwIP are
in the kernel and moving them is a separate project with no current benefit.
- **No driver sandboxing beyond address-space separation** — no seccomp-shaped
syscall restriction, no resource limits per driver.

View file

@ -1,183 +1,195 @@
# Roadmap — everything else still open # Roadmap — everything else still open
The three current decisions have their own documents; see [1.0.md](1.0.md) draws the finish line and holds the register of what is half-built.
[README.md](README.md). This file holds what is left: work that is scoped, This file holds what is left beyond it: work that is scoped, agreed and unstarted, with
agreed and unstarted, with nothing in it that is finished. nothing in it that is finished.
**Finished work is not here.** It moved to **Finished work is not here.** It moved to [../architecture/](../architecture/) and was
[../architecture/](../architecture/) and was rewritten as a description of what rewritten as a description of what it produced — see
it produced — see [the rule](../README.md#the-rule-that-keeps-this-from-rotting). [the rule](../README.md#the-rule-that-keeps-this-from-rotting).
What used to be phases 1 through 4 of this file, several hundred lines of ticked
boxes, is now `usb`, `networking.md`, `graphics.md` and `devices.md` over there.
--- ---
## 1. Storage and the boot path ## 1. Storage and the boot path
**Done: a USB disk, a SATA disk and their partitions can be booted from.** The A USB disk, a SATA disk, an NVMe SSD and their partitions boot, and the root is chosen at boot from
root is chosen at boot from what is present (`rootdev_mount()` in what is present ([boot.md](../architecture/boot.md)).
`kernel/proc/proc.c`), and `make disk` builds a GPT image with an ESP and a SchFS
partition that a PC boots from a stick or a disk.
NVMe (`kernel/drivers/storage/nvme/`) registers with the same block registry, - **An installer** that writes the layout `make disk` builds (a GPT disk with an ESP and a SchFS
so an NVMe SSD boots the same way. partition) to a machine's disk, so a root survives a reboot. See §5.
- **Interrupt-driven, queued I/O for AHCI and NVMe.** Both poll for completion, one command at a time
**Next:** an installer that writes that layout to a disk, and interrupt-driven, per disk: enough to boot and run, not to be fast.
queued I/O for AHCI and NVMe. - **An EHCI driver.** USB 2.0 ports wired only to EHCI do nothing; xHCI ports work.
## 2. Graphics ## 2. Graphics
- **A `/dev/dri`-style device with mode setting**, so resolution stops being - **A `/dev/dri` device with mode setting**, so a real panel's resolution stops being whatever the
whatever the loader asked for. This was to be done *with* loader's menu chose and Mesa has something to open. This is the GPU plan, [gpu.md](gpu.md).
[hybrid.md](hybrid.md)'s GPU eviction, and that eviction has now happened — - **Double buffering and vblank**, which `display_ops` specifies and no driver implements; the same plan.
`cmd/vgpud` drives virtio-gpu from userspace. The modeset API is the next thing - **CFF/Type 2 charstrings**, so OTF fonts work. A separate `font_ops` provider and a bytecode
it makes affordable, and is milestone H3 over there rather than here. interpreter, *not* an extension of the `glyf` path.
- **Double buffering and vblank.** `display_ops` specifies both and no driver - **Text layout** — advance widths, kerning — as a library, so the terminal and the file manager are not
implements either. Schcom needs both to not tear. each solving it.
- **Port the console onto the text layer and delete the cell grid.** The layer
exists, the rasteriser exists, and the console still indexes `font_data[256][16]`.
- **Load a font from disk** and point the console at it.
- **CFF/Type 2 charstrings**, so OTF fonts work. A separate `font_ops` provider
and a bytecode interpreter, *not* an extension of the `glyf` path.
- **Text layout** — advance widths, kerning — as a library in `lib/`, so the
terminal and the file manager are not each solving it.
The console keeps its fixed grid until the first item above; proportional text
belongs to the GUI.
## 3. Networking ## 3. Networking
Two things the stack owed on the day it landed, both still owed: - **A routing table** and `/proc/net/route`. With two interfaces up the first is the default route and the
second is reachable only by an address on its own subnet; that is a rule and not routing.
- **Wi-Fi.** The laptop has no other way to be wireless. The route is the same as for the NIC: a Linux
driver (`iwlwifi`, `mt76`…) through LDLS, which needs `nl80211`, a firmware loader that exists, and a
supplicant (`wpa_supplicant` is an Alpine package).
- **IPv6.**
- **A netlink event stream**, for anything that wants to be told a link changed.
- **`accept()` does not report the peer's address**, and there is no ## 4. The hardware that makes a laptop a laptop
`getsockname`/`getpeername`. Nothing has needed them yet; a server that wants
to log who connected will.
- **No routing table.** With two interfaces up, the first is the default route
and the second is reachable only by an address on its own subnet. That is a
rule rather than an accident, and it is not routing.
Two that were on this list are now done and described in - **HDA audio.** AC'97 is what QEMU and old boards have; a laptop built this century has Intel HDA,
[../architecture/networking.md](../architecture/networking.md): `setsockopt` which is codec enumeration, CORB/RIRB and stream descriptors. It arrives as a second `struct audio_ops`.
honours six options and refuses the rest, and `sendmsg`/`recvmsg` carry - **Embedded-controller events** (the ACPI SCI), so a lid, a power button on the EC and a battery change are seen.
`SCM_RIGHTS`. A third is closer than it was — toybox's `ping` needed `recvmsg` - **CPU frequency control** (AMD CPPC; Intel HWP/EIST). Idle already reaches `_CST`'s C2; under load the cores run at
*and* `IP_RECVTTL`, and only the second is still missing. the firmware's default clock.
- **Touchpad gestures** — two-finger scroll, palm rejection — on top of I2C-HID's mouse mode.
- **An IOMMU** (VT-d / AMD-Vi), which turns "a driver in a process is isolated against bugs" into "against malice" —
the largest honest caveat on [ldls.md](../architecture/ldls.md).
- **udev's job** — a uevent socket and `/sys/devices` — so X and others are told about devices instead of being
given a list.
## 4. The interactive crash screen ## 5. The desktop, and the road to Wayland
PSOGM renders and stops. The idea is *a small OS inside the crash*, and the SchOS does not write its own compositor. (Schcom, a 715-line v0, was deleted: its wire format was a dead end, and a
framing decides the design: compositor worth running is years of work that Alpine already packages.) The desktop is **Xorg with Window Maker** now
([desktop.md](../architecture/desktop.md)); **XLibre**, the Xorg fork, is a drop-in alternative if Alpine carries it, and a
Wayland compositor (labwc, sway, weston, a GNOME or KDE session) is the larger step. What Wayland — and a modern X — need
from the system, in the order they are useful:
> At panic time the kernel is the only thing running, which is exactly why it can 1. **udev — done** ([udev.md](../architecture/udev.md)). eudev runs from `schinit`, the uevent netlink socket and the
> offer what a live system cannot: walk the stack, read another process's memory, `/sys/devices` tree exist, PCI and USB devices are in it with `add`/`remove` events from USB hot-plug, devtmpfs takes udev's
> dump page tables, inspect the process table — with no scheduler and no locking links, and Xorg finds its keyboard and mouse through it. **Left:** USB interfaces, PCI bridges as parents, `hwdb`.
> to respect, because nothing else can race you. 2. **libinput and libseat — done.** `libinput list-devices` sees the keyboard and the pointer as an ordinary user, Xorg now
drives both through `xf86-input-libinput`, and `seatd` runs from `schinit`: a non-root program opens `seat0` through
`libseat`, is handed `/dev/input/event0` over the socket, and reads it. Getting there found a real kernel bug (sockets
returned by `accept()` could not carry file descriptors) and a second one in `sendmsg`'s control-buffer parsing; both would
have stopped a Wayland compositor and its clients too. **Not yet tried:** a compositor.
3. **A DRM device.** Wayland compositors want `/dev/dri/card0` with KMS, and Mesa wants `renderD128`. SchOS has neither: it has
`/dev/fb0`. This is the GPU plan ([gpu.md](gpu.md)) — Linux's own `amdgpu` run by LDLS and `/dev/dri` served by that
process — and, for machines without a real GPU driver, a *simple-framebuffer* DRM shim (Linux's `simpledrm`, which is a
`.ko` LDLS could also host).
4. **dma-buf, `epoll`-able sync files and the rest of the Linux graphics ABI**, as the compositors turn out to need them.
What makes it a *system* rather than a print statement is that it keeps the base None of this is a kernel rewrite: it is the Linux interfaces that already exist half-built (`/sys`, netlink, evdev, `SCM_RIGHTS`,
machine's drivers. Concretely, the ones that work with interrupts off: the `memfd`), finished until real programs stop asking for what is missing. The method is the one that has worked all along: `apk
framebuffer (`gfx_draw_pixel` is a plain memory write), the PS/2 keyboard add` the program, run it, and fix what it names.
(pollable via ports 0x60/0x64), and the serial port. **Those three are a complete
console.**
Everything else — IDE, USB, the timer, anything that sleeps or takes a Nothing here starts before [1.0.md](1.0.md)'s register is empty, except where the owner says otherwise.
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 ## 5b. The boot manager
`sleep`/`wakeup`, no `acquire` on anything a live path might hold, one static
command buffer, and that driver set. **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 wanting more should be answered by rebooting
into a recovery environment.
A useful precedent to read but not copy: DDB, the BSD in-kernel debugger, is The loader ([boot.md](../architecture/boot.md#the-boot-manager)) is now a Niveus-styled window: bevelled keys, wells, Instrument Sans,
exactly this, and its hard-won rule is the same one — *a debugger that can keyboard and pointer, a list of other systems it finds on the disks and can chain-load, the F8 options (resolution with its
deadlock is worse than no debugger.* "keep this?" confirmation, verbose boot), and real Firmware Setup, Restart and Shut Down.
## 5. The installer and `.sip` Still to do:
Scoped in [`projects/schinst/README.md`](../../projects/schinst/README.md), - [ ] boot entries from a file on the ESP (several kernels, command lines, "drivers off", a default and a timeout), instead of
which holds the detail. In short: the two fixed ways of starting this system
- [ ] a SchFS partition as a boot source, not just the volume the loader came from
- [ ] an honest failure screen for the loader's own errors (today `die()` prints to the console)
- [ ] the pointer path verified on a real machine; QEMU with OVMF never delivered a USB mouse event to the loader
- [ ] light appearance, and a theme read from the ESP
It is its own piece of work in `boot/uefi/`; the kernel's TrueType rasteriser is not available at that point, which is why the
loader carries a pre-rasterised face (`include/gfx/nvfont.h`).
### The installer and `.sip`
Scoped in [`projects/schinst/README.md`](../../projects/schinst/README.md), which holds the detail. In short:
- [ ] `.sip` format specification, **written down before any code** - [ ] `.sip` format specification, **written down before any code**
- [ ] `tools/mksip` — capture a directory tree into a package - [ ] `tools/mksip` — capture a directory tree into a package
- [ ] The apply path - [ ] The apply path
- [ ] An installation environment that boots from the ISO, partitions, applies - [ ] An installation environment that boots from the ISO, partitions, applies the `.sip`, and installs the loader
the `.sip`, and installs the loader
`.sip` is modelled on WIM, and the thing worth copying from WIM is that it is `.sip` is modelled on WIM, and the thing worth copying from WIM is that it is **file-based, not block-based**: one
**file-based, not block-based**: one file holds a whole filesystem, applies to a file holds a whole filesystem, applies to a partition of any size, and several images can share their common files.
partition of any size, and several images can share their common files.
**Most of this already exists.** `tools/mkschfs.c` and `tools/mkfs.c` both do **Most of this already exists.** `tools/mkschfs.c` and `tools/mkfs.c` both do content dedup with an `(st_dev, st_ino)`
content dedup with an `(st_dev, st_ino)` fast path, and SchFS's B-tree is content fast path, and SchFS's B-tree is content addressed. `mksip` should share that code rather than reimplement it. And
addressed. `mksip` should share that code rather than reimplement it — that is the live image is already a compressed block image of a SchFS root ([scff.md](../architecture/scff.md)); an installer
the reason the dedup was factored the way it was. may need nothing more than to write that, decompressed, to a partition.
Open questions, all wanting a decision before code: Open questions, all wanting a decision before code:
- **Compression.** Something with a small decompressor is worth more than - **Compression.** Something with a small decompressor is worth more than something with a good ratio, because the
something with a good ratio, because the decompressor runs in the installation decompressor runs in the installation environment. (zstd is already in the kernel for SCFF.)
environment. - **Multiple images per `.sip`** — WIM's real trick, install/recovery/boot sharing one blob store. Worth the complexity
- **Multiple images per `.sip`** — WIM's real trick, install/recovery/boot now, or not?
sharing one blob store. Worth the complexity now, or not? - **Can the kernel mount a `.sip` read-only directly?** If it can, the installation environment's root can simply *be* the `.sip`.
- **Can the kernel mount a `.sip` read-only directly?** It is close to a
filesystem already, and if it can, the installation environment's root can
simply *be* the `.sip`.
The GUI for this is a Wayland client; see [wayland.md](wayland.md). Its GUI is the boot manager's (5b) — the installation environment starts in it — or an X client.
## 6. SchFS beyond SchOS ## 6. SchFS beyond SchOS
In the order they should happen. In the order they should happen.
**~~An on-disk format version~~, and an upgrade path.** The version policy is **An upgrade step.** The format version policy is done ([schfs.md](../architecture/schfs.md)); the upgrade step cannot be
done — see [../architecture/schfs.md](../architecture/schfs.md). What remains is written until there are two formats to convert between, and the first bump is when it becomes real work.
the **upgrade step**, and it cannot be written until there are two formats to
convert between; the first bump is when that becomes real work rather than
speculation.
The format is now pinned, which is what the other two items were waiting on. **A Linux implementation, FUSE first.** Three reasons in order of weight: it can be tested with `fsx` and `xfstests`, which
are far harsher than anything written alongside the code; it carries no licence entanglement, since libfuse is LGPL and used
across a process boundary; and it can reuse the read/write logic already in `tools/mkschfs` and `tools/schfs_fsck` rather than
being written twice and having the two disagree. A kernel module comes only after FUSE passes `xfstests`; SchOS is GPL-2.0-or-later
([../licensing.md](../licensing.md)), so a Linux module is a technical project now and not a licensing one.
**A Linux implementation, FUSE first.** Three reasons in order of weight: it can **The missing features**, listed honestly before adding any: no journal replay across an unclean shutdown beyond what
be tested with `fsx` and `xfstests`, which are far harsher than anything written copy-on-write gives for free, no snapshots, no sparse-file support, and no online `fsck`. **Snapshots are the one where the
alongside the code; it carries no licence entanglement, since libfuse is LGPL and existing design already pays for most of the cost** — the B-tree is copy-on-write, which is most of the machinery; nothing takes
used across a process boundary; and it can reuse the read/write logic already in or names a snapshot.
`tools/mkschfs` and `tools/schfs_fsck` rather than being written twice and having
the two disagree.
A kernel module is a different project, comes only after FUSE passes `xfstests`,
and carries the licence problem [../licensing.md](../licensing.md) describes.
**The missing features**, listed honestly before adding any: no journal replay
across an unclean shutdown beyond what copy-on-write gives for free, no
snapshots, no sparse-file support, and no online `fsck`. **Snapshots are the one
where the existing design already pays for most of the cost** — the B-tree is
copy-on-write, which is most of the machinery; nothing takes or names a snapshot.
## 7. The kernel's own gaps ## 7. The kernel's own gaps
Not phases, and each one improves everything above it: Not phases, and each one improves everything above it:
- **A slab or pool allocator.** `kalloc` hands out one page at a time, and every - **A page cache**, which is what a general writable `MAP_SHARED` of a file needs. `memfd` is the narrow answer and has been enough.
subsystem written since has invented its own fixed-size table to work around - **A working `mprotect`, and NX.** `mprotect` is accepted and ignored and every mapping is executable; RELRO, guard pages
that. This is the single change that would most improve the rest of the kernel. and a W^X JIT all want the real thing.
- **A page cache**, which is what a general writable `MAP_SHARED` needs. - **Per-CPU run queues** and work stealing, with a load balancer that is not worse than the lock it replaces.
[foundations.md](foundations.md) delivers a `memfd` instead, which is narrower - **A garbage collector for descriptors in flight.** An `AF_UNIX` socket passed over itself and never received leaks the pair;
and sufficient for the GUI. bounded at thirty-two, and nothing in the tree does it.
- **Symlinks in procfs**, so `/proc/<pid>/exe` can be one and `ps -o comm` stops - **Kernel-time accounting**, so `ru_stime` is not zero.
printing `[init]`. Belongs with the rest of the VFS work rather than on its own. - **A contiguous multi-page allocator**, so `struct proc` and `struct socket` can come from somewhere other than fixed tables.
- **The `ls -lR` hang** — **not reproducible**, and kept here because the absence
of a result is a result. Instrumented and hunted at length; see Gate 0 of
[1.0.md](1.0.md). Two real defects were found on the way and fixed, neither
proven to be the cause.
- **No garbage collector for descriptors in flight.** An `AF_UNIX` socket passed
over itself and never received leaks the pair. Bounded at thirty-two sockets,
and nothing in the tree does it — but a third-party program could.
--- ---
## Kept out of scope on purpose ## Kept out of scope on purpose
- Anything that needs pushing or publishing. This work stays local.
- TrueType hinting. - TrueType hinting.
- XWayland. - DRM/KMS as a *kernel* subsystem; the GPU plan serves `/dev/dri` from a process.
## Unfinished, as of 2026-10-26
Left where it stood; none of it is hidden by the code.
- **`startx` as root fails.** The image now has only root, and in QEMU `startx`
ends with `xinit: unable to connect to X server: Bad file descriptor` and
writes `/var/log/Xorg.0.log` (not `~/.local/share/xorg/`). It worked as the
old `schos` user. The cause has not been looked at; start with that log.
- **Window Maker on the laptop hung** (no cursor movement, no exit, no VT
switch) while Xorg was using `xf86-input-libinput`. That package is out of the
image again; whether Xorg's own evdev driver is enough on that machine is not
confirmed. If it still hangs, the `Xorg.0.log` from the machine is what is
needed.
- **ldlsd's crash on real hardware** was a misaligned `movaps` in a variadic
function; LDLS guest code is now built with `-mstackrealign`. Not run on the
laptop since. Two `SchOS: permission denied (write)` lines appear just before
it; the message now names the process and inode so the next log says who.
- **Boot manager pointer**: the cursor is drawn whenever a pointer device
exists, but no click or movement has been seen from a real device (QEMU's USB
mouse never reached the loader). A laptop touchpad is usually not exposed to
UEFI at all.
- **Kernel-side splash**: the loader's splash is in the new style; what vgpud
draws once the kernel is up is still the old mark.
- **USB**: no `/dev/bus/usb` (usbfs), so libusb tools such as plain `lsusb` do
not work (`lsusb -t` does); `uname -r` says `0.1.0`, which libusb refuses; hub
port counts and string descriptors are missing.
- **Terminals**: all twelve ask for a login. There is no longer a login-free
root shell to fall back on if `login` itself breaks.

View file

@ -1,6 +1,11 @@
# SchAPI, and SchOS as a system # SchAPI, and SchOS as a system
Status: **direction agreed, not yet implemented.** Nothing in this document Status: **direction agreed, not yet implemented**, and frozen until 1.0 ships
([1.0.md](1.0.md)). Read it as a direction and not as a schedule: written when the plan was a
compositor of SchOS's own, it has not been revisited since the desktop became Xorg and the
packages became Alpine's, and §7 (what happens now) in particular predates both. It also assumed SchOS would write its
own compositor (Schcom) for the API to be designed *from*; that compositor was deleted, and SchOS's desktop
is other people's, so read every mention of Schcom below as "the desktop". Nothing in this document
changes what is being built this week; it changes what everything is being changes what is being built this week; it changes what everything is being
built *towards*. See §7 for what actually happens now. built *towards*. See §7 for what actually happens now.

View file

@ -1,177 +0,0 @@
# The GUI becomes Wayland
Schcom survives and changes its wire format. SchWM and SchDE are deleted. What
follows is why that is not a retreat, and what it costs.
Blocked on [foundations.md](foundations.md). Everything there is built except
item 4, `setsockopt` that does not lie: descriptor passing, shareable memory,
sockets in the filesystem, dynamic linking, and the epoll/eventfd/timerfd/
signalfd set libwayland's event loop is made of.
---
## What was deleted
`projects/schwm/` and `projects/schde/`, both README-only; no code was lost. They
are **gone from the tree now** rather than marked for deletion — a directory that
has said "to be deleted" for months is a corner, not a decision. The arguments
each made are kept below, because they are the part worth keeping.
### SchWM is not abandoned — Wayland dissolves it
In X11 the window manager is a separate privileged client because X11 puts
compositing in the server and policy nowhere. **Wayland's central decision is
that the compositor *is* the window manager**: there is no protocol by which a
second process places another's windows, and every attempt to add one has been a
private extension.
So SchWM's own argument —
> *"Compositing is mechanical and must never stall: it runs every frame and a
> mistake there is a frozen screen. Window management is policy, full of special
> cases and personal taste, and it changes constantly."*
— is a real and correct observation **about X11**, which Wayland answers a
different way. It stays worth honouring, but as a **module boundary inside
Schcom**, not a process boundary: policy in its own file behind its own
interface. Do not keep it in its own process, because the protocol to talk to it
does not exist.
### SchDE is re-sited rather than abandoned
A panel, a launcher and a wallpaper are ordinary Wayland clients using
`wlr-layer-shell` — MIT, an XML file, no dependency. They need no directory of
their own until one of them is written. SchDE's other observation stands on its
own and is recorded here so it does not go with the directory: **a GUI terminal
is not the kernel console with a window around it.** The console parses ANSI
because it draws straight to the framebuffer; a terminal client parses ANSI
itself and renders it with proportional-capable text. The two share no code.
### `projects/schinst/` stays
Its GUI was the part tied to SchWM, and that part becomes a Wayland client.
Everything else in it — the `.sip` format, `mksip`, the apply path, the
installation environment — is untouched by any of this, and remains the
best-specified unstarted work in the tree.
---
## What survives
**Schcom stays, and keeps its name.** It already owns the display, allocates and
owns client buffers, implements surface/attach/commit, routes input from
`/dev/input/eventN`, composites multiple clients bottom-to-top, and cleans up
after clients that die.
Its own README says the protocol is deliberately Wayland-shaped — surfaces rather
than windows, buffers attached and made visible only on commit, the compositor
telling clients when to draw. That was the right call and this is where it is
cashed in: **what changes is the wire format and the buffer handle, not the
architecture.**
---
## What has to exist first
Items 1–4 of [foundations.md](foundations.md), and the first three are absolute —
libwayland does not start without them.
The sharpest is descriptor passing. `wl_shm_create_pool` passes a **file
descriptor**, and there is no path in the protocol that passes an integer
instead. `struct schcom_surface`'s `handle_kind` field was written precisely so
that switching would be a new value rather than a protocol break, and it cannot
help: *our* protocol is the flexible one.
---
## Port libwayland. Do not port Weston.
**libwayland is the small piece**: a wire protocol, an object map, and an event
loop, needing `libffi` and — for the build-time scanner, on the host only —
`libexpat`. A real port, but a bounded one.
**Weston is the trap.** It arrives welded to `libinput`, `udev`, `libdrm`,
`logind`-shaped seat management and a GL renderer, and porting it means porting
Linux's device management first and finding the compositor somewhere underneath.
This is precisely the lesson already recorded twice in this tree — about
FreeBSD's `sys/dev/usb` (100k lines welded to newbus) and about FreeBSD's
`sys/netinet` (not a library; reaches into mbufs, epoch, netisr, VNET and uma).
> The tree has learned this twice. It should not pay to learn it a third time.
So **Schcom links `libwayland-server` and remains ours.** Input comes from
`/dev/input/eventN`, which already works, rather than from libinput. Output goes
through `display_ops`, which already exists.
### Software rendering first, and probably for a long time
`pixman` for composition, `wl_shm` for buffers. Mesa means EGL means `dlopen`
means a dynamic loader -- which now exists (see [foundations.md](foundations.md)
item 5), so it is no longer what stands in the way of Mesa; a GPU driver is.
Static binaries are still enough to reach a working desktop, and the desktop's
own programs can stay static until something concrete demands otherwise.
### One sharp edge
`wl_keyboard` hands the client **a keymap as an XKB text blob over a file
descriptor**. A compositor cannot avoid xkbcommon by having simpler opinions
about keyboards — the protocol requires the blob.
The cheap route is one precompiled keymap string shipped in the binary, with
xkbcommon and `xkeyboard-config` deferred until more than one layout is wanted.
---
## Licensing
Checked before anything is fetched — the standing rule is that the licence goes
in `NOTICE` **before** the file is committed, not after.
| | |
|---|---|
| wayland, wayland-protocols | MIT |
| `wlr-layer-shell` | MIT |
| pixman | MIT |
| libffi | MIT |
| libxkbcommon | MIT |
| xkeyboard-config | MIT |
All GPL-2.0-or-later compatible, all needing only a `NOTICE` entry. The
Wayland ecosystem is permissively licensed almost throughout, so nothing here
raises a licensing question at all.
---
## Milestones
| | |
|---|---|
| **W0** | [foundations.md](foundations.md) items 1–4, with the combined test that passes a descriptor *and* shares a `memfd` *and* binds a socket at a real path. |
| **W1** | libwayland builds and installs into the sysroot. `wayland-scanner` runs on the host. |
| **W2** | Schcom serves `wl_compositor`, `wl_shm`, `wl_surface`, `xdg_shell`. A client draws one window. |
| **W3** | `wl_seat`: keyboard and pointer, from the evdev path that exists. |
| **W4** | Two clients, focus, stacking, move and resize. **This is where SchWM's policy lives, as a file.** |
| **W5** | `wlr-layer-shell`, and a panel. This is SchDE's first pixel. |
Every milestone from W2 on is asserted **from the host, by pixel**, the way
`schcom-test` already does. The compositor's own report that it composited
something proves nothing.
---
## What it will still not do
- **No hardware acceleration.** Software rendering only, until Mesa, which is
not on this plan. Dynamic linking, which it also needs, is in.
- **No XWayland**, so no X11 clients, ever, unless someone ports it.
- **No mode setting.** Resolution stays whatever the loader asked for — see
[hybrid.md](hybrid.md), where the GPU driver's eviction is also where a modeset
API would arrive.
- **No damage tracking beyond "repaint everything"**, no multiple outputs, no
subsurfaces. Schcom's README already lists these; they are additions rather
than rewrites, which is the reason for committing to the surface/commit model
now.
- **One keyboard layout**, until xkbcommon is ported.
- **No clipboard, no drag and drop, no screen capture protocol**, none of which
W0–W5 touches.

View file

@ -1,44 +0,0 @@
# Xorg
The desktop is X11, on the framebuffer. This replaces [wayland.md](wayland.md)
as the plan for Gate 3 of [1.0.md](1.0.md); Wayland stays on file and is not
being worked on.
## Why X and not Wayland
Every Wayland compositor worth running -- labwc, and everything else built on
wlroots -- needs DRM/KMS, libinput, udev and libseat. DRM/KMS is the largest of
them by far: a kernel graphics subsystem, and in practice a great deal taken
from Linux. Xorg runs on a plain framebuffer through its `fbdev` driver and
reads input through `evdev`, both of which are interfaces SchOS already has.
It is the shorter road to a desktop by a long way, and it brings the window
managers with it: twm first, then Window Maker.
DRM/KMS is not planned. libinput, udev and libseat are worth having for their
own sake and may come later; Xorg does not need them.
## What is built
- **The kernel calls** (`b1323e1`, checked by `cmd/xready`): evdev's queries,
fbdev's mode-set to the current mode, `KDSETMODE`, `KDSKBMODE`, the
`VT_PROCESS` switch handshake, and `/sys/bus/pci/devices/*/{boot_vga,
enable,graphics/fb0}` for libpciaccess.
- **Dynamic linking and `dlopen`** (`4330576`): Xorg loads its drivers as
modules.
- **`<linux/*.h>`** in the build sysroot, as `ports/linux-headers`.
## What is left, in order
1. **The X client libraries.** xorgproto, xtrans, libXau, libXdmcp,
xcb-proto, libxcb, libX11, libXext, libXrender, libXfixes, libxkbfile,
libfontenc, libXfont2, freetype, zlib, pixman, libpciaccess.
2. **Keyboard and fonts.** xkbcomp and xkeyboard-config, the data X compiles
keymaps from; a misc-fixed font.
3. **The server.** xorg-server built with meson, GLX, glamor and DRI off;
xf86-video-fbdev; xf86-input-evdev with libevdev and mtdev.
4. **The first picture.** `twm` and `xterm`, started by hand from a VT.
5. **Window Maker**, and an `startx` that works.
The kernel calls were written against what these programs are documented
to do. Each step past it is expected to find a call that was not -- that is
what running the real program is for.

View file

@ -1,11 +1,11 @@
# The interactive shell on /dev/tty1, the first virtual terminal. # The interactive shell on /dev/tty1, the first virtual terminal.
Label = os.schos.console-shell Label = os.schos.console-shell
# zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to # A login prompt, not a shell: /etc/schos-getty hands the terminal to login,
# when no manifest loads -- so if zsh ever fails to start, the recovery shell # which asks who is there and starts their shell. tty6 keeps a root shell
# is a different program rather than the same broken one. See ports/zsh. # with no login, for when this part of the system is what is broken.
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
@ -15,8 +15,6 @@ KeepAlive = yes
# most: a line editor with no TERM cannot ask terminfo how to move a cursor. # most: a line editor with no TERM cannot ask terminfo how to move a cursor.
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
# This job reads the console, so it has to own it: schinit puts every job in # This job reads the console, so it has to own it: schinit puts every job in
# its own session, and a session with no controlling terminal is not delivered # its own session, and a session with no controlling terminal is not delivered

View file

@ -0,0 +1,17 @@
# The Realtek RTL8111/8168 ethernet, driven by Linux's own r8169.
#
# The modules (mdio-bus, libphy, mdio_devres, the Realtek PHY driver and r8169
# itself), the kernel's BTF and the RTL PHY firmware are put in the image by
# `make ldls-r8169`. ldlsd loads them in that order, binds the PHY driver
# through its driver core, and probes the NIC; the interface is ldls0.
#
# On a machine with no RTL8168 ldlsd exits SCHINIT_EXIT_NORESTART and that is
# the end of it. KeepAlive is off while this is new: a driver that crashes on
# real hardware would otherwise be restarted into the same crash every few
# seconds. Its output, every line of it, is in /var/log/ldls-r8169.log.
Label = os.schos.ldls-r8169
Program = /bin/sh
Args = sh /lib/modules/ldls-r8169.sh
RunAtLoad = yes
KeepAlive = no

17
etc/schinit/seatd.job Normal file
View file

@ -0,0 +1,17 @@
# The seat daemon: hands a Wayland compositor (through libseat) the input and
# display devices it needs without running the compositor as root. seatd opens
# them as itself and passes the file descriptors over its socket; the socket is
# group `video`, which the first user is in.
#
# Alpine's seatd package. A compositor that runs as root, or one started with
# seatd-launch, needs none of this.
Label = os.schos.seatd
Program = /usr/bin/seatd
Args = seatd -g video
RunAtLoad = yes
KeepAlive = yes
ThrottleInterval = 3
StandardOut = /dev/null
StandardError = /dev/null

View file

@ -0,0 +1,20 @@
# A login on /dev/tty10: every one of the twelve virtual terminals asks who is there.
# Alt+F10 (or Ctrl+Alt+F10) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty10-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty10
StandardOut = /dev/tty10
StandardError = /dev/tty10
ThrottleInterval = 1

View file

@ -0,0 +1,20 @@
# A login on /dev/tty11: every one of the twelve virtual terminals asks who is there.
# Alt+F11 (or Ctrl+Alt+F11) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty11-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty11
StandardOut = /dev/tty11
StandardError = /dev/tty11
ThrottleInterval = 1

View file

@ -0,0 +1,20 @@
# A login on /dev/tty12: every one of the twelve virtual terminals asks who is there.
# Alt+F12 (or Ctrl+Alt+F12) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty12-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty12
StandardOut = /dev/tty12
StandardError = /dev/tty12
ThrottleInterval = 1

View file

@ -6,15 +6,13 @@
# what /dev/tty1 and /dev/tty2 being different terminals means. # what /dev/tty1 and /dev/tty2 being different terminals means.
Label = os.schos.tty2-shell Label = os.schos.tty2-shell
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes ControllingTerminal = yes

View file

@ -1,24 +1,15 @@
# An interactive shell on /dev/tty3. # A login on /dev/tty3: every one of the twelve virtual terminals asks who is there.
# # Alt+F3 (or Ctrl+Alt+F3) shows it. Twelve manifests rather than one with a
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only # range, because schinit has no notion of a template.
# two ever had anything running on them -- so Alt+F3 switched to a terminal
# that was working perfectly and had nobody to talk to, which looks exactly
# like a broken one.
#
# Identical to tty2-shell apart from the number. Six manifests rather than one
# with a range, because schinit has no notion of a template and inventing one
# for six lines each is the wrong trade.
Label = os.schos.tty3-shell Label = os.schos.tty3-shell
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes ControllingTerminal = yes

View file

@ -1,24 +1,15 @@
# An interactive shell on /dev/tty4. # A login on /dev/tty4: every one of the twelve virtual terminals asks who is there.
# # Alt+F4 (or Ctrl+Alt+F4) shows it. Twelve manifests rather than one with a
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only # range, because schinit has no notion of a template.
# two ever had anything running on them -- so Alt+F4 switched to a terminal
# that was working perfectly and had nobody to talk to, which looks exactly
# like a broken one.
#
# Identical to tty2-shell apart from the number. Six manifests rather than one
# with a range, because schinit has no notion of a template and inventing one
# for six lines each is the wrong trade.
Label = os.schos.tty4-shell Label = os.schos.tty4-shell
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes ControllingTerminal = yes

View file

@ -1,24 +1,15 @@
# An interactive shell on /dev/tty5. # A login on /dev/tty5: every one of the twelve virtual terminals asks who is there.
# # Alt+F5 (or Ctrl+Alt+F5) shows it. Twelve manifests rather than one with a
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only # range, because schinit has no notion of a template.
# two ever had anything running on them -- so Alt+F5 switched to a terminal
# that was working perfectly and had nobody to talk to, which looks exactly
# like a broken one.
#
# Identical to tty2-shell apart from the number. Six manifests rather than one
# with a range, because schinit has no notion of a template and inventing one
# for six lines each is the wrong trade.
Label = os.schos.tty5-shell Label = os.schos.tty5-shell
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes ControllingTerminal = yes

View file

@ -1,24 +1,15 @@
# An interactive shell on /dev/tty6. # A login on /dev/tty6: every one of the twelve virtual terminals asks who is there.
# # Alt+F6 (or Ctrl+Alt+F6) shows it. Twelve manifests rather than one with a
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only # range, because schinit has no notion of a template.
# two ever had anything running on them -- so Alt+F6 switched to a terminal
# that was working perfectly and had nobody to talk to, which looks exactly
# like a broken one.
#
# Identical to tty2-shell apart from the number. Six manifests rather than one
# with a range, because schinit has no notion of a template and inventing one
# for six lines each is the wrong trade.
Label = os.schos.tty6-shell Label = os.schos.tty6-shell
Program = /bin/zsh Program = /bin/sh
Args = -zsh Args = sh /etc/schos-getty
RunAtLoad = yes RunAtLoad = yes
KeepAlive = yes KeepAlive = yes
Environment = TERM=linux Environment = TERM=linux
Environment = PATH=/bin:/usr/bin Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes ControllingTerminal = yes

View file

@ -0,0 +1,20 @@
# A login on /dev/tty7: every one of the twelve virtual terminals asks who is there.
# Alt+F7 (or Ctrl+Alt+F7) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty7-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty7
StandardOut = /dev/tty7
StandardError = /dev/tty7
ThrottleInterval = 1

View file

@ -0,0 +1,20 @@
# A login on /dev/tty8: every one of the twelve virtual terminals asks who is there.
# Alt+F8 (or Ctrl+Alt+F8) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty8-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty8
StandardOut = /dev/tty8
StandardError = /dev/tty8
ThrottleInterval = 1

View file

@ -0,0 +1,20 @@
# A login on /dev/tty9: every one of the twelve virtual terminals asks who is there.
# Alt+F9 (or Ctrl+Alt+F9) shows it. Twelve manifests rather than one with a
# range, because schinit has no notion of a template.
Label = os.schos.tty9-shell
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
ControllingTerminal = yes
StandardIn = /dev/tty9
StandardOut = /dev/tty9
StandardError = /dev/tty9
ThrottleInterval = 1

View file

@ -0,0 +1,14 @@
# Replay the devices that were already there when udevd started, so /run/udev
# knows about them. Runs once; see /etc/udev-trigger.
#
# udevd is started by its own job and schinit has no ordering, so the script
# waits for the daemon's control socket.
Label = os.schos.udev-trigger
Program = /bin/sh
Args = sh /etc/udev-trigger
RunAtLoad = yes
KeepAlive = no
StandardOut = /dev/tty1
StandardError = /dev/tty1

16
etc/schinit/udevd.job Normal file
View file

@ -0,0 +1,16 @@
# eudev's daemon: the thing that turns the kernel's uevents into /run/udev
# data and /dev symlinks, which is how Xorg and every Wayland compositor learn
# what input and display devices exist.
#
# Alpine's eudev package; the kernel side is the uevent netlink socket and the
# /sys device tree (kernel/dev/kdev.c, kernel/fs/sysfs.c).
Label = os.schos.udevd
Program = /sbin/udevd
Args = udevd
RunAtLoad = yes
KeepAlive = yes
ThrottleInterval = 3
StandardOut = /dev/tty1
StandardError = /dev/tty1

10
etc/schos-getty Normal file
View file

@ -0,0 +1,10 @@
#!/bin/sh
# What a virtual terminal runs: take the terminal back from whoever had it, and
# ask for a login. login hands the terminal to the user it lets in, and this
# runs again when their shell exits.
t=$(tty 2>/dev/null)
if [ -n "$t" ]; then
chown root:tty "$t" 2>/dev/null
chmod 0660 "$t" 2>/dev/null
fi
exec /bin/login

14
etc/udev-schos.rules Normal file
View file

@ -0,0 +1,14 @@
# SchOS's own device permissions, applied after Alpine's (the name sorts last
# once installed as 90-schos.rules).
#
# The kernel makes these nodes with the permissions it means them to have
# (kernel/fs/devfs.c). udev, with no rule that names a node, would chmod it to
# 0600 -- which made /dev/null unusable to everyone but root -- so every kernel
# device is named here with the same answer the kernel gave.
SUBSYSTEM=="mem", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="console", GROUP="root", MODE="0600"
SUBSYSTEM=="tty", KERNEL=="tty0|tty[0-9]|tty1[0-2]", GROUP="tty", MODE="0660"
SUBSYSTEM=="input", GROUP="input", MODE="0660"
SUBSYSTEM=="graphics", GROUP="video", MODE="0660"
SUBSYSTEM=="block", GROUP="disk", MODE="0660"

9
etc/udev-trigger Normal file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# Wait for udevd to listen, then ask the kernel to announce every device again.
i=0
while [ ! -S /run/udev/control ] && [ $i -lt 100 ]; do
sleep 0.1
i=$((i + 1))
done
udevadm trigger --action=add
udevadm settle --timeout=60

View file

@ -198,6 +198,14 @@ lcr3(uint64 val)
asm volatile("mov %0,%%cr3" : : "r" (val)); asm volatile("mov %0,%%cr3" : : "r" (val));
} }
static inline uint64
rcr3(void)
{
uint64 val;
asm volatile("mov %%cr3,%0" : "=r" (val));
return val;
}
// Invalidate the TLB entry for one page. Needed any time a PTE is changed // Invalidate the TLB entry for one page. Needed any time a PTE is changed
// or cleared for an address space that may still be the active CR3 — // or cleared for an address space that may still be the active CR3 —
// otherwise the CPU can keep translating through the stale entry until the // otherwise the CPU can keep translating through the stale entry until the

View file

@ -61,6 +61,39 @@ int pciuser_mmio_ok(uint64 phys, uint64 len);
void pciuser_release_all(struct proc*); void pciuser_release_all(struct proc*);
int pciuser_owner_pid(int idx); int pciuser_owner_pid(int idx);
int proc_reincarnate(int status); int proc_reincarnate(int status);
int sig_default_stops(int);
struct mm* mm_alloc(void);
void mm_get(struct mm*);
void mm_put(struct mm*);
int mm_shared(struct mm*);
void mm_lock(struct mm*);
void mm_unlock(struct mm*);
struct files* files_alloc(void);
void files_get(struct files*);
void files_put(struct files*);
void files_lock(void);
void files_unlock(void);
struct sighand* sighand_alloc(void);
void sighand_get(struct sighand*);
void sighand_put(struct sighand*);
struct proc* proc_leader(struct proc*);
int proc_thread_count(int);
void thread_exit(void) __attribute__((noreturn));
int futex_wake_key(void*, int);
int futex_op(addr_t, int, int, addr_t, addr_t, int);
int clone_proc(uint64, uint64, uint64, uint64, uint64);
void proc_exec_threads(void);
void proc_exec_done(void);
void thread_reaper_start(void);
void vm_defer_tick(void);
void mm_release_shared(struct mm*);
void proc_stop(int);
int sig_attention(struct proc*);
int sig_blocking_check(void);
void proc_cputime(uint64 *user, uint64 *sys);
// kernel/dev/procfs.c: /proc/strace
int strace_wanted(const char *name);
int proc_write_strace(int user_src, uint64 src, uint n);
void proc_kill_init(void); void proc_kill_init(void);
extern uint init_generation; extern uint init_generation;
void cprintf(char*, ...); void cprintf(char*, ...);
@ -147,6 +180,10 @@ void mountinit(void);
// 1 if the filesystem on this device refuses writes -- a mounted snapshot. // 1 if the filesystem on this device refuses writes -- a mounted snapshot.
// Ask before mutating, not after: the create path panics on a failed dirlink. // Ask before mutating, not after: the create path panics on a failed dirlink.
int fs_readonly(uint dev); int fs_readonly(uint dev);
int in_group(uint gid, int real);
uint inode_mode_bits(struct inode *ip);
int inode_permission(struct inode *ip, int mask, int real);
int capable(void);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root, int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type); const char *path, const char *source, const char *type);
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH // Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
@ -457,6 +494,8 @@ extern uint64 boot_epoch;
int cpunum(void); int cpunum(void);
extern volatile uint* lapic; extern volatile uint* lapic;
void lapiceoi(void); void lapiceoi(void);
extern volatile int panic_stop; // kernel/core/panic.c
void lapic_nmi_others(void);
void lapicinit(void); void lapicinit(void);
void lapicstartap(uchar, uint); void lapicstartap(uchar, uint);
void microdelay(int); void microdelay(int);
@ -498,8 +537,22 @@ int sockname_inet(struct socket*, int peer, uint *addr, ushort *port
#define SOCK_ABSTRACT_DEV 0xFFFFFFFFu // no filesystem has it; see unix.c #define SOCK_ABSTRACT_DEV 0xFFFFFFFFu // no filesystem has it; see unix.c
int sockreadable(struct socket*); int sockreadable(struct socket*);
int sock_hup(struct socket*); int sock_hup(struct socket*);
uint socknl_pending(struct socket*);
int sock_sendercred(struct socket*, int*, uint*, uint*);
int socknl_copy(struct socket*, uint, char*, int);
void socknl_take(struct socket*, uint);
int sock_shutdown(struct socket*, int how); int sock_shutdown(struct socket*, int how);
int sockpair(struct socket **a, struct socket **b); int sockpair(struct socket **a, struct socket **b);
int sockpair_type(struct socket **a, struct socket **b, int type);
int sock_is_packet(struct socket *s);
uint sock_nl_groups(struct socket *s);
int sock_netlink_bind(struct socket *s, uint pid, uint groups);
int sock_uevent_send(struct socket *s, const char *msg, int len, uint dpid, uint dgroups);
uint sock_nl_from(struct socket *s);
uint sock_nl_pid(struct socket *s);
uint sock_nl_bound_groups(struct socket *s);
void dev_stat_numbers(int major, int minor, int *maj, int *min);
void uevent_send(uint groups, const char *msg, int len, struct socket *from);
const char* sockname(struct socket*); const char* sockname(struct socket*);
// AF_INET addressing. Separate from sockbind/sockconnect because the families // AF_INET addressing. Separate from sockbind/sockconnect because the families
// do not name the same kind of thing; `addr` and `port` are network order. // do not name the same kind of thing; `addr` and `port` are network order.
@ -510,6 +563,8 @@ int socksendto(struct socket*, char *buf, int n,
int sockbind_in(struct socket*, uint addr, ushort port); int sockbind_in(struct socket*, uint addr, ushort port);
int sockconnect_in(struct socket*, uint addr, ushort port); int sockconnect_in(struct socket*, uint addr, ushort port);
int sock_domain(struct socket*); int sock_domain(struct socket*);
int sock_peercred(struct socket*, int*, uint*, uint*);
int sock_passcred(struct socket*);
// Socket options. The syscall layer does the user-memory marshalling and these // Socket options. The syscall layer does the user-memory marshalling and these
// do the work, because struct socket's fields live in kernel/net/unix.c and the // do the work, because struct socket's fields live in kernel/net/unix.c and the
// syscall layer deliberately cannot see them. // syscall layer deliberately cannot see them.
@ -550,6 +605,8 @@ int fork(void);
int growproc(int64); int growproc(int64);
int kill(int); int kill(int);
int sigsend_pgrp(int pgid, int sig); int sigsend_pgrp(int pgid, int sig);
int sigsend_user(int pid, int sig);
int sigsend_pgrp_user(int pgid, int sig);
int proc_pgid(int pid); int proc_pgid(int pid);
int proc_setpgid(int pid, int pgid); int proc_setpgid(int pid, int pgid);
int proc_setsid(void); int proc_setsid(void);
@ -564,6 +621,7 @@ int proc_exists(int);
int proc_num(void); int proc_num(void);
struct procinfo; struct procinfo;
int proc_snapshot(int idx, struct procinfo *out); int proc_snapshot(int idx, struct procinfo *out);
int proc_peek(int idx, struct procinfo *out); // no lock: GMMOS
int proc_snapshot_pid(int pid, struct procinfo *out); int proc_snapshot_pid(int pid, struct procinfo *out);
void pinit(void); void pinit(void);
void procdump(void); void procdump(void);
@ -696,6 +754,18 @@ int net_eth_detach(struct device *);
int net_detach(struct device *); int net_detach(struct device *);
int net_attach_late(struct device *); int net_attach_late(struct device *);
int net_eth_poll(void); int net_eth_poll(void);
void net_eth_link_check(void);
uint poll_generation(void);
// kernel/proc/mapname.c
struct mm;
void mapname_add(struct mm*, uint64 start, uint64 len, uint64 off, const char *path);
void mapname_unmap(struct mm*, uint64 start, uint64 len);
void mapname_free(struct mm*);
void mapname_copy(struct mm *dst, struct mm *src);
int mapname_find(struct mm*, uint64 addr, char *out, int cap, uint64 *off);
int mapname_gen(struct mm*, char *buf, int cap);
void mapname_report(struct proc*);
int proc_maps(int pid, char *buf, int cap);
int net_eth_count(void); int net_eth_count(void);
void net_eth_counts(int i, uint64 *rx, uint64 *tx); void net_eth_counts(int i, uint64 *rx, uint64 *tx);
struct netif *net_eth_netif(int i); struct netif *net_eth_netif(int i);
@ -760,9 +830,27 @@ void shm_ref(int id);
char* shm_page(int id, int pg); char* shm_page(int id, int pg);
uint64 memfd_size(int id); uint64 memfd_size(int id);
int memfd_truncate(int id, uint64 len); int memfd_truncate(int id, uint64 len);
int memfd_seals(int id, int add);
int dsp_writable(void);
// inotify (kernel/sys/sys_event.c), and the lookup it needs (sys_fs.c)
void inotify_event(uint dev, uint64 inum, uint32 mask, const char *name, uint32 cookie);
void inotify_file(struct file *f, uint32 mask);
uint32 inotify_cookie(void);
int ev_fionread(struct evobj *o);
int pipe_avail(struct pipe *p);
int vm_fault(addr_t va);
void tlb_init(void);
// keyboard lock keys and their lights: kernel/dev/kbd.c
void kbd_set_leds(uint locks);
void kbd_lock_key(uint lock, int down);
uint kbd_lock_state(void);
void hid_leds_want(int bits);
void lapic_send_ipi(int apicid, int vector);
pte_t* walkpte(pml4e_t *pgdir, const void *va);
int path_identity(char *path, int follow, int onlydir, uint *dev, uint64 *inum);
int memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing); int memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing);
void shm_detach(int id); void shm_detach(int id);
void proc_release_shared(struct proc *);
int copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int is_fb); int copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int is_fb);
void clearpteu(pml4e_t *pgdir, char *uva); void clearpteu(pml4e_t *pgdir, char *uva);

View file

@ -26,7 +26,7 @@
// //
// 4 MB was one framebuffer at 1280x800, which was fine while exactly one driver // 4 MB was one framebuffer at 1280x800, which was fine while exactly one driver
// wanted one. It stopped being fine the day a display driver could be handed to // wanted one. It stopped being fine the day a display driver could be handed to
// userspace (docs/plan/hybrid.md): either side of that handover **both drivers // userspace (docs/architecture/devices.md): either side of that handover **both drivers
// exist**, and the incoming one has to allocate its framebuffer before the // exist**, and the incoming one has to allocate its framebuffer before the
// outgoing one lets go of its own. So the real requirement is two framebuffers // outgoing one lets go of its own. So the real requirement is two framebuffers
// at once, not one. // at once, not one.

View file

@ -57,7 +57,31 @@ struct usb_info {
uint16 vid, pid, bcd; uint16 vid, pid, bcd;
uint8 cls, subcls, proto; // the device descriptor's uint8 cls, subcls, proto; // the device descriptor's
uint8 nconf; uint8 nconf;
uint16 bcdusb; // the version the device speaks
uint8 maxp0; // bMaxPacketSize0
uint8 nif, confval, attrs; // the configuration descriptor's
uint16 maxpower; // in mA
char path[24]; // Linux's name: "1-3", "2-1.4" char path[24]; // Linux's name: "1-3", "2-1.4"
char driver[16]; // the first interface's, or empty char driver[16]; // the first interface's, or empty
}; };
int usb_info_nth(int i, struct usb_info *out); int usb_info_nth(int i, struct usb_info *out);
// How many USB buses have shown a device so far; bus numbers run 1..this.
int usb_nbuses(void);
// Tell /sys and udev a USB device appeared ("add") or is about to go ("remove").
void sysfs_usb_uevent(const struct usb_info *u, const char *action);
void sysfs_usb_root_uevent(int bus, const char *action);
// A USB controller is up, at this PCI address; number its bus and announce its
// root hub.
void usb_bus_add(void *hc, int pci_bus, int pci_slot, int pci_func);
// The PCI address of the controller behind bus number `bus`; 0, or -1 if none.
int usb_bus_pci(int bus, int *pci_bus, int *pci_slot, int *pci_func);
// One interface of a configured USB device, in the order the configuration
// descriptor lists them (alternate setting 0 only).
struct usb_if_info {
uint8 num, cls, subcls, proto, nendp;
char driver[16]; // what is bound to it, or empty
};
int usb_if_nth(int bus, int devnum, int n, struct usb_if_info *out);
void sysfs_usb_if_uevent(const struct usb_info *u, const struct usb_if_info *i,
const char *action);

View file

@ -41,4 +41,5 @@ void klog_putc(int c);
int klog_read(uint64 off, char *dst, int n); int klog_read(uint64 off, char *dst, int n);
uint64 klog_len(void); uint64 klog_len(void);
uint64 klog_prev_bytes(void); uint64 klog_prev_bytes(void);
uint64 klog_boot_bytes(void);
int klog_recovered(void); int klog_recovered(void);

View file

@ -29,7 +29,7 @@
#define NOFILE 128 // open files per process #define NOFILE 128 // open files per process
#define NFILE 512 // open files per system #define NFILE 512 // open files per system
#define NINODE 200 // maximum number of active i-nodes #define NINODE 200 // maximum number of active i-nodes
#define NDEV 13 // maximum major device number #define NDEV 16 // maximum major device number
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c) #define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c) #define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)

View file

@ -31,6 +31,41 @@ struct vma {
int shmid; // shared segment this came from, or -1 int shmid; // shared segment this came from, or -1
}; };
// --- What threads share ---------------------------------------------------
//
// A thread is a struct proc like any other, scheduled like any other. What
// makes several of them one program is that they point at the same three
// objects below, each counted, each freed with its last user: clone() with
// CLONE_VM, CLONE_FILES and CLONE_SIGHAND shares them, fork() copies them.
// The address space. pgdir is mirrored in every struct proc that uses it,
// because the scheduler, trapasm and half the kernel read it from there; it
// only changes at exec, which makes a new mm.
struct mm {
int ref;
int locked; // mm_lock(): one mmap/munmap/brk at a time
uint64 sz; // top of the heap; p->sz mirrors it
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
pml4e_t *pgdir;
struct vma vma[NVMA]; // VMA tracking for mmap
struct mapname *names; // which file each span came from; mapname.c
};
// The descriptor table, and FD_CLOEXEC beside it.
struct files {
int ref;
struct file *ofile[NOFILE];
char flags[NOFILE];
};
// Signal dispositions: what sigaction() sets.
struct sighand {
int ref;
uint64 handler[32]; // 0 SIG_DFL, 1 SIG_IGN, else an address
uint64 restorer[32];
uint64 flags[32]; // sa_flags; SA_RESTART is the one read
};
// Per-CPU state // Per-CPU state
struct cpu { struct cpu {
uchar id; uchar id;
@ -174,8 +209,14 @@ struct procinfo {
uint64 nvcsw, nivcsw; // voluntary / involuntary context switches uint64 nvcsw, nivcsw; // voluntary / involuntary context switches
char name[16]; char name[16];
char exe[64]; // full path, for /proc/<pid>/exe char exe[64]; // full path, for /proc/<pid>/exe
int kthread; // a kernel thread, not a program
int stopped; // stopped by a signal (ps shows T)
int tgid; // the process this thread belongs to
int nthreads; // threads in that process
}; };
#define NGROUPS_MAX 32
struct proc { struct proc {
uint64 sz; // Size of process memory (bytes) [+0x00] uint64 sz; // Size of process memory (bytes) [+0x00]
pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08] pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08]
@ -189,7 +230,7 @@ struct proc {
void *chan; // [+0x40] void *chan; // [+0x40]
int killed; // [+0x48] int killed; // [+0x48]
int _pad; // [+0x4c] int _pad; // [+0x4c]
struct file *ofile[NOFILE]; // Open files [+0x50] struct files *files; // Open files, shared by threads [+0x50]
struct inode *cwd; // follows struct inode *cwd; // follows
char name[16]; // char name[16]; //
// The path exec() was given. `name` is only the last component and only 15 // The path exec() was given. `name` is only the last component and only 15
@ -236,7 +277,6 @@ struct proc {
// alarmd, in kernel/proc/proc.c, is what posts it. // alarmd, in kernel/proc/proc.c, is what posts it.
uint alarm_at; uint alarm_at;
uint alarm_every; uint alarm_every;
char ofile_flags[NOFILE];
// Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's // Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's
// deliver_signals() and kernel/proc/proc.c's exit()). sig_handler[sig]/ // deliver_signals() and kernel/proc/proc.c's exit()). sig_handler[sig]/
@ -245,8 +285,7 @@ struct proc {
// the pre-signal trapframe so sys_rt_sigreturn() can restore it; in_signal // the pre-signal trapframe so sys_rt_sigreturn() can restore it; in_signal
// prevents re-entering a handler while one is already running (we don't // prevents re-entering a handler while one is already running (we don't
// support nested signal delivery). // support nested signal delivery).
uint64 sig_handler[32]; struct sighand *sighand; // dispositions, shared by threads
uint64 sig_restorer[32];
struct trapframe saved_sigtf; struct trapframe saved_sigtf;
int in_signal; int in_signal;
@ -257,8 +296,7 @@ struct proc {
uint64 sigsusp_oldmask; uint64 sigsusp_oldmask;
int sigsusp_active; int sigsusp_active;
struct vma vma[NVMA]; // VMA tracking for mmap struct mm *mm; // address space, shared by threads
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
// ABI translation (Linux compat syscall number map) // ABI translation (Linux compat syscall number map)
int *syscall_map; int *syscall_map;
@ -299,6 +337,10 @@ struct proc {
// are tracked honestly so getuid()/setuid() stop lying to userland. // are tracked honestly so getuid()/setuid() stop lying to userland.
uint uid, euid, suid; uint uid, euid, suid;
uint gid, egid, sgid; uint gid, egid, sgid;
// Supplementary groups, as setgroups(2) set them: a file's group is
// consulted against these as well as against egid.
int ngroups;
uint groups[NGROUPS_MAX];
// --- Fair-share scheduling -------------------------------------------- // --- Fair-share scheduling --------------------------------------------
// The whole of the scheduler's per-process state. What each field means, // The whole of the scheduler's per-process state. What each field means,
@ -321,6 +363,34 @@ struct proc {
// chroot(2): where an absolute path starts, and where ".." stops. 0 is the // chroot(2): where an absolute path starts, and where ".." stops. 0 is the
// system's root. Inherited across fork, held as a reference like cwd. // system's root. Inherited across fork, held as a reference like cwd.
struct inode *root; struct inode *root;
// Syscall tracing: every call and its result go to the log. Set by exec
// when the program's name is the one written to /proc/strace, and kept
// across fork so that what a traced program starts is traced too.
int strace;
// umask(2). Inherited across fork; 022 for the first process.
uint umask;
// Job control. stopsig is the signal that stopped the process, 0 while it
// runs; stop_report and cont_report are what wait(WUNTRACED/WCONTINUED)
// has yet to tell the parent.
int stopsig;
int stop_report;
int cont_report;
// Threads. tgid is the pid of the thread group's leader -- what getpid()
// returns -- and equal to pid for every ordinary process.
int tgid;
uint64 clear_child_tid; // CLONE_CHILD_CLEARTID: zeroed and woken at exit
void *futex_key; // the futex word this thread waits on, or 0
int futex_woken;
int autoreap; // a thread: reapd frees it, nobody waits for it
struct proc *vfork_parent; // CLONE_VFORK: released at exec or exit
int vfork_wait; // the vfork child this process is waiting on
int thread_dying; // killed with its group: exits as a thread
int group_status; // a thread's exit_group status, for the leader
int group_status_set;
}; };
#define FD_CLOEXEC 1 #define FD_CLOEXEC 1

View file

@ -30,4 +30,35 @@ void acpi_thread_start(void);
void acpi_pci_route(void); void acpi_pci_route(void);
int acpi_poweroff(void); // S5; returns only on failure int acpi_poweroff(void); // S5; returns only on failure
void acpi_poweroff_now(void); // S5 from a panic, no AML; returns only on failure
int acpi_reboot(void); // the FADT reset register; ditto int acpi_reboot(void); // the FADT reset register; ditto
// Batteries and AC adapters, for /sys/class/power_supply. Units are the
// battery's own: unit 0 is mW/mWh, 1 is mA/mAh; ACPI_UNKNOWN where the
// firmware does not say.
#define ACPI_UNKNOWN 0xFFFFFFFFu
struct acpi_psu {
int battery; // else an AC adapter
int present; // battery: in the bay
int online; // adapter: plugged in
int unit;
uint32 state; // _BST: 1 discharging, 2 charging, 4 critical
uint32 rate, remaining, voltage; // _BST, voltage in mV
uint32 design, full, design_voltage, cycles;
int technology; // 0 primary, 1 rechargeable
char model[32], serial[32], kind[16], oem[32];
};
int acpi_psu_count(void);
int acpi_psu_get(int i, struct acpi_psu *out);
// How an idle CPU sleeps: kernel/acpi/aml.c reads it from _CST, and the
// scheduler's idle path in kernel/proc/proc.c acts on it.
#define ACPI_IDLE_HLT 0
#define ACPI_IDLE_IO 1 // read a port
#define ACPI_IDLE_MWAIT 2 // MWAIT with a hint
struct acpi_idle {
int kind;
uint64 addr; // the port, or the MWAIT hint
uint latency; // microseconds
};
extern struct acpi_idle acpi_idle;

View file

@ -79,6 +79,7 @@ int audio_configure(struct audio_format *f); // 0, or negative
int audio_write(const void *pcm, int bytes); // bytes queued, or negative int audio_write(const void *pcm, int bytes); // bytes queued, or negative
void audio_drain(void); void audio_drain(void);
void audio_stop(void); void audio_stop(void);
int dsp_writable(void); // poll(): room for a write
// --- what userspace uses --------------------------------------------------- // --- what userspace uses ---------------------------------------------------
// //
@ -92,6 +93,23 @@ void audio_stop(void);
#define SNDCTL_DSP_SYNC 0x00005001 // wait for the queue to drain #define SNDCTL_DSP_SYNC 0x00005001 // wait for the queue to drain
#define SNDCTL_DSP_GETOSPACE 0x8010500C // struct audio_buf_info out #define SNDCTL_DSP_GETOSPACE 0x8010500C // struct audio_buf_info out
#define SNDCTL_DSP_RESET 0x00005000 // stop, discard #define SNDCTL_DSP_RESET 0x00005000 // stop, discard
// The rest of what a buffer-and-pointer client needs -- ALSA's OSS plugin, which
// is how SDL and everything else that speaks ALSA reaches this device.
#define SNDCTL_DSP_SETFRAGMENT 0xC004500A // int in/out: count<<16 | log2(size)
#define SNDCTL_DSP_GETFMTS 0x8004500B // int out: AFMT_* mask
#define SNDCTL_DSP_GETCAPS 0x8004500F // int out: DSP_CAP_*
#define SNDCTL_DSP_SETTRIGGER 0x40045010 // int in: PCM_ENABLE_OUTPUT or 0
#define SNDCTL_DSP_GETOPTR 0x800C5012 // struct count_info out
#define SNDCTL_DSP_GETODELAY 0x80045017 // int out: bytes not yet heard
#define DSP_CAP_TRIGGER 0x00001000
#define PCM_ENABLE_OUTPUT 0x00000002
struct count_info {
int bytes; // played since the last reset
int blocks; // fragments played since the last GETOPTR
int ptr; // where playback is in a ring of fragsize * fragments
};
#define AFMT_S16_LE 0x00000010 #define AFMT_S16_LE 0x00000010

View file

@ -12,6 +12,13 @@ void *hid_attach(const uchar *desc, int n, const char *what);
// One input report, report ID included when the device uses them. // One input report, report ID included when the device uses them.
void hid_attached_input(void *h, const uchar *rep, int len); void hid_attached_input(void *h, const uchar *rep, int len);
// For GMMOS, after a panic: whether a device is a keyboard, and one of its
// reports rewritten into the 8-byte boot layout (modifiers, reserved, six
// usages) with nothing else done -- no queue, no lock, no events. 0 when the
// report was not a keyboard's.
int hid_is_keyboard(void *h);
int hid_boot_report(void *h, const uchar *rep, int len, uchar *boot);
// I2C-HID: the touchpads (and some keyboards) of laptops. Started once ACPI // I2C-HID: the touchpads (and some keyboards) of laptops. Started once ACPI
// and kernel threads are up; see kernel/drivers/i2c/i2c_hid.c. // and kernel threads are up; see kernel/drivers/i2c/i2c_hid.c.
void i2c_hid_start(void); void i2c_hid_start(void);

View file

@ -59,6 +59,9 @@
#define KBD_PGDN (KBD_SPECIAL + 0x07) #define KBD_PGDN (KBD_SPECIAL + 0x07)
#define KBD_INS (KBD_SPECIAL + 0x08) #define KBD_INS (KBD_SPECIAL + 0x08)
#define KBD_DEL (KBD_SPECIAL + 0x09) #define KBD_DEL (KBD_SPECIAL + 0x09)
// F1..F12, consecutive: KBD_F1 + n is F(n+1).
#define KBD_F1 (KBD_SPECIAL + 0x10)
#define KBD_F12 (KBD_SPECIAL + 0x1B)
// Switch virtual terminal. // Switch virtual terminal.
// //
@ -89,12 +92,10 @@
// leaves `chvt N` as the only way to switch, which is a legitimate choice on // leaves `chvt N` as the only way to switch, which is a legitimate choice on
// a machine whose host takes everything. // a machine whose host takes everything.
// //
// The default is AltGr+F1..F6, and the "AltGr wants it" line above needs // The default is Ctrl+Alt+F1..F12: the keys X switches with, so the console
// reading carefully: what a Polish layout wants is AltGr+*letter*. AltGr with // and the desktop answer the same chord. It used to be AltGr+F-key, which
// a function key produces nothing on any layout, no window manager binds it, // nothing else claims -- Right Alt being a separate modifier bit here is what
// and no application asks for it. Right Alt being a separate modifier bit // makes that expressible, and `chvt -c altgr+F` still sets it.
// here is what makes the distinction expressible at all -- it used to share
// ALT, which is harmless for typing and useless for this.
#define KBD_VT_SWITCH (KBD_SPECIAL + 0x20) #define KBD_VT_SWITCH (KBD_SPECIAL + 0x20)
// Previous / next terminal, on the same modifiers as the chord above. The // Previous / next terminal, on the same modifiers as the chord above. The

46
include/dev/kdev.h Normal file
View file

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// The devices the kernel implements itself, as /sys and udev see them.
//
// /dev holds them already (kernel/fs/devfs.c). This is the other half of what
// udev needs: for each node a place in /sys/devices, the class and dev/ links
// that lead back to it, the facts udev's rules read (MAJOR, MINOR, DEVNAME,
// input capabilities), and a way to announce it on the uevent netlink socket.
#pragma once
#include "core/types.h"
#define KDEV_NAME 24
#define KDEV_DEVNAME 32
#define KDEV_BLK0 256 // kdev indexes from here are block devices
enum { KD_NONE, KD_KEYBOARD, KD_MOUSE };
struct kdev {
char subsys[16]; // class: input, mem, tty, graphics, block
char name[KDEV_NAME]; // directory name under the parent
char devname[KDEV_DEVNAME]; // path below /dev, "" for a device with no node
short major, minor;
int block;
int parent; // kdev index of the device it hangs off, or -1
int cap; // KD_*: what an input device can do
};
int kdev_nsubsys(void);
const char *kdev_subsys(int s);
// Index of the Nth device that exists, 0.. until -1.
int kdev_nth(int n);
int kdev_get(int i, struct kdev *k);
// The numbers stat() shows for its node, which are what /sys must show too.
void kdev_numbers(const struct kdev *k, int *maj, int *min);
// "devices/virtual/input/input0/event0": where it lives below /sys.
int kdev_path(int i, char *buf, int cap);
// Announce a device on the uevent socket: action is "add", "remove", "change".
void kdev_uevent(int i, const char *action);
// The next SEQNUM: events from every source (kdev, PCI, USB) share one count.
uint kdev_seqnum(void);
// What reading a device's `uevent` file says.
int kdev_uevent_text(int i, char *buf, int cap);

View file

@ -151,7 +151,7 @@ struct pci_driver {
void (*attached)(struct pci_dev *, struct device *); void (*attached)(struct pci_dev *, struct device *);
// Give the device up, so a userspace driver can take it. // Give the device up, so a userspace driver can take it.
// //
// docs/plan/hybrid.md is built on the kernel *handing over* a device // docs/architecture/devices.md is built on the kernel *handing over* a device
// rather than a process wrestling it away: two drivers writing one set of // rather than a process wrestling it away: two drivers writing one set of
// registers is not a subtle failure, it is hardware that appears to have // registers is not a subtle failure, it is hardware that appears to have
// started lying. So the transfer is the incumbent's decision, and this is // started lying. So the transfer is the incumbent's decision, and this is

View file

@ -2,7 +2,7 @@
// //
// /dev/pci -- device access from userspace. // /dev/pci -- device access from userspace.
// //
// docs/plan/hybrid.md, mechanisms 2 and 4: MMIO and DMA to a process. This is // docs/architecture/devices.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
// the interface a driver that does not live in the kernel talks to, and it is // the interface a driver that does not live in the kernel talks to, and it is
// deliberately about *PCI devices*, not about any particular driver model. // deliberately about *PCI devices*, not about any particular driver model.
// //
@ -23,7 +23,7 @@
// //
// There is no IOMMU. A process holding a DMA buffer can point the device at // There is no IOMMU. A process holding a DMA buffer can point the device at
// any physical address, so this isolates a driver against its own *bugs* and // any physical address, so this isolates a driver against its own *bugs* and
// not against its own *intent*. docs/plan/hybrid.md says so at length and it // not against its own *intent*. docs/architecture/devices.md says so at length and it
// remains true here: a crashed driver that restarts instead of taking the // remains true here: a crashed driver that restarts instead of taking the
// machine down is the whole benefit, and it is not a security boundary. // machine down is the whole benefit, and it is not a security boundary.

42
include/dev/pty.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
// Pseudo-terminals, Unix98 style: open /dev/ptmx and get the master side of a
// new pair; the slave is /dev/pts/N. What sits between the two is the same
// line discipline a console has. See kernel/dev/pty.c.
#include "core/types.h"
#define PTMX_MAJOR 13 // /dev/ptmx: opening it allocates a pair
#define PTM_MAJOR 14 // a master, minor = pair number; never in a listing
#define PTS_MAJOR 15 // /dev/pts/N
#define NPTY 32
// A controlling terminal that is a pty, in proc->ctty: PTY_CTTY + N. The
// console's are 1..NVT.
#define PTY_CTTY 64
struct inode;
struct file;
int pty_alloc(void); // the new pair's number, or -1
int pty_used(int n);
int pty_ioctl(struct inode *ip, uint request, addr_t arg);
int pty_poll(struct inode *ip, int events, int readable, int writable);
struct inode *ptmx_open(struct inode *ip); // ip locked; returns the master, locked
void pty_init(void);
// Lines the devsw table needs.
int ptm_read(struct inode*, uint64, char*, int);
int ptm_write(struct inode*, uint64, char*, int);
int ptm_ready(struct inode*);
void ptm_open(struct inode*);
void ptm_close(struct inode*);
int pts_read(struct inode*, uint64, char*, int);
int pts_write(struct inode*, uint64, char*, int);
int pts_ready(struct inode*);
void pts_open(struct inode*);
void pts_close(struct inode*);
// From kernel/fs/devfs.c: the inode of master N.
struct inode *devfs_pty_master(int n);
void devfs_pty_claim(int n, uint uid);

View file

@ -4,7 +4,7 @@
// //
// It lived next to the driver while exactly one driver read it, with a note // It lived next to the driver while exactly one driver read it, with a note
// saying it should move when a second reader appeared. One has: cmd/vgpud is // saying it should move when a second reader appeared. One has: cmd/vgpud is
// the same device driven from userspace (docs/plan/hybrid.md's first // the same device driven from userspace (docs/architecture/devices.md's first
// eviction), and it needs the same register offsets and the same commands. // eviction), and it needs the same register offsets and the same commands.
// //
// Still one file rather than transport-and-protocol split in two. Both readers // Still one file rather than transport-and-protocol split in two. Both readers

View file

@ -26,10 +26,10 @@
#include "core/param.h" #include "core/param.h"
#include "uapi/posix.h" #include "uapi/posix.h"
// Six, and Linux's numbering: /dev/tty1 through /dev/tty6, reached with // Twelve, one per function key, and Linux's numbering: /dev/tty1 through
// Alt+1..Alt+6. A seventh is deliberately not bound -- that is where a // /dev/tty12, reached with Ctrl+Alt+F1..F12 (and plain Alt on the text
// display server goes, and Schcom will want it. // console). The key codes leave room for sixteen; see KBD_VT_SWITCH.
#define NVT 6 #define NVT 12
// A cell. // A cell.
// //
@ -82,7 +82,7 @@ struct vtcell {
// Escape parser states. E_OSC and E_OSC_ESC exist for hyperlinks: a terminal // Escape parser states. E_OSC and E_OSC_ESC exist for hyperlinks: a terminal
// that does not follow OSC 8 must still consume it, or the URL is printed as // that does not follow OSC 8 must still consume it, or the URL is printed as
// text. See the comment in kernel/core/console.c that this came from. // text. See the comment in kernel/core/console.c that this came from.
enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC }; enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC, VE_CHARSET };
struct vt { struct vt {
int index; // 0-based; /dev/tty<index+1> int index; // 0-based; /dev/tty<index+1>
@ -98,6 +98,10 @@ struct vt {
// CR, LF, backspace, an escape sequence -- clears it. // CR, LF, backspace, an escape sequence -- clears it.
int wrap_pending; int wrap_pending;
int saved_cx, saved_cy; // DECSC/DECRC int saved_cx, saved_cy; // DECSC/DECRC
// The scrolling region, DECSTBM: rows scroll_top..scroll_bot inclusive.
// scroll_bot 0 means unset, the whole screen -- which keeps a grid resized
// underneath it from holding a stale bottom.
int scroll_top, scroll_bot;
uint8 fg, bg; // current SGR colours uint8 fg, bg; // current SGR colours
int bold; int bold;
@ -108,6 +112,16 @@ struct vt {
int esc_parm[VT_MAXPARM]; int esc_parm[VT_MAXPARM];
int esc_nparm; int esc_nparm;
int esc_private; int esc_private;
int esc_qmark; // the marker was '?': a DEC private mode
// The text cursor, a steady block. Drawn by painting its cell inverted over
// the grid, so the grid never holds it: curs_drawn says whether the screen
// currently does, and where, because erasing it means repainting that cell
// from the grid -- after the byte that moves the cursor has been consumed.
// DECTCEM (ESC [ ? 25 l) hides it; curses programs do before drawing.
int curs_hidden;
int curs_drawn;
int curs_x, curs_y;
// UTF-8 decoder. // UTF-8 decoder.
uint32 u8_cp; uint32 u8_cp;

View file

@ -130,6 +130,13 @@ struct inode {
uint64 size; uint64 size;
uint rwmode; uint rwmode;
// POSIX permission bits from a filesystem that keeps them, or 0 for one
// that does not, in which case stat makes some up from the type. Bit 18 set
// alone means a mode of 0000, which chmod can ask for.
uint mode;
// Owner. Whatever the filesystem keeps: SchFS has stored them all along,
// FAT and the synthetic filesystems report root.
uint uid, gid;
uint supermode; uint supermode;
uint showmode; uint showmode;
uint atime; uint atime;

Some files were not shown because too many files have changed in this diff Show more