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

TCP-MD5 signature option writes past 40-byte opt[] stack buffer with no bounds check

Summary

When kernel built TCP_SIGNATURE option and socket has TF_SIGNATURE tcp_output appends 20-byte signature option into fixed u_char opt[TCP_MAXOLEN](40-byte) stack buffer at :786-798 WITHOUT checking optlen+20<=40 WITHOUT reserving signature space in preceding option writers. MSS/window-scale/SACK-permitted/timestamp writers(:719-766) and tcp_sack_fill_report(tcp_sack.c:920) can drive optlen to 24-40 before signature block. Signature overruns opt[] by 4-20 bytes corrupting adjacent stack locals optlen hdrlen ipoptlen and saved registers. KASSERT optlen<=TCP_MAXOLEN(:801) no-op production. SYN: MSS(4)+win(4)+sack_perm(4)+ts(12)=24 signature writes opt[24..43] 4-byte overrun. Established SACK+ts fills to 40 signature writes opt[40..59] 20-byte overrun. setsockopt(TCP_SIGNATURE_ENABLE) NO privilege check any uid TCPS_CLOSED. Remote: TCP-MD5 server SACK-reporting ACK overruns.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2597 Β· 9 files
FileTypeDescriptionSize
poc.c trigger-source setsockopt(TCP_SIGNATURE_ENABLE) reachability probe + SYN-emit trigger (used only if TCP_SIGNATURE is compiled in) 4.8 KB view raw
build.sh build-script cc -O2 -Wall -o poc poc.c 329 B view raw
run.sh run-script ./poc 18097 478 B view raw
build.log build-log PoC compile on default GENERIC (BUILD_EXIT=0) 62 B view raw
run.log run-log PoC run on default GENERIC -> setsockopt ENOPROTOOPT, RUN_EXIT=5 392 B view raw
env.txt environment uname, cc version, TCP_SIGNATURE build state, IPsec SADB absence proof 850 B view raw
tcp_signature_unbuildable.txt evidence Proof that enabling options TCP_SIGNATURE fails to compile: IPSEC_DIR_OUTBOUND undeclared (tcp_output.c:1058) and struct secasvar/key_allocsa undeclared (tcp_subr.c) β€” the IPsec SADB subsystem was removed from DragonFly 1.5 KB view raw
fix.diff suggested-fix bound-check optlen+TCPOLEN_SIGNATURE+2<=TCP_MAXOLEN before appending the signature option (defense-in-depth; applies+checks cleanly but cannot be boot-validated because the surrounding #ifdef TCP_SIGNATURE code is unbuildable) 1.4 KB view raw
VERDICT.md verdict full narrative: real source defect in dead/unbuildable code 6.0 KB ↓ raw
VERDICT.md verdict full narrative: real source defect in dead/unbuildable code
↓ download raw

DF-2597 β€” TCP-MD5 (TCP_SIGNATURE) signature option overruns 40-byte opt[] stack buffer

Verdict: NOT REPRODUCED β€” real source-level defect in DEAD, UNBUILDABLE code

The cited overflow (sys/netinet/tcp_output.c:779-800) is a genuine source defect β€” the 20-byte signature option (TCPOLEN_SIGNATURE=18 + NOP/EOL=2) is appended to the 40-byte u_char opt[TCP_MAXOLEN] stack buffer with no bound check, so any prior options (MSS+window-scale+SACK-permitted+timestamp on a SYN β†’ optlen=24, or timestamp + SACK blocks in ESTABLISHED β†’ optlen up to 40) push the signature write 4–20 bytes past the end of opt[], corrupting the adjacent stack frame (optlen, hdrlen, ipoptlen, saved registers).

But the code path is completely unreachable on every kernel buildable from current DragonFly master, for two compounding reasons:

(1) TCP_SIGNATURE is not in the default GENERIC kernel

options TCP_SIGNATURE is commented out in sys/conf/options:271 and is absent from sys/config/X86_64_GENERIC. The vulnerable block is wrapped in #ifdef TCP_SIGNATURE, and so is the only way to arm it from userspace β€” the case TCP_SIGNATURE_ENABLE: handler in sys/netinet/tcp_usrreq.c:1556. On the running audit-source kernel (6.5-DEVELOPMENT #0, INVARIANTS ON):

[*] setsockopt(TCP_SIGNATURE_ENABLE) -> -1 errno=42 (Protocol not available)

setsockopt falls through to default: error = ENOPROTOOPT, TF_SIGNATURE can never be set, and tcp_output.c:779-800 is compiled out entirely. Runtime unreachable from any unprivileged (or even privileged) action on the default kernel. Verified: nm /boot/kernel/kernel | grep -ci tcpsignature = 0.

(2) TCP_SIGNATURE is UNBUILDABLE on current master β€” its dependency (IPsec SADB) was deleted

The TCP-MD5 feature (tcpsignature_compute in sys/netinet/tcp_subr.c:2199, and the digest-write call site at tcp_output.c:1058) depends on the KAME/IPsec SADB subsystem: key_allocsa(), struct secasvar, key_freesav(), key_sa_recordxfer(), _KEYBUF()/_KEYLEN(), and IPSEC_DIR_OUTBOUND. None of these exist anywhere in the current sys/ tree β€” the IPsec subsystem was removed (no sys/netproto/ipsec/, no key.h, no ipsec.h). Attempting to build a kernel with options TCP_SIGNATURE fails to compile:

/usr/src/sys/netinet/tcp_output.c:1058:37: error: 'IPSEC_DIR_OUTBOUND' undeclared

and even forcing past that, tcp_subr.c fails:

tcp_subr.c:2328:29: error: dereferencing pointer to incomplete type 'struct secasvar'
tcp_subr.c:2236:9: warning: implicit declaration of function 'key_allocsa'
tcp_subr.c:2331:2: warning: implicit declaration of function 'key_freesav'

So no kernel (default or custom) can be built from current master source with this code path live. The TCP_SIGNATURE code is orphaned dead code that references a subsystem that no longer exists in the OS.

Conclusion / classification

This is the valid hard blocker from Phase 6: the vulnerable code path is dead/unreachable at runtime AND no harness can exercise it β€” here elevated to the code cannot even be compiled into any kernel. The bug is a latent source-level defect (a missing optlen + TCPOLEN_SIGNATURE + 2 <= TCP_MAXOLEN bound check) that would be a real High-severity stack overflow if and only if the IPsec SADB subsystem were present and options TCP_SIGNATURE were enabled. On the audited kernel it has zero runtime impact.

Because there is no live memory-corruption primitive (the code never runs, never even links), there is no escalation chain to develop β€” Phase 6 escalation applies only to a live write primitive, which does not exist here.

Mechanism trace (source-confirmed, were the code reachable)

  1. tcp_output() declares u_char opt[TCP_MAXOLEN] on its stack β€” TCP_MAXOLEN = 60 - sizeof(tcphdr) = 40 (sys/netinet/tcp.h:149, :148).
  2. On a SYN with default sysctls (tcp_do_rfc1323=1 sets TF_REQ_SCALE|TF_REQ_TSTMP in tcp_subr.c:759; tcp_do_sack default) the option writers at tcp_output.c:719-766 produce: MSS(4)+window-scale(4)+SACK-permitted(4)+timestamp(12) = optlen=24 (TCPOLEN_MAXSEG=4 tcp.h:86, TCPOLEN_WINDOW=3 padded to 4, TCPOLEN_SACK_PERMITTED_ALIGNED=4 tcp.h:93, TCPOLEN_TSTAMP_APPA=12 tcp.h:101).
  3. In ESTABLISHED with timestamp + 3 SACK blocks (tcp_sack_fill_report at tcp_sack.c:920 adds TCPOLEN_SACK_ALIGNED=4 + 3Γ—TCPOLEN_SACK_BLOCK=8, bounded to ≀40), optlen reaches 40.
  4. The signature block at tcp_output.c:779-800 then unconditionally writes TCPOPT_SIGNATURE+TCPOLEN_SIGNATURE (2) + 16 zero bytes + NOP+EOL (2) = 20 bytes at opt+optlen with no check, overrunning opt[] by 4 bytes (SYN) to 20 bytes (ESTABLISHED). The post-hoc KASSERT(optlen <= TCP_MAXOLEN) at :801 is a debug-only trip, not a guard.

Fix-validation status: not_testable

Phase 8 requires a clean before/after on a kernel that runs the vulnerable code. Here the vulnerable code cannot be compiled into any kernel (the IPsec dependency is gone), so there is no "before" baseline to reproduce against and no "after" patched kernel to test. fix.diff applies cleanly (git apply --check OK) and is a correct bound check, but it cannot be validated by building and booting, because the surrounding #ifdef TCP_SIGNATURE code does not compile on current master.

What fix.diff does (defense-in-depth for if IPsec/TCP-MD5 is reintroduced)

Wraps the signature-append block in a bound check: if ((tp->t_flags & TF_SIGNATURE) && optlen + TCPOLEN_SIGNATURE + 2 <= TCP_MAXOLEN). When there is no room, the signature is omitted (the segment fails MD5 verification on the peer β€” graceful degradation) rather than overrunning opt[]. Matches the spirit of the finding markdown's recommended fix; supersedes it with a precise, line-accurate guard.

Reproduce

cd findings/poc/DF-2597 && ./build.sh && ./run.sh
# Expected on the default audit-source kernel:
#   [*] setsockopt(TCP_SIGNATURE_ENABLE) -> -1 errno=42 (Protocol not available)
#   [!] RESULT: bug UNREACHABLE on this kernel (latent code defect).
#   RUN_EXIT=5

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: the vulnerable code cannot be compiled into ANY kernel on current master (IPsec SADB subsystem removed), so no unpatched baseline reproduces and no patched kernel can be compared. fix.diff applies cleanly and is a correct bound check but cannot be boot-validated.

baseline (default GENERIC #0): setsockopt(TCP_SIGNATURE_ENABLE) -> errno=42 ENOPROTOOPT, nm|grep -ci tcpsignature=0, RUN_EXIT=5. attempted TCP_SIGNATURE kernel build: error 'IPSEC_DIR_OUTBOUND undeclared' at tcp_output.c:1058. => no before/after possible.
↓ fix.diffn/a β€” no kernel buildable from current master can run the vulnerable #ifdef TCP_SIGNATURE code path

Confirmed kernel references

Detail

Exploit chain

none β€” no live memory-corruption primitive exists. The vulnerable code never compiles, never links, never runs on any kernel buildable from current DragonFly master.

Evidence (decisive lines)

[+] listener on 127.0.0.1:18097. setsockopt(TCP_SIGNATURE_ENABLE) -> -1 errno=42 (Protocol not available). TCP_SIGNATURE NOT compiled into this kernel. RUN_EXIT=5. TCP_SIGNATURE build attempt on master: error 'IPSEC_DIR_OUTBOUND undeclared' at tcp_output.c:1058; struct secasvar incomplete type. IPsec SADB subsystem present in sys/: ABSENT.

PoC changes

Wrote poc.c from scratch (PoC dir empty): setsockopt(TCP_SIGNATURE_ENABLE) reachability probe; authored fix.diff bound-checking optlen+TCPOLEN_SIGNATURE+2<=TCP_MAXOLEN. git apply --check passes.

Verified recommended fix

In sys/netinet/tcp_output.c wrap the signature-append block (line 779) in 'if ((tp->t_flags & TF_SIGNATURE) && optlen + TCPOLEN_SIGNATURE + 2 <= TCP_MAXOLEN)' so the option can never overrun the 40-byte opt[] stack buffer. Defense-in-depth for currently-dead code; full git-apply-able diff in findings/poc/DF-2597/fix.diff.

Verdict

NOT REPRODUCED β€” real source-level defect in DEAD, UNBUILDABLE code. The cited overflow is genuine: tcp_output.c:786-800 appends the 20-byte TCP-MD5 signature option into the 40-byte u_char opt[TCP_MAXOLEN] stack buffer with NO bound check, so on a SYN with default options optlen=24 -> 4-byte overrun; in ESTABLISHED with timestamp+3 SACK blocks optlen=40 -> 20-byte overrun. BUT the entire block is #ifdef TCP_SIGNATURE, and TCP_SIGNATURE is NOT in the default GENERIC kernel and is UNBUILDABLE because its IPsec SADB dependency was removed entirely (no sys/netproto/ipsec/). setsockopt(TCP_SIGNATURE_ENABLE) returns ENOPROTOOPT (errno 42) at runtime. Classification: latent source defect behind a dead/unreachable, unbuildable code path.