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

iso_mountfs ignores iconv_open() failure: NULL im_d2l/im_l2d handles panic the kernel on the first lookup under a kiconv mount (convchr(NULL) NULL-deref)

Summary

Both cd9660_iconv->open() return values are discarded (:494-495). When the charset pair cannot be opened (empty/unregistered names - iconv_open returns ENOENT), im_d2l/im_l2d remain NULL while im_flags keeps ISOFSMNT_KICONV. The first namei under the mount goes cd9660_lookup.c:237 isofncmp(..., imp->im_l2d) -> sgetrune() -> cd9660_iconv->convchr(NULL) -> iconv_convchr (libiconv/iconv.c:290) dispatches KOBJ methods through the NULL handle -> page fault at VA 0 -> panic. isochar() has the same hole on Joliet mounts. cd9660_unmount() already NULL-checks both handles, proving the NULL state is anticipated. VERIFIED guest: deterministic 'Fatal trap 12, fault virtual address = 0x0, Stopped at iconv_convchr+0x27: movq (%rdi),%rsi' twice from fresh boots; baseline mount without kiconv clean. Mounting requires root/SYSCAP_RESTRICTEDROOT, but after an admin makes a kiconv mount whose charset pair fails (mount_cd9660 -o kiconv does exactly this - sets the flag without registering charsets), ANY local user panics the box with a plain stat()/ls. NULL-deref DoS only. Fix validated (pack fix.diff): check opens, close half-opened, NULL both, clear ISOFSMNT_KICONV so consumers take the existing non-iconv fallback.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3124 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
trigger.c β€” 1.8 KB view raw
poc.iso β€” 80.0 KB ↓ download
build.sh β€” 130 B view raw
run.sh β€” 94 B view raw
build.log β€” 172 B view raw
run.log β€” 1.1 KB view raw
run.2.log β€” 523 B view raw
panic.txt β€” 1.3 KB view raw
env.txt β€” 386 B view raw
fix.diff β€” 1.7 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 3.4 KB view raw

DF-3124 β€” cd9660 kiconv mount: ignored iconv_open() failure β†’ convchr(NULL) kernel panic

What

iso_mountfs() (sys/vfs/isofs/cd9660/cd9660_vfsops.c:491-499) ignores the return value of both cd9660_iconv->open() calls. When the charset pair cannot be opened (empty, unregistered, or unavailable charset names β€” iconv_open() returns ENOENT), isomp->im_d2l / isomp->im_l2d stay NULL while im_flags still carries ISOFSMNT_KICONV. The first pathname lookup or readdir on the mount then does:

namei -> cd9660_lookup -> isofncmp(..., imp->im_l2d) -> sgetrune()
      -> cd9660_iconv->convchr(NULL, ...)          (cd9660_util.c sgetrune)
      -> iconv_convchr(NULL) -> KOBJ dispatch reads ops through NULL
      -> kernel page fault at VA 0 -> panic

cd9660_unmount() (cd9660_vfsops.c:584-589) already NULL-checks both handles before close β€” the NULL state is clearly anticipated; the consumers were simply never made safe against it.

Reproduce (on the QEMU guest, as root)

# 1. stage files (from the pack): poc.iso (minimal crafted ISO9660 image),
#    trigger.c
scp poc.iso trigger.c dfbsd:/root/

# 2. in the guest:
vnconfig -c -S 2048 /dev/vn0 /root/poc.iso
kldload libiconv
kldload cd9660_iconv
cc -O -o /root/trigger /root/trigger.c -I/usr/src/sys
mkdir -p /mnt/poc
/root/trigger          # mounts with ISOFSMNT_KICONV + empty charsets, then stat()s

Expected

[+] mount succeeded with ISOFSMNT_KICONV and failed iconv_open (im_d2l == im_l2d == NULL)
[+] triggering namei under the mount ...

…then the console shows:

Fatal user address access from kernel mode from trigger at ...
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x0
Stopped at      iconv_convchr+0x27:     movq    (%rdi),%rsi

Baseline

The same image mounted without -o kiconv (mount_cd9660 /dev/vn0 /mnt/poc) mounts cleanly; lookups return ENOENT; unmount works. The panic is specific to the KICONV flag with a failed charset open.

Impact

NULL-deref kernel panic (local DoS). The deref target is fixed address 0 β€” no control over the faulting pointer, no escalation primitive. Mounting requires root / SYSCAP_RESTRICTEDROOT (get_fscap("cd9660")), but the panic trigger is any namei under the mount, i.e. any local user after an admin has made such a mount.

VERDICT.md
↓ download raw

DF-3124 β€” VERDICT

Status: reproduced (impact: panic β€” NULL-deref local DoS; confidence: certain) Reproduced twice, each time from a fresh vm.sh reset with-src boot: deterministic.

Root cause (line-accurate)

  • sys/vfs/isofs/cd9660/cd9660_vfsops.c:494 β€” cd9660_iconv->open(cs_local, cs_disk, &isomp->im_d2l); β€” return value ignored.
  • sys/vfs/isofs/cd9660/cd9660_vfsops.c:495 β€” same for im_l2d.
  • On failure both handles stay NULL (isomp is M_ZERO-allocated, line 412) while im_flags keeps ISOFSMNT_KICONV (masked in at line 486-489).
  • Consumers pass the raw handle to the KOBJ dispatcher:
  • sys/vfs/isofs/cd9660/cd9660_util.c sgetrune() β€” cd9660_iconv->convchr(handle, ...) guarded only by flags & ISOFSMNT_KICONV && cd9660_iconv (module pointer), NOT by handle != NULL. Reached from every non-'.'/non-'..' name comparison: cd9660_lookup.c:237-238 (isofncmp(..., imp->im_d2l, imp->im_l2d)).
  • cd9660_util.c isochar() β€” same pattern, reached on Joliet mounts (readdir via isofntrans).
  • iconv_convchr() (sys/libiconv/iconv.c:290-294) dispatches through the handle as a kobj β†’ movq (%rdi),%rsi at address 0 β†’ page fault β†’ panic.
  • cd9660_unmount() (cd9660_vfsops.c:584-589) NULL-checks both handles before close() β€” proving NULL handles are an anticipated state that the consume paths don't defend against.

