Skip to content

Fenrir fixes: MQTT v5 client and broker - #576

Open
embhorn wants to merge 15 commits into
wolfSSL:masterfrom
embhorn:fenrir-fixes
Open

Fenrir fixes: MQTT v5 client and broker#576
embhorn wants to merge 15 commits into
wolfSSL:masterfrom
embhorn:fenrir-fixes

Conversation

@embhorn

@embhorn embhorn commented Aug 20, 2026

Copy link
Copy Markdown
Member
  • f-8609: suppress duplicate delivery of a retransmitted QoS 2 PUBLISH awaiting PUBREL
  • f-8611: reject an empty ShareName in v5 shared-subscription filters
  • f-8612: reject No Local on a v5 shared subscription
  • f-8613: reject a Subscription Identifier on a client-to-server PUBLISH
  • f-8614: refuse AUTH when the CONNECT carried no Authentication Method
  • f-8616: refuse a v5 CONNECT whose Will QoS exceeds Maximum QoS instead of downgrading
  • f-8617: refuse a v5 CONNECT with Will Retain when retained messages are unsupported
  • f-10343: enforce Maximum Packet Size over the whole PUBLISH, not per transport write
  • f-10344: refuse a CONNACK reporting Session Present after Clean Session
  • f-10345: enforce Receive Maximum quota for write-only publishes, releasing on ack, cancel, and disconnect
  • f-556: make the broker fan-out use-after-free guard O(1) via a subscription generation counter

Copilot AI lite review requested due to automatic review settings August 20, 2026 15:35
@embhorn embhorn self-assigned this Aug 20, 2026

Copilot AI 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.

Pull request overview

This PR applies a set of MQTT v5 correctness and safety fixes across the client and embedded broker portions of wolfMQTT, with accompanying regression tests to lock in protocol compliance and concurrency behavior.

Changes:

  • Adds stricter MQTT v5 protocol enforcement (shared-subscription validation, No Local rules, AUTH negotiation gating, illegal properties rejection, CONNACK session-present cross-check).
  • Improves client correctness under QoS 2 and v5 flow control (inbound QoS2 de-dup, Receive Maximum enforcement for write-only publishes, whole-packet Maximum Packet Size enforcement).
  • Hardens broker fan-out against re-entrant subscription frees with an O(1) guard using a subscription generation counter.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
wolfmqtt/mqtt_packet.h Adjusts message/quota tracking fields and exposes MqttPacket_TopicFilterValid_ex API for v5-aware topic filter validation.
wolfmqtt/mqtt_client.h Adds v5 auth negotiation state and inbound QoS2 packet-id tracking for duplicate suppression.
wolfmqtt/mqtt_broker.h Adds subs_gen generation counter to support O(1) fan-out UAF guarding in dynamic-subscription mode.
src/mqtt_packet.c Implements v5 shared-subscription topic-filter validation and enforces No Local constraints on shared subscriptions in SUBSCRIBE encode/decode.
src/mqtt_client.c Implements inbound QoS2 de-dup, v5 AUTH gating, CONNACK Session Present validation, whole-packet max-size check, and Receive Maximum quota lifecycle changes.
src/mqtt_broker.c Increments subs_gen on subscription list mutations and uses snapshot+revalidation in fan-out loops to avoid UAF under re-entrant closes.
tests/test_mqtt_packet.c Adds tests for v5 shared-subscription filter rules and No Local rejection behavior (encode and decode).
tests/test_mqtt_client.c Adds tests covering CONNACK session-present mismatch, AUTH gating, subscription-id rejection on client PUBLISH, max packet size enforcement, Receive Maximum behavior for write-only publishes, and QoS2 duplicate suppression.
tests/test_broker_connect.c Adds a test that validates subs_gen bumps on unsubscribe and preserves baseline fan-out delivery behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/mqtt_client.c

@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 #576

Scan targets checked: wolfmqtt-bugs, wolfmqtt-src

Findings: 4
4 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

@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 #576

Scan targets checked: wolfmqtt-bugs, wolfmqtt-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

@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 #576

Scan targets checked: wolfmqtt-bugs, wolfmqtt-src

Findings: 3
3 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

@embhorn embhorn removed their assignment Aug 21, 2026

@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 #576

