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

Unvalidated logical block size lb_size enables divide-by-zero and UB shifts

Summary

udf_vfsops.c:306 bsize=lb_size from disk. :307 bmask=bsize-1. :308 bshift=ffs(bsize)-1. lb_size=0 => bshift=-1 UB shift bmask=-1. :667 p_sectors=packet_len/bsize divide-by-zero panic. Non-power-of-2 corrupts all block math masking. Mount-time DoS. Fix: validate power-of-2 range [DEV_BSIZE,65536].

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0883 Β· 14 files
FileTypeDescriptionSize
craft_evil_udf.py trigger-source Python UDF image crafter: lb_size=0, packet_len=2048 5.5 KB view raw
evil.udf trigger-binary Crafted UDF image (614400 bytes, lb_size=0) 600.0 KB ↓ download
build.sh build-script Runs craft_evil_udf.py or uses pre-built image 684 B view raw
run.sh run-script vnconfig + mount -t udf commands 811 B view raw
fix.diff suggested-fix Validate lb_size as power-of-2 in [DEV_BSIZE,MAXBSIZE] at udf_vfsops.c:306 910 B view raw
VERDICT.md verdict Full analysis: mechanism, evidence, fix validation 4.3 KB ↓ raw
README.md readme Summary and reproduce instructions 1.8 KB ↓ raw
run.log run-log Unpatched kernel panic output (decisive) 1.2 KB view raw
fix_run.log fix-run-log Patched udf.ko clean output (EINVAL, no panic) 1.2 KB view raw
fix_build.log build-log UDF module rebuild log (rc=0) 5.6 MB ↓ download
panic.txt panic-signature Fatal trap 18 integer divide fault at udf_mount.part.2+0x814 208 B view raw
env.txt environment uname, cc version, udf.ko md5 282 B view 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 Summary and reproduce instructions
↓ download raw

DF-0883 β€” Unvalidated logical block size lb_size β†’ divide-by-zero panic

Summary

In udf_mountfs() (sys/vfs/udf/udf_vfsops.c:306), the Logical Volume Descriptor's lb_size field β€” a uint32_t read verbatim from the on-disk image β€” is assigned to udfmp->bsize with no validation. When lb_size=0, udf_find_partmaps() at line 667 divides packet_len / bsize β†’ integer divide fault (#DE) β†’ kernel panic at mount time.

A crafted UDF image with lb_size=0 and a Type 2 Sparable partition map triggers a deterministic single-shot panic.

Reproduce

# On the host (python3 available): craft the image
cd findings/poc/DF-0883
./build.sh                    # produces evil.udf (lb_size=0, packet_len=2048)

# On the DragonFlyBSD guest (root):
scp evil.udf run.sh dfbsd:/root/poc/
ssh dfbsd 'cd /root/poc && mkdir -p /mnt/udf && sh run.sh'

