SchOS/kernel
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Nexoniarz 370605452c rename: it was writing v1 dirents into a B-tree, and nobody noticed
`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>
2026-09-20 01:33:15 +02:00
..
arch/x86_64 boot: one line when it works, all of it when it does not 2026-09-14 21:19:50 +02:00
core kbd: three layouts, and the arrow keys were sitting on top of UTF-8 2026-09-20 01:09:32 +02:00
dev kbd: three layouts, and the arrow keys were sitting on top of UTF-8 2026-09-20 01:09:32 +02:00
drivers boot: one line when it works, all of it when it does not 2026-09-14 21:19:50 +02:00
fs boot: one line when it works, all of it when it does not 2026-09-14 21:19:50 +02:00
net boot: one line when it works, all of it when it does not 2026-09-14 21:19:50 +02:00
proc debugger: D5, the network transport -- and the plan closes 2026-09-14 15:33:53 +02:00
sys rename: it was writing v1 dirents into a B-tree, and nobody noticed 2026-09-20 01:33:15 +02:00
kernel.ld debugger: the kernel can be asked a question 2026-09-14 00:58:45 +02:00