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)
PoC verification
Evidence pack
findings/poc/DF-0883 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
EINVALcleanly (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 |
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)
- Trigger: root (or
vfs.usermount=1unprivileged user) mounts a crafted UDF image viamount -t udf -o rdonly /dev/vn0 /mnt. - Anchor read:
udf_mountfsreads the Anchor VDP at sector 256 (udf_vfsops.c:280-284), which points to the VDS at sector 0. - LVD parse: the VDS loop (
:298-327) finds the Logical Volume Descriptor at sector 0 and enters theTAGID_LOGVOLbranch (:305). - Unvalidated assignment:
udfmp->bsize = lvd->lb_size = 0(:306).bmaskbecomes0xFFFFFFFF,bshiftbecomes-1(undefined behavior). - Divide-by-zero:
udf_find_partmapsis called (:311). For the Type 2 Sparable partition map, line 667 executes2048 / 0β integer divide fault (trap 18). - 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 withlb_size=0in the LVD and a Type 2 Sparable partition map withpacket_len=2048to drive the division atudf_vfsops.c:667. - Fixed a field-order bug in the Anchor VDP's
extent_ad(len before loc, matchingstruct 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
fixedVALIDATED 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).
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.
No comments yet.