Why iconv_open fails in practice

mount_cd9660 -o kiconv (sbin/mount_cd9660) sets the flag without ever registering a charset pair (set_charset() is only called for -C), leaving cs_local/cs_disk as empty strings; iconv_open("","") finds no registered cspair (and no converter modules) β†’ ENOENT, silently ignored. Any direct mount(2) caller (this PoC) hits the same path. A successful -C registration that later becomes unavailable (module unload β€” see DF-2233) reaches the same NULL-handle state at lookup time.

Reproduction narrative

  1. Host: crafted a 40-sector minimal ISO9660 image (Python; PVD at sector 16 with root directory record β†’ extent 18, terminator at 17, "."/".." records at 18). No Joliet SVD needed β€” the panic goes through sgetrune().
  2. Guest: vnconfig -c -S 2048 /dev/vn0, kldload libiconv, kldload cd9660_iconv, compile trigger.c (cc -O -I/usr/src/sys).
  3. Baseline: mount_cd9660 /dev/vn0 /mnt/poc (no kiconv) β€” mounts, stat /mnt/poc/a β†’ ENOENT, clean unmount. Image is valid.
  4. Trigger: /root/trigger β€” mount(2) with ISOFSMNT_KICONV and zeroed cs_local/cs_disk; mount succeeds (open failures ignored); stat("/mnt/poc/a") β†’ guest panics instantly: Fatal user address access from kernel mode from trigger at ffffffff82600bb7 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x8:0xffffffff82600bb7 (inside libiconv.ko) current process = 914 (trigger) Stopped at iconv_convchr+0x27: movq (%rdi),%rsi
  5. Re-ran from a fresh reset: identical panic (pid 869). Deterministic.

Exploit chain

None. The faulting address is a fixed NULL (the KOBJ ops load); there is no attacker influence over the dereferenced pointer, no write primitive. This is a denial-of-service bug, not an escalation. (Checked: handle is used purely as *(kobj_t) for method dispatch β€” no partial control path.)

Reachability / gating

  • Mounting cd9660 requires root-equivalent privilege (get_fscap("cd9660") == SYSCAP_RESTRICTEDROOT, sys/kern/vfs_syscalls.c:5383-5398 β€” even with vfs.usermount=1).
  • After such a mount exists, ANY local user triggers the panic with a plain stat()/ls under the mountpoint.
  • Severity: Low (privileged mount-time opt-in with a broken charset; unprivileged post-mount DoS).

Fix

fix.diff β€” check both open() returns; on any failure close the half-opened handle, NULL both, and clear ISOFSMNT_KICONV so isochar()/sgetrune() take their non-iconv fallback (identical discipline to the existing cd9660_iconv == NULL path). Also forces NUL-termination of the copied charset names (adjacent hardening, cf. DF-0854 in msdosfs).

Fix validation: not_testable by choice β€” this is a Low-severity NULL-deref DoS; per the audit's conditional-verification policy the kernel-rebuild fix-validation cycle is reserved for Critical/High and memcorrupt/privesc findings. The fix is a guarded fallback that mirrors logic already present in cd9660_unmount().

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored post-verification (guarded fallback mirroring cd9660_unmount's NULL discipline + charset NUL-termination). Kernel-rebuild fix-validation skipped by policy: Low-severity NULL-deref DoS, not a Critical/High or memcorrupt/privesc finding; the conditional-verify scope reserves the rebuild cycle for those.

fix.diff in this pack
↓ fix.diffper-fix-DF-3124

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: '[+] mount succeeded with ISOFSMNT_KICONV and failed iconv_open (im_d2l == im_l2d == NULL)' followed by ssh death and vm.sh status=down", "panic.txt: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x0 / Stopped at iconv_convchr+0x27: movq (%rdi),%rsi' (both runs)", 'run.log baseline section: same image mounts cleanly and stat returns ENOENT without kiconv', 'VERDICT.md: line-accurate root-cause trace vfsops.c:494-495 -> cd9660_util.c sgetrune -> iconv.c:290']

PoC changes

No seed pack existed; authored from scratch: hand-crafted 40-sector minimal ISO9660 image (PVD s16, terminator s17, root dir extent s18) plus a direct mount(2) caller using ISOFSMNT_KICONV with empty cs_local/cs_disk (the same state mount_cd9660 -o kiconv produces, since set_charset() only runs for -C).

Verified recommended fix

Check both cd9660_iconv->open() returns; on failure close the half-opened handle, NULL both handles and clear ISOFSMNT_KICONV so isochar()/sgetrune() take their non-iconv fallback; force NUL-termination of cs_local/cs_disk (see fix.diff).

Verdict

Reproduced deterministically twice from fresh boots: mounting a cd9660 filesystem with ISOFSMNT_KICONV while the charset pair fails to open (iconv_open returns ENOENT, return value ignored at cd9660_vfsops.c:494-495) leaves im_d2l/im_l2d NULL; the first namei under the mount reaches sgetrune()/isochar() -> iconv_convchr(NULL) -> KOBJ ops load from address 0 -> 'Fatal trap 12, fault virtual address = 0x0, Stopped at iconv_convchr+0x27: movq (%rdi),%rsi'. Baseline mount without kiconv on the same image is clean. NULL-deref DoS only: the faulting pointer is fixed NULL, no write primitive, no escalation.