Checking the rest of ZSH_CACHE after the killesrch fix, on the principle that
one wrong answer in a hand-written block is rarely the only one. Two more, both
`yes`, both measurable on the running system in a line each.
**zsh_cv_sys_path_dev_fd=/dev/fd.** Neither /dev/fd nor /proc/self/fd exists --
/proc/self does not exist at all. So `<(cmd)` failed at the point of use with
cat: /dev/fd/11: No such file or directory
which reads as a broken command rather than as a feature the system does not
have.
**zsh_cv_sys_fifo=yes.** mkfifo() does not fail here. It *succeeds*, and
produces a character device: mknod() turns S_IFIFO into a T_DEV and keeps the
major/minor, so `ls -l` shows `crw-rw-rw- 438, 0` where a `p` belongs, and
nothing written to it comes back out. Succeeding wrongly is what made `yes`
look defensible.
Both `no` now. `<(cmd)` says "doesn't look like your system supports FIFOs",
which names the missing thing instead of blaming cat, and `$( )` is unaffected.
The two gaps are recorded in docs/architecture/filesystem.md rather than left
implied by a build flag -- named pipes and a path to one's own descriptors are
both real features, and neither exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`rename()` refused an existing destination, so write-to-a-temporary-then-rename
-- how every careful program saves a file -- failed on the second save of
anything. zsh's history was the visible case: `.zsh_history.new` accumulated
for ever and the history itself never updated.
An earlier attempt to fix this was reverted after it lost both files, and the
note it left behind blamed HTree slot reuse. That was the wrong suspect.
**sys_rename was removing the old name by hand.** memset a `struct dirent` to
zero and writei() it into the parent directory at the offset dirlookup()
returned. That is the v1 filesystem's on-disk layout, written directly,
bypassing dp->ops entirely. On SchFS a directory is a B-tree with an index over
it -- "the record at that offset" is not a thing, and writing zeroes there
corrupted the directory while leaving the name in the index. Adding a second
removal on top of a corrupting one is how both files disappeared.
Every other caller in the file already used dirunlink(). This one had been
open-coding a filesystem it no longer ran on, and the bug stayed invisible for
as long as nothing asked it to remove two names instead of one.
With that corrected, replacing the destination is the straightforward thing it
should always have been: look it up, refuse if it is a directory, unlink it,
drop its nlink. rename(a, a) returns 0 and touches nothing, per POSIX -- both
names already reach the same file, and removing one to add it back is only
equivalent when nothing else is watching.
The lock discipline is inherited rather than invented, and it is the delicate
part: dp_old and dp_new are frequently the *same* directory reached through two
nameiparent() calls, which is two references to one inode. So no two directory
locks are held at once -- each is locked, used, unlocked before the next -- and
the source inode stays unlocked throughout, held alive by an nlink bump.
Directories are still refused, in both roles: moving one needs the `..` fixup
and the ancestor-loop check that are not written, and replacing one needs them
plus an emptiness test. EISDIR either way.
mv a c -> 0, c has a's contents
mv c b -> 0, b replaced, b has a's contents (this is the fix)
mv b b -> 0, nothing happens
mv nope z -> ENOENT
mv b d/ -> 0, b lands inside d
60 create-and-rename cycles leave one file and 4 KB of disk, not 60. /root
holds .zsh_history and no .zsh_history.new, and a marker written in one boot is
still in the history file on the next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ctrl-C killed the shell. consoleread() breaks off the read in progress when
SIGINT arrives and returned -1 to do it -- and -1 is -EPERM by the time it
reaches a program, so every program that handles SIGINT by carrying on saw a
permanent error instead of an interrupted call:
zsh: error on TTY read: operation not permitted
schinitd: restarting os.schos.console-shell
and the next Ctrl-C did it to the replacement. EINTR now, which is what a
signal-interrupted read is required to give. Verified: Ctrl-C an interruptible
command, shell survives, zero restarts.
Same mistake, same file family: link, unlink, rename and renameat returned -1
on every error path. Callers branch on errno -- `if (unlink(p) < 0 && errno !=
ENOENT)` is the standard shape, and against EPERM it always complains -- which
is why zsh reported failures for things that had worked. They return ENOENT,
EEXIST, EXDEV, EISDIR, ENOTEMPTY and EFAULT now. About ninety other `return -1`s
remain in sys_fs.c; these four are done because these four were what a program
tripped over. /dev/mem's unknown minors return ENODEV rather than -1 too.
/dev/urandom was a linear congruential generator seeded with a **constant**, so
every boot produced the same bytes in the same order -- not weak, the same. A
key, a nonce, a temporary filename: shared with every SchOS machine that had
ever run. Seeded at boot from RDRAND where the CPU has it, from the cycle
counter and the wall clock where it does not, and re-stirred per byte. Still an
LCG and still not a CSPRNG; what changed is the part that was dangerous rather
than merely poor.
**Not fixed, and recorded as such:** rename() still will not replace an existing
target, so zsh's history save fails on every exit after the first. An attempt to
unlink the target first was reverted -- the directory is an HTree, dirunlink
followed by dirlink of the same name did not behave, and it panicked in
filewrite with the old file already gone. Losing both files is worse than
refusing, so it refuses until the HTree's slot reuse is understood.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
A fresh boot spent 2600 ms in schinit's bootstrap() before printing its first
line, and the whole of it was device nodes. Measured rather than guessed: an
instrumented bootstrap reported 2600 ms on a fresh image and 0 ms on the next
boot, because the nodes already existed by then and mknod failed fast. The cost
was therefore invisible to anyone not building images -- which is to say,
invisible to everyone except whoever works on this.
Each node was a real inode on the root filesystem, and SchFS commits every
mutation as its own transaction group: a copy-on-write rewrite of the path to
the root, then an uberblock behind a cache flush. Twenty of those is seconds.
The duplication mattered more than the time. Major and minor numbers are the
kernel's -- devsw[] in kernel/dev/cdev.c is what they dispatch on -- and schinit
held a second copy of that mapping with nothing checking the two agreed. Adding
a character device meant editing a userspace program, and a wrong minor produced
a node that opened successfully and did nothing.
kernel/fs/devfs.c puts the table next to the thing it describes and serves /dev
behind the same fs_ops vtable procfs uses. The namespace is all it provides:
fileread and filewrite see T_DEV and go through devsw themselves, so a read
never reaches it. It is read-only, and dirlink and ialloc answer EROFS rather
than pretending -- a node exists because the kernel has the device, or it does
not exist.
This also makes /proc/filesystems true. It has advertised "nodev devtmpfs" for
as long as it has existed, against a kernel that had no such filesystem.
Two things found while measuring what remained:
* The thirteen mknod() calls for /proc ran *after* the procfs mount, so every
one was resolving a path into a read-only synthetic filesystem and failing.
procfs.c's own note called them a fallback to be cleaned up later; they have
been dead for longer than that.
* The four mkdir() calls for mount points were the rest of it. They are made
by the image builder now (sysroot-install), and the ones in init are a
fallback -- a mkdir that fails because the directory exists costs a path
walk and no writes, where creating one costs a transaction.
bootstrap() is now 0 ms on a fresh image, down from 2600.
init opens /dev/console after the mount rather than mknod'ing a node called
"console" into the root directory, which is what used to leave a stray /console
in `ls /`. The old path is kept as a fallback: if the mount fails there is no
/dev/console and no way to report it.
socktest passes, /proc still lists per-process directories, ps -e still
enumerates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2