fs: follow symlinks like Linux; Linux mknod; /proc/partitions

Symlinks are followed in namex_from() now, in the middle of a path as
well as at the end, and a relative target resolves from the directory
that holds the link rather than from the caller's cwd. `ln -s f d1/s;
cat d1/s` said "Symbolic link loop" because it looked for ./f. A loop is
ELOOP after 40 links, a dangling link ENOENT, and open(O_CREAT) through
a link to a file opens the file. divesymlink() is gone.

mknod from a Linux program passes (mode, dev) where the native call
passes (major, minor), and was read as the native one: `mknod n c 1 2`
made node 432,258, and mkfifo made a character device. Both are decoded
now; a FIFO is EPERM, and an existing name is EEXIST.

/proc/mounts listed devtmpfs twice: once from a fixed line and once
from the mount table. The fixed line is gone, and mount_add() refuses
a filesystem mounted on its own root, which is where a second devtmpfs
mount lands. A read-only FAT mount says ro.

/proc/partitions exists, in Linux's format, and /proc/filesystems lists
vfat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 10:50:41 +02:00
commit 7a1e63a553
8 changed files with 274 additions and 136 deletions

View file

@ -153,6 +153,11 @@ Three things worth knowing:
length until it is generated, and generating every one on every `stat()` to
fill in a number almost nothing reads would be absurd.
`/proc/mounts`, `/proc/filesystems` and `/proc/partitions` are in Linux's
formats to the column, since what reads them (`df`, `mount`, `fdisk -l`,
installer scripts) parses them. Every disk and partition in `/proc/partitions`
has major 8 and its device number as the minor, matching `/dev`.
## devtmpfs
`/dev` is generated, not stored. `kernel/fs/devfs.c`, behind the same `fs_ops`
@ -283,6 +288,23 @@ held alive by an `nlink` bump.
Renaming a *directory* is still refused with `EISDIR`, and so is replacing one:
both need the `..` fixup and the ancestor-loop check that are not written.
## Symlinks
Followed the way Linux follows them, in `namex_from()`. A link in the middle of
a path (`/a/link/b`) always is; the last component only for the calls that
follow (`open`, `stat`, `chdir`, `execve`, `access`, `truncate`, `statfs`,
`utimensat`), not for `lstat`, `readlink`, `unlink`, `rename`, `link` and the
create paths. A relative target resolves from the directory holding the link,
not from the caller's cwd, and a 41st link in one lookup is `ELOOP`.
The walk never holds a directory lock while it reads a link, only a reference
to the directory it is in, so following one is restarting the walk from there
with the target spliced in front of what is left of the path.
`open(O_CREAT)` through a link to an existing file opens the file, so
`echo hi > link` writes where the link points. A dangling link is not created
through.
## What it does not do
- **No page cache.** `mmap` of a file reads the whole thing in at map time, and
@ -290,20 +312,13 @@ both need the `..` fixup and the ancestor-loop check that are not written.
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]`.
- **Symlinks resolve on the final path component only.** `open()`, `stat()` and
friends follow a link they are pointed at; a link in the *middle* of a path —
`/a/link/b` — is not resolved. That belongs in `namex()`, which would have to
re-enter path lookup while holding a directory lock, and getting that wrong is
a deadlock rather than a wrong answer.
- **`rename()` does not move directories**, and will not replace one. `EISDIR`
either way.
- **No named pipes.** `mkfifo()` does not fail — it *succeeds* and produces a
character device, because `mknod()` turns `S_IFIFO` into a `T_DEV` and keeps
the major/minor. `ls -l` shows `crw-rw-rw- 438, 0` where a `p` belongs, and
nothing written to it comes back out. Succeeding wrongly is worse than
refusing: the zsh port was configured with `zsh_cv_sys_fifo=yes` on the
strength of it, and process substitution failed at the point of use rather
than being reported as unavailable.
- **No named pipes.** `mkfifo()` fails with `EPERM`, Linux's answer for a
filesystem that cannot hold one. It used to *succeed* and make a character
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
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
descriptors by path. `<(cmd)` needs one or the other — zsh now knows it has
neither and says so.