Scan targets checked: wolfmqtt-bugs, wolfmqtt-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread src/mqtt_client.c
* The property lives on this stack frame and is linked only across the
* encode below, so the caller's list is unchanged on return. */
if (mc_connect->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 &&
!MqttConnect_HasRecvMax(mc_connect)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Application-supplied Receive Maximum is not bounded by the QoS 2 dedup table size · Protocol state machine violations

When the application already supplies MQTT_PROP_RECEIVE_MAX, MqttClient_Connect sends it unchecked. A value above MQTT_MAX_RECV_QOS2 lets the server keep more QoS 2 messages in flight than recv_qos2_pending has slots; MqttClient_RecvQos2_Add (src/mqtt_client.c:501) then silently drops entries and retransmitted PUBLISHes are re-delivered to msg_cb, defeating the f-8609 dedup.

Related known finding #10345 (similar but distinct): Both involve MQTT v5 Receive Maximum enforcement in the client, but #10345 bypasses the server-advertised outbound publish quota in MqttPublishMsg. This candidate sends an application-supplied inbound limit exceeding the local QoS 2 dedup capacity in MqttClient_Connect; the faulting operations, root causes, and fixes are separate.

Fix: Clamp an application-supplied MQTT_PROP_RECEIVE_MAX to MQTT_MAX_RECV_QOS2, or reject the CONNECT when it exceeds that bound.

Comment thread tests/test_mqtt_client.c
test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
/* Larger than any single tx_buf-sized fragment (<=256), smaller than the
* whole PUBLISH (~417 bytes). */
test_client.packet_sz_max = 300;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New Maximum Packet Size guard: header-overhead branch is never exercised · Missing edge-case coverage on a function the PR also changed

Both new tests only hit the publish->total_len > client->packet_sz_max disjunct (400 vs 300) or clear the limit outright (400 vs 1000). The substance of the f-10343 fix — the ((word32)rc - publish->intBuf_len) > (client->packet_sz_max - publish->total_len) term at src/mqtt_client.c:2879 — has no test, so deleting it would not fail CI.

Fix: Add a case where total_len is just under packet_sz_max so only the fixed+variable header overhead pushes the packet over the limit.

@embhorn embhorn changed the title Fenrir high-severity fixes — MQTT v5 client and broker Fenrir fixee— MQTT v5 client and broker Aug 21, 2026
@embhorn embhorn changed the title Fenrir fixee— MQTT v5 client and broker Fenrir fixes: MQTT v5 client and broker Aug 21, 2026

@aidangarske aidangarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-security

Overall recommendation: REQUEST_CHANGES
Findings: 6 total — 6 posted, 0 skipped
6 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [High] [review+review-security] Canceled write-only publishes can bypass Receive Maximum when reusedsrc/mqtt_client.c:613-616,2839-2844,3962-3968,4072-4075
  • [High] [review+review-security] Reentrant subscription-list mutation guard truncates broker fan-outsrc/mqtt_broker.c:5611-5615,7453-7457
  • [High] [review] Rejected inbound QoS 2 messages leave a stale deduplication entrysrc/mqtt_client.c:1174-1181
  • [High] [review] Write-only publish timeouts are converted into an unbounded loopexamples/multithread/multithread.c:687-693
  • [Medium] [review] Write-only PUBREC rejection behavior contradicts the public contractsrc/mqtt_client.c:1232-1251
  • [Medium] [review] AUTH positive test bypasses CONNECT property detectiontests/test_mqtt_client.c:780-789

Review generated by Skoll

Comment thread src/mqtt_client.c
}
}
if (tmpResp) {
#ifdef WOLFMQTT_V5

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Canceled write-only publishes can bypass Receive Maximum when reused · Logic

The PR newly reserves a Receive Maximum token for nonblocking write-only publishes and stores &publish->stat in recvQuotaStat, but MqttClient_RespList_Remove only nulls that pointer without clearing stat.recvQuotaHeld. MqttClient_CancelMessage resets the message to MQTT_MSG_BEGIN, and MqttClient_NetDisconnect also removes the pending response, so reusing the canceled object with a new packet ID makes MqttClient_RecvQuotaReserve treat the stale flag as an existing reservation and send without decrementing the live/new-connection quota. With Receive Maximum 1, cancel-and-reuse or disconnect/reconnect-and-reuse can put a second PUBLISH on the wire while the server still counts the first, exceeding the advertised Receive Maximum. A late acknowledgement cannot repair the accounting since its response entry was already removed. This violates the send-quota requirement in OASIS MQTT 5.0 section 4.9. The added cancellation test only asserts the quota stays held and the response disappears; it does not cover object reuse, reconnection, or a late ACK.

Fix: Move outstanding quota ownership into connection-owned state rather than the caller's reusable message object; for an already-transmitted canceled publish, retain an acknowledgement tombstone until PUBACK/PUBCOMP or connection close, and for disconnect, clear every associated recvQuotaHeld flag since that quota belongs to the old connection. Add Receive Maximum 1 tests for cancel-then-reuse, disconnect-then-reuse, and a late acknowledgement.

Comment thread src/mqtt_broker.c
/* The write above can drive a re-entrant WS close that frees next_sub.
* Only re-validate it when a subscription was actually removed during
* the write (generation changed), so the common case stays O(1). */
if (next_sub != NULL && broker->subs_gen != subs_gen_snapshot &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reentrant subscription-list mutation guard truncates broker fan-out · Logic

The new generation guard avoids dereferencing a freed successor by breaking out of the fan-out loop, but that silently drops delivery to every still-live subscription after it, not just the freed one. For a list A -> B -> C, if writing to A services a WebSocket close for B (forwarding calls MqttPacket_Write, WebSocket writes synchronously call lws_service, and the close callback removes B's subscriptions and bumps subs_gen), next_sub is no longer linked and the new break prevents C from receiving the PUBLISH or Will even though C remained connected and subscribed; the publisher is then acknowledged as though fan-out completed. review-security rated this Medium (change_risk HIGH, blast_radius LOW) while review rated it High; kept at the stricter High given it is a confirmed message-loss bug reachable through executable broker paths. The added test only checks that an ordinary unsubscribe bumps the generation counter; it never exercises this reentrant multi-subscriber case.

Fix: Use mutation-safe iteration that can continue with the next surviving node — e.g. defer BrokerSub frees while fan-out is active and sweep tombstoned nodes afterward, or iterate over a stable snapshot. Add a deterministic re-entrant test (normal PUBLISH and Will) where the middle subscriber closes during the preceding subscriber's write and the trailing subscriber still receives exactly once.

Comment thread src/mqtt_client.c
MQTT_PACKET_TYPE_PUBLISH_ACK :
MQTT_PACKET_TYPE_PUBLISH_REC;
resp->packet_id = packet_id;
#if WOLFMQTT_MAX_QOS >= 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rejected inbound QoS 2 messages leave a stale deduplication entry · Logic

The application callback may set a v5 failure reason in publish->resp.reason_code, which is copied into the outgoing PUBREC. A PUBREC reason code at least 0x80 ends the QoS 2 exchange, so no PUBREL will arrive and the sender may reuse the packet identifier. The PR nevertheless records every QoS 2 identifier as awaiting PUBREL regardless of the reason code. That entry is never removed, and a later legitimate PUBLISH reusing the identifier is suppressed as a duplicate. There is no regression test for callback rejection followed by packet-ID reuse.

Fix: Only add the identifier when the PUBREC continues the QoS 2 exchange. Add a v5 test whose callback rejects a QoS 2 PUBLISH, verifies the negative PUBREC, then sends a fresh PUBLISH with the same identifier and verifies that the callback runs again.

Suggestion:

Suggested change
#if WOLFMQTT_MAX_QOS >= 2
if (packet_qos == MQTT_QOS_2
#ifdef WOLFMQTT_V5
&& (client->protocol_level <
MQTT_CONNECT_PROTOCOL_LEVEL_5 ||
(resp->reason_code & 0x80) == 0)
#endif
) {
MqttClient_RecvQos2_Add(client, packet_id);
}

NULL);
rc[i] = check_response(mqttCtx, rc[i], &startSec[i],
MQTT_PACKET_TYPE_PUBLISH, mqttCtx->cmd_timeout_ms);
#ifndef WOLFMQTT_TEST_CANCEL

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Write-only publish timeouts are converted into an unbounded loop · Logic

The new code converts every command timeout back to MQTT_CODE_CONTINUE. mqtt_check_timeout resets startSec when it reports a timeout, so each conversion starts another complete timeout interval. If a broker never acknowledges an otherwise-written PUBLISH, the worker now polls forever and THREAD_JOIN cannot complete without an unrelated external stop. Before this PR, the timeout left the loop and the worker reported a terminal failure.

Fix: Keep the command timeout terminal, then disconnect or otherwise preserve outstanding quota conservatively. If periodic polling is desired, retain a separate bounded overall deadline or explicit stop condition. Add a test that withholds the ACK and verifies the multithread example terminates with an error.

Comment thread src/mqtt_client.c
if (packet_type == MQTT_PACKET_TYPE_PUBLISH_REC &&
client->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 &&
(((MqttPublishResp*)packet_obj)->reason_code & 0x80)) {
#ifdef WOLFMQTT_MULTITHREAD

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Write-only PUBREC rejection behavior contradicts the public contract · api

The PR now marks the write-only publisher's PUBCOMP pending response done with MQTT_CODE_ERROR_PUBLISH_REJECTED, so its next nonblocking MqttClient_Publish_WriteOnly poll returns that error. The public documentation in wolfmqtt/mqtt_client.h still states this function never returns MQTT_CODE_ERROR_PUBLISH_REJECTED, and the adjacent source comment and ChangeLog still say the originating response is not completed and waits until timeout. The existing test inspects the pending node but never polls the public API, letting the contradiction pass.

Fix: Choose and document one contract. If returning the rejection on a later poll is intended, update the API documentation, source comment, and ChangeLog, and extend the test to poll MqttClient_Publish_WriteOnly and assert the error. Otherwise, do not complete the publisher's pending response with this code.

Comment thread tests/test_mqtt_client.c
/* Positive control: after a CONNECT that did carry an Authentication Method,
* MqttClient_Auth must not be blocked by the new guard and the AUTH must reach
* the wire. Guards against the guard over-rejecting legitimate re-auth. */
TEST(auth_with_connect_method_allowed)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AUTH positive test bypasses CONNECT property detection · test

The positive test directly assigns test_client.auth_method_set = 1, so it tests only the final guard and cannot detect a failure in the new MqttConnect_HasAuthMethod helper or its assignment in MqttClient_Connect. It also does not verify the stated reconnect behavior that a subsequent CONNECT without an Authentication Method resets the flag.

Fix: Drive a canned successful v5 CONNECT carrying an Authentication Method, then call MqttClient_Auth. Reconnect without the property and verify the same AUTH call is rejected. This covers both the new detection helper and its per-connection reset.

@aidangarske aidangarske assigned embhorn and unassigned aidangarske Aug 21, 2026
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