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

TAG_OVERLOAD consumed without length validation β€” zero-length overload option at the end of an options region reads one byte past the region and adopts an arbitrary overload value

Field Value
ID DF-3046
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N
CWE CWE-125 / CWE-20
File sys/vfs/nfs/bootp_subr.c
Lines 1283, 1299-1300, 1323-1332
Area vfs/nfs (netboot)
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

In bootpc_tag_helper the overload option's value is taken as tctx->overload = *j with no check that its length byte is 1. Two consequences: (1) a TAG_OVERLOAD with len != 1 sets the overload flag from the first value byte regardless (RFC 2131 requires len==1); (2) with len == 0 positioned so its length byte is the final byte of the region, the guard admits it and *j reads one byte past the end of the parsed region (for the vend walk the first byte past struct bootp_packet's last member β€” still inside the parent kmalloc'd ifctx allocation; for the file/sname walks it lands on adjacent in-struct fields). The read value only steers which additional bounded areas get re-walked β€” no leak, no corruption, no crash demonstrated. Same netboot threat model as DF-0802. Fix: require len == 1 for TAG_OVERLOAD (row diff).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of bootp_subr.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3046 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
dfrig12_bootp.c β€” 31.8 KB view raw
build.sh β€” 199 B view raw
run.sh β€” 192 B view raw
run.log β€” 752 B view raw
run.fixed.log β€” 619 B view raw
fix_validation.log β€” 2.3 KB view raw
fix.diff β€” 287 B view raw
fix_combined.diff β€” 2.4 KB view raw
build.log β€” 5.7 MB ↓ download
build_fixed.log β€” 5.7 MB ↓ download
env.txt β€” 2.2 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 3.4 KB view raw

DF-3046 β€” TAG_OVERLOAD consumed without length validation (1-byte OOB read)

Verified in VERIFY mode on the QEMU guest with the verbatim-interposition kernel-compiled harness: crafted option regions fed directly to the verbatim bootpc_tag_helper / bootpc_tag with guard bytes planted exactly where the walk would over-read.

What was reproduced (baseline kernel)

  • T1a (core) β€” a TAG_OVERLOAD whose length byte (0) is the FINAL byte of the parsed region passes the guard at :1283 (j + *j + 1 > ej evaluates to ej > ej, false), len = *j++ advances j to exactly ej, and tctx->overload = *j (:1300) reads one byte past the region β€” the planted guard 0xa5 was adopted (overload=0xa5, badopt=0).
  • T1b β€” TAG_OVERLOAD with len=3: the first value byte is taken as the overload flag regardless (RFC 2131 requires len==1).
  • T1c β€” in-packet, replylen = sizeof(struct bootp_packet) = 1460 (the struct has 2 bytes of tail padding): the byte read is at bp + sizeof β€” past the struct (planted guard 0x03 adopted).
  • T1c2 β€” replylen = 1458: the read lands on the struct tail padding just past the vend[1222] member (planted 0xee adopted).
  • T1d/T1e β€” sname-walk variant: a len=0 TAG_OVERLOAD as the final two bytes of sname[64] reads bp->file[0], the adjacent in-struct field β€” attribution tracked across two planted values (0x5c, 0x77).
  • T1f β€” file-walk variant shows chained propagation: the file walk reads bp->vend[0] (the cookie byte 99), whose SNAME bit then triggers a sname walk that adopts bp->file[0].

Impact (as the Info finding states)

The read value only steers which bounded areas (file[128]/sname[64]) get re-walked. For both production contexts (gctx->reply / ifctx->reply) the byte past the region lies inside the same kmalloc allocation (struct tail padding or the adjacent replylen member / cookie byte); no crash, no leak to userspace, no corruption β€” verified on the INVARIANTS kernel. impact: none, defect mechanically confirmed.

Files

  • run.log β€” baseline test 1 console trace (all subtests).
  • run.fixed.log β€” fixed-kernel test 1 status (every planted byte rejected, badopt=1; only legitimate len==1 values survive).
  • fix_validation.log β€” subtest-by-subtest A/B.
  • fix.diff β€” one hunk: require len == 1 for TAG_OVERLOAD.
VERDICT.md
↓ download raw

DF-3046 β€” VERDICT

Status: reproduced (the unvalidated read demonstrably happens, with planted-byte attribution at every production shape), impact: none (bounded, in-allocation, no leak/corruption/crash β€” exactly what the Info finding claims), confidence: certain.

Line-accurate mechanics

bootpc_tag_helper (bootp_subr.c:1263-1304) walks options with the guard if (j >= ej || j + *j + 1 > ej) { badopt = 1; return; } (:1283-1286), consumes len = *j++ (:1287), and for code == TAG_OVERLOAD executes tctx->overload = *j (:1299-1300) with no check that len == 1.

  • len == 0, length byte is the final byte of the region: the guard admits it (j + *j + 1 == ej, not > ej), len = *j++ leaves j == ej, and *j reads one byte past the region end. Reproduced at every production shape:
  • bare region (guard byte 0xa5 adopted);
  • vend walk of a full-size reply (replylen == sizeof(struct bootp_packet) == 1460 β€” note the struct carries 2 bytes of tail padding): reads bp + sizeof β€” past the struct (0x03 adopted);
  • vend walk of a 1458-byte reply: reads the tail padding just past the vend[1222] member (0xee adopted);
  • sname walk (overload=SNAME from a valid option): reads bp->file[0], the adjacent in-struct field (0x5c / 0x77 tracked on repeat);
  • file walk: reads bp->vend[0] (the cookie byte, 99) β€” and because 99 has OVERLOAD_SNAME set, that OOB value then triggers a further sname walk which adopts bp->file[0] β€” a chained double-OOB adoption.
  • len != 1 (e.g. 3): the first value byte is taken as the overload flag regardless of length (0x02 adopted from {2,7,9}).

Impact analysis (why none)

  • The adopted value only selects which fixed-size, in-struct areas (file[128], sname[64]) are subsequently walked by the same bounded loop.
  • For the vend walk at replylen == sizeof, the byte past the region is the struct tail padding or, at exactly sizeof, the first byte past the struct β€” which in both production contexts (struct bootpc_globalcontext and struct bootpc_ifcontext, both kmalloc'd) is another member of the same allocation (replylen). Never out of the allocation; no page fault, no info leak to userspace, no corruption. Ran on the INVARIANTS kernel with zero side effects across repeated runs.
  • Not attacker-reachable beyond what a DHCP reply already controls: the value read is packet-adjacent bytes the attacker usually also authored.

Fix validation

One-hunk fix.diff (require len == 1 for TAG_OVERLOAD, else badopt=1 and abort the walk β€” with len==1 the existing :1283 guard already proves *j is in-region). Fixed-kernel run (run.fixed.log / fix_validation.log): every planted byte rejected (badopt=1, overload stays 0 or the legitimate len==1 value, constant across planted-byte changes); no behavior change for well-formed replies.

Residual note: the chained-propagation shape (T1f) disappears entirely because the malformed overload never sets the re-walk flags.

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
↓ fix.diffper-fix-DF-3046

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: T1a overload=0xa5 badopt=0 with the planted guard byte at ej; T1c overload=0x03 from bp+sizeof; T1c2 overload=0xee from past vend[1221]; T1d/T1e overload tracking planted bp->file[0] values 0x5c and 0x77; T1f chained adoption 0x77; run.fixed.log: all subtests rejected with badopt=1, overload constant at the legitimate len==1 value

PoC changes

no seed PoC existed; first in-packet crafting used sizeof(struct bootp_packet) unaware of its 2 tail padding bytes and targeted sname[0] from the file walk (file+128 is actually vend[0], sname precedes file in the struct) - corrected after observing the adopted values, and the corrected shapes now demonstrate the exact production geometry including the padding and the chained double over-read

Verified recommended fix

require len == 1 for TAG_OVERLOAD in bootpc_tag_helper and set badopt on any other length (validated in fix.diff)

Verdict

The unvalidated TAG_OVERLOAD read was reproduced mechanically at every production shape on the guest kernel, calling the verbatim bootpc_tag_helper and bootpc_tag with guard bytes planted exactly where the walk over-reads: a zero-length TAG_OVERLOAD whose length byte is the final byte of the region passes the guard at :1283 (j + j + 1 == ej), advances j to ej at :1287, and tctx->overload = j at :1300 adopts the byte past the region (planted 0xa5, tracked attribution). In-packet variants adopted the byte past the struct at replylen 1460 (guard 0x03), the tail padding past the vend[1222] member at replylen 1458 (0xee), the adjacent in-struct field bp->file[0] via the sname walk (0x5c/0x77 tracked on repeat), and a chained double over-read via the cookie byte in T1f. A len=3 overload adopted its first value byte (0x02), confirming the missing len==1 check. Impact is none, exactly as the Info finding states: the adopted value only steers which bounded fixed-size areas (file[128], sname[64]) are re-walked, and in both production contexts the byte past the region lies inside the same kmalloc allocation (struct tail padding, the replylen member, or the cookie byte) - no crash, no leak to userspace, no corruption on the INVARIANTS kernel. The one-hunk fix (require len == 1 for TAG_OVERLOAD, else badopt) rejected every planted byte in A/B runs while leaving well-formed replies working.