Skip to content

Fenrir fixes 2026 09 28 - #179

Merged
gasbytes merged 11 commits into
wolfSSL:masterfrom
danielinux:fenrir-fixes-2026-09-28
Sep 29, 2026
Merged

gasbytes merged 11 commits into
wolfSSL:masterfrom
danielinux:fenrir-fixes-2026-09-28

Conversation

@danielinux

Copy link
Copy Markdown
Member

71112da F-14337: drop zero-source transit packets in the forwarding RPF block
70ed046 F-14336: stop calling the MSK tail the EMSK in eap_tls_engine docs
b2cf666 F-13775: reword wolfIP_ip_mtu comments to say IP MTU, not payload MTU
1d0d7aa F-14332: drop unreachable inner forwarding guards in the route API
240470a F-14331: split raw-socket ingress filter from the egress route
9cf2e60 F-14330: send pure ACKs queued behind window-blocked data
3657c95 F-14355: revert SYN_RCVD listener to LISTEN on ICMP unreachable
580ac1c F-14329: keep ICMP sendto destination per-datagram
ff73471 F-14328: retransmit TCP data in CLOSE_WAIT and LAST_ACK on RTO

tcp_rto_cb() skipped the data-retransmission path in TCP_CLOSE_WAIT and let
the control RTO take over TCP_LAST_ACK even with payload outstanding, so a
segment lost after the peer half-closed was never retransmitted and only the
FIN was later re-sent.

- Include CLOSE_WAIT and LAST_ACK in the data-retransmission gate.
- Give LAST_ACK the same payload-drain check as FIN_WAIT_1 so the control RTO
  only takes over once the data is fully drained.
- When the control RTO fires over outstanding data, stop it and hand the timer
  back to the data path so the data RTO is re-armed from the in-flight
  descriptors.
- When a forward ACK drains the last payload in FIN_WAIT_1/LAST_ACK with the
  FIN still in flight, re-arm the control RTO so the FIN keeps being re-sent.
  Gate that re-arm on !tcp_has_pending_unsent_payload: arming the control RTO
  over window-blocked data leaves ctrl_rto_active set, and the data timer that
  flush swaps in then fires into a bail in tcp_rto_cb, delaying the first data
  retransmit by an RTO.
- Clear the stale tmr_rto on the data-gate early return so a later resync can
  re-arm the data RTO.

Adds unit tests driving tcp_rto_cb in CLOSE_WAIT and LAST_ACK-with-data plus
the LAST_ACK drain check and the control-RTO-over-blocked-data gate; they fail
before the fix and pass after.
wolfIP_sock_sendto()'s ICMP arm wrote the per-datagram destination straight
into ts->remote_ip, the field that connect() sets as the connected peer and
that icmp_try_recv() uses as its source filter. A single sendto(A) narrowed
the receive filter to A (dropping router-generated errors and late replies
from other hosts), and a sendto() on a connected socket silently replaced the
peer.

Resolve the destination into a local (defaulting to ts->remote_ip when no
address is given), validate and route with that local, and pass it to
ip_output_add_header(). ts->remote_ip is now changed only by connect().

Updates the sendto-branch test that asserted the clobber and adds a test
covering the unconnected-filter and connected-peer cases; both fail before
the fix and pass after.
icmp_try_deliver_tcp_error() called close_socket() unconditionally on a
SYN_SENT/SYN_RCVD socket matched by a PROT/PORT-unreachable ICMP error. A
passive listener transitions in place to SYN_RCVD (is_listener stays set), so
close_socket() wiped the whole tsocket: proto cleared, bound port released,
and the listening endpoint destroyed by one crafted ICMP error.

Apply the same is_listener guard the RST path and the control-RTO give-up use:
in SYN_RCVD with is_listener set, revert to LISTEN via
tcp_listener_revert_to_listen() instead of closing, so only the half-open
connection attempt is aborted (RFC 1122 4.2.3.9).

