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

One-byte OOB read in TCP option parser correct_mss when olen==1 after consuming NOPs

Summary

correct_mss :419-427 option loop reads *opt (safe while olen>0) but for non-EOL/NOP options unconditionally reads *(opt+1) at :426 to fetch option length WITHOUT checking olen>=2 first. NOP consumption (optlen=1) drives olen from initial multiple-of-4 down to 1. When olen==1 and remaining byte is TLV kind *(opt+1) reads 1 byte past m_pullup region (pullup_len=iphlen+tcphlen at :335). Trigger: SYN with th_off=6 (tcphlen=24 olen=4) options NOP NOP NOP MAXSEG-kind. OOB byte immediately bounds-checked as optlen if(optlen<=0||optlen>olen)break no follow-on corruption. No exfiltration (byte not returned to userspace). Impact: 1-byte residual mbuf read nearly always lands in mbuf data buffer (MHLEN 216 >> pullup 120) no page fault. Requires tcpmss node wired into packet path maxMSS!=0. Fix: if(olen<2) break before *(opt+1).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0714 Β· 14 files
FileTypeDescriptionSize
trigger.c trigger-source netgraph socket-to-socket topology + crafted SYN packets (NOP NOP NOP MAXSEG-kind) 12.8 KB view raw
build.sh build-script cc -o trigger trigger.c 109 B view raw
run.sh run-script ./trigger (requires root + ng_tcpmss loaded) 393 B view raw
setup.sh setup-script build + load ng_tcpmss.ko from source 556 B view raw
VERDICT.md verdict full code trace, mechanism, fix analysis 5.9 KB ↓ raw
README.md readme how to build and run 1.6 KB ↓ raw
fix.diff suggested-fix add if(olen<2) break before *(opt+1) at line 426 342 B view raw
build.log build-log trigger compilation output 66 B view raw
run.log run-log unpatched baseline run (full output) 1.6 KB view raw
fix_run.log run-log patched module run (full output, identical to baseline β€” silent bug) 1.6 KB view raw
fix_build.log build-log fixed ng_tcpmss.ko build output 7.1 KB view raw
env.txt environment uname, cc version, kldstat, abi_version 533 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to build and run
↓ download raw

DF-0714 β€” One-byte OOB read in ng_tcpmss correct_mss

Finding

The TCP MSS option parser in ng_tcpmss (sys/netgraph7/tcpmss/ng_tcpmss.c:426) reads *(opt+1) (the option-length byte) for non-EOL/NOP options without checking olen >= 2 first. After consuming NOPs (optlen=1 each), olen can reach 1, and *(opt+1) reads 1 byte past the TCP options boundary.

The OOB read is silent: no panic (byte lands in mbuf data buffer), no leak (byte not returned to userspace), no corruption (byte only used as optlen for a bounds check).

Build

./build.sh

Requires: DragonFlyBSD with netgraph7 headers (in base). No external libraries.

Run

# Prerequisites: ng_tcpmss module built and loaded (see setup.sh)
./run.sh

Requires root (netgraph socket access). In a real deployment, the admin wires tcpmss into the packet path; the trigger is a crafted TCP SYN from a remote host.

Expected output

  • Test 1 (valid MSS SYN): MSS lowered from 1460 to 536, FixedPkts=1.
  • Test 2 (trigger SYN): Packet forwarded unmodified, SYNPkts incremented, FixedPkts unchanged. No panic, no crash β€” the OOB read is silent.
  • The definitive proof is the code-level trace in VERDICT.md.

Files

  • trigger.c β€” PoC source (netgraph topology + crafted SYN packets)
  • build.sh / run.sh β€” build/run scripts
  • setup.sh β€” module build/load helper
  • VERDICT.md β€” full analysis and code trace
  • fix.diff β€” git-apply-able fix
  • build.log / run.log / fix_run.log / fix_build.log β€” logs
  • env.txt β€” guest environment
  • manifest.json β€” artifact catalog
VERDICT.md verdict full code trace, mechanism, fix analysis
↓ download raw

