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)
PoC verification
Evidence pack
findings/poc/DF-0714 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 scriptssetup.shβ module build/load helperVERDICT.mdβ full analysis and code tracefix.diffβ git-apply-able fixbuild.log/run.log/fix_run.log/fix_build.logβ logsenv.txtβ guest environmentmanifest.jsonβ artifact catalog
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
-
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. -
No corruption: The OOB byte is used only as
optlenand immediately bounds-checked:Line 427: if (optlen <= 0 || optlen > olen) break;Sinceolen = 1, anyoptlen >= 2breaks the loop.optlen == 0also breaks.optlen == 1passes but*opt == TCPOPT_MAXSEGβoptlen(1) != TCPOLEN_MAXSEG(4)βcontinueβolen -= 1 β 0, loop ends. In all cases, no data is modified. -
No exfiltration: The OOB byte is never returned to userspace. It is used only internally as
optlenfor 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). Drivesolento 1, triggering the*(opt+1)OOB read at line 426. The packet is forwarded unmodified (FixedPktsstays 1), confirmingcorrect_msswas 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.kobuilt from unmodified source. PoC Test 2 exercisescorrect_msswitholen=1(SYNPkts=2), and the OOB read at line 426 occurs (code trace). Runtime is silent (no panic). - Patched:
ng_tcpmss.kobuilt withfix.diffapplied. PoC Test 2 still exercisescorrect_mss(SYNPkts=2), but theif (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
fixedVALIDATED: 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).
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.
No comments yet.