Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.
The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.
LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A leaf's value area holds variable-length records end to end, so a record begins
wherever the one before it finished -- a multiple of 8 only by luck. The code
reads them by casting a pointer into the block, which tells the compiler they
are aligned when they are not.
That works on x86, which is why it has never shown as a bug, and it is undefined
in C. A compiler entitled to assume 8-byte alignment may emit an instruction
that requires it, and the failure would arrive at a different optimisation level
or on a different architecture looking like filesystem corruption rather than
like codegen. UndefinedBehaviorSanitizer reported 31 of them across obj.c,
objw.c, attr.c and space.c.
Seven types now carry SCHFS_PACKED: dirent_rec, extent, extref, index_rec,
attr_rec, snap and inode. Two of those -- attr_rec and inode -- were never
reported, because schfs_objtest does not reach an unaligned instance of either.
They were latent rather than absent, and casting into a leaf is the same lie
whether or not a test has caught it yet.
NO ON-DISK LAYOUT CHANGED, and that was measured before the annotation was
applied rather than assumed: every one of these already carries explicit `pad`
fields and therefore has no implicit padding, so sizes and member offsets are
identical with and without it. Seven _Static_asserts now pin the sizes, because
a record that silently grows misreads every existing volume from that point on
and looks like corruption rather than like a struct that changed.
schfs_node_hdr, schfs_item, schfs_kptr and schfs_uberblock are deliberately left
alone. The node header is exactly 32 bytes, so the items and key pointers after
it are already 8-aligned, and annotating them would cost byte-wise access on the
tree's hottest paths to fix a problem they do not have.
Verified by reading an image written before the change: schfs_fsck --check
passes on structure, checksums, key ordering, data blocks and derived free
space, and the kernel mounts its own root unchanged.
Also here: mkschfs passed a null pointer to qsort for an empty directory. Every
implementation ignores it when the count is zero; the standard does not say they
have to.
make schfs-check now runs with no sanitizer output at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Started as "fix the ASAN leak in schfs_objtest" and the test turned out to be
the messenger. The allocation map was taken through a palloc callback and there
was no way to return it: no schfs_space_release, and a comment in
schfs_space_init reading "caller frees what was taken" for an API the caller
did not have.
Three separate leaks from that one gap:
* The kernel leaked the entire map on every unmount -- up to 2 MB, at 8 KiB
per GiB of volume -- because kmount_unmount flushed, cleared `mounted` and
walked away. This is the one that matters; the others are test-only.
* schfs_space_init stranded a page on its own error path. npages was raised
only after *both* of a pair of allocations succeeded, so a failure on the
second left the first outside the count -- unreachable to any cleanup that
could later exist.
* schfs_objtest leaked 2121728 bytes in 518 allocations, which is what made
the whole SchFS suite exit non-zero regardless of its assertions.
The map now carries its own pfree rather than taking one at release time, so it
can only ever be freed by the allocator that filled it -- the kernel's pages
come from kalloc and a host tool's from calloc, and the two are not
interchangeable. schfs_vol_open and schfs_vol_format release any map the volume
is already holding before building another, which is what stops a re-open
leaking; struct schfs_vol must therefore be zeroed before first use, and the
header now says so.
The test's own leak was the last one found and the simplest: it does
memset(&VOL, 0, sizeof(VOL)) before reopening, which wiped the map's pointers
before vol_open could release them. Eight of those, each now releasing first.
schfs_objtest asserts the balance -- 520 pages taken, 520 given back -- because
a suite that leaks cannot detect a leak: the filesystem's own allocations would
arrive as noise inside the harness's. The next one fails the run instead.
make schfs-check now exits 0. The 31 UBSAN misalignment reports are still
there and are still a Gate 0 item.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--snapshots is a range scan over SCHFS_SNAP_OBJECTID, printing each
retained root's id, name, block and the generation it was taken at. The
name is printed with an explicit precision: it is bounded but a damaged
image need not have terminated it.
mkschfs grows --snapshot NAME, which takes one of the finished tree. It
runs after the commit, because a snapshot names a *committed* root --
taking it before would record a root that exists only in memory.
Verified end to end: an image built with --snapshot golden lists the
snapshot and still passes --check. An image without one prints "no
snapshots" rather than nothing at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The disc carried one file -- boot/schos.bin -- with 8.3 uppercase names,
one El Torito entry, and no way to boot a UEFI machine. Now:
/boot/schos.bin BIOS entry, unchanged: no-emulation, 1+STAGE2_SECTORS
/boot/efiboot.img UEFI entry: a FAT image built with mtools
/EFI/BOOT/BOOTX64.EFI the loader, also visible as a file
/bin /etc /usr ... the staged sysroot as an ordinary tree
Rock Ridge and Joliet restore real names, permissions and symlinks;
-cache-inodes matters more than it looks, because the sysroot is a toybox
multicall binary with a couple of hundred hard links to it and without
inode caching each link becomes its own copy.
-isohybrid-gpt-basdat adds a GPT so the .iso can be dd'd to a stick and
booted by UEFI. That is UEFI only and deliberately so: the BIOS isohybrid
trick needs syslinux's isohdpfx.bin, and even with it this loader reads
the kernel back over ATAPI, which a stick presented as a hard disk cannot
serve. usbstick.img is what that is for.
The FAT image carries the kernel as well as the loader, because
BOOTX64.EFI opens \schos\kernel on the volume it was loaded from, and for
a UEFI boot that is the FAT image rather than the ISO 9660 tree. Without
it the loader halted with EFI_NOT_FOUND.
Also fixes `make iso` and `make ramroot`, which were already broken:
fs-ram.img was still built by the v1 mkfs while the kernel accepts only
SchFS as root, so the ramdisk was a format it could not mount and it
panicked with "schfs root: no SchFS volume on the root device". It is a
mkschfs image now, which needed --exclude in mkschfs -- dropping the
exclude list was not an option, because the ramdisk is linked into the
kernel binary and what goes in it is a licensing question. RAMDISK_NINODES
is gone with the v1 image: SchFS has no preallocated inode table.
Verified: BIOS ISO boots to a shell and runs commands over serial
(241 files in /bin, /etc/motd reads back). UEFI ISO boots to a prompt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Developed from both Windows and Linux, so nothing in the build should
care which host it runs on. It did, in four places.
Toolchain: one path instead of two. clang is a cross-compiler by
construction, so a single LLVM install targets bare-metal ELF, 16/32-bit
code below 1 MB, and Linux/musl userland from any host -- which is how
the Windows build already worked. The GNU branch beside it had drifted
into passing --no-warn-rwx-segments, a hard error on lld, which is the
default linker on both FreeBSD and macOS; and it required a brewed
x86_64-elf-gcc before anything built on a Mac at all. GNU is kept as a
fallback (USE_GNU=1) but clang+lld is chosen whenever both are present.
Host tools use the host's own cc; only mingw needs naming explicitly.
tools/mkschfs.c included <sys/sysmacros.h> unconditionally on non-mingw
hosts. That header is glibc/musl only -- macOS and the BSDs have neither
the header nor a need for it, since major()/minor() come from
<sys/types.h> there. Now guarded on __linux__.
Ports verified tarballs with `sha256sum -c -`, which does not exist on
macOS (shasum), FreeBSD/NetBSD (sha256), or OpenBSD (cksum -a sha256),
and each spells its check mode differently. tools/sha256check.sh picks
whichever of the five the host has and compares digests itself.
nproc is coreutils, so absent on macOS and BSD; falls through to sysctl
-n hw.ncpu before defaulting to 4.
The PowerShell tools now run under pwsh on any OS: QEMU is found on PATH
(the scoop location is a Windows-only fallback), and path literals use
forward slashes -- Join-Path $root 'build\x' produces a *filename
containing a backslash* on a Unix host. The backslash sweep is limited
to known directory prefixes on purpose: bootcheck sends
printf 'caf\303\251' to the guest to test UTF-8, and a blanket
substitution would have quietly changed what that asserts.
Also fixes six tools left booting build/fs.img, which stopped being
bootable when SchFS became the root filesystem: crashshot, dispshot,
fbshot, splashshot, textshot and ttyshot all take build/schfs.img now,
and the -SchFS switches that used to select it are gone.
Verified on Windows: full build, and bootcheck green. The Linux, macOS
and BSD paths are written from their documented behaviour and are NOT
executed here -- this machine has no WSL and no second host.
Removes the "-unix" element and with it any trademark exposure to The
Open Group.
The split follows the ecosystem layout: SchOS is the system, and the
kernel has its own name. Kernel-identity strings are the only places
that take SchIFT -- uname(2) sysname, /proc/version, and the two licence
sentences that say "kernel" outright. That is what every Unix does here:
`uname -s` on Linux reports the kernel, not the distribution. Everything
a user reads as the product -- panic screen, boot menu, error prefixes,
docs, LICENSE, NOTICE -- is SchOS.
schunix.img is schos.img, the logo assets and the musl TLS patch are
renamed, and __SCHUNIX__ is __SCHOS__ (the define and the patch that
consumes it moved together).
Two things the rename broke, both found by the bootcheck suite:
- shmtest wrote "SCHUNIX-MMAP-OK" with a hardcoded length of 15 and
compared it back by hand-counted offsets (m[3]=='U', m[14]=='K').
The shorter marker left the write running two bytes past the literal
and the compare reading past the data, failing a file-backed mmap
that was working correctly. Now written and compared through one
constant with sizeof.
- bootcheck asserted a 31-byte file after appending to
"hello-from-schunix"; the shorter string makes it 29.
Both are the same mistake: a string literal whose length is written out
somewhere else. Full suite green -- base, Tty, Ps, Sock, Fb, Usb, Evdev.
An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.
No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.