bootpc trusts interface sdl_alen without bounding it to chaddr[16]/hexstr[64] β over-long link address from an IFT_ETHER driver overflows the outgoing packet in-struct and smashes a 64-byte stack buffer via hexncpy
| Field | Value |
|---|---|
| ID | DF-3045 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-807 |
| File | sys/vfs/nfs/bootp_subr.c |
| Lines | 718-720, 1046-1058, 1192-1197 |
| Area | vfs/nfs (netboot) |
| Confidence | speculative |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
bootpc_fakeup_interface selects any link address whose sdl_type == IFT_ETHER but never validates sdl_alen. The value is then used as a copy length into fixed buffers: (a) bcopy(LLADDR(sdl), &call.chaddr, sdl_alen) β chaddr is 16 bytes inside struct bootp_packet, so sdl_alen>16 overruns into sname/file/vend of the client's own outgoing query; (b) call.hlen = sdl_alen makes the reply matcher bcmp() chaddr beyond 16 bytes (in-struct overread); (c) hexncpy passes a 64-byte stack buffer with outlen = alen*3 computed from the same untrusted length β libkern hexncpy trusts that outlen, so sdl_alen>=22 writes up to 765 bytes past hexstr[64]: a boot-time stack overflow. All Ethernet drivers set sdl_alen=6 via ether_ifattach, so exploitation requires a driver claiming IFT_ETHER with an over-long address (broken or virtual/future driver) β hardware-gated, boot-stage only, hence speculative/Low; flagged because the boot path performs no defense-in-depth check. Not testable on the audit guest (netboot-only + synthetic driver). Fix: clamp the HW address length once at interface selection + pass sizeof(hexstr) to hexncpy (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-3045 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| dfrig12_bootp.c | β | 31.8 KB | view raw | |
| build.sh | β | 199 B | view raw | |
| run.sh | β | 338 B | view raw | |
| run.log | β | 3.8 KB | view raw | |
| run50.log | β | 3.7 KB | view raw | |
| run.fixed.log | β | 2.6 KB | view raw | |
| panic.txt | β | 564 B | view raw | |
| panic50_console_full.txt | β | 3.6 KB | view raw | |
| panic_fix_rejection_console.txt | β | 2.2 KB | view raw | |
| fix_validation.log | β | 3.7 KB | view raw | |
| fix.diff | β | 1.1 KB | 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 | β | 2.0 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-3045 β unvalidated sdl_alen overflows the BOOTP query and smashes a 64-byte stack buffer
Verified in VERIFY mode on the QEMU guest with the verbatim-interposition
kernel-compiled harness (dfrig12_bootp.c): a fake IFT_ETHER link address
with sdl_alen > 16 is fed through the verbatim
bootpc_fakeup_interface (:1046-1053 selection), bootpc_compose_query
(:1192 hlen, :1197 bcopy), and the bootpc_call matcher (:846-848 bcmp),
plus the verbatim hexncpy call form (:718-720) with canaries.
What was reproduced (baseline kernel)
- Selection accepts any sdl_alen (:1046-1053 filters only
sdl_type == IFT_ETHER):ACCEPTED IFT_ETHER sdl_alen=17 (>16, never checked). - In-struct overrun of chaddr[16] via
bcopy(LLADDR(sdl), &call.chaddr, sdl_alen)(:1197): with alen=17 the 17th MAC byte (0x50) lands incall.sname[0]; with alen=255, 239 bytes spill into sname/file/vend of the outgoing query (console hexdumps inrun50.log). - hlen = sdl_alen (:1192) drives the reply matcher
bcmp(reply.chaddr, call.chaddr, call.hlen)(:846-848) past the 16-byte chaddr member on both sides (len=17 β 1 byte; len=255 β 239 bytes), and the exchange still completes (bootpc_call error=0). - Stack buffer smash via hexncpy (:718-720,
HEX_NCPYLEN(alen) = alen*3intochar hexstr[64]at :590): - contained canary measurement: alen=22 β 2 bytes past the 64-byte buffer; alen=106 β 254 bytes past (linear; alen=255 β 701 bytes past);
- in-situ, alen=255: 765-byte write from
hexstr[64]insidebootpc_call's own frame βFatal trap 9GPF atbootpc_call+0x83d: retwith frame pointer 0x3a37363a36363a35 β the saved RBP is filled with 0x3a (':' the hexncpy separator) βpanic.txt.
Honest reachability (matches the finding's Low/speculative rating)
- All in-tree Ethernet drivers get
sdl_alen = 6fromether_ifattach(if_ethersubr.c:620,647); no in-tree driver claims IFT_ETHER with a longer address. Triggering requires a broken/virtual/future driver that does β hardware/driver-gated, boot-stage only. Not reachable by an unprivileged local user, not by crafted network input alone. - The verification proves the code path performs no defense-in-depth bound and the primitive is a full kernel-stack smash once such a driver exists; on this intentionally unhardened guest that smash is a fatal GPF with attacker-influenced frame content (0x3a pattern), i.e. a control- flow-corruption class defect, not a benign overprint.
Files
run.logβ baseline tests 3 and 5 (console traces).run50.logβ baseline test 50: full console incl. the 255-byte MAC hexdump line and the fatal trap.panic.txtβ GPF header with the 0x3a-laden frame pointer + backtrace.run.fixed.log/panic_fix_rejection_console.txtβ fixed-kernel test 3: selection rejects alen=17, verbatim fail-loud panicbootpc: Unable to find HW address for fak0.fix_validation.logβ A/B table including the documented first-fix bug (appending predicates to the(sdl = ...)condition leaves sdl non-NULL; caught by the A/B run and corrected).fix.diffβ the corrected, validated fix.
DF-3045 β VERDICT
Status: reproduced (mechanically: primitive-level canary smash + in-situ
fatal kernel stack corruption through the verbatim production call sites),
impact on this guest: panic (fatal GPF with control-flow-corrupting
frame overwrite), real-world reachability: driver-gated β exactly the
gate the finding describes; on a stock system with only ether_ifattach
interfaces (sdl_alen=6, if_ethersubr.c:620,647) none of the cited uses can
see alen > 16, so unprivileged-local and crafted-network reach is nil.
Chain, line-accurate
bootpc_fakeup_interface:1046-1053 selects any AF_LINK sockaddr withsdl_type == IFT_ETHERβsdl_alenis never validated. Proven on the guest with a fabricated alen=17 / alen=255 sockaddr_dl on a fake ifnet walked by the verbatim TAILQ_FOREACH.bootpc_compose_query:1192call.hlen = ifctx->sdl->sdl_alenand :1197bcopy(LLADDR(sdl), &ifctx->call.chaddr, sdl_alen)β chaddr isunsigned char chaddr[16](:102). Observed: alen=17 puts MAC byte 17 (0x50) intocall.sname[0]; alen=255 spills 239 bytes into sname/file/vend of the outgoing query (in-struct; the vend prefix is then legitimately rewritten by the cookie/options writes at :1199+).- Reply matcher :846-848
bcmp(reply.chaddr, ifctx->call.chaddr, ifctx->call.hlen)with hlen=17/255 reads past the 16-byte chaddr member on both sides (interposed bcmp logged len and over-read count); the verbatim exchange completes (error=0), i.e. an attacker reply crafted with the matching tail is accepted. bootpc_call:718-720hexncpy((u_char *)LLADDR(ifctx->sdl), sdl_alen, hexstr, HEX_NCPYLEN(sdl_alen), ":")withchar hexstr[64](:590) andHEX_NCPYLEN(s) = (s * 3)(sys/sys/libkern.h:67). libkern hexncpy (libkern/hexncpy.c:56-62) writes exactly 3 bytes per input byte whileoutlen >= 3, then a NUL: outlen = alen3 is trusted, so the footprint is 3alen bytes from a 64-byte buffer. Contained canary runs: alen=22 β 2 bytes past; alen=106 β 254 bytes past (offsets 0..253 of the post-canary, next byte intact).- In-situ (test 50, alen=255): 765-byte write from
hexstr[64]insidebootpc_call's own stack frame β saved RBP/return address overwritten with hex-digit/':' bytes βFatal trap 9: general protection fault,Stopped at bootpc_call+0x83d: ret, frame pointer0x3a37363a36363a35(0x3a = ':'). Kernel built with-fno-stack-protector(stock flags, see build.log), so nothing caught it before control flow used the smashed words.
Privesc assessment (standing directive)
The primitive is a linear, fully attacker-length-controlled stack overwrite (up to 701 bytes past a 64-byte buffer) whose content is a printable hex-with-colons encoding of the interface MAC β i.e. controllable in length, constrained in alphabet (0-9a-f and ':'), landing on saved registers/return address in a boot-time kernel thread. On an unhardened target this is control-flow corruption; but the WRITE occurs only when a driver presents IFT_ETHER with alen>16, which no in-tree driver does and which an unprivileged user cannot cause (no ioctl path sets sdl_alen for an existing ether interface: net/if.c:2885 refuses length changes). Therefore: real kernel-stack corruption class, hardware/driver-gated trigger, no unprivileged-local route to uid=0 on this guest β consistent with the finding's Low/speculative rating, now with the primitive proven rather than inferred.
Fix validation
First attempt (append predicates to the existing condition) was caught
broken by the A/B run: (sdl = ...) in :1050 assigns on every AF_LINK
ifaddr, so a rejected address still leaves sdl non-NULL and the clamp never
engaged (test 3 still accepted alen=17). Corrected fix.diff assigns a
local sdl2 only when sdl_type == IFT_ETHER && sdl_alen > 0 && sdl_alen
<= sizeof(ifctx->call.chaddr), and bounds the hexncpy outlen with
sizeof(hexstr). Fixed-kernel results: selection rejects alen=17/255 β
verbatim panic("bootpc: Unable to find HW address for %s") (:1056-1057)
β a loud boot failure instead of silent corruption; call-form canary run
with the bounded outlen writes 0 bytes past the buffer. No cited use can
observe alen > 16 anymore.
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
- sys/vfs/nfs/bootp_subr.c:1046-1053
- sys/vfs/nfs/bootp_subr.c:1192
- sys/vfs/nfs/bootp_subr.c:1197
- sys/vfs/nfs/bootp_subr.c:846-848
- sys/vfs/nfs/bootp_subr.c:718-720
- sys/vfs/nfs/bootp_subr.c:590
- sys/vfs/nfs/bootp_subr.c:102
- sys/sys/libkern.h:67
- sys/libkern/hexncpy.c:56-62
- sys/net/if_ethersubr.c:620
- sys/net/if_ethersubr.c:647
Detail
Exploit chain
broken or virtual driver attaches an IFT_ETHER ifaddr with sdl_alen above 16 (no in-tree driver does; ether_ifattach forces 6) -> verbatim selection at :1046-1053 accepts it -> compose_query bcopy at :1197 overruns chaddr[16] into sname/file/vend of the outgoing query and hlen at :1192 drives an in-struct bcmp overread at :846-848 -> hexncpy at :718-720 writes 3*alen bytes from hexstr[64] in the bootpc_call stack frame -> saved registers and return address overwritten with hex and colon bytes -> fatal GPF on return (observed) or, on an unhardened target, potential control-flow hijack at boot
Evidence (decisive lines)
run.log: selection ACCEPTED sdl_alen=17, chaddr hexdump 40..50 spilling into sname, bcmp len=17 with 1 byte past chaddr[15], error=0; run50.log + panic.txt: Fatal trap 9, frame pointer 0x3a37363a36363a35, Stopped at bootpc_call+0x83d ret; canary lines 2 bytes past (alen=22) and 254 bytes past (alen=106); run.fixed.log + panic_fix_rejection_console.txt: selection REJECTED and verbatim panic bootpc: Unable to find HW address for fak0 with backtrace through bootpc_fakeup_interface
PoC changes
no seed PoC existed (finding was marked not testable on the audit guest); built the verbatim-interposition rig with a fake ifnet whose only AF_LINK ifaddr is a crafted sockaddr_dl; fixed two harness bugs found during the run (missing gctx->starttime made the BOOTP_TIMEOUT knob fire early; separate static canary arrays were reordered by the linker, merged into one struct for guaranteed adjacency); first proposed selection hunk was itself buggy and was corrected after the A/B run caught it still accepting alen=17
Verified recommended fix
accept a link address only when 0 < sdl_alen <= sizeof(call.chaddr) at the bootpc_fakeup_interface selection (assigning the local sdl pointer only when all predicates hold) and pass sizeof(hexstr) as the hexncpy outlen (validated in fix.diff)
Verdict
The unvalidated sdl_alen chain was reproduced end to end through the verbatim production code on the guest kernel: bootpc_fakeup_interface at :1046-1053 accepted an IFT_ETHER link address with sdl_alen=17 with no length check; bootpc_compose_query set call.hlen=17 (:1192) and the bcopy at :1197 overran chaddr[16] by one byte into call.sname[0] (byte-identical attribution to the 17th MAC byte); the reply matcher bcmp at :846-848 compared 17 bytes, reading past the 16-byte chaddr member on both sides, and the full BOOTP exchange completed. The hexncpy call at :718-720 with HEX_NCPYLEN(alen)=alen*3 against char hexstr[64] (:590) was measured with guard canaries (2 bytes past at alen=22, 254 at alen=106, scaling to 701 past at alen=255) and driven in situ through verbatim bootpc_call with sdl_alen=255, producing a fatal kernel GPF at bootpc_call+0x83d ret with frame pointer 0x3a37363a36363a35, i.e. saved control flow overwritten with the 0x3a separator bytes of hexncpy. Real-world reachability remains driver-gated exactly as the finding states: every in-tree Ethernet interface gets sdl_alen=6 from ether_ifattach (if_ethersubr.c:620,647) and no unprivileged path can present an over-long IFT_ETHER address, so no uid0 route exists on this guest; what the run upgrades from speculative to certain is the primitive itself, including full-magnitude boot-time kernel stack corruption once such a driver exists. Fix validation A/B caught and corrected a bug in the first proposed hunk (appending predicates to the sdl assignment leaves sdl non-NULL on rejection); the corrected fix.diff rejects alen outside 1..16 at selection (verbatim loud panic Unable to find HW address) and bounds the hexncpy outlen with sizeof(hexstr), after which none of the cited uses can observe alen above 16.
No comments yet.