Skip to content

Fenrir fixes 2026-06-04#127

Merged
gasbytes merged 24 commits into
wolfSSL:masterfrom
danielinux:fixes-20260604
Jun 5, 2026
Merged

Fenrir fixes 2026-06-04#127
gasbytes merged 24 commits into
wolfSSL:masterfrom
danielinux:fixes-20260604

Conversation

@danielinux
Copy link
Copy Markdown
Member

bf2c625 F-5488: reject overlong TCP Timestamp options instead of accepting them
6e1f584 F-4949: coalesce overlapping OOO segments instead of consuming a slot each
58f791b F-5490: make iphdr_set_checksum clear the csum field before computing
99cbd86 F-5904: validate IGMP query TTL and destination in igmp_input
eae67c1 F-5479: validate TCP connect local binding before mutating socket state
57e8aff F-5495: reject a NULL receive buffer in wolfIP_sock_recvfrom
9d05b09 F-5069: defer UDP/ICMP bind src_port commit until the filter approves
5d62a3e F-5733: validate ECHO_REPLY destination IP before delivering to ICMP sockets
0c050fa F-5485: null-check the public DHCP helper APIs
71e81b8 F-5482: reject a DHCPACK that lacks the mandatory lease-time option
129d267 F-5698: re-randomize the DHCP xid for each renewal cycle
1aa3587 F-4948: purge ARP state for a VLAN slot on delete to stop cross-VLAN L2 trust
435a976 F-5484: document that raw sockets intentionally tap before source-route policy
b9e84e3 F-5784: strip IP options before ESP unwrap so IHL>5 ESP packets are handled
16e20df F-5697: never forward a packet whose source is one of our own IPs
7b9e310 F-5011: deliver 802.1Q-tagged frames to parent-bound AF_PACKET sockets
ebd7109 F-4501: drop filter-blocked packet-socket TX frames instead of resending
ae1f904 F-5696: deregister httpd callback on the lingering socket in fail_close
62bdc9f F-5070: enforce raw socket bound_local_ip and if_idx on receive
1ed5dc9 F-4946: guard DNS lookups against clobbering in-flight query state
227c30e F-5481: put the ACK-triggering segment in the first SACK block
6202a32 F-5009: reject path traversal and absolute paths in TFTP RRQ/WRQ filenames

danielinux added 22 commits June 4, 2026 09:39
…names

The server copied the wire-supplied RRQ/WRQ filename verbatim and handed it
straight to io.open() with no sanitization. Because TFTP is unauthenticated,
an off-path sender could request "../../etc/passwd" (read) or "/etc/cron.d/evil"
(write) and the raw traversal path reached an integrator-supplied,
possibly-filesystem-backed open() unchanged.

