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

afd_describe(): ksprintf of device-controlled u64 mediasize into char[16] β€” deterministic 1-byte NUL stack overflow at attach

Summary

Both capacity paths (READ_CAPACITY and READ_CAPACITY_16 with u64 wrap) can drive mediasize/1048576 to 14 digits - the render is 17 bytes into char[16], a fixed-value 1-byte NUL stack write corrupting an adjacent pointer local. Arithmetic proof in pack (smallest trigger 10^13 MiB). In-situ blocked by the same QEMU blocker. char[32]+ksnprintf fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4875 Β· 8 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
proof.c β€” 2.4 KB view raw
proof.txt β€” 896 B view raw
env.txt β€” 195 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 630 B view raw
verdict.json β€” 2.4 KB view raw

DF-4875 β€” afd_describe() 1-byte stack NUL overflow via device-controlled mediasize

sys/dev/disk/nata/atapi-fd.c:446-451:

char sizestring[16];
if (fdp->mediasize > 1048576 * 5)
    ksprintf(sizestring, "%juMB", fdp->mediasize / 1048576);

mediasize (u_int64_t) is fully device-controlled: * READ_CAPACITY path (atapi-fd.c:384): (u64)be32toh(capacity) * blocksize β†’ up to (2^32-1)^2 β‰ˆ 1.84e19; * READ_CAPACITY_16 path (atapi-fd.c:396): be64toh(capacity) * blocksize computed in u64 (wraps mod 2^64) β†’ any 64-bit value.

mediasize/1048576 can therefore have up to 14 decimal digits, and "%juMB" renders 14 + 2 + NUL = 17 bytes into char sizestring[16] β€” a 1-byte (NUL) stack buffer overflow at device attach time.

Proof

See proof.txt (generated by proof.c; pure arithmetic, host-side):

path1 READ_CAPACITY : cap=blocksize=0xFFFFFFFF -> '17592186036224MB' = 16 chars + NUL = 17 bytes -> OVERFLOW
path2 READ_CAP_16   : cap=2^64-1,blocksize=1   -> '17592186044415MB' = 16 chars + NUL = 17 bytes -> OVERFLOW

The lowest triggering mediasize is 10^13 MiB β‰ˆ 1.05e19 (e.g. a device answering READ_CAPACITY_16 with capacity β‰₯ 2.44e16 sectors @512B, or READ_CAPACITY with maxed capacity+blocksize fields).

Status: untested (in situ) β€” proof is arithmetic

The write is deterministic the moment a (crafted, buggy or wedged) ATAPI floppy reports an oversized capacity; no runtime is needed to establish the 17 > 16 fact. On-guest execution is blocked because the audit guest cannot host an ATAPI direct-access device (QEMU 7.2: only ide-hd/ide-cd; see env.txt and the DF-4874 pack for the recorded blocker).

Impact ceiling

One NUL byte one past a 16-byte stack local in afd_describe(), at attach. Depending on compiler frame layout it can land on an adjacent local pointer (ch/atadev/fdp) and turn later device_printf derefs into a faulted pointer β€” a device-triggered kernel memory-corruption primitive of very limited range (fixed value 0x00, fixed position). Crafted-hardware class.

Fix

fix.diff (git-apply-able, checked): enlarge to char sizestring[32] and switch to ksnprintf() with the buffer size so no device input can ever overflow it.

VERDICT.md
↓ download raw

DF-4875 VERDICT β€” afd_describe() 1-byte stack NUL overflow

Status: untested in situ (no ATAPI direct-access hardware on guest); mathematically certain

