β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0852

Unvalidated user ssector causes signed-integer overflow in volume descriptor loop and volume_space_size

Summary

cd9660_vfsops.c:197 copyin args.ssector int no validation. :326 16+ssector :327 100+ssector signed overflow INT_MAX. :427 isomp->volume_space_size+=ssector int+int overflow wraps negative. Weakened bound check :775 lbn>=vss passes negative lbn. statfs :633 reports bogus f_blocks. Trigger: mount(2) cd9660 ssector=INT_MAX. Fix: if(ssector<0||ssector>INT_MAX-200) EINVAL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0852 Β· 18 files
FileTypeDescriptionSize
craft_iso.py trigger-source host-side generator for a minimal valid ISO 9660 image with crafted volume_space_size near INT_MAX (and a normal baseline) 4.9 KB view raw
poc.c trigger-source drives mount(2) with controllable ssector; reports errno + statfs f_blocks with bogus-value detector 3.9 KB view raw
iso_overflow.iso test-image crafted ISO: PVD at sector 32, volume_space_size = 0x7FFFFFF0 78.0 KB ↓ download
iso_normal.iso test-image baseline ISO: PVD at sector 16, volume_space_size = 100 46.0 KB ↓ download
build.sh build-script cc -o poc poc.c 188 B view raw
run.sh run-script vnconfig + ./poc /dev/vn0 <mnt> <ssector> 584 B view raw
build.log build-log final successful PoC build 80 B view raw
run.log run-log decisive unpatched run: f_blocks = -2147483648 (overflow confirmed) + loop-overflow variant 1.0 KB view raw
run.2.log run-log stress-test copy (3x deterministic) 1.0 KB view raw
env.txt environment uname, cc, sysctls 402 B view raw
fix.diff suggested-fix git-apply-able: validate ssector + guard volume_space_size += ssector 1.7 KB view raw
fix_build.log build-log full single-fix kernel build (nativekernel), rc=0 5.6 MB ↓ download
fix_run.log run-log patched-kernel #1 run: overflow rejected EINVAL, normal case f_blocks=100 1.5 KB view raw
fix_before_after.txt fix-evidence before/after contrast 2.6 KB view raw
README.md readme human-readable summary + repro 3.7 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, reachability, fix, validation 5.3 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-readable summary + repro
↓ download raw

DF-0852 β€” Unvalidated user ssector β†’ signed-integer overflow in cd9660

File: sys/vfs/isofs/cd9660/cd9660_vfsops.c Severity: Low (auditor) β€” confirmed: real signed-integer overflow, but root-only reachability and no write/corruption primitive (int size field), so impact ceiling = DoS/info-confusion (poisoned statfs).

Claim

iso_args.ssector (a signed int, copied in from userspace in cd9660_mount at cd9660_vfsops.c:197) is never range-checked. It then drives: - the volume-descriptor scan loop for (iso_blknum = 16 + ssector; iso_blknum < 100 + ssector; ...) (:326-327) β€” 16+ssector / 100+ssector wrap when ssector is near INT_MAX; - the multi-session adjustment isomp->volume_space_size += argp->ssector; (:427) β€” overflows when volume_space_size (read from the attacker-controlled ISO Primary Volume Descriptor) plus ssector exceeds INT_MAX.

The poisoned volume_space_size is published via cd9660_statfs (:633 sbp->f_blocks = isomp->volume_space_size;) and used as a bound in cd9660_vget_internal (:775).

Reproduction

A crafted ISO 9660 image (craft_iso.py) places a valid Primary Volume Descriptor at sector 16 + SSECTOR with volume_space_size = 0x7FFFFFF0. Mounting it with ssector = 16 makes line 427 compute 0x7FFFFFF0 + 16 = 0x80000000 β†’ INT_MIN, and statfs reports the wrapped value.

$ ./poc /dev/vn0 /tmp/df0852_mnt 16
[poc] mount() rc=0 errno=2 (No such file or directory)
[poc] statfs f_blocks = -2147483648  (0xffffffff80000000)  f_bsize=2048
[poc] !!! BOGUS f_blocks: signed-integer overflow confirmed !!!

Reachability / threat model (why Low, not higher)

get_fscap("cd9660") returns SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c get_fscap): cd9660 is not in the user-mountable set (only null/devfs/procfs/tmpfs/fuse are). The mount(2) syscall gates non-root callers with caps_priv_check_td(td, priv | __SYSCAP_NOROOTTEST) even when vfs.usermount=1. Verified empirically: an unprivileged user (maxx, uid 1001) gets EPERM even with vfs.usermount=1 + a group-readable vn device + a self-owned mountpoint. Only root (or a RESTRICTEDROOT-capable process) can reach the ssector input. Root→kernel is game-over by definition; there is no privilege boundary to cross and no escalation chain.