DF-0714 β€” Verdict

Verdict: REPRODUCED (code-confirmed, runtime-silent)

Impact: 1-byte OOB read (silent β€” no panic, no leak, no corruption). Severity: Low (matches finding). Confidence: certain.

The Bug

File: sys/netgraph7/tcpmss/ng_tcpmss.c:426 Function: correct_mss() (lines 410–445)

The TCP option parser in correct_mss() iterates over the TCP options area of a SYN packet. For non-EOL/NOP options, it reads the option-length byte *(opt+1) at line 426 without first checking that olen >= 2. After consuming NOPs (which advance opt by 1 and decrement olen by 1 each), olen can reach 1 while the loop condition (olen > 0) still holds. When the remaining byte is a TLV-kind option, *(opt+1) reads 1 byte past the TCP options boundary.

Code trace (trigger: SYN with options NOP NOP NOP MAXSEG-kind)

Line 419: for (olen = hlen - sizeof(struct tcphdr), opt = (u_char *)(tc + 1);
Line 420:      olen > 0; olen -= optlen, opt += optlen)

Initial state: olen = 4 (th_off=6 β†’ tcphlen=24, options=4 bytes).

Iter *opt Branch optlen olen after opt after
1 0x01 TCPOPT_NOP 1 3 +1
2 0x01 TCPOPT_NOP 1 2 +1
3 0x01 TCPOPT_NOP 1 1 +1
4 0x02 else (line 425)

At iteration 4: olen = 1, *opt = 0x02 (TCPOPT_MAXSEG, not EOL/NOP).