Expected behavior

  • Unpatched kernel/udf.ko (#0): mount panics β€” Fatal trap 18: integer divide fault ... Stopped at udf_mount.part.2+0x814: idivl
  • Fixed udf.ko: mount returns EINVAL cleanly (udf: invalid logical block size 0), guest stays up

Files

File Description
craft_evil_udf.py Python UDF image crafter (lb_size=0, packet_len=2048)
evil.udf Crafted image (614400 bytes)
build.sh Runs crafter (or uses pre-built image)
run.sh vnconfig + mount commands
fix.diff git-apply-able fix: validate lb_size as power-of-2 in [512,65536]
VERDICT.md Full analysis: mechanism, evidence, fix validation
run.log Unpatched kernel panic output
fix_run.log Patched module clean output
fix_build.log Module build log
panic.txt Panic signature from serial console
env.txt Guest environment
manifest.json Machine-readable catalog
VERDICT.md verdict Full analysis: mechanism, evidence, fix validation
↓ download raw

DF-0883 β€” Unvalidated logical block size lb_size enables divide-by-zero and UB shifts

Verdict

REPRODUCED β€” deterministic kernel divide-by-zero (#DE) panic at mount time. Fix VALIDATED: single-fix udf.ko module rejects lb_size=0 with EINVAL; the previously-fatal mount now returns cleanly with no panic.

Summary

In udf_mountfs() (sys/vfs/udf/udf_vfsops.c:306), the Logical Volume Descriptor's lb_size field β€” a uint32_t read verbatim from the on-disk image β€” is assigned directly to udfmp->bsize with no validation:

306:  udfmp->bsize = lvd->lb_size;              // from disk, UNVALIDATED
307:  udfmp->bmask = udfmp->bsize - 1;           // bsize=0 => bmask = 0xFFFFFFFF
308:  udfmp->bshift = ffs(udfmp->bsize) - 1;     // bsize=0 => ffs(0)=0, bshift = -1

Then in udf_find_partmaps() (line 667), bsize is used as a divisor:

667:  udfmp->p_sectors = pms->packet_len / udfmp->bsize;   // #DE divide-by-zero

A crafted UDF image with lb_size=0 and a Type 2 Sparable partition map (packet_len=2048) triggers a fatal integer-divide fault (#DE, trap 18) in kernel mode the moment the image is mounted.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger: root (or vfs.usermount=1 unprivileged user) mounts a crafted UDF image via mount -t udf -o rdonly /dev/vn0 /mnt.
  2. Anchor read: udf_mountfs reads the Anchor VDP at sector 256 (udf_vfsops.c:280-284), which points to the VDS at sector 0.
  3. LVD parse: the VDS loop (:298-327) finds the Logical Volume Descriptor at sector 0 and enters the TAGID_LOGVOL branch (:305).
  4. Unvalidated assignment: udfmp->bsize = lvd->lb_size = 0 (:306). bmask becomes 0xFFFFFFFF, bshift becomes -1 (undefined behavior).
  5. Divide-by-zero: udf_find_partmaps is called (:311). For the Type 2 Sparable partition map, line 667 executes 2048 / 0 β†’ integer divide fault (trap 18).
  6. Effect: kernel panic, guest enters DDB, system halted. Deterministic, single-shot, no race conditions.

Escalation

This is a pure Denial of Service (divide-by-zero panic). There is no memory corruption β€” the #DE trap fires before any write occurs. No escalation to uid=0 is possible or relevant. The finding's impact ceiling is local DoS via crafted filesystem image mount.

Evidence

Unpatched kernel #0 (panic)

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 2; lapic id = 2
instruction pointer        = 0x8:0xffffffff82600fc4
stack pointer               = 0x10:0xfffff80118295340
current process             = 1019
kernel: type 18 trap, code=0

Stopped at      udf_mount.part.2+0x814: idivl   0x200(%r14),%eax
db>

Patched udf.ko (fix applied, no panic)

[+] attempting UDF mount (lb_size=0, packet_len=2048)...
mount_udf: /dev/vn0: Invalid argument
[!] mount returned 1
[+] guest still alive:
10:59PM  up 2 mins, 0 users, load averages: 0.11, 0.06, 0.02

dmesg: udf: invalid logical block size 0

PoC changes

  • Authored craft_evil_udf.py: Python3 UDF image crafter that builds a minimal image with lb_size=0 in the LVD and a Type 2 Sparable partition map with packet_len=2048 to drive the division at udf_vfsops.c:667.
  • Fixed a field-order bug in the Anchor VDP's extent_ad (len before loc, matching struct extent_ad { uint32_t len; uint32_t loc; }).
  • build.sh / run.sh: standard vnconfig + mount harness.

Fix

fix.diff adds a validation check immediately after udfmp->bsize = lvd->lb_size (line 306): if lb_size is not a power of two in [DEV_BSIZE (512), MAXBSIZE (65536)], print a diagnostic and return EINVAL. This closes both the divide-by-zero (lb_size=0) and the undefined-shift / mask-corruption (non-power-of-2) variants.

Supersedes the finding proposal, which recommended the same range but did not specify the power-of-2 bitmask check (lb_size & (lb_size - 1)) that additionally rejects non-power-of-2 values which would corrupt bmask/bshift arithmetic throughout the UDF mount/read paths.

Note: UDF is a loadable kernel module (kldload udf). The fix lives in /boot/kernel/udf.ko, not the kernel binary. The before/after comparison uses the same #0 kernel with the original vs fixed udf.ko module β€” the only variable is the one-file source change in udf_vfsops.c.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: evil.udf (lb_size=0) on the unpatched #0 kernel with original udf.ko panics with Fatal trap 18 integer divide fault at udf_mount.part.2+0x814 (idivl); on the SAME #0 kernel with the single-fix udf.ko (fix.diff applied, only udf_vfsops.c changed), mount returns EINVAL cleanly with dmesg 'udf: invalid logical block size 0' -- no panic, guest stays up. Run twice, deterministic. Fix closes the bug. Note: UDF is a kld module, so the fix lives in udf.ko not the kernel binary; before/after comparison isolates the one-file source change as the only variable.

BEFORE (unpatched udf.ko): Fatal trap 18: integer divide fault while in kernel mode | Stopped at udf_mount.part.2+0x814: idivl 0x200(%r14),%eax | db> (guest dead). AFTER (fixed udf.ko): mount_udf: /dev/vn0: Invalid argument | dmesg: udf: invalid logical block size 0 | guest alive (10:59PM up 2 mins).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same kernel binary; UDF is a loadable module -- fix applied to rebuilt /boot/kernel/udf.ko, strings confirm 'udf: invalid logical block size %u')

Confirmed kernel references

Detail

Exploit chain

none -- pure divide-by-zero (#DE) panic; no memory corruption, no write primitive, no escalation path. Impact ceiling is local DoS (kernel panic) via crafted UDF image mount.

Evidence (decisive lines)

Unpatched #0 udf.ko: mount -t udf evil.udf -> Fatal trap 18: integer divide fault while in kernel mode | cpuid=2 | ip=0x8:0xffffffff82600fc4 | current process=1019 | Stopped at udf_mount.part.2+0x814: idivl 0x200(%r14),%eax | db>. Patched udf.ko: mount_udf: /dev/vn0: Invalid argument | dmesg: udf: invalid logical block size 0 | guest stays up (uptime confirms).

PoC changes

Authored craft_evil_udf.py (Python3 UDF image crafter producing lb_size=0 + Type 2 Sparable partition map with packet_len=2048); build.sh (runs crafter or uses pre-built evil.udf since guest lacks python3); run.sh (vnconfig + mount harness). Fixed an initial field-order bug in the Anchor VDP extent_ad (len before loc, matching struct extent_ad layout) that caused the VDS scan to miss the LVD. All files in findings/poc/DF-0883/.

Verified recommended fix

In udf_vfsops.c immediately after line 306 (udfmp->bsize = lvd->lb_size), validate lb_size is a power of two in [DEV_BSIZE (512), MAXBSIZE (65536)]: reject with EINVAL if lb_size < DEV_BSIZE || lb_size > MAXBSIZE || (lb_size & (lb_size-1)) != 0. This closes both the divide-by-zero (lb_size=0) and the UB shift / mask-corruption (non-power-of-2) variants. Supersedes the finding proposal which recommended the same range but omitted the power-of-2 bitmask check that also rejects non-power-of-2 values corrupting bmask/bshift arithmetic throughout the UDF mount/read paths. Full git-apply-able diff in findings/poc/DF-0883/fix.diff.

Verdict

REPRODUCED. The bug is real: udf_vfsops.c:306 assigns lvd->lb_size (uint32 from the on-disk Logical Volume Descriptor) directly to udfmp->bsize with no validation; when lb_size=0, udf_find_partmaps() at line 667 computes packet_len/bsize = 2048/0, firing a fatal integer-divide fault (#DE trap 18). Confirmed on the unpatched #0 kernel+udf.ko with a crafted UDF image (evil.udf): Fatal trap 18 at udf_mount.part.2+0x814 (idivl 0x200(%r14),%eax). This is a deterministic single-shot local DoS via crafted filesystem image mount -- no memory corruption, no escalation possible.