The primitive is an int size/bound field, not a pointer/refcount/function-pointer β€” there is no write that yields code execution. The realistic impact is a poisoned statfs (DoS/info-confusion for df/quota/NFS-export stats). No uid=0 chain exists (valid hard blocker: root-only reachability + non-write primitive).

Build / run

./build.sh                 # cc -o poc poc.c
python3 craft_iso.py 16 0x7FFFFFF0 iso_overflow.iso   # host: build the crafted ISO
python3 craft_iso.py 0  100          iso_normal.iso    # host: build a normal baseline ISO
# on guest (root):
vnconfig -c -s labels vn0 /root/iso_overflow.iso
./run.sh 16                 # overflow case  -> bogus f_blocks
./run.sh 2147483647         # loop-overflow  -> ssector=INT_MAX wraps loop bounds

Fix

fix.diff (git-apply-able) adds two guards in iso_mountfs(): 1. validate ssector before the loop (ssector < 0 || ssector > INT_MAX - 100 β†’ EINVAL), closing the loop-bound overflow; 2. guard the multi-session sum (volume_space_size < 0 || volume_space_size > INT_MAX - ssector β†’ EINVAL), closing the line-427 overflow for crafted images.

It supersedes the finding markdown's proposal (which only bounded ssector) by also guarding the image-controlled volume_space_size addition. Validated on a built-and-booted single-fix kernel (#1): the overflow is rejected with EINVAL, normal mounts still report correct f_blocks.

VERDICT.md verdict full narrative: mechanism, reachability, fix, validation
↓ download raw

DF-0852 β€” VERDICT

Verdict: REPRODUCED (signed-integer overflow confirmed, observable) β€” impact DoS/info-confusion, root-only reachability, no escalation. Fix VALIDATED on a built single-fix kernel.

Root cause

