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

afd_strategy(): no mediasize bounds check and 32-bit LBA truncation β€” out-of-range raw I/O wraps onto low LBAs instead of failing

Summary

bio_offset narrows to u32 and rides 32 CDB LBA bits with no clamp on the raw path (unlike ad_strategy which keeps u64) - I/O beyond 2^32 sectors that must EINVAL silently writes to (offset/sectorsize) mod 2^32, clobbering the start of the medium. Data-integrity class (the DF-4798 'size lie' family). Reject-beyond-mediasize fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4877 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
env.txt β€” 195 B view raw
fix.diff β€” 827 B view raw
manifest.json β€” 566 B view raw
verdict.json β€” 2.1 KB view raw

DF-4877 β€” afd_strategy(): no media-bounds check + 32-bit LBA truncation

sys/dev/disk/nata/atapi-fd.c:258, 277-280:

u_int32_t lba;
...
lba = bp->bio_offset / fdp->sectorsize;      /* u64 offset -> u32 lba */
...
ccb[2] = lba >> 24;  ccb[3] = lba >> 16;     /* 32-bit LBA in the CDB */
ccb[4] = lba >> 8;   ccb[5] = lba;
  • No bounds check: afd_strategy() never compares bio_offset (or the transfer end) against fdp->mediasize. The raw-cdev path does not do it either: bounds_check_with_mediasize() (sys/kern/subr_disk.c:1458) is only used by device-mapper; a raw read/write on /dev/afd0 goes physio β†’ afd_strategy with whatever offset the caller passes.
  • 32-bit truncation: unlike ad_strategy() which keeps the LBA in a u_int64 (request->u.ata.lba, ata-disk.c), the afd driver narrows the block number to u_int32_t, and ATAPI_READ_BIG/WRITE_BIG carry only 32 LBA bits.

Net effect: any I/O at bio_offset β‰₯ 2^32 * sectorsize (2 TiB at 512B) β€” which must fail as out-of-range β€” silently wraps onto low LBAs. A write the caller intended for beyond-the-end (expecting EINVAL) instead lands at (offset/sectorsize) mod 2^32 β€” e.g. LBA 0..N, clobbering the start of the medium. Floppy-class media can never legitimately reach 2 TiB, so on an honest device every wrapped access is out-of-bounds by construction.

Threat model

The raw device already grants full-media read/write to its opener (typically root/operator), so this is not a privilege boundary crossing by itself; the security-relevant delta is silent misplacement of I/O that the kernel contractually must reject (correctness β†’ data-integrity), matching the "geometry lie" class of DF-4798.

Status: untested (in situ) β€” requires an afd device

The audit guest has no ATAPI direct-access device (see DF-4874 pack, run.log), so the wrap cannot be demonstrated on-target. The trace is complete in source: no clamp before atapi-fd.c:258, and the CDB fields hold at most 32 bits (atapi-fd.c:277-280).

Fix

fix.diff: reject any bio that starts at/after mediasize or whose end passes it (EINVAL) before building the CDB β€” mirroring bounds_check_with_mediasize() semantics for the raw path.

VERDICT.md
↓ download raw

DF-4877 VERDICT β€” afd_strategy missing bounds check + u32 LBA wrap

Status: untested in situ (hardware absent); established by line trace

Root cause

  1. lba = bp->bio_offset / fdp->sectorsize; assigns a 64-bit byte offset (in sectors) into u_int32_t lba (sys/dev/disk/nata/atapi-fd.c:241, 258) β€” silent truncation.
  2. Nothing between physio and the CDB build bounds-checks the I/O: - afd_strategy() has no mediasize comparison (whole function, atapi-fd.c:233-316); - the raw cdev path applies no disk-layer clamp β€” bounds_check_with_mediasize() exists (sys/kern/subr_disk.c:1458-1478) but its only caller is device-mapper (sys/dev/disk/dm/device-mapper.c:400); - contrast with ad_strategy() which at least keeps the LBA 64-bit (sys/dev/disk/nata/ata-disk.c:299).
  3. The CDB carries 32 LBA bits (atapi-fd.c:277-280), so the wrap is not even detectable downstream.

Consequence

I/O that must be rejected as out-of-range instead targets (bio_offset/sectorsize) mod 2^32: silent wrong-block writes on the raw device. Not a privilege escalation (the raw device already grants the medium to its opener) β€” a kernel contract violation in the same "geometry/size lie" family as DF-4798.

Why untested

Requires an attached ATAPI direct-access device; the audit guest cannot emulate one (QEMU 7.2: no ATAPI floppy class β€” DF-4874/run.log records "afd0 device_t is NULL" from the loaded driver). Every element of the trace was verified in source instead.

Fix validation

fix_status: not_testable (same hardware blocker). fix.diff adds the explicit media-bounds rejection before the CDB build; semantics match bounds_check_with_mediasize() (reject past-EOF with EINVAL).

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 no→ patch + rebuild →patched clean

No afd hardware on guest; fix adds the missing media-bounds rejection with bounds_check_with_mediasize()-equivalent semantics

fix.diff (git apply --check OK)
↓ fix.diffper-fix-DF-4877

Confirmed kernel references

Detail

Exploit chain

raw open /dev/afd0 -> pwrite at offset >= 2 TiB (legitimately out-of-range) -> lba = (offset/sectorsize) mod 2^32 (atapi-fd.c:258) -> ATAPI_WRITE_BIG CDB with wrapped LBA (atapi-fd.c:277-280) -> silent write to low blocks instead of EINVAL

Evidence (decisive lines)

VERDICT.md (trace: no clamp in strategy; dm-only bounds helper; ad keeps u64 lba); README.md

PoC changes

n/a β€” source-trace finding; hardware blocker shared with DF-4874

Verified recommended fix

reject bio_offset/end beyond mediasize with EINVAL before building the CDB in afd_strategy()

Verdict

afd_strategy() never bounds-checks bio_offset against mediasize (and the raw cdev path applies no clamp β€” bounds_check_with_mediasize() is dm-only), while narrowing the block number to u_int32_t lba and a 32-bit CDB LBA: any I/O at >= 2^32 sectors (2 TiB @512B) that must fail with EINVAL silently wraps onto low LBAs, so beyond-end writes clobber the start of the medium. Certain by construction; not runnable on a guest without ATAPI direct-access hardware.