Adds a test driving a port-unreachable at a SYN_RCVD listener; it fails before
the fix (proto wiped) and passes after. The non-listener SYN_SENT close test is
unchanged and still passes.
flush_tcp_tx() walks the TX FIFO from the oldest descriptor and breaks at
the first data segment that does not fit the send window, so pure ACKs queued
behind that data were withheld. With the peer window closed, ACKs for data the
peer sends were held until a persist probe, and the accumulated ACKs were then
flushed back-to-back as duplicate ACKs, triggering spurious fast retransmit on
the peer.

When a data segment is window-blocked, jump to the first queued pure ACK behind
it and send it (RFC 5681 4.2). Only a true pure ACK (flags == ACK) is jumped to:
a FIN-ACK is a control segment and stays behind the data. A pure ACK sent ahead
of queued data now carries the first unsent sequence instead of SND.NXT, which
counts the queued bytes and can fall outside the peer's receive window
(RFC 9293 3.10.7.4); a FIN/SYN/RST keeps its own sequence. After the jump the
walk stops at the next non-pure-ACK segment (data, FIN, RST), so a zero-length
FIN queued behind the ACK cannot pass the send condition and go out ahead of the
blocked data. tcp_first_unsent_seq() skips a retransmit that is still pending:
marking a descriptor for retransmission clears PKT_FLAG_SENT, so without the
RETRANS guard the helper would report the retransmit's old seq.

Adds tcp_find_pending_ack() and tcp_first_unsent_seq(), plus tests for the
pure-ACK-behind-data case, the FIN-behind-blocked-data-after-ACK case, and the
retransmit-skip; all fail before the fix and pass after.
rawsocket.if_idx doubled as the egress route and the receive filter:
connect, sendto and flush_raw_tx wrote the transmit route into the same
field that raw_try_recv read as the ingress interface bind. After a
single sendto routed through interface N, an unbound raw socket (a ping
socket, a monitoring tap) silently stopped receiving on every other
interface. A wildcard bind made it worse: wolfIP_if_for_local_ip(ANY)
returns the primary interface, so INADDR_ANY received only there, and
interface 0 could not be selected at all because 0 meant "any".

Store the ingress bind in a new recv_if_idx field with the sentinel
WOLFIP_RAWSOCK_ANY_IF (0xFF) for "any", so interface 0 stays selectable.
Only bind sets it (wildcard -> any); connect/sendto/flush keep writing
if_idx, which is now egress-only. The VLAN-delete guard that refuses to
remove an interface a socket is bound to now checks recv_if_idx, matching
the TCP/UDP/ICMP arms.

Adds test_raw_socket_send_and_wildcard_bind_keep_any_if_recv: send to the
second subnet (egress lands in if_idx), then receive on both interfaces,
and a wildcard bind that keeps receiving on the non-primary interface.
Fails before the fix (second-interface frames filtered), passes after.
wolfIP_route_get, wolfIP_route_add and wolfIP_route_delete each repeated
"#if WOLFIP_ENABLE_FORWARDING ... #else (void)...; return -WOLFIP_EINVAL;
#endif" inside a block that is already wrapped in the same outer guard.
The inner #else stubs can never be compiled: they advertise a
non-forwarding fallback that does not exist, because the prototypes in
wolfip.h are guarded the same way. Anyone maintaining or testing those
stubs is working on dead code.

Remove the three redundant inner #if/#else/#endif wrappers, keeping the
forwarding bodies. The outer guard already excludes the whole block in
non-forwarding builds, so behavior is unchanged.
wolfIP_ip_mtu() only ever subtracts ETH_HEADER_LEN, so its return value is
the IP datagram MTU (20-byte header included), not a payload size. Every
caller confirms this: wolfIP_socket_tcp_mss subtracts IP_HEADER_LEN+TCP_HEADER_LEN
to get the TCP MSS, and the UDP/ICMP sendto paths check len against
ip_mtu - IP_HEADER_LEN - <proto> header. The comments at the function and
at the 1500-byte cap both said "IP payload MTU", which would lead a
maintainer sizing a new protocol's data to overshoot the interface budget
by 20 bytes.