Line 425: else {
Line 426:     optlen = *(opt + 1);   // *** OOB READ ***

opt points to the last byte of the 4-byte options area. opt + 1 is 1 byte past the options boundary β€” into the mbuf data buffer beyond the pulled-up region (pullup_len = iphlen + tcphlen = 44).

Why the OOB is silent

  1. No page fault: The mbuf data buffer is MHLEN (~213 bytes), far larger than the 44-byte pullup region. The OOB byte at offset 44 is well within the allocated buffer.

  2. No corruption: The OOB byte is used only as optlen and immediately bounds-checked: Line 427: if (optlen <= 0 || optlen > olen) break; Since olen = 1, any optlen >= 2 breaks the loop. optlen == 0 also breaks. optlen == 1 passes but *opt == TCPOPT_MAXSEG β†’ optlen(1) != TCPOLEN_MAXSEG(4) β†’ continue β†’ olen -= 1 β†’ 0, loop ends. In all cases, no data is modified.

  3. No exfiltration: The OOB byte is never returned to userspace. It is used only internally as optlen for the bounds check.

Reachability

ng_tcpmss is an optional netgraph7 module β€” not in the default kernel config (sys/conf/files: optional netgraph7_tcpmss), not pre-built in /boot/kernel/. An admin must: 1. Build and kldload ng_tcpmss. 2. Create a tcpmss netgraph node and wire it into the packet path (e.g., between an interface and the IP stack via ng_ether). 3. Configure maxMSS != 0 via NGM_TCPMSS_CONFIG.

This is a realistic admin action β€” tcpmss exists specifically as a PMTUD workaround tool. Once configured, any remote host sending a crafted TCP SYN triggers the OOB read. The trigger is unprivileged network traffic.

Runtime PoC

The PoC (trigger.c) builds ng_tcpmss.ko from source, loads it, creates a netgraph socket-to-socket topology (sender β†’ tcpmss:in β†’ tcpmss:out β†’ receiver), and sends two crafted SYN packets:

  • Test 1 (control): Valid MAXSEG option (kind=2, len=4, MSS=1460). tcpmss correctly lowers MSS to 536 (FixedPkts=1). Confirms tcpmss works.
  • Test 2 (trigger): Options NOP NOP NOP MAXSEG-kind (th_off=6, olen=4). Drives olen to 1, triggering the *(opt+1) OOB read at line 426. The packet is forwarded unmodified (FixedPkts stays 1), confirming correct_mss was called but the OOB read broke the loop before MAXSEG processing. No panic, no crash, no observable side effect.

Note: The PoC bypasses libnetgraph (which links against old <netgraph/ng_message.h> headers, NG_VERSION=2) and uses raw sendto/ recvfrom with <netgraph7/ng_message.h> (NG_VERSION=8) to match the rebuilt kernel modules. The pre-built modules in /boot/kernel/ had abi_version=2 (from an older build); all three modules (netgraph, ng_socket, ng_tcpmss) were rebuilt from the /usr/src source tree to get abi_version=12.

PoC changes

The PoC folder was empty (never seeded). All files were created from scratch: - trigger.c β€” netgraph socket-to-socket topology + crafted SYN packets. - build.sh, run.sh β€” build/run scripts. - setup.sh β€” module build/load helper (run as root).

Fix

fix.diff: Add if (olen < 2) break; before optlen = *(opt + 1); at line 426. This ensures the option-length byte is only read when at least 2 bytes remain in the options area.

--- a/sys/netgraph7/tcpmss/ng_tcpmss.c
+++ b/sys/netgraph7/tcpmss/ng_tcpmss.c
@@ -423,6 +423,8 @@
        else if (*opt == TCPOPT_NOP)
            optlen = 1;
        else {
+           if (olen < 2)
+               break;
            optlen = *(opt + 1);

This matches the finding's recommended fix: "if(olen<2) break before *(opt+1)."

Fix Validation

  • Unpatched baseline: ng_tcpmss.ko built from unmodified source. PoC Test 2 exercises correct_mss with olen=1 (SYNPkts=2), and the OOB read at line 426 occurs (code trace). Runtime is silent (no panic).
  • Patched: ng_tcpmss.ko built with fix.diff applied. PoC Test 2 still exercises correct_mss (SYNPkts=2), but the if (olen < 2) break; guard at line 426 prevents the OOB read β€” the loop breaks before *(opt+1). Runtime output is identical (silent bug, no regression). Test 1 still correctly lowers MSS (FixedPkts=1).

The fix compiles, loads, and prevents the OOB read without regression. Since the bug is a silent 1-byte read, the before/after runtime output is identical β€” the fix is validated by code trace (the guard catches olen < 2 before the read) plus no functional regression.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: code trace confirms guard catches olen<2 before *(opt+1); no regression (Test 1 MSS lowering still works). Fix compiles, loads, runs.

baseline: *(opt+1) OOB read at line 426 (code trace). patched: if(olen<2)break prevents read. Test 1 both: MSS 1460->536 FixedPkts=1 (no regression).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; ng_tcpmss.ko module rebuilt with fix)

Confirmed kernel references

Detail

Exploit chain

none. Pure 1-byte read-only OOB read within mbuf data buffer. No write, no leak to userspace, no corruption. No escalation.

Evidence (decisive lines)

Test 2: trigger SYN (NOP NOP NOP MAXSEG-kind, olen=4->1) forwarded through tcpmss, SYNPkts=2, no panic. Code trace: *(opt+1) at line 426 reads 1 byte OOB when olen==1.

PoC changes

Created all files from scratch (folder was empty). trigger.c: netgraph socket-to-socket topology + crafted SYN. Bypasses libnetgraph (version mismatch). Rebuilt all 3 modules from source. Added build.sh, run.sh, setup.sh, VERDICT.md, fix.diff, manifest.json.

Verified recommended fix

Add 'if (olen < 2) break;' before 'optlen = *(opt + 1);' at ng_tcpmss.c:426. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0714/fix.diff.

Verdict

REPRODUCED. correct_mss() at ng_tcpmss.c:426 reads (opt+1) for non-EOL/NOP options WITHOUT checking olen>=2 first. After consuming 3 NOPs, olen reaches 1; (opt+1) reads 1 byte past TCP options boundary. Confirmed by code-level trace + runtime reachability (PoC sends crafted SYN through ng_tcpmss node; SYNPkts incremented). Silent 1-byte OOB read within mbuf data buffer.