[PATCH 00/10] ufs: correctness fixes for valid on-disk filesystems

Ali Ahmet Memis <[email protected]> Sat, 1 Aug 2026 22:55:20 +0000
Newsgroups org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
The recent fs/ufs traffic has mostly been hardening against crafted
images. This is the other kind: filesystems that are entirely valid,
written by FreeBSD or by Linux itself, that fs/ufs then handles wrongly.
Most of it comes from comparing the code against what modern FreeBSD
writes; Linux last looked at the UFS2 layout around FreeBSD 5.x, and
several fields it still treats as spare have been in use for years.

Patch 1 is preparation with no functional change: it splits
fs_44.fs_sparecon[50] into the fields FreeBSD defines. The layout does
not move. sizeof(struct ufs_super_block_third) stays 356 and
fs_contigsumsize stays at offset 292, checked with the compiler before
and after, and cross-checked against a superblock written by mkfs.ufs,
where the same field sits at absolute offset 1316 with usb3 based at
1024.

Patches 2 and 3 are extended attribute fallout. A UFS2 inode can own
attribute blocks in di_extb[], which ffs_alloc() counts in di_blocks, so
deciding fast symlinks by i_blocks misfires on a short symlink that has
an attribute, and unlinking any inode with attributes leaks the blocks.

Patches 4 and 5 are the on-disk file flags. New inodes inherit the
parent directory's chflags, which FreeBSD never does, and the flags that
are there never reach S_IMMUTABLE or S_APPEND.

Patch 6 is an arithmetic bug in ufs_change_blocknr(), where a buffer
index within the folio is used as an offset into the relocated range.
It needs a page larger than the filesystem block. It does not touch the
deeper relocation problems described in [1], only the offset.

Patches 7 to 10 are mount policy. The read-only decision taken at mount
time can be undone with remount,rw, and nothing looks at the modern
feature flags, so filesystems needing fsck or journal replay, or holding
snapshots or metadata check hashes, are written to as if ordinary.

Testing
-------

Built with W=1 and checkpatch --strict clean on all ten. Runtime tested
in qemu against images from mkfs.ufs (Debian ufsutils 8.2). Cases that
need on-disk state Linux cannot create were produced by editing the
images directly. Every case was run twice, once on this series and once
on the unpatched base, same images, same script.

x86_64, KASAN, UFS2 16384/2048 and UFS1 8192/1024:

  case                                    unpatched          patched
  readlink, short symlink with extattrs   I/O beyond end     correct target
  unlink of the same symlink              ufs_panic          clean
  extattr fragment after inode delete     still allocated    free
  new inode under a flagged directory     flags 0x1          flags 0
  create inside an SF_IMMUTABLE dir       succeeds           EPERM
  remount,rw with fs_clean FSACTIVE       succeeds           refused
  FS_NEEDSFSCK / FS_SUJ read-write        read-write         read-only
  active snapshot read-write              read-write         read-only
  FS_METACKHASH read-write                read-write         read-only
  FS_METACKHASH with no hashes set        read-write         read-write

The unpatched readlink failure is the chain patch 2 describes, with the
ASCII of the link target used as a block pointer:

  vdl: rw=8388608, sector=15116822126868137148, nr_sectors = 4 limit=131072
  ufs: panic: ufs_free_fragments: freeing blocks are outside device

The last line of the table is a negative control: a filesystem that
advertises check hashes without maintaining any stays writable, so patch
10 is not simply refusing everything. 26 of 26 checks pass on the
series, KASAN quiet on both runs.

arm64 with 64 KiB pages, UFS1 4096/512, for patch 6:

  case                                    unpatched          patched
  file survives a tail relocation         corrupt at 4096    intact
  neighbouring file after relocation      intact             intact

A block is 8 fragments there and blks_per_page is 128, so a tail in the
second block gives beg 8 and beg & mask 8. With the fragments that
physically follow the tail occupied, extending the file relocates it and
the first byte of the moved tail reads back as zero. The same test on 4
KiB pages never reaches a nonzero beg & mask, which is why the bug is
invisible there.

Patch 10 is the one I am least sure belongs. Refusing read-write on
check-hashed filesystems is correct, but FreeBSD enables check hashes by
default, so images that mount read-write today would stop. That is a
policy call rather than a bug fix, and the patch is independent; drop it
and the other nine still apply.

Not addressed: UFS still has no extended attribute or ACL support, so
FS_ACLS filesystems are read with their access control ignored, and
fs_clean is never set to FSACTIVE while mounted read-write, so a crash
leaves a filesystem looking clean. Both are larger jobs.

Two things I looked at and dropped rather than send: the 4 GiB directory
truncation in ufs_last_byte() is already posted by YANXIN LI [2], and
the UFS_FSLOG read-write behaviour turned out to be deliberate, added in
2010 by Alex Viskovatoff with Evgeniy Dushistov's ack [3].

[1] https://lore.kernel.org/all/20241018231428.GC1172273@ZenIV/
[2] https://lore.kernel.org/all/[email protected]/
[3] https://lore.kernel.org/all/[email protected]/

Ali Ahmet Memis (10):
  ufs: name the modern UFS2 superblock fields
  ufs: use i_size to detect fast symlinks
  ufs: free UFS2 external attribute blocks on inode deletion
  ufs: do not inherit file flags from the parent directory
  ufs: honour on-disk immutable and append-only flags
  ufs: fix fragment relocation offsets within a folio
  ufs: revalidate filesystem state before remounting read-write
  ufs: refuse read-write mount when fsck or journal replay is needed
  ufs: refuse read-write mount of filesystems with active snapshots
  ufs: refuse read-write mount of check-hashed filesystems

 fs/ufs/balloc.c |   8 +--
 fs/ufs/ialloc.c |   4 +-
 fs/ufs/inode.c  | 126 +++++++++++++++++++++++++++++++++++++-------
 fs/ufs/super.c  | 136 +++++++++++++++++++++++++++++++++++-------------
 fs/ufs/ufs.h    |   2 +
 fs/ufs/ufs_fs.h |  33 +++++++++++-
 6 files changed, 246 insertions(+), 63 deletions(-)


base-commit: 2d2338c93da79b3bfe4b6099a931d9468d539952
-- 
2.55.0