DF-3172 / fix.diff
--- a/sys/vfs/ufs/ffs_vfsops.c 2026-09-07 17:48:32.085811365 +0000 +++ b/sys/vfs/ufs/ffs_vfsops.c 2026-09-07 17:48:32.105811105 +0000 @@ -644,6 +644,17 @@ error = EINVAL; /* XXX needs translation */ goto out; } + /* + * fs_nindir must describe the actual capacity of an indirect + * block. It is untrusted media data; a crafted (or re-read) + * value poisons every NINDIR(fs) consumer: ufs_getlbns offset + * math (ffs_balloc OOB reads), ffs_truncate lastiblock levels, + * and the ffs_indirtrunc bzero/scan bounds. + */ + if (fs->fs_nindir != fs->fs_bsize / sizeof(ufs_daddr_t)) { + error = EINVAL; /* crafted geometry */ + goto out; + } fs->fs_fmod = 0; fs->fs_flags &= ~FS_UNCLEAN; if (fs->fs_clean == 0) { @@ -754,6 +765,25 @@ /* Enforce limit caused by vm object backing (32 bits vm_pindex_t). */ if (maxfilesize > (uint64_t)0x80000000u * PAGE_SIZE - 1) maxfilesize = (uint64_t)0x80000000u * PAGE_SIZE - 1; + /* + * Also cap maxfilesize at what the NDADDR + 3-level indirect + * tree can actually address (same formula newfs/mkfs.c uses). + * Otherwise lbn arithmetic in ffs_truncate can exceed the last + * representable block of a level and ffs_indirtrunc computes an + * out-of-range index for its bzero()/scan of the indirect block. + */ + { + uint64_t sizepb = fs->fs_bsize; + uint64_t representable = fs->fs_bsize * UFS_NDADDR - 1; + int j; + + for (j = 0; j < UFS_NIADDR; j++) { + sizepb *= NINDIR(fs); + representable += sizepb; + } + if (maxfilesize > representable) + maxfilesize = representable; + } if (fs->fs_maxfilesize > maxfilesize) /* XXX */ fs->fs_maxfilesize = maxfilesize; /* XXX */ if (ronly == 0) { --- a/sys/vfs/ufs/ffs_inode.c 2026-09-07 17:48:32.085811365 +0000 +++ b/sys/vfs/ufs/ffs_inode.c 2026-09-07 17:48:32.109811053 +0000 @@ -485,6 +485,15 @@ last = lastbn; if (lastbn > 0) last /= factor; + /* + * Defense in depth: `last' is an index into bap[], which holds + * NINDIR(fs) entries. The caller derives lastbn from the + * (media-controlled) new file size; if the superblock geometry + * is inconsistent the division above can still exceed the range + * of this level, driving the bzero() below out of bounds. + */ + if (last > NINDIR(fs) - 1) + last = NINDIR(fs) - 1; nblocks = btodb(fs->fs_bsize); /* * Get buffer of block pointers, zero those entries corresponding |