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)
PoC verification
Evidence pack
findings/poc/DF-0852 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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():
:326for (iso_blknum = 16 + argp->ssector;β wraps whenssector > INT_MAX - 16:327iso_blknum < 100 + argp->ssector;β wraps whenssector > INT_MAX - 100:427isomp->volume_space_size += argp->ssector;β wraps whenvolume_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)
- User issues
mount("cd9660", mnt, &args, MNT_RDONLY)withargs.ssectorcontrolled.:197copyin β:246iso_mountfs(devvp, mp, &args). :326-327the volume-descriptor loop iteratesiso_blknumover[16+ssector, 100+ssector). With a modestssectorand a crafted ISO whose PVD sits at sector16+ssector, the loop finds the PVD andpriis set.:414-417isomp->volume_space_size = isonum_733(pri->volume_space_size)reads the image-controlled value (e.g.0x7FFFFFF0).:427isomp->volume_space_size += argp->ssectoroverflows:0x7FFFFFF0 + 16 = 0x80000000 = INT_MIN. The mount succeeds (:542 return 0).:633cd9660_statfs:sbp->f_blocks = isomp->volume_space_sizepublishesINT_MIN(sign-extended tolongβ-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.cget_fscap()returnsSYSCAP_RESTRICTEDROOTforcd9660(only null/devfs/procfs/tmpfs/fuse are user-mountable).sys_mountgates non-root callers withcaps_priv_check_td(td, priv | __SYSCAP_NOROOTTEST)even withvfs.usermount=1. Verified empirically:maxx(uid 1001) getsEPERMmounting cd9660 even withvfs.usermount=1+ a group-readablevn0+ a self-owned mountpoint. Only root (or RESTRICTEDROOT) reachesssector. Rootβkernel is game-over; there is no privilege boundary to cross. - Non-write primitive.
volume_space_sizeis anintsize/bound field, not a pointer/refcount/function-pointer. The corruption poisonsstatfsand weakens/strengthens the:775 lbn >= volume_space_sizecheck, but yields no code-exec primitive. There is no chain touid=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(unpatchedwith-src): overflow case βmount rc=0,f_blocks = -2147483648. Reproduced. - Single-fix kernel
#1(builtmake nativekernel, installed viamake 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.txtfor the before/after contrast andfix_run.logfor the full patched-kernel run.
Fix verification
fixedVALIDATED: baseline f_blocks=-2147483648 (BOGUS); patched EINVAL; regression f_blocks=100 (normal).
BEFORE: f_blocks=-2147483648. AFTER: EINVAL. REGRESSION: f_blocks=100.
Confirmed kernel references
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:197
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:326
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:327
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:427
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:633
- sys/vfs/isofs/cd9660/cd9660_vfsops.c:775
- sys/vfs/isofs/cd9660/cd9660_mount.h:54
- sys/kern/vfs_syscalls.c:152
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).
No comments yet.