userland: 268 applets, an editor, names that resolve, and a web server

Four complaints, and only one of them was the thing it looked like.

**"toybox is too small."** Half true, and the fix cost no licensing at all:
236 of toybox's applets are what defconfig builds, and the rest live in
toys/pending/ -- awk, bc, diff, more, expr, tr, hexdump, stty among them. Same
0BSD tarball, switched off by a config default. 26 of them are on now, 236 ->
268. Not everything: strace wants ptrace, syslogd wants BSD's facilitynames[],
git wants OpenSSL, and those three break the link.

The toybox build also works again on this machine. Its scripts start
#!/bin/bash, which does not exist on NixOS, so the binary in the tree was one
nobody could reproduce; the shebangs are rewritten to /usr/bin/env bash on the
way in. Pending applets are enabled by editing .config directly, because
`make oldconfig` is the obvious way and undoes it -- every pending symbol is
`default n`.

**BusyBox was asked for and is not here.** It is GPL-2.0-only, and `make
ramroot` links the root image *into the kernel binary*, so it would put GPLv2
inside an Apache-2.0 executable -- which is the exact case ports/Makefile's
NOTPORTS filter and docs/licensing.md §4 exist for. nano is the same answer one
version later. Both recorded in licensing.md with what was done instead, since
"no" without an alternative is how a rule gets quietly broken later.

**An editor that is not vi.** kilo, BSD-2-Clause, one 1300-line file, no
dependencies beyond termios and TIOCGWINSZ, both of which zsh already needed.
Installed as /bin/edit as well as /bin/kilo, because somebody with a file to
fix will type `edit`.

**"wget doesn't work."** Not a userland problem, and BusyBox's wget would have
failed identically: nothing on this system could turn a name into an address,
so every program taking a hostname failed in its own way and each looked like
a separate bug. There is still no DNS client in SchOS and there does not need
to be -- musl has a complete resolver and it works over ordinary UDP sockets.
The one missing fact was which nameserver, which the kernel has had all along
from DHCP and never published. /proc/resolv.conf publishes it in resolv.conf's
exact syntax; cmd/resolvconf decides whether it becomes /etc/resolv.conf, and
will not overwrite a hand-written one without -f.

  # wget -O - http://example.com/ | head -1
  <!doctype html><html lang="en"><head><title>Example Domain</title>

**Something to host with.** cmd/httpd: GET and HEAD, static files, index.html
for a directory and a listing otherwise, a child per connection. No CGI, no
keep-alive, no TLS, no config file. The path check is the only
security-relevant code in it and it refuses rather than normalises -- no
component may be `..`, and percent-decoding happens first, because %2e%2e is
`..` and a check before decoding does not see it. Both forms answer 403.

And the disk is 100 MB, from 32. It is sparse, so the image is 21 MB on the
host. RAMDISK_BLOCKS went 6000 -> 8000 with it: the ramdisk was at 86% and the
next thing added would have broken `make ramroot`, which nobody would have
noticed until they used it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 19:24:55 +02:00
commit b08432b2c3
17 changed files with 994 additions and 21 deletions

View file

@ -108,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
# Third-party software does NOT go here — see ports/.
# -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schdbgd schmode netecho ldlsd
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# -----------------------------------------------------------------------
@ -379,11 +379,18 @@ CFLAGS_SYS = $(CFLAGS) -I./kernel/sys
#
# Links a complete SCHFS image into the kernel and boots from it instead of
# the IDE disk, so the physical disk can be reformatted from a running system.
# Off by default because it adds RAMDISK_BLOCKS KB to the kernel, and stage2
# loads the kernel by PIO one sector at a time -- a 6 MB kernel takes visibly
# longer to boot than a 350 KB one.
# Off by default because the image is linked into the kernel binary, and stage2
# loads that by PIO one sector at a time -- a kernel with a userland inside it
# takes visibly longer to boot than a 350 KB one.
#
# 8000 blocks of 4 KB is 32 MB. It was 6000, which the userland had quietly
# grown to fill: 5157 of 6000 used, 86%, after toybox gained its pending
# applets and an editor arrived. That still fit, and the *next* thing added
# would not have -- and what mkschfs says when an image is full is "try a
# larger --size", which is a poor way to find out that a build variant you were
# not using has been broken for a while.
# -----------------------------------------------------------------------
RAMDISK_BLOCKS ?= 6000
RAMDISK_BLOCKS ?= 8000
# RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table.
# The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates
@ -1051,12 +1058,18 @@ ETC_PLAIN = $(filter-out etc/schinit etc/zsh,$(ETC_FILES))
# anything with more room.
LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII)
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos
# What httpd serves, if nobody has said otherwise. One page, whose job is to
# prove the path works end to end -- a directory listing of an empty /srv
# proves the same thing far less clearly.
SRV_FILES = $(wildcard srv/*)
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
@cp $(ETC_PLAIN) $(SYSROOT)/etc/
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
@cp $(SRV_FILES) $(SYSROOT)/srv/
@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
@ -1213,10 +1226,19 @@ qemu: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret().
#
# 25600 blocks of 4 KB is 100 MB. It was 8000 (32 MB), which was comfortable
# when the whole userland was one multicall binary and stopped being so once
# there was more than one thing in it -- and mkschfs's answer to a full image
# is "try a larger --size", which is a worse thing to read than a number here
# is to change. A raw disk image is sparse on any filesystem worth the name,
# so the unused part costs nothing until it is used.
SCHFS_BLOCKS ?= 25600
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size 8000 --label schos --sysroot $(SYSROOT)
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(SYSROOT)
# Boot with a SchFS volume attached as the second disk.
#