Root cause (path:line)

  • afd_describe() declares char sizestring[16] (sys/dev/disk/nata/atapi-fd.c:446) and formats the device-reported media size with an unbounded ksprintf(sizestring, "%juMB", fdp->mediasize / 1048576) (atapi-fd.c:448-449) β€” reached whenever mediasize > 5 MiB.
  • fdp->mediasize is attacker(device)-controlled 64-bit:
  • READ_CAPACITY: fdp->mediasize = (u_int64_t)be32toh(capacity.capacity) * fdp->sectorsize (atapi-fd.c:384) β€” product up to (2^32-1)^2 β‰ˆ 1.84e19;
  • READ_CAPACITY_16: fdp->mediasize = be64toh(capacity_big.capacity) * fdp->sectorsize (atapi-fd.c:396) β€” u64 multiply, wraps to any value.
  • Max rendered length: (2^64-1)>>20 = 17592186044415 β†’ 14 digits β†’ "17592186044415MB" + NUL = 17 bytes into a 16-byte stack array.

Why certain without a run

The proof is pure arithmetic on the exact types involved (proof.txt, regenerable via proof.c): both capacity paths demonstrably produce mediasize β‰₯ 1.049e19 from well-formed device responses, and ksprintf/vsnprintf semantics write exactly strlen+1 bytes. There is no code path that clamps mediasize before afd_describe() is called at attach (atapi-fd.c:130). What a run could only add is the identity of the stack slot clobbered by the 17th byte β€” that depends on compiler frame layout, not on the bug.

In-situ blocker (documented, not a refutation)

afd_describe() runs only from afd_attach() (atapi-fd.c:130), which runs only for ATAPI direct-access devices (atapi-fd.c:90-91). QEMU 7.2 provides no ATAPI direct-access emulation (-device help lists only ide-hd, ide-cd), so no afd device can attach on the audit guest. Recorded in the DF-4874 pack (run.log: "afd0 device_t is NULL").

Impact

Single NUL byte written one past a 16-byte stack local at driver attach β€” a real kernel stack memory-corruption bug (CWE-787) with a hostile-device precondition and very constrained primitive (value 0x00, fixed offset, attach-time only). Worst realistic outcome: corrupted adjacent local pointer (ch/atadev/fdp at atapi-fd.c:443-445) β†’ faulted deref inside device_printf (atapi-fd.c:455-458) β†’ kernel panic; escalation to controlled exploitation is not plausible with a fixed 0x00 byte.

Fix validation

fix_status: not_testable β€” the trigger device cannot exist on the audit guest (see above); the fix (sizestring[32] + ksnprintf with size, fix.diff) removes the overflow by construction (bounded formatting API, buffer larger than the 17-byte worst case).

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 for an A/B run; fix removes the overflow by construction (bounded formatter, 32-byte buffer > 17-byte worst case)

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

Confirmed kernel references

Detail

Exploit chain

crafted/buggy ATAPI floppy answers READ_CAPACITY with capacity=blocksize=0xFFFFFFFF (or READ_CAPACITY_16 with huge capacity) -> mediasize >= 1.049e19 -> afd_attach -> afd_describe ksprintf writes 17 bytes into char[16] -> NUL lands on adjacent stack local (ch/atadev/fdp at atapi-fd.c:443-445) -> faulted deref in device_printf (attach-time panic); primitive limited to a fixed 0x00 byte

Evidence (decisive lines)

proof.txt (exact 17>16 for both paths, smallest trigger value); VERDICT.md; DF-4874/run.log for the shared hardware blocker

PoC changes

proof.c generated fresh; no seed existed

Verified recommended fix

char sizestring[32] + ksnprintf(sizestring, sizeof(sizestring), ...) for all three branches

Verdict

afd_describe() ksprintf's a device-controlled u64 media size into char sizestring[16]; mediasize from READ_CAPACITY (up to (2^32-1)^2) or READ_CAPACITY_16 (arbitrary u64, wraps) renders up to '17592186044415MB' = 17 bytes incl NUL β€” a deterministic 1-byte NUL stack overflow at attach. Proven by exact arithmetic (proof.txt) on both capacity paths; in-situ run blocked only by the absence of ATAPI direct-access hardware under QEMU 7.2.