Add a component-aware wolftftp_filename_is_safe() in wolftftp_parse_request():
reject absolute paths (leading '/' or '\') and any ".." path component, while
still allowing relative subdirectories and dots embedded in a name ("fw..bin").
A rejected request fails parsing as WOLFTFTP_ERR_PACKET, so the server replies
with an error and never reaches io.open(). Document the (now-enforced) contract
on the open callback in the header.
tcp_rebuild_rx_sack() derived SACK blocks from the out-of-order cache and
emitted them in descending-sequence order, so the first block was whichever
island had the highest sequence rather than the segment that triggered the
ACK. RFC 2018 sec.4 (2) requires the first SACK block to contain the segment
that triggered the ACK (unless that segment advanced the cumulative ACK). With
descending order the freshest island can be reported last and is the first to
be dropped when the TCP option header lacks room for every block (e.g. four
islands alongside a timestamp option, leaving room for only three blocks),
delaying the peer's loss recovery.

Pass the triggering segment's range into tcp_rebuild_rx_sack() from the
store-OOO path and move the merged block containing it to rx_sack[0], keeping
the remaining islands in descending order behind it. The consume/promote path
advances the cumulative ACK, which is the RFC exemption, so it passes
trig_len == 0 and keeps plain descending order.
nslookup() and wolfIP_dns_ptr_lookup() overwrote the shared dns_lookup_cb,
dns_ptr_cb and dns_query_type fields before calling dns_send_query(), whose
"dns_id != 0" busy-guard only fires afterwards. A second DNS API call made
while a query was in flight was rejected with -16, but by then it had already
replaced the in-flight query's callback/type state. When the original query's
response arrived (its id still matched dns_id, since the second call never
changed it) dns_callback() invoked the wrong handler with that response's
rdata (A->A handler hijack), or the PTR path NULLed the A callback and flipped
the query type, stalling the resolver until the retry timer gave up.

Move the busy-guard ahead of any state mutation in both entry points: when
dns_id != 0 return -16 before touching the shared fields. The guard inside
dns_send_query() is retained as defence in depth.
raw_try_recv() delivered every protocol-matching IPv4 packet to a bound raw
socket without consulting r->bound_local_ip or r->if_idx (the arriving
interface was explicitly discarded with (void)if_idx). wolfIP_sock_bind()
accepts a local IP and interface for raw sockets and records them, so a socket
bound to one local address still captured traffic for every other destination,
and a socket bound to one interface still saw traffic from all of them. With
forwarding enabled raw_try_recv() runs on the IP input path, so a bound socket
even saw transit traffic for unrelated destinations. The TCP/UDP receive paths
already gate on bound_local_ip; raw sockets were the outlier.

Add the same two guards inside the socket-match loop: skip when bound_local_ip
is set and differs from the packet destination, and skip when if_idx is set and
differs from the arriving interface (IPADDR_ANY / 0 keep "match any"). Unbound
catch-all sockets are unaffected.
http_recv_cb's fail_close path freed hc->ssl, called wolfIP_sock_close(sd) and
zeroed hc->client_sd, but never deregistered the socket callback. For an
ESTABLISHED connection wolfIP_sock_close only begins the active close: it moves
the socket to FIN_WAIT_1, returns -EAGAIN, and leaves ts->callback /
ts->callback_arg pointing at this http_client slot. With client_sd zeroed the
slot looks free, so the next accept reuses it for a new connection (fresh sd,
fresh ssl) and re-registers the callback with the same &clients[i] arg. The
half-closed socket still accepts data in FIN_WAIT_1, so a late segment on it
makes wolfIP_poll fire the stale callback against the new connection's state -
freeing its live TLS context and zeroing its client_sd, a use-after-free /
repeatable denial of service against concurrent HTTPS clients.

Deregister the callback on the lingering socket (wolfIP_register_callback with
NULL cb/arg) before zeroing client_sd, so a segment arriving on the half-closed
socket can no longer dispatch into a reused slot.
The WOLFIP_PACKET_SOCKETS TX loop in wolfIP_poll walked the queue with
fifo_next() when the SENDING filter blocked a frame, but fifo_pop() only ever
removes the tail (oldest) descriptor. With [A,B] queued and A blocked, the loop
advanced to B, sent it, then popped A; the next fifo_peek() returned B again,
so B was transmitted a second time and the SENDING filter re-fired for it. A
was silently dropped and B sent twice.

fifo has no way to remove an arbitrary descriptor, so the fifo_next()/fifo_pop()
mix cannot be made correct. Drop the filter-blocked frame from the head with
fifo_pop() and re-peek, matching firewall-drop semantics and sending each
accepted frame exactly once. The sibling TX loops (TCP/UDP/ICMP/raw) already
break on a filter block and were not touched.
wolfIP_recv_on() rebased if_idx from the physical parent to the matched VLAN
sub-interface and stripped the tag in place before calling packet_try_recv().
Since packet_try_recv() matches sockets by if_idx, an AF_PACKET socket bound to
the parent never matched and received zero tagged frames - blinding a
parent-bound sniffer or IDS to all VLAN traffic - while wildcard listeners only
saw the tag-stripped frame stamped with the sub-interface index, losing the
VLAN membership information.

Tap the physical interface with the original, still-tagged frame (parent
if_idx) before the demux/strip, so parent-bound and wildcard sockets observe
VLAN traffic in wire form. The existing post-demux delivery now runs in
"exact interface" mode (new match_wildcard arg to packet_try_recv) so sockets
bound explicitly to the sub-interface still get the tag-stripped copy while
wildcard sockets are not delivered the same frame a second time. Non-VLAN and
non-packet-socket paths are unchanged.
The strict-RPF loop in ip_recv's forwarding block skips the ingress interface
(i == if_idx), so a source address equal to the router's own IP on that
interface was never checked. An L2-adjacent attacker could send a packet with
src set to the router's own LAN address and have it forwarded out another
interface with the source unchanged, impersonating the router to downstream
hosts. (Other interfaces' own IPs were already caught by the RPF subnet match,
but the ingress interface's own /32 slipped through.)

Add an explicit anti-spoof check before the strict-RPF loop: drop the packet if
its source equals any configured interface address. The router originates such
packets locally and never legitimately receives them from the wire. The check
matches only the exact own /32, so legitimate hosts in the ingress subnet are
still forwarded.

Two existing forwarding tests used the router's own IP as a convenience source;
they are updated to a real in-subnet host so they keep exercising the TTL-
exceeded and ARP-queue forward paths.
…andled

ip_recv dispatched proto=50 (ESP) to esp_transport_unwrap before the IP
option-strip block. esp_transport_unwrap reads the ESP header (SPI, sequence,
ICV, payload) at a fixed 20-byte-IP-header offset (ip->data) and derives
esp_len from a fixed IP_HEADER_LEN. When the outer IPv4 packet carried options
(IHL>5, e.g. Router-Alert), ip->data pointed into the options region: the SPI
was read from option bytes, the SA lookup failed, and every such ESP packet was
silently dropped regardless of SA registration.

Move the ESP unwrap into the dispatch block, after the existing option-strip
that normalizes the header to IHL=5. esp_transport_unwrap now always receives a
20-byte IP header, so the ESP header is at the offset it expects. Non-ESP and
non-option paths are unchanged (the strip and protocol dispatch are identical;
only the ESP step moved down).
…te policy

ip_recv delivers to raw_try_recv before the RFC 7126 LSRR/SSRR source-route
drop, so raw sockets observe source-routed packets the stack later refuses to
forward or deliver. This was flagged as a possible policy-ordering issue, but
it is the intended and correct behaviour: a raw socket is a passive ingress tap
whose purpose is to see wire traffic - including hostile packets the stack
itself rejects - which is exactly what monitoring/IDS use cases need. Moving the
scan ahead of the tap would blind those listeners to source-routed attacks.
raw_try_recv only copies the frame to the socket queue; it never parses options
or honours a source route, so tapping early cannot cause the stack to act on
one. Add a comment recording this intent (the alternative the report offered)
rather than changing behaviour. No functional change.
…L2 trust

wolfIP_vlan_delete wiped ll_dev[if_idx] and ipconf[if_idx] but left the ARP
neighbor cache, the in-flight ARP request tracker, the queued-packet array and
last_arp[if_idx] untouched. ARP neighbor entries are keyed only by (ip, if_idx)
- there is no VID, generation counter, or liveness tie to the interface - and
wolfIP_vlan_create immediately reuses the freed slot. So a new VLAN on the same
if_idx silently inherited the deleted VLAN's IP->MAC mappings for up to
ARP_AGING_TIMEOUT_MS, breaking per-VLAN L2 isolation without any ARP exchange on
the new VID.

After wiping the slot, evict every arp.neighbors[] and arp.pending[] /
arp_pending[] entry whose if_idx matches the deleted slot and clear
last_arp[if_idx], so the reused slot starts from a clean ARP state.

Note: the report's dedupe group mentions a sibling multicast-membership variant
(f_vlan_stale_mcast_1); that is a separate finding and is not touched here.
dhcp_xid was assigned once in dhcp_client_init and reused verbatim for every
T1/T2 renewal DHCPREQUEST. Both the xid and the server-id are observable from
the initial broadcast DORA exchange, and dhcp_parse_ack accepts a DHCPACK on
matching xid + server-id while overwriting the gateway unconditionally. Since
the RENEWING DHCPREQUEST is unicast to the server, a LAN-adjacent attacker who
captured the initial DORA could blindly forge a renewal DHCPACK (same xid, real
server-id, attacker-controlled router option) at every renewal without ever
seeing the renewal request, redirecting routed traffic through their host.

Generate a fresh wolfIP_getrandom() xid at the BOUND->RENEWING transition, so
each renewal is a new transaction with an unpredictable id (RFC 2131).
Retransmissions within the cycle and the RENEWING->REBINDING continuation keep
the xid, as required. An attacker who only saw the initial DORA can no longer
predict the renewal xid, so a replayed-xid ACK is rejected.
dhcp_parse_ack initialized lease_s to 0 and transitioned to DHCP_BOUND without
checking that option 51 (IP Address Lease Time) was present. RFC 2131 makes
this option mandatory in a DHCPACK. With it absent, dhcp_schedule_lease_timer
no-ops on lease_s == 0, so the client binds with no renewal/rebind/expiry
timer and treats a dynamic lease as permanent - it never renews and never
notices the server reclaiming the address.

Gate the BOUND transition on lease_s != 0. lease_s is only ever assigned by the
LEASE_TIME option (and a <4-byte option already returns -1 earlier), so the
check accepts only an ACK carrying a present, nonzero lease time and rejects
both the missing and zero-duration cases; a rejected ACK is ignored and the
client keeps waiting/retransmitting.

Several existing tests built DHCPACK fixtures without a lease-time option and
relied on the prior (non-compliant) acceptance; they are updated to include the
mandatory option so they keep exercising their actual intent (offer/ack flow,
renewing, rebinding, unknown-option skipping).
dhcp_bound(), dhcp_client_is_running() and dhcp_client_init() are public (in
wolfip.h) but dereferenced the stack pointer without validating it, so a NULL
argument crashed. Other public entry points in the stack already guard against
NULL; these three were inconsistent.

Return a deterministic value on NULL, matching the existing convention: the two
predicates report 0 (not bound / not running) and dhcp_client_init() returns
-WOLFIP_EINVAL.
…sockets

icmp_input dispatched ICMP_ECHO_REPLY straight to icmp_try_recv without the
wolfIP_if_for_local_ip() guard the sibling ECHO_REQUEST path applies. With
forwarding disabled there is no general "is this dst ours" gate before local
dispatch, and icmp_try_recv skips the per-socket destination check when the
socket's local_ip is 0 (reachable during/after DHCP) and ignores the ingress
interface. An L2-adjacent attacker could thus address an echo reply to our MAC
with an arbitrary ip.dst and a guessed 16-bit echo id and have it delivered to
an application ICMP socket (fake ping-reply / covert-channel injection).

Mirror the ECHO_REQUEST guard onto the reply branch: drop broadcast/multicast
destinations and any reply whose ip.dst is not one of our configured local IPs.
This bounds delivery (including to wildcard local_ip==0 sockets) to replies
actually addressed to us, matching the RFC 1122 s3.2.2.6 policy already applied
to requests, TCP and UDP.

An existing test bound an ICMP socket to 10.0.0.1 without configuring that IP on
an interface; it now also configures the interface so it keeps exercising
reply delivery under the destination check.
The UDP and ICMP arms of wolfIP_sock_bind wrote ts->src_port with the requested
port/echo-id before calling the WOLFIP_FILT_BINDING filter callback, then rolled
it back if the callback rejected the bind. The TCP arm already defers the
assignment to the success path. wolfIP_filter_dispatch holds wolfip_filter_lock
across the callback and makes any re-entrant filter dispatch return 0 (allow),
so a callback that re-enters the stack via wolfIP_poll would have an ingress
UDP/ICMP datagram matching local_ip:src_port delivered to the socket receive
FIFO without WOLFIP_FILT_RECEIVING inspection - even when the bind is ultimately
rejected.

Move the src_port assignment after wolfIP_filter_notify_socket_event returns 0
in both arms, matching the TCP arm. The socket is no longer matchable by
udp_try_recv / icmp_try_recv while the bind is being vetted. (Exploitation
needs CONFIG_IPFILTER and a re-entrant filter callback; no in-tree callback
does this, but it closes the ordering gap for such integrations.)
wolfIP_sock_recvfrom validated sockfd and s but never buf, then copied queued
data into it for every socket family (TCP via queue_pop, UDP/ICMP/raw/packet
via memcpy). A caller passing buf == NULL with a nonzero length, once a datagram
had queued, dereferenced NULL and crashed.

Reject a NULL buffer with nonzero length up front (before any family branch),
returning -WOLFIP_EINVAL and consuming nothing. len == 0 (a no-op
data-availability probe) remains allowed. This guards TCP, UDP, ICMP, raw and
packet sockets uniformly, matching the existing sockfd/s validation.
The TCP arm of wolfIP_sock_connect set ts->sock.tcp.state = TCP_SYN_SENT and
ts->remote_ip before validating that bound_local_ip still resolves to an
interface. When the bound address had been removed/changed (via
wolfIP_ipconfig_set_ex), the bound_match check returned -WOLFIP_EINVAL with the
socket left in TCP_SYN_SENT - but no SYN was queued and no RTO timer started.
Every later connect then hit the "state == TCP_SYN_SENT -> return EAGAIN" early
path, wedging the socket permanently. The other connect error paths (filter
reject, tcp_send_syn failure) already roll state back to CLOSED; only the
binding-validation path did not.

Resolve and validate the binding into locals first and only then commit
state/remote_ip/if_idx/local_ip, mirroring the UDP arm. A failed validation now
returns EINVAL with the socket still CLOSED, so a retry re-validates instead of
returning a phantom in-flight SYN.

The existing test only checked the return code; it now also asserts the socket
stays CLOSED and that a second connect re-validates (EINVAL, not stuck EAGAIN).
igmp_input accepted any IGMP membership query that passed the length, checksum,
type and group-is-multicast checks, ignoring ip->ttl and ip->dst. ip_recv's
source filter only drops broadcast/multicast/zero sources, so an off-link or
unicast-addressed query (e.g. TTL=64, dst=our unicast IP) reached igmp_input and
solicited an IGMPv3 membership report per joined group - disclosing the host's
multicast membership table and violating RFC 3376 §4.1.

Add the RFC 3376 §4.1 query guards: drop the query unless ip->ttl == 1 (IGMP is
link-local and never transits a router) and its destination is either all-hosts
(224.0.0.1) for a general query or the queried group for a group-specific query.
Both fields are already reachable via ip; no new parsing or helper is needed,
and legitimate on-link queries (TTL 1, dst 224.0.0.1/group) are unaffected.

Note: the report also suggested an on-link source-address check; that needs a
new subnet helper and is largely redundant with the TTL==1 guard (a TTL==1 query
cannot have transited a router), so it is not added here.
iphdr_set_checksum summed the whole header including whatever was already in
ip->csum, so it produced a correct value only if the caller had zeroed the
field first. All current callers do (explicitly or via memset), so this was not
a live bug, but the implicit precondition is a footgun: a future caller that
forgets to clear csum would emit a silently-wrong checksum.

Zero ip->csum inside the setter before summing (RFC 1071). The setter is now
correct-by-construction and idempotent (set/verify holds for any input,
set;set;verify holds); existing callers that already clear the field are
unaffected (the extra clear is a no-op).
… each

tcp_store_ooo_segment treated only an exact (seq,len) match as a duplicate, so
segments with distinct (seq,len) pairs that cover overlapping byte ranges each
took a separate slot. With only TCP_OOO_MAX_SEGS=4 slots, an in-window injector
(or a peer re-segmenting retransmissions) could fill the cache with four
overlapping segments, after which every further legitimate out-of-order segment
was silently dropped, degrading goodput until the in-order hole was refilled.

Replace the exact-match dedup with an overlap check that coalesces the incoming
segment into the overlapping slot: store the union of the two ranges in place
(incoming bytes win in the overlap) when it fits a single slot. This bounds how
many slots an overlapping cluster can occupy. A simple replace-on-overlap would
have lost the non-overlapping tail of the cached segment (breaking the existing
coalesce-on-consume behaviour); the union merge preserves it. When the union
would exceed one slot the segments are kept separate, as before, and
tcp_consume_ooo coalesces them on promotion.
The TS branch in tcp_parse_options accepted any option with
olen >= TCP_OPTION_TS_LEN, so a SYN carrying kind 8 with length 11-40
still set ts_found, negotiated ts_enabled, and recorded TSval/TSEcr.
RFC 7323 fixes the Timestamp option at length 10, and every other
fixed-length option here (WS, MSS, SACK-permitted) already enforces an
exact olen; only TS used >=. There is no memory-safety issue (the 8
read bytes are within the already-validated olen bounds), but accepting
a non-canonical length violates the encode/decode roundtrip property.

Require olen == TCP_OPTION_TS_LEN. When the length is wrong the branch
is now skipped and opt += olen (already bounds-checked) advances
parsing correctly, so the remaining options are unaffected.

Test test_tcp_parse_options_timestamp_overlong_ignored injects a SYN
with an olen=11 TS option and asserts ts_enabled stays 0; it fails
pre-fix (ts_enabled==1) and passes after.
Copilot AI review requested due to automatic review settings June 4, 2026 16:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR is a roll-up of correctness and security hardening fixes across the wolfIP network stack (TCP, raw/packet sockets, DHCP/DNS, ICMP/IGMP, VLAN/ARP, ESP) along with new unit tests to lock in the corrected behaviors.

Changes:

  • Tighten input validation and state transitions (e.g., TCP option parsing, TCP connect state mutation, IGMP/ICMP/DHCP validation, DNS in-flight query guarding).
  • Fix packet processing behaviors in edge cases (e.g., VLAN demux delivery to AF_PACKET sockets, ESP unwrap with IPv4 options, ARP cache purge on VLAN delete, raw socket receive bind contract).
  • Add/adjust unit tests covering the new validation and regression fixes.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/wolfip.c Implements the bulk of protocol/socket fixes (raw/packet sockets, TCP SACK/OOO, IGMP/ICMP/DHCP/DNS, VLAN/ARP, ESP ordering, checksum idempotence, TX filter behavior).
src/tftp/wolftftp.h Documents filename sanitization expectations for the server open callback.
src/tftp/wolftftp.c Adds request filename sanitization to block traversal/absolute paths before invoking IO callbacks.
src/http/httpd.c Deregisters callbacks on lingering sockets to avoid stale callback_arg use after close.
src/test/unit/unit.c Registers new unit tests covering the added/changed behaviors.
src/test/unit/unit_shared.c Adds an additional DNS callback used by the new in-flight query regression test.
src/test/unit/unit_tests_vlan.c Adds VLAN packet-socket delivery tests and VLAN delete ARP purge regression coverage.
src/test/unit/unit_tests_tftp.c Adds unit test for path traversal/absolute path rejection in TFTP RRQ/WRQ parsing.
src/test/unit/unit_tests_tcp_state.c Adds regression test ensuring overlong TCP Timestamp options do not negotiate TS.
src/test/unit/unit_tests_tcp_ack.c Updates DHCP ACK tests for mandatory lease-time requirement; adjusts forwarding tests for self-IP spoof drop; adds TCP SACK/OOO regression tests.
src/test/unit/unit_tests_proto.c Adds tests for checksum idempotence, raw socket bind filtering, and packet-socket TX filter drop behavior.
src/test/unit/unit_tests_multicast.c Adds IGMP spoofed-query drop coverage (TTL/destination validation).
src/test/unit/unit_tests_ip_arp_recv.c Adds forwarding regression test for dropping packets spoofed with the router’s own source IP.
src/test/unit/unit_tests_dns_dhcp.c Adds tests for recvfrom NULL buffer rejection, ICMP echo-reply dst validation, and DNS in-flight query state protection; updates DHCP message builders for lease-time.
src/test/unit/unit_tests_dhcp_edges.c Adds DHCP renewal xid rerandomization and DHCPACK lease-time mandatory/zero rejection tests; adds NULL-safe public DHCP API test.
src/test/unit/unit_tests_branches.c Adds bind TOCTOU regression tests for deferring UDP/ICMP src_port commit until filters approve.
src/test/unit/unit_tests_api.c Adds regression assertions that failed TCP connect validation does not wedge the socket in SYN_SENT.
src/test/unit/unit_esp.c Adds ESP transport regression test for IHL>5 outer IPv4 headers (options) being unwrapped correctly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tftp/wolftftp.c
Comment thread src/test/unit/unit_tests_tftp.c
… check

wolftftp_filename_is_safe() blocked leading '/' and '\' absolute paths and
'..' components, but a Windows drive-letter path ("C:\windows\system32") or a
drive-relative path ("C:foo") slipped through: the first segment is "C:", which
is neither a separator-led absolute path nor a ".." component, so the name
reached io.open(). NTFS alternate data streams ("name:stream") had the same
gap. All three hinge on ':', which is never valid in a portable filename, so
reject it wherever it appears.

Adds the drive-letter, drive-relative, and ADS cases to
test_tftp_parse_request_rejects_path_traversal; they fail before the fix
(the names are accepted) and pass after, on both the RRQ and WRQ paths.
The autocov job installs gcovr 7.0 from apt (ubuntu-noble). That version does
not recognize the "<count>:<lineno>-block <n>" lines emitted by the runner's
gcov and aborts with "UnknownLineType ... -block 0" before any coverage gate
runs, failing the whole job. gcovr 7.1+ parses these lines, which is why it
reproduces only on the CI toolchain.

Add --gcov-ignore-parse-errors=all to every gcovr invocation (the Makefile
coverage/autocov targets and the workflow JSON step), alongside the existing
--gcov-ignore-errors flag. The auxiliary -block lines are skipped while the
canonical "count:lineno:" and function records still parse, so function
coverage is unchanged: default 224/224, vlan 227/227, multicast 238/238.
@gasbytes gasbytes merged commit bed7228 into wolfSSL:master Jun 5, 2026
31 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.

3 participants