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)
PoC verification
Evidence pack
findings/poc/DF-3124 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 forim_l2d.- On failure both handles stay NULL (isomp is
M_ZERO-allocated, line 412) whileim_flagskeepsISOFSMNT_KICONV(masked in at line 486-489). - Consumers pass the raw handle to the KOBJ dispatcher:
sys/vfs/isofs/cd9660/cd9660_util.csgetrune()βcd9660_iconv->convchr(handle, ...)guarded only byflags & 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.cisochar()β same pattern, reached on Joliet mounts (readdir viaisofntrans).iconv_convchr()(sys/libiconv/iconv.c:290-294) dispatches through the handle as a kobj βmovq (%rdi),%rsiat address 0 β page fault β panic.cd9660_unmount()(cd9660_vfsops.c:584-589) NULL-checks both handles beforeclose()β 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
- 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().
- Guest:
vnconfig -c -S 2048 /dev/vn0,kldload libiconv,kldload cd9660_iconv, compiletrigger.c(cc -O -I/usr/src/sys). - Baseline:
mount_cd9660 /dev/vn0 /mnt/poc(no kiconv) β mounts,stat /mnt/poc/aβ ENOENT, clean unmount. Image is valid. - Trigger:
/root/triggerβ mount(2) withISOFSMNT_KICONVand zeroedcs_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 - 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 withvfs.usermount=1). - After such a mount exists, ANY local user triggers the panic with a
plain
stat()/lsunder 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_testablefix.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
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.
No comments yet.