iso_args.ssector is a signed int copied in from userspace by cd9660_mount (sys/vfs/isofs/cd9660/cd9660_vfsops.c:197, copyin(data, &args, sizeof(struct iso_args))) with no validation anywhere (confirmed by grep: the only other ssector use is the kernel's own iso_get_ssector root-mount probe at :162, not user input). It flows directly into three signed-arithmetic sites in iso_mountfs():

  • :326 for (iso_blknum = 16 + argp->ssector; β€” wraps when ssector > INT_MAX - 16
  • :327 iso_blknum < 100 + argp->ssector; β€” wraps when ssector > INT_MAX - 100
  • :427 isomp->volume_space_size += argp->ssector; β€” wraps when volume_space_size + ssector > INT_MAX

The kernel is built with -fno-strict-overflow (confirmed in nk_fix.log CFLAGS), so the signed overflow wraps silently rather than trapping β€” no INVARIANTS/KASSERT fires (none exists in this file; verified by grep).

Mechanism (confirmed, path:line at each hop)

  1. User issues mount("cd9660", mnt, &args, MNT_RDONLY) with args.ssector controlled. :197 copyin β†’ :246 iso_mountfs(devvp, mp, &args).
  2. :326-327 the volume-descriptor loop iterates iso_blknum over [16+ssector, 100+ssector). With a modest ssector and a crafted ISO whose PVD sits at sector 16+ssector, the loop finds the PVD and pri is set.
  3. :414-417 isomp->volume_space_size = isonum_733(pri->volume_space_size) reads the image-controlled value (e.g. 0x7FFFFFF0).
  4. :427 isomp->volume_space_size += argp->ssector overflows: 0x7FFFFFF0 + 16 = 0x80000000 = INT_MIN. The mount succeeds (:542 return 0).
  5. :633 cd9660_statfs: sbp->f_blocks = isomp->volume_space_size publishes INT_MIN (sign-extended to long β†’ -2147483648 / 0xffffffff80000000).

The PoC observes this directly via statfs(2) β†’ f_blocks = -2147483648. Reproduced deterministically 3/3.

The loop-bound variant (ssector = INT_MAX) makes 16+ssector and 100+ssector wrap to -2147483633 / -2147483549; the kernel executes the overflowed arithmetic and then fails the mount (negative-offset bread), proving ssector is accepted unvalidated.

Why no escalation (Phase 6 hard blocker β€” valid)

  • Root-only reachability. sys/kern/vfs_syscalls.c get_fscap() returns SYSCAP_RESTRICTEDROOT for cd9660 (only null/devfs/procfs/tmpfs/fuse are user-mountable). sys_mount gates non-root callers with caps_priv_check_td(td, priv | __SYSCAP_NOROOTTEST) even with vfs.usermount=1. Verified empirically: maxx (uid 1001) gets EPERM mounting cd9660 even with vfs.usermount=1 + a group-readable vn0 + a self-owned mountpoint. Only root (or RESTRICTEDROOT) reaches ssector. Rootβ†’kernel is game-over; there is no privilege boundary to cross.
  • Non-write primitive. volume_space_size is an int size/bound field, not a pointer/refcount/function-pointer. The corruption poisons statfs and weakens/strengthens the :775 lbn >= volume_space_size check, but yields no code-exec primitive. There is no chain to uid=0.

Realistic impact ceiling = DoS/info-confusion (poisoned statfs for df/quota/NFS-export accounting), gated behind root. This matches the auditor's Low severity.

PoC changes

The finding shipped with no PoC folder (only the DB row). I created the full evidence pack from scratch: - craft_iso.py β€” host-side generator for a minimal valid ISO 9660 image with a controllable ssector layout and a crafted volume_space_size near INT_MAX so line 427 overflows; also generates a normal baseline image. - poc.c β€” drives mount(2) directly with a controllable ssector, reports errno, and on success statfs(2) f_blocks (with a bogus-value detector). - build.sh / run.sh β€” exact build & run. - fix.diff β€” the validated fix (supersedes the finding proposal).

Fix

fix.diff adds two minimal guards in iso_mountfs(): 1. after joliet_level = 0; (:325), before the loop: if (argp->ssector < 0 || argp->ssector > INT_MAX - 100) { error = EINVAL; goto out; } β€” closes the loop-bound overflow (:326-327); 2. before :427: if (isomp->volume_space_size < 0 || isomp->volume_space_size > INT_MAX - argp->ssector) { error = EINVAL; goto out; } β€” closes the image-controlled volume_space_size + ssector overflow.

Adds #include <machine/limits.h> for INT_MAX (the kernel's <sys/param.h> only includes <machine/limits.h> under #ifndef _KERNEL). This supersedes the finding's proposal (which bounded only ssector) by also guarding the image-controlled sum.

Fix validation (Phase 8)

  • Baseline #0 (unpatched with-src): overflow case β†’ mount rc=0, f_blocks = -2147483648. Reproduced.
  • Single-fix kernel #1 (built make nativekernel, installed via make installkernel, booted): overflow case β†’ mount rc=-1 errno=EINVAL (2Γ— deterministic); loop-overflow (ssector=INT_MAX) β†’ EINVAL; normal ISO (VSS=100, ssector=0) β†’ mount rc=0, f_blocks=100 (no regression).
  • fix_status = fixed. See fix_before_after.txt for the before/after contrast and fix_run.log for the full patched-kernel run.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline f_blocks=-2147483648 (BOGUS); patched EINVAL; regression f_blocks=100 (normal).

BEFORE: f_blocks=-2147483648. AFTER: EINVAL. REGRESSION: f_blocks=100.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 03:56:01 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- root-only (SYSCAP_RESTRICTEDROOT) + non-write primitive (int size field). No escalation.

Evidence (decisive lines)

BASELINE: mount rc=0, statfs f_blocks=-2147483648 (BOGUS). PATCHED: mount EINVAL (overflow rejected). REGRESSION: normal ISO f_blocks=100.

PoC changes

Authored from scratch: poc.c (mount with controllable ssector + statfs bogus detector), craft_iso.py (crafted PVD volume_space_size near INT_MAX), fix.diff (guard ssector range + volume_space_size+ssector overflow), VERDICT.md, manifest.json.

Verified recommended fix

Guard both overflow paths in iso_mountfs: (1) reject ssector<0 || ssector>INT_MAX-100 before loop; (2) reject volume_space_size+ssector overflow before line 427. Supersedes finding proposal (adds VSS guard). Full git-apply-able diff in findings/poc/DF-0852/fix.diff.

Verdict

REPRODUCED. iso_args.ssector (signed int) copied unvalidated; line 427 volume_space_size += ssector wraps INT_MIN for VSS=0x7FFFFFF0 + ssector=16. statfs reports f_blocks=-2147483648. Root-only (cd9660 is SYSCAP_RESTRICTEDROOT).