Reword both comments to "IP MTU (header + payload)" / "the IP MTU remains
capped at the standard 1500 bytes", matching how every caller treats the
value. Doc-only change.
eap_tls_engine_export_msk() derives a single 64-octet MSK (TLS <= 1.2 via
wolfSSL_make_eap_keys, TLS 1.3 via the RFC 9190 exporter) under one
MSK-specific label. Per RFC 5216 / RFC 9190 the EMSK is a separate
64-octet value from a distinct, longer/differently-labeled export - it is
not the upper half of the MSK. The header comment said "msk[32..63] becomes
the EMSK (currently unused)" and the caller in supplicant.c repeated "the
remaining 32 bytes form the EMSK". A maintainer adding ERP/re-auth
bootstrapping (RFC 5295) and following those comments would split the MSK
and hand out bytes that are neither the spec EMSK nor independent of the
PMK.

Reword both comments: msk[0..31] is the PMK (RFC 5216 §2.3), msk[32..63]
is the unused MSK tail, and the EMSK is not derived by this function.
Doc-only change.
The general per-packet filter in ip_recv() drops src=0.0.0.0 datagrams,
but suppresses that arm while the DHCP client is unbound (for local
DHCP/BOOTP traffic). With forwarding enabled, a zero-source datagram
arriving on a transit interface then reached the forwarding branch, whose
RPF checks (loopback, link-local, own-address) never test for a zero
source. With TTL<=1 (or DF+oversize, or a malformed option) the router
generated an ICMP error whose destination was set to orig->src = 0.0.0.0,
which RFC 1812 §4.3.2.7 explicitly forbids.

In the forwarding RPF block, unconditionally drop any transit packet whose
source is IPADDR_ANY. Local DHCP delivery is unaffected: it is handled by
the is_local path earlier in ip_recv(), not the forwarding branch.

Adds test_fwd_zero_source_transit_ttl1_silent_drop: a zero-source transit
packet with TTL=1 while DHCP is unbound must be dropped silently (no
Time Exceeded addressed to 0.0.0.0). Fails before the fix (ICMP sent),
passes after.
tcp_listener_revert_to_listen zeroes sock.tcp (including tmr_rto) without an
explicit timer cancel, which looks like it leaves a stale SYN-ACK retransmit
timer in the heap. It does not: tcp_preaccept_timeout_stop, called before the
state reset, cancels the shared tmr_rto slot the control RTO uses. Add a test
that pins the invariant (timer heap empty after reverting a SYN_RCVD listener
with an armed control RTO) so a future change that reintroduces a stale timer
is caught.
Copilot AI lite review requested due to automatic review settings September 28, 2026 15:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@danielinux
danielinux requested a lite review from Copilot September 28, 2026 15:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #179

Scan targets checked: wolfip-src, wolfip-bugs
Coverage: 1 of 3 in-scope changed file(s) opened by the reviewer; not opened: src/supplicant/eap_tls_engine.h, src/supplicant/supplicant.c

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The LAST_ACK timer handoff delays lost payload retransmission by an additional RTO interval.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)

Comment thread src/wolfip.c Outdated
Comment thread src/test/unit/unit_tests_forwarding.c Outdated
When close() replaced an active data RTO with the control RTO, a control-RTO
expiry over outstanding payload (LAST_ACK/CLOSE_WAIT) stopped the control RTO,
re-armed the data timer, and returned. The control timeout had already expired,
so the lost payload was retransmitted only after a second RTO interval.

Stop the control RTO and fall through to the data-loss recovery, which
retransmits the outstanding payload now and re-arms the data RTO. The control
retransmit path now runs only when the state actually needs the control RTO
(else branch), so the yield path no longer re-arms it.

Also fix a stale assertion comment in the forwarding zero-source test: the
injected source is 0.1.2.3, so any Time Exceeded would be addressed to 0.1.2.3,
not 0.0.0.0.

The LAST_ACK test now asserts the retransmit on the first expiry.
@gasbytes
gasbytes merged commit bf0e11b into wolfSSL:master Sep 29, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants