NextBSD · Kernel patch · Plan
nextbsd-kernel patchThe pragmatic alternative to porting a whole HFS+ filesystem just to get Mac-friendly case behavior: teach FreeBSD's UFS/FFS to fold case at name lookup. A tightly-bounded patch — three code changes and a superblock flag — that gives Macs the case-insensitivity Apple's own UFS never had.
A genuinely small, ~4–6 week kernel change for the ASCII-only version — versus years for the HFS+ port that HFS+ was partly being adopted to get case-insensitivity from. The whole fold is essentially three lines and a flag.
ufs_lookup.c (the one bcmp), fold the ufs_dirhash key + verify, latch an FS_CASEFOLD superblock bit at mount. One shared ufs_fold() helper.msdosfs — not Apple: Apple's UFS was case-sensitive (a known pain point), and even ravynos (a macOS-compat FreeBSD fork) skipped this — a signal it's tractable but not free.fsck_ffs (case-collision detection, ~1–2 weeks) and the honest on-disk-compat caveat: a FS_CASEFOLD volume read by stock FreeBSD mounts case-sensitive → collision hazard. Treat as NextBSD-only volumes.All in sys/ufs/; line numbers from the baked releng/15.1 tree
| Site | Change |
|---|---|
ufs/ufs_lookup.c → ufs_lookup_ino() ~l.422 | The single name-match bcmp(cn_nameptr, ep->d_name, namlen) — THE comparison. Fold it. Because CREATE/RENAME/DELETE all run ufs_lookup_ino first, fixing this one point also makes create/rename collision detection case-insensitive (creating FOO when foo exists now correctly returns EEXIST). |
ufs/ufs_dirhash.c → ufsdirhash_hash() ~l.1033 | The keystone. fnv_32_buf(name,...) must hash the case-folded bytes, so foo and FOO land in the same slot (or the fast path silently misses case-variants the linear path would hit). |
ufs/ufs_dirhash.c → ufsdirhash_lookup() ~l.645 | The dirhash verify compare — fold it too, matching l.422. |
ffs/fs.h struct fs.fs_flags | Add FS_CASEFOLD (next free bit, alongside FS_ACLS/FS_NFS4ACLS/…); latch into ufsmount in ffs_mountfs() so the fold path is gated per-volume. |
ufs_makedirentry()'s name write verbatim — the original case is stored; only comparison and hashing fold. Same as ext4/HFS+ ("case-insensitive, case-preserving"). The single ufs_fold() helper must live in a shared header so fsck_ffs can #include the identical logic — any drift between kernel and fsck is a guaranteed corruption source.| System | Fold | Normalize? | Enable | Borrow |
|---|---|---|---|---|
| Linux ext4/f2fs "casefold" (2019) | fs/unicode utf8_casefold over a trie, at compare+hash | NFD + full case-fold | superblock encoding stamp + per-dir +F flag | The whole model — closest analogue |
FreeBSD msdosfs (already CI, in-tree) | win2unixfn + iconv codepage fold | codepage, not Unicode | mount flags (MSDOSFSMNT_*) | Proves an in-kernel CI FS ships in FreeBSD today; mount-flag gate is idiomatic |
| HFS+ (Apple) | FastUnicodeCompare + gLowerCaseTable (compact, no ICU) | NFD on create | volume-wide | Compact fold table — but APSL (same provenance burden as the HFS+ port) |
sys/ufs/). A project that would most want this chose not to build it. So: novel, tractable, but the effort/risk is real enough that a Mac-compat project passed. The framing that survives: this is "Apple's UFS + the case-insensitivity Apple never bothered to add."| Choice | Recommendation |
|---|---|
| ASCII-only fold vs full Unicode | ASCII-only first. ~5-line tolower, no in-kernel table, handles English/code/app-bundle names (the vast majority). Non-ASCII names simply stay case-sensitive — never corrupts. Build the machinery so a Unicode table drops into the one ufs_fold() later. Full Unicode = a ~324 KB table in a NO_MODULES kernel + the provenance problem → defer. |
| Enable granularity | Superblock flag, volume-wide (set by newfs/tunefs, read at mount). Matches HFS+/msdosfs, touches no inode format. Skip a mount option as the primary switch (a CI volume mounted case-sensitive = collision hazard); allow a mount option only to override off for recovery. |
| Normalization | Casefold only, no NFD, in v1. NFD-on-write changes on-disk bytes (breaks POSIX "bag of bytes" + needs the table). Accept the gap: names differing only by NFC/NFD stay distinct (same as APFS default). Add NFD later if real interop failures appear. |
fsck_ffs / newfs / tunefsThese are FreeBSD-base tools (fbsdglue), so changes are source patches in nextbsd-freebsd-compat — but that repo is scaffold-only today (the fbsdglue source hasn't migrated yet), so these are blocked on that migration.
newfs — a -o casefold flag that sets FS_CASEFOLD in the superblock (~20 lines).tunefs — toggle FS_CASEFOLD, but only on an empty FS (turning it on with case-colliding files present is undefined — mirror ext4's rule) (~30 lines).fsck_ffs — the real work. On a FS_CASEFOLD volume, pass 2 must detect two entries in one directory that fold to the same name as corruption (illegal on a CI volume). Must #include the identical ufs_fold() as the kernel. ~1–2 weeks for correct collision detection + tests.dumpfs — trivially print the flag.nextbsd-kernelEdits to existing sys/ufs/ files → a single patches/ entry (not src-overlay/). The overlay repo applies patches/series over the baked /usr/src.
patches/0008-ufs-caseinsensitive.patch # edits ufs_lookup.c, ufs_dirhash.c, fs.h,
# ufsmount.h, ffs_vfsops.c + a new ufs_casefold.h
patches/series # append the one line (verbatim, no comments)
config/NEXTBSD # add: options UFS_CASEFOLD (#ifdef-gated code)
# register the option token in sys/conf/options (opt_ufs.h already exists,
# used by UFS_DIRHASH/UFS_EXTATTR) — add within the patch, no build.yml change
Iterate loop: a push touching patches/**/config/** rebuilds the kernel (NO_MODULES=yes) and, on a PR, boots the image under qemu — but the boot smoke-test only proves boot (root is a normal case-sensitive UFS), so you must add a functional test: newfs a scratch image with the flag, mount, touch FOO; stat foo, verify the collision.
| Piece | Effort |
|---|---|
| ASCII-only fold, kernel only (working → hardened + qemu test) | ~1 → 2–3 weeks |
newfs/tunefs flag | ~2–3 days (blocked on fbsdglue migration) |
fsck_ffs collision detection | ~1–2 weeks |
| Shippable ASCII-only CI-UFS w/ tools + fsck | ~4–6 weeks |
| Full Unicode NFD+CF (table import + provenance + fsck sharing) | +1–3 months |
| Risk | |
|---|---|
| Stock-FreeBSD read = silent data hazard (highest) | A FS_CASEFOLD volume mounted on vanilla FreeBSD (or with the option not compiled) mounts case-sensitive and ignores the flag → foo/FOO become two files → a later CI mount sees a fold-collision. Stock fsck_ffs won't flag it. Accept "NextBSD-only volumes" (or use a feature bit stock fsck rejects). |
| dirhash correctness (high) | If the fold in the hash and the fold in the compares ever disagree → intermittent "file exists but can't be opened." Single shared ufs_fold() mandatory; existing dirhashes must be invalidated when fold logic changes. |
| fsck/kernel fold divergence (high) | fsck must use the identical fold routine or it mis-classifies collisions. |
| rename case-change (medium) | rename(foo→FOO) on a CI volume is an in-place case rewrite (allowed, must update on-disk name); rename(foo→BAR) is distinct. Stock ufs_rename doesn't distinguish — needs explicit handling. |
| Unicode table provenance (medium, only if full-Unicode) | The one ready fold table is HFS+'s gLowerCaseTable (APSL) — importing it re-imports the provenance burden HFS+ was avoided to dodge. Generating fresh from the UCD sidesteps it but is extra tooling. |
#ifdef UFS_CASEFOLD + a shared ufs_fold() is a genuinely pragmatic ~4–6 week path to Mac-friendly case behavior — provided you accept NextBSD-only volumes and defer Unicode/NFD. Paired with the AFP server, it delivers "serve Macs case-insensitively over AFP" at a fraction of the HFS+ port's cost. That even ravynos skipped it should temper expectations — the fsck-consistency and on-disk-compat tails are where the time goes.