| Filename | Latest commit message | Latest commit date |
|---|---|---|
Porting zsh found four defects in the kernel and one in the filesystem. None of them were about zsh; all of them had been there the whole time. **exec threw the environment away.** kernel/fs/exec.c wrote a single zero for envp with the comment "(no environment)" -- not yet, but ever, for every process. musl has been passing a perfectly good envp to execve (Linux 59, which SchOS answers) and it was never read. So $TERM was unset, $PATH was whatever each program invented, and a variable exported in a shell did not survive the exec meant to carry it. exec() takes an envp, sys_exec reads argument three, and schinit manifests grew an Environment key. The userland two-argument exec(path, argv) stays as a wrapper that passes null explicitly -- the raw stub is execve now, so a short caller cannot leave %rdx holding a stray pointer. **Which overflowed the kernel stack.** Three pointer arrays, ~1.8 KB, on a 4096-byte stack, under exec -> loaduvm -> readi -> SchFS -> btree. Pid 1 went down on the first exec. The canary caught it and named the process, which is what it is for. They live in one kalloc'd page now. **waitpid returned a bare -1** for both "no children" and "interrupted". musl reads [-4095,-1] as -errno, so userspace saw EPERM; zsh printed "wait failed: operation not permitted" and abandoned the rest of /etc/zsh/zshrc. -ECHILD and -EINTR now. Third time a bare -1 has cost real time -- on a system with no permission model, EPERM should be read as "a syscall returned -1". **NVMA was 64.** musl's allocator takes a mapping per size class and does not release them eagerly, so compinit ran out of slots and got -1, which is EPERM, which malloc turns into NULL, which zsh reports as "fatal error: out of memory" -- on a machine with 1000 MB free. 256 slots, and -ENOMEM. **And a small write to SchFS costs 58 ms.** 200 writes to one open file take 11.75 s; the same to /dev/null take 0.14 s. That is the design stated at the top of kmount.c -- one transaction group per operation, and a commit is two flushes and an uberblock -- not a bug. But compdump writes its digest with thousands of separate prints, so it takes minutes and looks like a hang. compinit -D, which skips the dump, finishes in 3.6 s. This one is a decision rather than a fix, so it is written up and left: /etc/zsh/zshrc keeps compinit switched off and explains exactly why, with the measurements. zsh runs as /bin/zsh and is the console shell. ZLE works -- arrow keys recall history through terminfo, $COLUMNS reads the real 160 because TIOCGWINSZ was fixed, colour and prompt expansion work, startup is 0.14 s. mksh keeps /bin/sh, so schinit's no-jobs fallback is a different program from the one that could have failed. Also fixed while here: AT_CLKTCK was the literal 100 and is USER_HZ; the extract rule in four ports could move a source tree inside itself when make re-ran it, which is how zsh's configure came to write modfile=zsh-5.9/... into config.modules and fail three steps later. schfs-check passes. Full writeup in docs/history/kernel.md section 14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 |
||
| .. | ||
| console-shell.job | ||
| README | ||
Job manifests for schinit, SchOS process 1.
Every *.job file in this directory is loaded at boot. Format is sectioned
key = value; # starts a comment, keys are case-insensitive.
Label a reverse-DNS name, as launchd uses. Identity, not a path.
Program the binary to execute
Args argv, space separated. argv[0] is conventionally the
basename, so `Args = sh` for `/bin/sh`.
RunAtLoad start it when schinit loads
KeepAlive restart it when it exits
Disabled load it but do not start it
ThrottleInterval seconds between respawns (default 1)
StandardIn \
StandardOut > paths to open as fd 0, 1, 2
StandardError /
[Socket <name>]
Path a unix socket schinit will hold on the job's behalf and
start the job on first connect
Socket sections are parsed but not yet acted on -- that is milestone 2, and
demand launching is the whole reason this is shaped after launchd rather than
sysvinit. The key exists now so manifests do not have to change later.
Unknown keys and unknown sections are ignored rather than fatal: a manifest
written for a newer schinit must not stop this one booting the machine.