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)
PoC verification
Evidence pack
findings/poc/DF-3046 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_OVERLOADwhose length byte (0) is the FINAL byte of the parsed region passes the guard at :1283 (j + *j + 1 > ejevaluates toej > ej, false),len = *j++advances j to exactly ej, andtctx->overload = *j(:1300) reads one byte past the region β the planted guard 0xa5 was adopted (overload=0xa5, badopt=0). - T1b β
TAG_OVERLOADwith 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]readsbp->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 adoptsbp->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: requirelen == 1for TAG_OVERLOAD.
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*jreads 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 adoptsbp->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_globalcontextandstruct 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_testableConfirmed 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.
No comments yet.