From ca39ae6e93ecf6a2fa74dc28a16b77d7ac24d9d0 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 11:14:07 -0500 Subject: [PATCH 01/52] Fix Coverity scan issues --- src/mqtt_packet.c | 32 +++-- src/mqtt_socket.c | 93 +++++++++------ tests/test_mqtt_packet.c | 248 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 322 insertions(+), 51 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 63dcd052b..60b2692c0 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -4481,7 +4481,8 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, int timeout_ms) { - int rc, len, remain_read = 0; + int rc, len, remain_read = 0, header_len = 0; + word32 read_len; MqttPacket* header; if (client == NULL || rx_buf == NULL) { @@ -4583,21 +4584,27 @@ int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, case MQTT_PK_READ: { - /* read remainder of packet */ + /* read remainder of packet; use locals the socket read cannot + * modify */ remain_read = client->packet.remain_len; + header_len = client->packet.header_len; client->packet.stat = MQTT_PK_READ; /* Make sure it does not overflow rx_buf */ - if (rx_buf_len < client->packet.header_len) { + if (header_len < 0 || header_len > rx_buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } - if (remain_read > rx_buf_len - client->packet.header_len) { - remain_read = rx_buf_len - client->packet.header_len; + if (remain_read < 0) { + return MqttPacket_HandleNetError(client, + MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA)); + } + if (remain_read > rx_buf_len - header_len) { + remain_read = rx_buf_len - header_len; } /* Read remaining */ if (client->packet.remain_len > 0) { - rc = MqttSocket_Read(client, &rx_buf[client->packet.header_len], + rc = MqttSocket_Read(client, &rx_buf[header_len], remain_read, timeout_ms); if (rc <= 0) { return MqttPacket_HandleNetError(client, rc); @@ -4616,14 +4623,13 @@ int MqttPacket_Read(MqttClient *client, byte* rx_buf, int rx_buf_len, /* reset state */ client->packet.stat = MQTT_PK_BEGIN; - /* Return read length. header_len and remain_read are each bounded by - * rx_buf_len above, so their sum cannot exceed it; clamp to keep the - * returned length provably in range for static analysis. */ - rc = client->packet.header_len + remain_read; - if (rc > rx_buf_len) { - rc = rx_buf_len; + /* Return read length. Both terms are non-negative ints, so the unsigned + * sum cannot wrap; the clamp is defensive. */ + read_len = (word32)header_len + (word32)remain_read; + if (read_len > (word32)rx_buf_len) { + read_len = (word32)rx_buf_len; } - return rc; + return (int)read_len; } #ifndef WOLFMQTT_NO_ERROR_STRINGS diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index e98f80ba1..20b99da9a 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -192,6 +192,22 @@ int MqttSocket_Init(MqttClient *client, MqttNet *net) return rc; } +/* Saturate rather than overflow: the broker never resets its read total. */ +static int MqttSocket_AddTotal(int total, int len) +{ + word32 sum = 0; + + if (total > 0) { + sum = (word32)total; + } + /* both terms are non-negative ints, so this cannot wrap */ + sum += (word32)len; + if (sum > (word32)0x7FFFFFFF) { + sum = (word32)0x7FFFFFFF; + } + return (int)sum; +} + static int MqttSocket_WriteDo(MqttClient *client, const byte* buf, int buf_len, int timeout_ms) { @@ -253,6 +269,7 @@ int MqttSocket_Write(MqttClient *client, const byte* buf, int buf_len, int timeout_ms) { int rc; + int remain; /* Validate arguments */ if (client == NULL || client->net == NULL || client->net->write == NULL || @@ -261,47 +278,50 @@ int MqttSocket_Write(MqttClient *client, const byte* buf, int buf_len, } /* check for buffer position overflow */ - if (client->write.pos >= buf_len) { + if (client->write.pos < 0 || client->write.pos >= buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + /* Track bytes left in a local the net callback cannot reach */ + remain = buf_len - client->write.pos; #ifdef WOLFMQTT_NONBLOCK - rc = MqttSocket_WriteDo(client, &buf[client->write.pos], - buf_len - client->write.pos, timeout_ms); + rc = MqttSocket_WriteDo(client, &buf[buf_len - remain], remain, timeout_ms); if (rc >= 0) { /* Clamp return value: write callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->write.pos) { - rc = buf_len - client->write.pos; + if (rc > remain) { + rc = remain; } - client->write.pos += rc; - client->write.total += rc; - if (client->write.pos < buf_len) { + remain -= rc; + client->write.pos = buf_len - remain; + client->write.total = MqttSocket_AddTotal(client->write.total, rc); + if (remain > 0) { rc = MQTT_CODE_CONTINUE; } } #else do { - rc = MqttSocket_WriteDo(client, &buf[client->write.pos], - buf_len - client->write.pos, timeout_ms); + rc = MqttSocket_WriteDo(client, &buf[buf_len - remain], remain, + timeout_ms); if (rc <= 0) { break; } /* Clamp return value: write callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->write.pos) { - rc = buf_len - client->write.pos; + if (rc > remain) { + rc = remain; } - client->write.pos += rc; - client->write.total += rc; - } while (client->write.pos < buf_len); + remain -= rc; + client->write.pos = buf_len - remain; + client->write.total = MqttSocket_AddTotal(client->write.total, rc); + } while (remain > 0); #endif /* WOLFMQTT_NONBLOCK */ /* handle return code */ if (rc > 0) { /* return length write and reset position */ - rc = client->write.pos; + rc = buf_len - remain; client->write.pos = 0; } @@ -371,6 +391,7 @@ static int MqttSocket_ReadDo(MqttClient *client, byte* buf, int buf_len, int MqttSocket_Read(MqttClient *client, byte* buf, int buf_len, int timeout_ms) { int rc; + int remain; /* Validate arguments */ if (client == NULL || client->net == NULL || client->net->read == NULL || @@ -379,53 +400,51 @@ int MqttSocket_Read(MqttClient *client, byte* buf, int buf_len, int timeout_ms) } /* check for buffer position overflow */ - if (client->read.pos >= buf_len) { + if (client->read.pos < 0 || client->read.pos >= buf_len) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + /* Track bytes left in a local the net callback cannot reach */ + remain = buf_len - client->read.pos; #ifdef WOLFMQTT_NONBLOCK - rc = MqttSocket_ReadDo(client, &buf[client->read.pos], - buf_len - client->read.pos, timeout_ms); + rc = MqttSocket_ReadDo(client, &buf[buf_len - remain], remain, timeout_ms); if (rc >= 0) { /* Clamp return value: read callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->read.pos) { - rc = buf_len - client->read.pos; + if (rc > remain) { + rc = remain; } - client->read.pos += rc; - client->read.total += rc; - if (client->read.pos < buf_len) { + remain -= rc; + client->read.pos = buf_len - remain; + client->read.total = MqttSocket_AddTotal(client->read.total, rc); + if (remain > 0) { rc = MQTT_CODE_CONTINUE; } } #else do { - rc = MqttSocket_ReadDo(client, &buf[client->read.pos], - buf_len - client->read.pos, timeout_ms); + rc = MqttSocket_ReadDo(client, &buf[buf_len - remain], remain, + timeout_ms); if (rc <= 0) { break; } /* Clamp return value: read callback is user-provided and may * return more than the requested length */ - if (rc > buf_len - client->read.pos) { - rc = buf_len - client->read.pos; + if (rc > remain) { + rc = remain; } - client->read.pos += rc; - client->read.total += rc; - } while (client->read.pos < buf_len); + remain -= rc; + client->read.pos = buf_len - remain; + client->read.total = MqttSocket_AddTotal(client->read.total, rc); + } while (remain > 0); #endif /* WOLFMQTT_NONBLOCK */ /* handle return code */ if (rc > 0) { /* return length read and reset position */ - rc = client->read.pos; + rc = buf_len - remain; client->read.pos = 0; - /* Never report more than the caller requested; guards against a - * read callback that returns more bytes than buf_len */ - if (rc > buf_len) { - rc = buf_len; - } } return rc; diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index d85fb6cbd..7c5cc31f5 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -312,6 +312,8 @@ typedef struct PacketReadFixture { int data_len; int pos; int calls; + int max_chunk; /* bytes delivered per read, 0 for no limit */ + int extra; /* added to the reported length to simulate over-report */ } PacketReadFixture; static int packet_read_fixture(void* context, byte* buf, int buf_len, @@ -325,12 +327,15 @@ static int packet_read_fixture(void* context, byte* buf, int buf_len, if (read_len > buf_len) { read_len = buf_len; } + if (fixture->max_chunk > 0 && read_len > fixture->max_chunk) { + read_len = fixture->max_chunk; + } if (read_len <= 0) { return MQTT_CODE_ERROR_NETWORK; } XMEMCPY(buf, fixture->data + fixture->pos, (size_t)read_len); fixture->pos += read_len; - return read_len; + return read_len + fixture->extra; } static int packet_net_connect(void* context, const char* host, word16 port, @@ -411,6 +416,242 @@ TEST(packet_read_rejects_buffer_shorter_than_max_header) MqttClient_DeInit(&client); } +static int packet_write_count(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + PacketReadFixture* fixture = (PacketReadFixture*)context; + + (void)buf; + (void)timeout_ms; + fixture->calls++; + return buf_len + fixture->extra; +} + +static int packet_io_client_init(MqttClient* client, MqttNet* net, + PacketReadFixture* fixture, byte* tx, int tx_len, byte* rx, int rx_len) +{ + XMEMSET(net, 0, sizeof(*net)); + XMEMSET(fixture, 0, sizeof(*fixture)); + net->context = fixture; + net->connect = packet_net_connect; + net->read = packet_read_fixture; + net->write = packet_write_count; + net->disconnect = packet_net_disconnect; + return MqttClient_Init(client, net, NULL, tx, tx_len, rx, rx_len, 0); +} + +/* A corrupted negative socket position must be rejected before it is used + * as a buffer index or in the remaining-length arithmetic. */ +TEST(packet_read_write_reject_negative_socket_pos) +{ + static const byte ping_resp[] = { 0xD0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = ping_resp; + fixture.data_len = (int)sizeof(ping_resp); + + client.read.pos = -1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + XMEMSET(buf, 0, sizeof(buf)); + client.write.pos = -1; + rc = MqttPacket_Write(&client, buf, 2); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + MqttClient_DeInit(&client); +} + +/* Resuming in MQTT_PK_READ with corrupted header/remaining lengths must fail + * without reading, rather than computing an out-of-range return length. */ +TEST(packet_read_rejects_corrupt_resume_state) +{ + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = -1; + client.packet.remain_len = 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = (int)sizeof(buf) + 1; + client.packet.remain_len = 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); + ASSERT_EQ(0, fixture.calls); + + client.packet.stat = MQTT_PK_READ; + client.packet.header_len = 2; + client.packet.remain_len = -1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_EQ(0, fixture.calls); + + MqttClient_DeInit(&client); +} + +/* The running byte totals saturate instead of overflowing a signed int. */ +TEST(packet_read_write_total_saturates) +{ + static const byte ping_resp[] = { 0xD0, 0x00 }; + static const byte ping_req[] = { 0xC0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte buf[8]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = ping_resp; + fixture.data_len = (int)sizeof(ping_resp); + + client.read.total = 0x7FFFFFFF - 1; + rc = MqttPacket_Read(&client, buf, (int)sizeof(buf), 0); + ASSERT_EQ((int)sizeof(ping_resp), rc); + ASSERT_EQ(0x7FFFFFFF, client.read.total); + ASSERT_EQ(0, client.read.pos); + + XMEMCPY(buf, ping_req, sizeof(ping_req)); + client.write.total = 0x7FFFFFFF; + rc = MqttPacket_Write(&client, buf, (int)sizeof(ping_req)); + ASSERT_EQ((int)sizeof(ping_req), rc); + ASSERT_EQ(0x7FFFFFFF, client.write.total); + ASSERT_EQ(0, client.write.pos); + + MqttClient_DeInit(&client); +} + +/* A callback that reports more bytes than requested must be clamped: the + * returned length stays exact and nothing outside the buffer is touched. */ +TEST(packet_read_write_clamp_over_reported_length) +{ + /* PUBLISH QoS 0: fixed header 0x30, Remaining Length 3, + * Topic Name length 0x0001, Topic Name "a" (MQTT 5.0 section 3.3) */ + static const byte publish[] = { 0x30, 0x03, 0x00, 0x01, 0x61 }; + static const byte ping_req[] = { 0xC0, 0x00 }; + byte client_rx[16]; + byte client_tx[16]; + byte guarded[10]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = publish; + fixture.data_len = (int)sizeof(publish); + fixture.extra = 5; + + XMEMSET(guarded, 0xA5, sizeof(guarded)); + rc = MqttPacket_Read(&client, &guarded[1], (int)sizeof(guarded) - 2, 0); + ASSERT_EQ((int)sizeof(publish), rc); + ASSERT_EQ(0, XMEMCMP(&guarded[1], publish, sizeof(publish))); + ASSERT_EQ(0xA5, guarded[0]); + ASSERT_EQ(0xA5, guarded[sizeof(publish) + 1]); + ASSERT_EQ(0xA5, guarded[sizeof(guarded) - 1]); + ASSERT_EQ(0, client.read.pos); + ASSERT_EQ((int)sizeof(publish), fixture.pos); + + XMEMCPY(guarded, ping_req, sizeof(ping_req)); + rc = MqttPacket_Write(&client, guarded, (int)sizeof(ping_req)); + ASSERT_EQ((int)sizeof(ping_req), rc); + ASSERT_EQ(0, client.write.pos); + + MqttClient_DeInit(&client); +} + +/* One byte per callback: blocking builds reassemble the packet in a single + * MqttPacket_Read; non-blocking builds resume from read.pos on each call. */ +TEST(packet_read_reassembles_one_byte_reads) +{ + /* PUBLISH QoS 0 with Remaining Length 128, encoded as the two-byte + * Variable Byte Integer 0x80 0x01 [MQTT-1.5.5]. Variable header is + * Topic Name length 0x0001 and "a"; the remaining 125 bytes are + * payload (MQTT 5.0 section 3.3). */ + byte publish[3 + 128]; + byte client_rx[16]; + byte client_tx[16]; + byte rx[160]; + MqttClient client; + MqttNet net; + PacketReadFixture fixture; + int rc; + int i; +#ifdef WOLFMQTT_NONBLOCK + int prev_pos = 0; + int iters = 0; +#endif + + publish[0] = 0x30; + publish[1] = 0x80; + publish[2] = 0x01; + publish[3] = 0x00; + publish[4] = 0x01; + publish[5] = 0x61; + for (i = 6; i < (int)sizeof(publish); i++) { + publish[i] = (byte)i; + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, packet_io_client_init(&client, &net, + &fixture, client_tx, (int)sizeof(client_tx), client_rx, + (int)sizeof(client_rx))); + fixture.data = publish; + fixture.data_len = (int)sizeof(publish); + fixture.max_chunk = 1; + + XMEMSET(rx, 0, sizeof(rx)); +#ifdef WOLFMQTT_NONBLOCK + do { + rc = MqttPacket_Read(&client, rx, (int)sizeof(rx), 0); + if (rc == MQTT_CODE_CONTINUE) { + /* progress either continues the current read or starts the + * next one */ + ASSERT_TRUE(client.read.pos == prev_pos + 1 || + client.read.pos == 1); + prev_pos = client.read.pos; + } + iters++; + } while (rc == MQTT_CODE_CONTINUE && iters <= (int)sizeof(publish)); +#else + rc = MqttPacket_Read(&client, rx, (int)sizeof(rx), 0); +#endif + ASSERT_EQ((int)sizeof(publish), rc); + ASSERT_EQ(0, XMEMCMP(rx, publish, sizeof(publish))); + ASSERT_EQ(0, client.read.pos); + ASSERT_EQ((int)sizeof(publish), fixture.calls); + + MqttClient_DeInit(&client); +} + /* ============================================================================ * UTF-8 well-formedness validation [MQTT-1.5.3-1] * @@ -7032,6 +7273,11 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_vbi_overlong_2byte_127); RUN_TEST(encode_decode_vbi_roundtrip); RUN_TEST(packet_read_rejects_buffer_shorter_than_max_header); + RUN_TEST(packet_read_write_reject_negative_socket_pos); + RUN_TEST(packet_read_rejects_corrupt_resume_state); + RUN_TEST(packet_read_write_total_saturates); + RUN_TEST(packet_read_write_clamp_over_reported_length); + RUN_TEST(packet_read_reassembles_one_byte_reads); /* UTF-8 well-formedness validation [MQTT-1.5.3-1] */ #ifdef WOLFMQTT_BROKER From e7347bbd6d7feb154d2635b6be4a5ebcd968e88b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 11:28:34 -0500 Subject: [PATCH 02/52] F-15990 - Avoid double close of a rejected broker socket --- src/mqtt_broker.c | 2 ++ tests/test_broker_connect.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 0e0d6df05..f096862b6 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2767,6 +2767,8 @@ static BrokerClient* BrokerClient_Add(MqttBroker* broker, #endif } else if (bc != NULL) { + /* The caller closes the rejected socket */ + bc->sock = BROKER_SOCKET_INVALID; BrokerClient_Free(bc); bc = NULL; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 5844231b9..da1f1243d 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -176,6 +176,7 @@ typedef struct MockClient { byte out_buf[MOCK_BUF_SZ]; size_t out_len; int closed; + int close_count; /* net->close calls on this socket */ int read_err; /* when set, mock_read returns a network error (peer RST) */ int write_err; /* when set, mock_write returns a network error */ int write_continue; /* when set, mock_write reports would-block */ @@ -353,6 +354,7 @@ static int mock_close(void* ctx, BROKER_SOCKET_T sock) (void)ctx; if (idx >= 0) { g_clients[idx].closed = 1; + g_clients[idx].close_count++; } else if (sock == MOCK_LISTEN_SOCK) { g_listen_close_count++; @@ -1386,6 +1388,34 @@ TEST(broker_start_partial_listener_failure_cleans_up) broker.tls_ctx = NULL; MqttBroker_Free(&broker); } + +/* A TLS accept with no tls_ctx fails BrokerClient_Add after the socket was + * recorded in the client. The caller closes the rejected socket, so the + * failed add must not close it too; a second close could hit a reused fd. */ +TEST(broker_add_failure_closes_socket_once) +{ + MqttBroker broker; + MqttBrokerNet net; + + install_mock_net(&net); + reset_mock_clients(1); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + ASSERT_TRUE(broker.tls_ctx == NULL); + + /* Route the next accept through the TLS listener only */ + broker.listen_sock = BROKER_SOCKET_INVALID; + broker.listen_sock_tls = MOCK_LISTEN_SOCK; + + MqttBroker_Step(&broker); + ASSERT_EQ(1, g_accept_count); + ASSERT_EQ(1, g_clients[0].close_count); + + broker.listen_sock = MOCK_LISTEN_SOCK; + broker.listen_sock_tls = BROKER_SOCKET_INVALID; + MqttBroker_Free(&broker); +} #endif /* Defense in depth: the MqttBroker struct is public, so a caller can set @@ -12628,6 +12658,7 @@ int main(int argc, char** argv) #endif #ifdef ENABLE_MQTT_TLS RUN_TEST(broker_start_partial_listener_failure_cleans_up); + RUN_TEST(broker_add_failure_closes_socket_once); #endif #ifdef WOLFMQTT_V5 RUN_TEST(connect_v5_emptyid_assigned_id_emitted); From 6b72d900b3e8e422e23df8cecf5efceb18dce7ec Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:00:14 -0500 Subject: [PATCH 03/52] F-15405 - Publish the Will for non-zero v5 DISCONNECT reason codes --- src/mqtt_broker.c | 18 +++--- tests/test_broker_connect.c | 106 ++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index f096862b6..85e761d64 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -9156,13 +9156,14 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) bc->session_expiry_sec = se_prop->data_int; } #ifdef WOLFMQTT_BROKER_WILL - /* [MQTT-3.14.4-3] Reason 0x04 requests Will publish. */ - if (disc.reason_code == - MQTT_REASON_DISCONNECT_W_WILL_MSG) { - BrokerClient_PublishWill(broker, bc); + /* [MQTT-3.14.4-3] Only Reason 0x00 discards the + * Will; any other code publishes it (MQTT 5.0 + * section 3.1.2.5). */ + if (disc.reason_code == MQTT_REASON_SUCCESS) { + BrokerClient_ClearWill(bc); } else { - BrokerClient_ClearWill(bc); + BrokerClient_PublishWill(broker, bc); } #endif } @@ -9176,12 +9177,11 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) "broker: DISCONNECT properties dropped, out of " "memory sock=%d", (int)bc->sock); #ifdef WOLFMQTT_BROKER_WILL - if (disc.reason_code == - MQTT_REASON_DISCONNECT_W_WILL_MSG) { - BrokerClient_PublishWill(broker, bc); + if (disc.reason_code == MQTT_REASON_SUCCESS) { + BrokerClient_ClearWill(bc); } else { - BrokerClient_ClearWill(bc); + BrokerClient_PublishWill(broker, bc); } #endif } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index da1f1243d..c36dfeac2 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -5343,6 +5343,97 @@ TEST(disconnect_v5_malformed_fires_will) MqttBroker_Free(&broker); } +/* Connect a v5 publisher with a Will and a v3.1.1 subscriber to its Will + * topic, then send a well-formed DISCONNECT carrying only reason_code. + * Returns the number of PUBLISH packets the subscriber received. */ +static int disconnect_v5_reason_wills(byte reason_code, int* closed) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* Same v5 CONNECT with Will as disconnect_v5_malformed_fires_will. */ + static const byte pub_connect[] = { + 0x10, 0x19, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x06, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 DISCONNECT (MQTT 5.0 section 3.14): Remaining Length 1, Reason + * Code only, Property Length omitted (section 3.14.2.2.1). */ + byte disconnect[] = { 0xE0, 0x01, 0x00 }; + disconnect[2] = reason_code; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(2); + mock_client_input_append(0, sub_connect, sizeof(sub_connect)); + mock_client_input_append(0, sub_subscribe, sizeof(sub_subscribe)); + mock_client_input_append(1, pub_connect, sizeof(pub_connect)); + mock_client_input_append(1, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + publishes = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + *closed = g_clients[1].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return publishes; +} + +/* Only Reason Code 0x00 discards the Will [MQTT-3.14.4-3]. */ +TEST(disconnect_v5_reason_0_discards_will) +{ + int closed = 0; + + ASSERT_EQ(0, disconnect_v5_reason_wills(MQTT_REASON_SUCCESS, &closed)); + ASSERT_TRUE(closed); +} + +TEST(disconnect_v5_reason_4_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, disconnect_v5_reason_wills( + MQTT_REASON_DISCONNECT_W_WILL_MSG, &closed)); + ASSERT_TRUE(closed); +} + +/* Any other Reason Code is not a normal disconnection, so the Will is + * published (MQTT 5.0 section 3.1.2.5, [MQTT-3.1.2-10]). */ +TEST(disconnect_v5_reason_error_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, disconnect_v5_reason_wills(MQTT_REASON_UNSPECIFIED_ERR, + &closed)); + ASSERT_TRUE(closed); +} + #ifndef WOLFMQTT_DYN_PROP /* Connect a v5 publisher with a Will and a v3.1.1 subscriber to its Will * topic, leave the shared property pool one slot short of the publisher's @@ -5463,6 +5554,17 @@ TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will) MQTT_REASON_DISCONNECT_W_WILL_MSG, &closed)); ASSERT_TRUE(closed); } + +/* An error Reason Code publishes the Will when the DISCONNECT Properties + * cannot be allocated (MQTT 5.0 section 3.1.2.5). */ +TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, disconnect_v5_props_pool_exhausted_wills( + MQTT_REASON_UNSPECIFIED_ERR, &closed)); + ASSERT_TRUE(closed); +} #endif /* !WOLFMQTT_DYN_PROP */ #endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ @@ -12747,9 +12849,13 @@ int main(int argc, char** argv) #endif #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) RUN_TEST(disconnect_v5_malformed_fires_will); + RUN_TEST(disconnect_v5_reason_0_discards_will); + RUN_TEST(disconnect_v5_reason_4_publishes_will); + RUN_TEST(disconnect_v5_reason_error_publishes_will); #ifndef WOLFMQTT_DYN_PROP RUN_TEST(disconnect_v5_props_pool_exhausted_reason_0_discards_will); RUN_TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will); + RUN_TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will); #endif #endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) From 6217995984e2f60c49614b906fea5a545b4d91a6 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:03:55 -0500 Subject: [PATCH 04/52] F-15988 - Publish the old v5 Will on session takeover when required --- src/mqtt_broker.c | 14 +++- tests/test_broker_connect.c | 138 ++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 85e761d64..494adce52 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -7309,7 +7309,19 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, else { BrokerSend_Disconnect(old, MQTT_REASON_SESSION_TAKEN_OVER); - BrokerClient_ClearWill(old); +#ifdef WOLFMQTT_BROKER_WILL + /* MQTT 5.0 section 3.1.4: a resumed Session suppresses a + * delayed Will [MQTT-3.1.3-9]; an ending Session or a zero + * Will Delay publishes it now. */ + if (old->will_delay_sec > 0 && !mc.clean_session && + old->session_expiry_sec != 0) { + BrokerClient_ClearWill(old); + } + else { + old->will_delay_sec = 0; + BrokerClient_PublishWill(broker, old); + } +#endif } #else BrokerClient_PublishWill(broker, old); diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index c36dfeac2..5fe1bebdd 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -5434,6 +5434,140 @@ TEST(disconnect_v5_reason_error_publishes_will) ASSERT_TRUE(closed); } +/* Connect a v3.1.1 subscriber to "lwt" and a v5 client "P" with a Will on + * "lwt", then take over "P" from a second v5 connection. Returns the number + * of PUBLISH packets the subscriber received by the time the Will Delay of + * the old connection would have elapsed. */ +static int takeover_v5_wills(word32 session_expiry, word32 will_delay, + int new_clean_start, int* old_closed) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x08, + 0x00, 0x01, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT (MQTT 5.0 section 3.1): flags 0x04 (Will, Clean Start 0), + * keep alive 60, Properties length 5 holding Session Expiry Interval + * (0x11, four byte integer), ClientId "P", Will Properties length 5 + * holding Will Delay Interval (0x18, four byte integer), Will Topic + * "lwt", Will Payload "bye". Remaining Length 35. */ + byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 CONNECT, no Will, Properties length 0, ClientId "P". Flags byte is + * 0x02 for Clean Start 1 or 0x00 for Clean Start 0. Remaining Length 14. */ + byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + old_connect[14] = (byte)(session_expiry >> 24); + old_connect[15] = (byte)(session_expiry >> 16); + old_connect[16] = (byte)(session_expiry >> 8); + old_connect[17] = (byte)session_expiry; + old_connect[23] = (byte)(will_delay >> 24); + old_connect[24] = (byte)(will_delay >> 16); + old_connect[25] = (byte)(will_delay >> 8); + old_connect[26] = (byte)will_delay; + if (new_clean_start) { + new_connect[9] = 0x02; + } + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(3); + mock_client_input_append(0, sub_connect, sizeof(sub_connect)); + mock_client_input_append(0, sub_subscribe, sizeof(sub_subscribe)); + mock_client_input_append(1, old_connect, sizeof(old_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(2, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Let any deferred Will come due */ + g_broker_time_s += will_delay + 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + publishes = count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH); + *old_closed = g_clients[1].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return publishes; +} + +/* MQTT 5.0 section 3.1.4: with a zero Will Delay the old Will is published + * because its Network Connection is closed, even though the Session + * survives the takeover. */ +TEST(takeover_v5_zero_will_delay_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(60, 0, 0, &closed)); + ASSERT_TRUE(closed); +} + +/* The Session resumes before the Will Delay elapses, so the Will is not + * sent [MQTT-3.1.3-9]. */ +TEST(takeover_v5_will_delay_resumed_session_discards_will) +{ + int closed = 0; + + ASSERT_EQ(0, takeover_v5_wills(60, 30, 0, &closed)); + ASSERT_TRUE(closed); +} + +/* Clean Start 1 ends the old Session at takeover, which publishes the Will + * ahead of its delay (MQTT 5.0 sections 3.1.3.2.2 and 3.1.4). */ +TEST(takeover_v5_clean_start_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(60, 30, 1, &closed)); + ASSERT_TRUE(closed); +} + +/* A zero Session Expiry ends the old Session at takeover, so the Will is + * published (MQTT 5.0 section 3.1.4). */ +TEST(takeover_v5_zero_session_expiry_publishes_will) +{ + int closed = 0; + + ASSERT_EQ(1, takeover_v5_wills(0, 30, 0, &closed)); + ASSERT_TRUE(closed); +} + #ifndef WOLFMQTT_DYN_PROP /* Connect a v5 publisher with a Will and a v3.1.1 subscriber to its Will * topic, leave the shared property pool one slot short of the publisher's @@ -12852,6 +12986,10 @@ int main(int argc, char** argv) RUN_TEST(disconnect_v5_reason_0_discards_will); RUN_TEST(disconnect_v5_reason_4_publishes_will); RUN_TEST(disconnect_v5_reason_error_publishes_will); + RUN_TEST(takeover_v5_zero_will_delay_publishes_will); + RUN_TEST(takeover_v5_will_delay_resumed_session_discards_will); + RUN_TEST(takeover_v5_clean_start_publishes_will); + RUN_TEST(takeover_v5_zero_session_expiry_publishes_will); #ifndef WOLFMQTT_DYN_PROP RUN_TEST(disconnect_v5_props_pool_exhausted_reason_0_discards_will); RUN_TEST(disconnect_v5_props_pool_exhausted_reason_4_publishes_will); From d043220e2dbb3ad8fcb371e64823af6b68632e67 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:08:47 -0500 Subject: [PATCH 05/52] F-15376 - Persist subscriptions when the SUBACK or UNSUBACK is deferred --- src/mqtt_broker.c | 12 ++-- tests/test_broker_connect.c | 140 ++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 494adce52..ce0d3b304 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -7779,8 +7779,10 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, #ifdef WOLFMQTT_BROKER_PERSIST /* Shadow-write the full subscription list for this client. Only * meaningful for clean_session=0 sessions; the persist layer no-ops - * when no hooks are installed. */ - if (rc > 0 && bc->session_expiry_sec != 0 && + * when no hooks are installed. A deferred SUBACK still committed the + * subscriptions, so it is persisted too. */ + if ((rc > 0 || rc == MQTT_CODE_CONTINUE) && + bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } @@ -7868,8 +7870,10 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, } #ifdef WOLFMQTT_BROKER_PERSIST - /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs). */ - if (rc > 0 && bc->session_expiry_sec != 0 && + /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs), including + * when the UNSUBACK write is deferred. */ + if ((rc > 0 || rc == MQTT_CODE_CONTINUE) && + bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 5fe1bebdd..ae62e4a1a 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -11146,6 +11146,145 @@ TEST(persist_restore_packet_id_wrap_preserves_fifo) MqttBroker_Free(&restored_again); MqttBroker_Free(&source); } + +#ifdef WOLFMQTT_NONBLOCK +typedef struct PersistSubsCount { + int puts; + int dels; +} PersistSubsCount; + +static int persist_subs_count_put(void* ctx, byte ns, const byte* key, + word16 key_len, const byte* blob, word32 blob_len) +{ + (void)key; + (void)key_len; + (void)blob; + (void)blob_len; + if (ns == BROKER_PERSIST_NS_SUBS) { + ((PersistSubsCount*)ctx)->puts++; + } + return MQTT_CODE_SUCCESS; +} + +static int persist_subs_count_get(void* ctx, byte ns, const byte* key, + word16 key_len, byte* out, word32* inout_len) +{ + (void)ctx; + (void)ns; + (void)key; + (void)key_len; + (void)out; + (void)inout_len; + return MQTT_CODE_ERROR_NOT_FOUND; +} + +static int persist_subs_count_del(void* ctx, byte ns, const byte* key, + word16 key_len) +{ + (void)key; + (void)key_len; + if (ns == BROKER_PERSIST_NS_SUBS) { + ((PersistSubsCount*)ctx)->dels++; + } + return MQTT_CODE_SUCCESS; +} + +static int persist_subs_count_iter(void* ctx, byte ns, + MqttBrokerPersist_IterCb cb, void* cb_ctx) +{ + (void)ctx; + (void)ns; + (void)cb; + (void)cb_ctx; + return MQTT_CODE_SUCCESS; +} + +/* A SUBACK or UNSUBACK whose write is deferred must still shadow-write the + * persistent session's subscription list; otherwise a restart restores the + * list from before the change. */ +TEST(persist_subs_written_when_ack_deferred) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistSubsCount count; + int i; + int puts_before; + int dels_before; + /* v3.1.1 CONNECT, CleanSession=0, keep alive 60, ClientId "S" */ + static const byte connect_s[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 1 (MQTT 3.1.1 section 3.8) */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x01 + }; + /* UNSUBSCRIBE packet id 2, filter "x" (MQTT 3.1.1 section 3.10) */ + static const byte unsubscribe_x[] = { + 0xA2, 0x05, 0x00, 0x02, 0x00, 0x01, 'x' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&count, 0, sizeof(count)); + hooks.kv_put = persist_subs_count_put; + hooks.kv_get = persist_subs_count_get; + hooks.kv_del = persist_subs_count_del; + hooks.kv_iter = persist_subs_count_iter; + hooks.ctx = &count; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&broker, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect_s, sizeof(connect_s)); + run_broker_one_connect(&broker); + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_CONNECT_ACK)); + + /* SUBACK write defers */ + puts_before = count.puts; + g_clients[0].write_continue = 1; + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(puts_before + 1, count.puts); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_SUBSCRIBE_ACK)); + + /* UNSUBACK write defers; the now empty list is deleted */ + dels_before = count.dels; + g_clients[0].write_continue = 1; + mock_client_input_append(0, unsubscribe_x, sizeof(unsubscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(dels_before + 1, count.dels); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_UNSUBSCRIBE_ACK)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK */ #endif static int persist_test_stream_open(void* ctx, byte ns, const byte* key, @@ -13203,6 +13342,7 @@ int main(int argc, char** argv) #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(persist_restore_packet_id_wrap_preserves_fifo); #ifdef WOLFMQTT_NONBLOCK + RUN_TEST(persist_subs_written_when_ack_deferred); RUN_TEST(persist_partial_publish_restart_keeps_dup); #endif RUN_TEST(persist_mixed_qos_queue_preserves_fifo); From 9ee1d89199eebad0c7d7584c8e39ae907faf5aef Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:13:47 -0500 Subject: [PATCH 06/52] F-15989 - Release the inbound QoS 2 id when the PUBLISH topic copy fails --- src/mqtt_broker.c | 7 ++++ tests/test_broker_connect.c | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index ce0d3b304..d51ec95b0 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -8057,6 +8057,13 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, * delivered. */ WBLOG_ERR(broker, "broker: PUBLISH topic alloc failed sock=%d", (int)bc->sock); + #if WOLFMQTT_MAX_QOS >= 2 + /* Nothing was delivered, so the retransmission must not be + * treated as a duplicate [MQTT-4.3.3-2]. */ + if (pub.qos == MQTT_QOS_2 && !qos2_duplicate) { + BrokerInboundQos2_Remove(bc, pub.packet_id); + } + #endif rc = MQTT_CODE_ERROR_MEMORY; goto publish_cleanup; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ae62e4a1a..fafb4a322 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -2918,6 +2918,86 @@ TEST(qos2_publish_then_abrupt_close_offline_subscriber) MqttBroker_Free(&broker); } +#ifndef WOLFMQTT_STATIC_MEMORY +/* A QoS 2 PUBLISH whose topic copy fails was never delivered, so its Packet + * Identifier must not stay recorded as awaiting PUBREL. Otherwise the + * retransmission after reconnect is treated as a duplicate and acknowledged + * without delivery, losing the message [MQTT-4.3.3-2]. */ +TEST(qos2_topic_alloc_failure_retransmit_delivered) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 0 */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x00 + }; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* PUBLISH QoS 2 (MQTT 3.1.1 section 3.3): topic "x", packet id 1, + * payload "hi". */ + static const byte publish_q2[] = { + 0x34, 0x07, 0x00, 0x01, 'x', 0x00, 0x01, 'h', 'i' + }; + /* Same PUBLISH retransmitted with DUP set [MQTT-4.4.0-1] */ + static const byte publish_q2_dup[] = { + 0x3C, 0x07, 0x00, 0x01, 'x', 0x00, 0x01, 'h', 'i' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* The inbound QoS 2 record allocates first and succeeds; the topic + * copy is the next allocation and fails. */ + broker_test_fail_alloc_after(1); + mock_client_input_append(1, publish_q2, sizeof(publish_q2)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + broker_test_disable_alloc_failure(); + ASSERT_EQ(1, g_alloc_failure_count); + ASSERT_TRUE(g_clients[1].closed); + ASSERT_EQ(0, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + /* Resume the Session and retransmit */ + mock_client_input_append(2, connect_pub, sizeof(connect_pub)); + mock_client_input_append(2, publish_q2_dup, sizeof(publish_q2_dup)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH_REC)); + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + /* The out_q-cap tests are dynamic-memory-only (they inspect out_q_count and * the linked client list); the v5 variant additionally uses the v5-only * MQTT_REASON_QUOTA_EXCEEDED. Guard accordingly so the non-V5 and @@ -13057,6 +13137,7 @@ int main(int argc, char** argv) RUN_TEST(qos2_publish_with_offline_durable_subscriber); RUN_TEST(qos2_publish_then_abrupt_close_offline_subscriber); #ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(qos2_topic_alloc_failure_retransmit_delivered); #ifdef WOLFMQTT_V5 RUN_TEST(online_qos1_flood_disconnects_slow_v5_subscriber); #endif /* WOLFMQTT_V5 */ From d790647e6ecc0245770b715fd3303b1b5d8adb95 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:18:02 -0500 Subject: [PATCH 07/52] F-15991 - Keep the CONNECT Session Expiry when reclaiming an orphan --- src/mqtt_broker.c | 9 ++-- tests/test_broker_connect.c | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index d51ec95b0..91a760e4a 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3945,9 +3945,6 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) o->out_q_tail = NULL; o->out_q_count = 0; o->out_q_inflight = 0; - if (new_bc->session_expiry_sec == 0xFFFFFFFFu) { - new_bc->session_expiry_sec = o->session_expiry_sec; - } #if WOLFMQTT_MAX_QOS >= 2 /* Move QoS 2 dedup state back before any new PUBLISH is processed. */ new_bc->qos2_pending = o->qos2_pending; @@ -7451,9 +7448,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #else else { /* Persistent session reconnect: pick up any queued messages - * left by the prior incarnation. Inherits session_expiry - * from the orphan when this CONNECT did not specify one. - * Drain is invoked further down, after CONNACK is sent. */ + * left by the prior incarnation. The Session Expiry Interval + * comes from this CONNECT [MQTT-3.1.2.11.2]. Drain is invoked + * further down, after CONNACK is sent. */ if (BrokerOrphan_Reclaim(broker, bc)) { session_present = 1; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index fafb4a322..636cb1200 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -5782,6 +5782,95 @@ TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will) #endif /* !WOLFMQTT_DYN_PROP */ #endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* Leave a v5 Session for "P" with Session Expiry 60, resume it with + * reconnect, and return the Session Expiry Interval the resumed client + * ends up with (0 on setup failure). */ +static word32 reclaim_session_expiry(const byte* reconnect, + size_t reconnect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* bc; + word32 expiry = 0; + int i; + /* v5 CONNECT (MQTT 5.0 section 3.1): Clean Start 0, keep alive 60, + * Properties length 5 holding Session Expiry Interval 60 (0x11), + * ClientId "P". Remaining Length 19. */ + static const byte connect_60[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* v5 DISCONNECT, Remaining Length 0: Reason Code 0x00 */ + static const byte disconnect[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return 0; + } + + reset_mock_clients(2); + mock_client_input_append(0, connect_60, sizeof(connect_60)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + if (broker.orphan_sessions != NULL && + broker.orphan_sessions->session_expiry_sec == 60) { + mock_client_input_append(1, reconnect, reconnect_len); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + bc = find_broker_client(&broker, "P"); + if (bc != NULL && broker.orphan_sessions == NULL) { + expiry = bc->session_expiry_sec; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return expiry; +} + +/* [MQTT-3.1.2.11.2] The resuming CONNECT's explicit 0xFFFFFFFF means the + * Session never expires; it must not be replaced by the old interval. */ +TEST(reclaim_keeps_explicit_v5_never_expire) +{ + /* As connect_60 in reclaim_session_expiry, with Session Expiry + * 0xFFFFFFFF */ + static const byte connect_never[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_never, + sizeof(connect_never))); +} + +/* A v3.1.1 CleanSession=0 Session never expires (MQTT 3.1.1 section + * 3.1.2.4), even when it resumes a Session created over v5. */ +TEST(reclaim_v311_resume_never_expires) +{ + static const byte connect_v311[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_v311, + sizeof(connect_v311))); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + /* Parsed details of a single PUBLISH packet on the wire. found = 0 means * no PUBLISH was present in the buffer; subsequent fields are zero. */ typedef struct { @@ -13216,6 +13305,10 @@ int main(int argc, char** argv) RUN_TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will); #endif #endif +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(reclaim_keeps_explicit_v5_never_expire); + RUN_TEST(reclaim_v311_resume_never_expires); +#endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_will_scrub_after_failed_write); #ifdef WOLFMQTT_NONBLOCK From 351a4648f026463aa42d8eaf3f99fd0614269730 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:23:19 -0500 Subject: [PATCH 08/52] F-14357 - Send the first payload chunk when the PUBLISH header fills tx_buf --- src/mqtt_client.c | 4 ++- tests/test_mqtt_client.c | 66 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 60983c190..9b1828101 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -3938,7 +3938,9 @@ static int MqttClient_Publish_WritePayload(MqttClient *client, } while (publish->buffer_pos < publish->total_len); } else if (publish->buffer_pos < publish->total_len) { - if (publish->buffer_pos > 0) { + /* buffer_pos is 0 when the header filled tx_buf; build the first + * payload chunk then too. */ + if (publish->buffer != NULL) { client->write.len = (publish->total_len - publish->buffer_pos); if (client->write.len > client->tx_buf_len) { client->write.len = client->tx_buf_len; diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index bfa74e224..1e29fb087 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -5544,6 +5544,71 @@ static int connect_wire_props(const byte* wire, int len, const byte** props) return prop_len; } +/* A PUBLISH whose fixed and variable header exactly fill tx_buf leaves no room + * for payload in the first write. The whole payload must then follow in later + * writes instead of the client issuing an empty write after the header. */ +TEST(publish_header_fills_tx_buf_sends_payload) +{ + int rc; + int i; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + /* CONNACK v3.1.1: type=0x20, remain=2, flags=0x00, return_code=0x00. */ + static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + /* MQTT 3.1.1 section 3.3: PUBLISH QoS 0, Remaining Length 8, Topic Name + * "t" (length 1), no Packet Identifier, payload "hello". */ + static const byte expected[] = { + 0x30, 0x08, 0x00, 0x01, 't', 'h', 'e', 'l', 'l', 'o' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "test_client"; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + /* Fixed header (2) plus Topic Name (3) exactly fill tx_buf */ + test_client.tx_buf_len = 5; + test_net.write = mock_net_write_capture; + g_connect_wire_len = 0; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && (rc == MQTT_CODE_CONTINUE || + rc == MQTT_CODE_PUB_CONTINUE); i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + test_client.tx_buf_len = TEST_TX_BUF_SIZE; + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ((int)sizeof(expected), g_connect_wire_len); + ASSERT_EQ(0, XMEMCMP(expected, g_connect_wire, sizeof(expected))); +} + /* [MQTT-3.3.4] The inbound QoS 2 dedup table holds MQTT_MAX_RECV_QOS2 packet * ids; without an advertised Receive Maximum a server may keep more QoS 2 * PUBLISHes un-PUBRELed than fit, and the ids that go untracked lose duplicate @@ -8165,6 +8230,7 @@ void run_mqtt_client_tests(void) RUN_TEST(connect_clientid_change_clears_qos2_dedup); RUN_TEST(connect_clean1_clears_qos2_dedup); RUN_TEST(connect_resume_declined_clears_qos2_dedup); + RUN_TEST(publish_header_fills_tx_buf_sends_payload); RUN_TEST(connect_v5_advertises_receive_max); RUN_TEST(connect_v5_app_receive_max_preserved); #endif From 638f0b846e3278dc1aa5a60e29e5ff97d1cecf32 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:28:26 -0500 Subject: [PATCH 09/52] F-15377 - Report a zero-byte blocking socket write as a network error --- src/mqtt_socket.c | 5 ++++ tests/test_mqtt_client.c | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index 20b99da9a..2f028f941 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -316,6 +316,11 @@ int MqttSocket_Write(MqttClient *client, const byte* buf, int buf_len, client->write.pos = buf_len - remain; client->write.total = MqttSocket_AddTotal(client->write.total, rc); } while (remain > 0); + /* A blocking write that makes no progress cannot finish the packet; + * returning 0 would read as success to callers. */ + if (rc == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_NETWORK); + } #endif /* WOLFMQTT_NONBLOCK */ /* handle return code */ diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 1e29fb087..d561384b0 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -3103,6 +3103,59 @@ TEST(wait_message_puback_survives_shared_ack_overwrite) } #endif /* WOLFMQTT_MULTITHREAD && WOLFMQTT_NONBLOCK && WOLFMQTT_MAX_QOS >= 1 */ +#if !defined(WOLFMQTT_NONBLOCK) && WOLFMQTT_MAX_QOS >= 1 +static int g_zero_write_calls; + +/* A blocking transport that accepts nothing, e.g. a send interrupted before + * any byte left. */ +static int mock_net_write_zero(void *context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; (void)buf; (void)buf_len; (void)timeout_ms; + g_zero_write_calls++; + return 0; +} + +static int mock_msg_cb_zero_write(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; (void)msg; (void)msg_new; (void)msg_done; + return MQTT_CODE_SUCCESS; +} + +/* A blocking PUBACK write that sends zero bytes did not acknowledge the + * PUBLISH [MQTT-4.3.2-2], so it must be reported as a network error instead + * of success. */ +TEST(wait_message_zero_byte_puback_write_is_error) +{ + int rc; + /* v3.1.1 QoS 1 PUBLISH: topic "a", packet id 1, payload "h". */ + static const byte publish_id1[] = { + 0x32, 0x06, 0x00, 0x01, 'a', 0x00, 0x01, 'h' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = mock_msg_cb_zero_write; + + g_zero_write_calls = 0; + test_net.write = mock_net_write_zero; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, publish_id1, sizeof(publish_id1)); + g_canned_len = (int)sizeof(publish_id1); + g_canned_pos = 0; + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + + ASSERT_EQ(1, g_zero_write_calls); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); +} +#endif /* !WOLFMQTT_NONBLOCK && WOLFMQTT_MAX_QOS >= 1 */ + /* MqttClient_WaitType dispatches on the stat of the object it is handed, which * for the CONNACK wait is mc_connect->ack. Left at MQTT_MSG_PAYLOAD after a * completed handshake, a second MqttClient_Connect on the same MqttConnect @@ -8137,6 +8190,9 @@ void run_mqtt_client_tests(void) #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_NONBLOCK) && \ WOLFMQTT_MAX_QOS >= 1 RUN_TEST(wait_message_puback_survives_shared_ack_overwrite); +#endif +#if !defined(WOLFMQTT_NONBLOCK) && WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(wait_message_zero_byte_puback_write_is_error); #endif RUN_TEST(net_connect_clears_connect_sent_flag); RUN_TEST(publish_after_disconnect_rejected); From 00a1f6155995a83846ebeeb535cd757369f4f120 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:30:50 -0500 Subject: [PATCH 10/52] F-15378 - Add MQTT-SN test for a zero-byte automatic response write --- tests/test_mqtt_sn_client.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index c0cec1809..366c5c065 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -2707,6 +2707,42 @@ TEST(sn_publish_qos1_zero_write_returns_network_error) ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); ASSERT_NO_PENDRESP(); } + +static int g_zero_puback_publish_calls; + +static int sn_count_incoming_publish(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg; + (void)msg_new; + (void)msg_done; + g_zero_puback_publish_calls++; + return MQTT_CODE_SUCCESS; +} + +/* MQTT-SN 1.2 section 5.4.4: a QoS 1 PUBLISH is acknowledged with PUBACK. A + * blocking PUBACK write that sends zero bytes did not acknowledge it, so it + * must be reported as a network error instead of success. */ +TEST(sn_publish_auto_reply_zero_write_returns_network_error) +{ + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + g_client.msg_cb = sn_count_incoming_publish; + g_zero_puback_publish_calls = 0; + g_mock.write_zero_count = 1; + g_mock.write_zero_type = SN_MSG_TYPE_PUBACK; + mock_net_push(&g_mock, SN_PUBLISH_QOS1_FRAME, + (int)sizeof(SN_PUBLISH_QOS1_FRAME)); + + rc = SN_Client_WaitMessage(&g_client, 1000); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(1, g_zero_puback_publish_calls); + ASSERT_EQ(0, g_mock.write_zero_count); /* the PUBACK write was tried */ + ASSERT_EQ(0, g_mock.out_len); +} #endif /* !WOLFMQTT_NONBLOCK */ /* ============================================================================ @@ -3278,6 +3314,7 @@ int main(int argc, char** argv) RUN_TEST(sn_connect_willmsg_zero_write_returns_network_error); RUN_TEST(sn_willmsgupd_zero_write_returns_network_error); RUN_TEST(sn_publish_qos1_zero_write_returns_network_error); + RUN_TEST(sn_publish_auto_reply_zero_write_returns_network_error); #endif RUN_TEST(sn_unsubscribe_no_continue); RUN_TEST(sn_publish_incoming_null_msg_cb_errors_no_ack); From 08ce42a0896227858e4f5cca901d1e27ed6d4e35 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:36:01 -0500 Subject: [PATCH 11/52] F-13833 - Give MQTT v5 Will Properties their own property scope --- src/mqtt_packet.c | 23 +++++-- tests/test_mqtt_packet.c | 143 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 5 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 60b2692c0..2a3cdf3a5 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -33,22 +33,29 @@ struct MqttPropMatrix { MqttDataType data; word16 packet_type_mask; /* allowed packets */ }; +/* Will Properties have their own allow-list [MQTT-3.1.3.2]. Packet type 0 is + * reserved and never sent, so its bit marks the Will scope. */ +#define MQTT_PROP_SCOPE_WILL MQTT_PACKET_TYPE_RESERVED static const struct MqttPropMatrix gPropMatrix[] = { { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_PAYLOAD_FORMAT_IND, MQTT_DATA_TYPE_BYTE, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_MSG_EXPIRY_INTERVAL, MQTT_DATA_TYPE_INT, - (1 << MQTT_PACKET_TYPE_CONNECT) | + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_CONTENT_TYPE, MQTT_DATA_TYPE_STRING, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_RESP_TOPIC, MQTT_DATA_TYPE_STRING, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_CORRELATION_DATA, MQTT_DATA_TYPE_BINARY, + (1 << MQTT_PROP_SCOPE_WILL) | (1 << MQTT_PACKET_TYPE_PUBLISH) }, { MQTT_PROP_NONE, MQTT_DATA_TYPE_NONE, 0 }, { MQTT_PROP_SUBSCRIPTION_ID, MQTT_DATA_TYPE_VAR_INT, @@ -79,7 +86,7 @@ static const struct MqttPropMatrix gPropMatrix[] = { { MQTT_PROP_REQ_PROB_INFO, MQTT_DATA_TYPE_BYTE, (1 << MQTT_PACKET_TYPE_CONNECT) }, { MQTT_PROP_WILL_DELAY_INTERVAL, MQTT_DATA_TYPE_INT, - (1 << MQTT_PACKET_TYPE_CONNECT) }, + (1 << MQTT_PROP_SCOPE_WILL) }, { MQTT_PROP_REQ_RESP_INFO, MQTT_DATA_TYPE_BYTE, (1 << MQTT_PACKET_TYPE_CONNECT) }, { MQTT_PROP_RESP_INFO, MQTT_DATA_TYPE_STRING, @@ -1563,7 +1570,8 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect) } /* Determine length of properties */ - lwt_props_len = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, + lwt_props_len = MqttEncode_Props( + (MqttPacketType)MQTT_PROP_SCOPE_WILL, mc_connect->lwt_msg->props, NULL); if (lwt_props_len >= 0) { int tmp_len = MqttEncode_Vbi(NULL, lwt_props_len); @@ -1682,7 +1690,7 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect) /* Encode the lwt property length */ tx_payload += tmp_len; - tmp_len = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, + tmp_len = MqttEncode_Props((MqttPacketType)MQTT_PROP_SCOPE_WILL, mc_connect->lwt_msg->props, tx_payload); if (tmp_len < 0) { return tmp_len; @@ -1919,10 +1927,15 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) rx_payload += lwt_tmp; if (lwt_props_len > 0) { /* Decode, then enforce the tighter Will allow-list below. */ - lwt_tmp = MqttDecode_Props(MQTT_PACKET_TYPE_CONNECT, + lwt_tmp = MqttDecode_Props( + (MqttPacketType)MQTT_PROP_SCOPE_WILL, &mc_connect->lwt_msg->props, rx_payload, (word32)(rx_buf_len - (rx_payload - rx_buf)), lwt_props_len); + /* Report a non-Will property as the encoder does */ + if (lwt_tmp == MQTT_CODE_ERROR_PROPERTY_MISMATCH) { + lwt_tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } if (lwt_tmp < 0) { rc = lwt_tmp; goto cleanup; diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 7c5cc31f5..6e6b517cf 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -3774,6 +3774,77 @@ TEST(encode_connect_v5_invalid_will_property_rejected) ASSERT_EQ(0xA5, buf[0]); } +/* [MQTT-3.1.3.2] Payload Format Indicator and Content Type are Will + * Properties, so a CONNECT carrying them in the Will must encode. */ +TEST(encode_connect_v5_will_publish_properties_accepted) +{ + byte buf[64]; + MqttConnect connect; + MqttMessage lwt; + MqttProp pfi; + MqttProp content_type; + int rc; + /* MQTT 5.0 section 3.1: CONNECT, Remaining Length 28 */ + static const byte expected[] = { + 0x10, 0x1C, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x00, /* v5, Will Flag, Keep Alive 0 */ + 0x00, /* CONNECT Properties length 0 */ + 0x00, 0x03, 'c', 'i', 'd', /* Client Identifier */ + 0x06, /* Will Properties length 6 */ + 0x01, 0x01, /* Payload Format Indicator 1 */ + 0x03, 0x00, 0x01, 't', /* Content Type "t" */ + 0x00, 0x01, 'w', /* Will Topic */ + 0x00, 0x00 /* empty Will Payload */ + }; + + XMEMSET(&connect, 0, sizeof(connect)); + XMEMSET(&lwt, 0, sizeof(lwt)); + XMEMSET(&pfi, 0, sizeof(pfi)); + XMEMSET(&content_type, 0, sizeof(content_type)); + connect.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + connect.client_id = "cid"; + connect.enable_lwt = 1; + connect.lwt_msg = &lwt; + lwt.topic_name = "w"; + pfi.type = MQTT_PROP_PAYLOAD_FORMAT_IND; + pfi.data_byte = 1; + pfi.next = &content_type; + content_type.type = MQTT_PROP_CONTENT_TYPE; + content_type.data_str.str = (char*)"t"; + content_type.data_str.len = 1; + lwt.props = &pfi; + + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ((int)sizeof(expected), rc); + ASSERT_EQ(0, XMEMCMP(expected, buf, sizeof(expected))); +} + +/* MQTT 5.0 section 3.1.2.11: Message Expiry Interval and Will Delay Interval + * are not CONNECT Properties; they belong only in the Will Properties. */ +TEST(encode_connect_v5_will_only_properties_in_connect_rejected) +{ + byte buf[64]; + MqttConnect connect; + MqttProp prop; + int rc; + + XMEMSET(&connect, 0, sizeof(connect)); + XMEMSET(&prop, 0, sizeof(prop)); + connect.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + connect.client_id = "cid"; + connect.props = ∝ + prop.data_int = 30; + + prop.type = MQTT_PROP_MSG_EXPIRY_INTERVAL; + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + + prop.type = MQTT_PROP_WILL_DELAY_INTERVAL; + rc = MqttEncode_Connect(buf, (int)sizeof(buf), &connect); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); +} + TEST(encode_connect_v5_cyclic_will_properties_rejected) { byte buf[256]; @@ -7045,6 +7116,74 @@ TEST(decode_connect_v5_will_props_session_expiry_rejected) rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); } + +/* [MQTT-3.1.3.2] Payload Format Indicator and Content Type are valid Will + * Properties and must decode. */ +TEST(decode_connect_v5_will_publish_properties_accepted) +{ + /* Hand-built v5 CONNECT, same layout as + * encode_connect_v5_will_publish_properties_accepted. */ + byte buf[] = { + 0x10, 0x1C, /* CONNECT, Remaining Length 28 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', /* Protocol Name */ + 0x05, 0x04, 0x00, 0x00, /* v5, Will Flag, Keep Alive */ + 0x00, /* CONNECT Properties length */ + 0x00, 0x03, 'c', 'i', 'd', /* Client Identifier */ + 0x06, /* Will Properties length 6 */ + 0x01, 0x01, /* Payload Format Indicator 1 */ + 0x03, 0x00, 0x01, 't', /* Content Type "t" */ + 0x00, 0x01, 'w', /* Will Topic */ + 0x00, 0x00 /* empty Will Payload */ + }; + MqttConnect dec; + MqttMessage dec_lwt; + MqttProp* prop; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + XMEMSET(&dec_lwt, 0, sizeof(dec_lwt)); + dec.lwt_msg = &dec_lwt; + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ((int)sizeof(buf), rc); + prop = dec_lwt.props; + ASSERT_NOT_NULL(prop); + ASSERT_EQ(MQTT_PROP_PAYLOAD_FORMAT_IND, prop->type); + ASSERT_EQ(1, prop->data_byte); + prop = prop->next; + ASSERT_NOT_NULL(prop); + ASSERT_EQ(MQTT_PROP_CONTENT_TYPE, prop->type); + ASSERT_EQ(1, prop->data_str.len); + ASSERT_EQ('t', prop->data_str.str[0]); + ASSERT_NULL(prop->next); + MqttProps_Free(dec_lwt.props); + MqttProps_Free(dec.props); +} + +/* MQTT 5.0 section 3.1.2.11: Message Expiry Interval (0x02) and Will Delay + * Interval (0x18) are not CONNECT Properties. */ +TEST(decode_connect_v5_will_only_properties_in_connect_rejected) +{ + byte buf[] = { + 0x10, 0x15, /* CONNECT, Remaining Length 21 */ + 0x00, 0x04, 'M', 'Q', 'T', 'T', /* Protocol Name */ + 0x05, 0x02, 0x00, 0x00, /* v5, Clean Start, Keep Alive */ + 0x05, 0x02, 0x00, 0x00, 0x00, 0x1E, /* Property, four byte value 30 */ + 0x00, 0x03, 'c', 'i', 'd' /* Client Identifier */ + }; + MqttConnect dec; + int rc; + + XMEMSET(&dec, 0, sizeof(dec)); + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY_MISMATCH, rc); + MqttProps_Free(dec.props); + + buf[13] = MQTT_PROP_WILL_DELAY_INTERVAL; + XMEMSET(&dec, 0, sizeof(dec)); + rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY_MISMATCH, rc); + MqttProps_Free(dec.props); +} #endif /* WOLFMQTT_BROKER && WOLFMQTT_V5 */ #ifdef WOLFMQTT_V5 @@ -7474,6 +7613,8 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_connect_lwt_topic_oversized_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(encode_connect_v5_invalid_will_property_rejected); + RUN_TEST(encode_connect_v5_will_publish_properties_accepted); + RUN_TEST(encode_connect_v5_will_only_properties_in_connect_rejected); RUN_TEST(encode_connect_v5_cyclic_will_properties_rejected); RUN_TEST(encode_connect_v5_excess_will_properties_rejected); #endif @@ -7511,6 +7652,8 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_connect_v5_max_packet_size_zero_rejected); RUN_TEST(decode_connect_v5_auth_data_without_auth_method_rejected); RUN_TEST(decode_connect_v5_will_props_session_expiry_rejected); + RUN_TEST(decode_connect_v5_will_publish_properties_accepted); + RUN_TEST(decode_connect_v5_will_only_properties_in_connect_rejected); RUN_TEST(decode_connect_v5_empty_will_topic_rejected); #endif From 1931ae97ea977a8b08887800ada0a778bf4aad41 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:41:57 -0500 Subject: [PATCH 12/52] F-13835 - Skip broker PUBLISH writes whose payload was clamped to tx_buf --- src/mqtt_broker.c | 26 +++++ tests/test_broker_connect.c | 194 +++++++++++++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 91a760e4a..ae054ae36 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5811,6 +5811,12 @@ static int BrokerRetained_DeliverToClient(MqttBroker* broker, #endif enc_rc = MqttEncode_Publish(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length declaring + * bytes the frame does not carry [MQTT 3.1.1 section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(bc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " "len=%u qos=%d", (int)bc->sock, @@ -6151,6 +6157,13 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length + * declaring bytes the frame does not carry [MQTT 3.1.1 + * section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(wc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { wr_rc = BrokerClient_WriteDirect(wc, enc_rc); /* Scrub tx_buf unless still in-progress (CONTINUE). */ @@ -6315,6 +6328,12 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length declaring + * bytes the frame does not carry [MQTT 3.1.1 section 2.2.3]. */ + if (enc_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(wc->tx_buf, enc_rc); + enc_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (enc_rc > 0) { wr_rc = BrokerClient_WriteDirect(wc, enc_rc); /* Scrub tx_buf unless still in-progress (CONTINUE). */ @@ -8202,6 +8221,13 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #endif sub_rc = MqttEncode_Publish(sub->client->tx_buf, BROKER_CLIENT_TX_SZ(sub->client), &out_pub, 0); + /* A payload clamped to tx_buf leaves Remaining Length + * declaring bytes the frame does not carry [MQTT 3.1.1 + * section 2.2.3]. */ + if (sub_rc > 0 && out_pub.buffer_pos != out_pub.total_len) { + BROKER_FORCE_ZERO(sub->client->tx_buf, sub_rc); + sub_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } if (sub_rc > 0) { WBLOG_DBG(broker, "broker: PUBLISH fwd sock=%d -> sock=%d " diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 636cb1200..66014314d 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -165,18 +165,21 @@ static void broker_test_disable_free_capture(void) #define MOCK_LISTEN_SOCK 100 #define MOCK_CLIENT_SOCK_BASE 101 /* sock = base + index */ #define MOCK_BUF_SZ 1024 +/* Room for a PUBLISH that fills the broker receive buffer */ +#define MOCK_IN_BUF_SZ (BROKER_RX_BUF_SZ + 64) #define MOCK_MAX_CLIENTS 4 /* Per-mock-client state. Multi-client tests (e.g. QoS 2 dedup) need a * subscriber and a publisher running through the same broker instance. */ typedef struct MockClient { - byte in_buf[MOCK_BUF_SZ]; + byte in_buf[MOCK_IN_BUF_SZ]; size_t in_len; size_t in_pos; byte out_buf[MOCK_BUF_SZ]; size_t out_len; int closed; int close_count; /* net->close calls on this socket */ + int publish_writes; /* PUBLISH frames the broker tried to write */ int read_err; /* when set, mock_read returns a network error (peer RST) */ int write_err; /* when set, mock_write returns a network error */ int write_continue; /* when set, mock_write reports would-block */ @@ -297,6 +300,10 @@ static int mock_write(void* ctx, BROKER_SOCKET_T sock, return MQTT_CODE_ERROR_NETWORK; } mc = &g_clients[idx]; + if (buf != NULL && buf_len > 0 && + ((buf[0] & 0xF0) >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + mc->publish_writes++; + } if (mc->write_err_pending) { mc->write_err_pending = 0; return MQTT_CODE_ERROR_NETWORK; @@ -5871,6 +5878,184 @@ TEST(reclaim_v311_resume_never_expires) } #endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ +#ifdef WOLFMQTT_V5 +/* Build a v3.1.1 QoS 0 PUBLISH to "x" that exactly fills the broker receive + * buffer (MQTT 3.1.1 section 3.3): fixed header byte, two byte Remaining + * Length, Topic Name "x", then payload. Forwarded to a v5 subscriber it gains + * a Property Length byte and no longer fits the transmit buffer. */ +static size_t build_full_rx_publish(byte* buf, size_t buf_sz, byte retain) +{ + word32 remain_len = (word32)BROKER_RX_BUF_SZ - 3; + size_t i; + + if (buf_sz < (size_t)BROKER_RX_BUF_SZ || remain_len < 128 || + remain_len > 16383) { + return 0; + } + buf[0] = (byte)(0x30 | (retain ? 0x01 : 0x00)); + buf[1] = (byte)((remain_len & 0x7F) | 0x80); + buf[2] = (byte)(remain_len >> 7); + buf[3] = 0x00; + buf[4] = 0x01; + buf[5] = 'x'; + for (i = 6; i < (size_t)BROKER_RX_BUF_SZ; i++) { + buf[i] = 'A'; + } + return (size_t)BROKER_RX_BUF_SZ; +} + +/* A forwarded PUBLISH that does not fit the subscriber's transmit buffer + * must not be written: a clamped payload under the full Remaining Length + * would desynchronize the subscriber (MQTT 3.1.1 section 2.2.3). Returns the + * subscriber's PUBLISH writes, or -1 on setup failure. */ +static int oversize_forward_publish_writes(int retained, int* sub_closed) +{ + MqttBroker broker; + MqttBrokerNet net; + static byte big[BROKER_RX_BUF_SZ]; + size_t big_len; + int i; + int writes; + /* v5 CONNECT, Clean Start, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 0 */ + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x00 + }; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + + big_len = build_full_rx_publish(big, sizeof(big), (byte)retained); + if (big_len == 0) { + return -1; + } + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(2); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + if (!retained) { + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + } + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, big, big_len); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + if (retained) { + /* The retained copy is delivered on subscribe */ + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + } + + writes = g_clients[0].publish_writes; + *sub_closed = g_clients[0].closed; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return writes; +} + +TEST(live_forward_exceeding_tx_buf_not_sent_truncated) +{ + int closed = 1; + + ASSERT_EQ(0, oversize_forward_publish_writes(0, &closed)); + ASSERT_FALSE(closed); +} + +TEST(retained_delivery_exceeding_tx_buf_not_sent_truncated) +{ + int closed = 1; + + ASSERT_EQ(0, oversize_forward_publish_writes(1, &closed)); + ASSERT_FALSE(closed); +} +#endif /* WOLFMQTT_V5 */ + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_BROKER_WILL) +/* A QoS 0 Will written directly to a subscriber whose transmit buffer cannot + * hold it must be skipped rather than sent with a clamped payload (MQTT 3.1.1 + * section 2.2.3). */ +TEST(will_qos0_exceeding_tx_buf_not_sent_truncated) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* sub_bc; + int i; + /* v3.1.1 CONNECT, CleanSession=1, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt", QoS 0 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x00 + }; + /* v3.1.1 CONNECT, Will Flag (QoS 0) and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x06, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + sub_bc = find_broker_client(&broker, "S"); + ASSERT_NOT_NULL(sub_bc); + + /* The Will PUBLISH is 10 bytes: fixed header 2, Topic Name 5, payload 3 */ + sub_bc->tx_buf_len = 8; + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[1].closed); + ASSERT_EQ(0, g_clients[0].publish_writes); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_BROKER_WILL */ + /* Parsed details of a single PUBLISH packet on the wire. found = 0 means * no PUBLISH was present in the buffer; subsequent fields are zero. */ typedef struct { @@ -13309,6 +13494,13 @@ int main(int argc, char** argv) RUN_TEST(reclaim_keeps_explicit_v5_never_expire); RUN_TEST(reclaim_v311_resume_never_expires); #endif +#ifdef WOLFMQTT_V5 + RUN_TEST(live_forward_exceeding_tx_buf_not_sent_truncated); + RUN_TEST(retained_delivery_exceeding_tx_buf_not_sent_truncated); +#endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_BROKER_WILL) + RUN_TEST(will_qos0_exceeding_tx_buf_not_sent_truncated); +#endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_will_scrub_after_failed_write); #ifdef WOLFMQTT_NONBLOCK From da26830213e85acfcf5eb994527d9ed7bae61e4b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 12:57:49 -0500 Subject: [PATCH 13/52] F-13801 - Queue QoS 1/2 Wills for offline persistent subscribers --- src/mqtt_broker.c | 21 ++++++++++++ tests/test_broker_connect.c | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index ae054ae36..584bc4860 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -6228,6 +6228,27 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BrokerFanout_Add(&targets, sub->client, (qos < sub->qos) ? qos : sub->qos); } + /* Offline persistent Session: queue QoS 1/2 for delivery on + * reconnect, as the PUBLISH fan-out does. */ + else if (sub->client == NULL && sub->client_id != NULL && + BROKER_STR_VALID(sub->filter) && + BrokerTopicMatch(sub->filter, topic)) { + MqttQoS eff_qos = (qos < sub->qos) ? qos : sub->qos; + if (eff_qos > MQTT_QOS_0) { + BrokerOrphanSession* o = BrokerOrphan_Find(broker, + sub->client_id); + if (o != NULL && BrokerOrphan_Enqueue(broker, o, topic, + payload, payload_len, eff_qos, 0 + #ifdef WOLFMQTT_V5 + , NULL + #endif + ) < 0) { + WBLOG_ERR(broker, + "broker: offline will queue failed client_id=%s", + BrokerLog_Sanitize(sub->client_id)); + } + } + } } if (targets.truncated) { WBLOG_ERR(broker, diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 66014314d..04729280e 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -6054,6 +6054,72 @@ TEST(will_qos0_exceeding_tx_buf_not_sent_truncated) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* A QoS 1 Will matching an offline persistent subscriber is queued in its + * Session and delivered when it reconnects, as a QoS 1 PUBLISH would be + * [MQTT-3.1.2-4]. */ +TEST(will_qos1_queued_for_offline_persistent_subscriber) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v3.1.1 CONNECT, Will Flag, Will QoS 1 and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x0E, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + ASSERT_NOT_NULL(broker.orphan_sessions); + + /* The Will publisher drops, firing its Will while "S" is offline */ + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + + /* "S" resumes its Session and receives the queued Will */ + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} #endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_BROKER_WILL */ /* Parsed details of a single PUBLISH packet on the wire. found = 0 means @@ -13500,6 +13566,7 @@ int main(int argc, char** argv) #endif #if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_BROKER_WILL) RUN_TEST(will_qos0_exceeding_tx_buf_not_sent_truncated); + RUN_TEST(will_qos1_queued_for_offline_persistent_subscriber); #endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_will_scrub_after_failed_write); From a2c718868ab9add1edca33abd6e3dc802208d955 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 14:47:29 -0500 Subject: [PATCH 14/52] F-15404 - Apply Message Expiry to dynamic broker queued deliveries --- src/mqtt_broker.c | 101 +++++++++++++++++++++-------- tests/test_broker_connect.c | 123 ++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 27 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 584bc4860..4d5721964 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2040,6 +2040,9 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, { BrokerOutPub* e; size_t topic_len; +#ifdef WOLFMQTT_V5 + const MqttProp* prop; +#endif if (topic == NULL) { return NULL; @@ -2078,8 +2081,17 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, BrokerOutPub_Free(e); return NULL; } + /* MQTT 5.0 section 3.3.2.3.3: the copy expires this long after it + * was queued. */ + for (prop = e->props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + e->expiry_sec = prop->data_int; + break; + } + } } #endif + e->enq_time = WOLFMQTT_BROKER_GET_TIME_S(); return e; } @@ -2133,6 +2145,36 @@ static void BrokerClient_FreeOutQueue(BrokerClient* bc) * preserved: even a QUEUED QoS 0 behind a capped QoS>0 stays put. Returns the * number of completed writes, MQTT_CODE_CONTINUE for an in-progress write, * or a negative error for a terminal write failure. */ +/* Unlink cur (preceded by prev) from bc's out_q, drop any persisted copy and + * free it. Returns the entry that followed cur. */ +static BrokerOutPub* BrokerClient_OutQRemove(BrokerClient* bc, + BrokerOutPub* prev, BrokerOutPub* cur) +{ + BrokerOutPub* next = cur->next; + + if (prev == NULL) { + bc->out_q_head = next; + } + else { + prev->next = next; + } + if (bc->out_q_tail == cur) { + bc->out_q_tail = prev; + } + bc->out_q_count--; +#ifdef WOLFMQTT_BROKER_PERSIST + /* If this entry was previously shadow-written (orphan path), drop the + * disk record now so a future restart cannot replay it. Idempotent for + * entries that were never persisted (QoS 0, or not-yet-orphaned). */ + if (cur->qos > MQTT_QOS_0 && BROKER_STR_VALID(bc->client_id)) { + (void)BrokerPersist_DelOutPub(bc->broker, bc->client_id, + cur->packet_id); + } +#endif + BrokerOutPub_Free(cur); + return next; +} + static int BrokerClient_DrainOutQueue(BrokerClient* bc) { BrokerOutPub* cur; @@ -2156,6 +2198,10 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) while (cur != NULL) { MqttPublish out_pub; int enc_rc; + WOLFMQTT_BROKER_TIME_T now; + #ifdef WOLFMQTT_V5 + MqttProp* prop; + #endif if (cur->state != BROKER_OUTQ_QUEUED) { #if WOLFMQTT_MAX_QOS >= 2 @@ -2203,6 +2249,26 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) cur = cur->next; continue; } + /* Expiry applies until onward delivery starts; a deferred write or a + * DUP retransmission reuses the interval it was first sent with. */ + if (cur->expiry_sec > 0 && bc->out_q_pending_len == 0 && + !cur->retransmit_dup && !cur->partial_send) { + now = WOLFMQTT_BROKER_GET_TIME_S(); + if (now >= cur->enq_time && + now - cur->enq_time >= cur->expiry_sec) { + /* [MQTT-3.3.2-5] Delete the expired copy */ + WBLOG_DBG(bc->broker, + "broker: drain dropped expired PUBLISH sock=%d topic=%s", + (int)bc->sock, BrokerLog_Sanitize(cur->topic)); + cur = BrokerClient_OutQRemove(bc, prev, cur); + continue; + } + if (now > cur->enq_time) { + /* Keep the same deadline, measured from now */ + cur->expiry_sec -= (word32)(now - cur->enq_time); + cur->enq_time = now; + } + } if (cur->qos > MQTT_QOS_0 && bc->out_q_inflight >= effective_cap) { /* Cap reached. Stop here - cannot send anything behind it * either, because [MQTT-4.6.0-3] requires ordered delivery. */ @@ -2224,6 +2290,13 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) #ifdef WOLFMQTT_V5 out_pub.protocol_level = cur->protocol_level; out_pub.props = cur->props; + /* [MQTT-3.3.2-6] Send the interval remaining after the wait */ + for (prop = cur->props; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + prop->data_int = cur->expiry_sec; + break; + } + } #endif enc_rc = MqttEncode_Publish(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), @@ -2245,33 +2318,7 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) BROKER_FORCE_ZERO(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc)); /* Drop just this entry and continue. Encoding failure for * a single message is not fatal to the connection. */ - if (prev == NULL) { - bc->out_q_head = cur->next; - } - else { - prev->next = cur->next; - } - if (bc->out_q_tail == cur) { - bc->out_q_tail = prev; - } - bc->out_q_count--; - { - BrokerOutPub* free_me = cur; - cur = cur->next; - #ifdef WOLFMQTT_BROKER_PERSIST - /* If this entry was previously shadow-written (orphan - * path), drop the disk record now so a future restart - * cannot replay an undeliverable message. Idempotent - * for entries that were never persisted (QoS 0, or - * not-yet-orphaned). */ - if (free_me->qos > MQTT_QOS_0 && - BROKER_STR_VALID(bc->client_id)) { - (void)BrokerPersist_DelOutPub(bc->broker, - bc->client_id, free_me->packet_id); - } - #endif - BrokerOutPub_Free(free_me); - } + cur = BrokerClient_OutQRemove(bc, prev, cur); continue; } if (bc->out_q_pending_len != 0 && diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 04729280e..92b882d14 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -6122,6 +6122,125 @@ TEST(will_qos1_queued_for_offline_persistent_subscriber) } #endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_BROKER_WILL */ +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) +/* Queue a v5 QoS 1 PUBLISH with Message Expiry Interval 10 for an offline + * persistent subscriber, let wait_sec pass, then resume the Session. + * Returns the PUBLISH packets delivered and sets *wire_expiry to the Message + * Expiry Interval of the first one (-1 if not found). */ +static int queued_publish_after_wait(word32 wait_sec, long* wire_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + size_t pos; + const byte* out; + size_t out_len; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 1 */ + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT, Clean Start 1, Properties length 0, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + /* v5 PUBLISH QoS 1 (MQTT 5.0 section 3.3): topic "x", packet id 1, + * Properties length 5 holding Message Expiry Interval 10 (0x02), + * payload "hi". Remaining Length 13. */ + static const byte publish_x[] = { + 0x32, 0x0D, + 0x00, 0x01, 'x', + 0x00, 0x01, + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 'h', 'i' + }; + + *wire_expiry = -1; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, publish_x, sizeof(publish_x)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + publishes = count_packets_of_type(out, out_len, + MQTT_PACKET_TYPE_PUBLISH); + /* Forwarded layout: 0x32 0x0D, topic "x", 2 byte packet id, Properties + * length 5, then 0x02 and the four byte Message Expiry Interval. */ + for (pos = 0; pos + 13 <= out_len; pos++) { + if (out[pos] == 0x32 && out[pos + 1] == 0x0D && + out[pos + 2] == 0x00 && out[pos + 3] == 0x01 && + out[pos + 4] == 'x' && out[pos + 7] == 0x05 && + out[pos + 8] == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + *wire_expiry = ((long)out[pos + 9] << 24) | + ((long)out[pos + 10] << 16) | ((long)out[pos + 11] << 8) | + (long)out[pos + 12]; + break; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return publishes; +} + +/* [MQTT-3.3.2-5] A queued copy whose Message Expiry Interval passed before + * onward delivery started is deleted, not delivered. */ +TEST(queued_publish_expired_before_delivery_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_publish_after_wait(11, &wire_expiry)); +} + +/* [MQTT-3.3.2-6] A queued copy is delivered with the received Message Expiry + * Interval minus the time it waited in the Server. */ +TEST(queued_publish_expiry_reduced_by_wait) +{ + long wire_expiry; + + ASSERT_EQ(1, queued_publish_after_wait(4, &wire_expiry)); + ASSERT_EQ(6, wire_expiry); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ + /* Parsed details of a single PUBLISH packet on the wire. found = 0 means * no PUBLISH was present in the buffer; subsequent fields are zero. */ typedef struct { @@ -13568,6 +13687,10 @@ int main(int argc, char** argv) RUN_TEST(will_qos0_exceeding_tx_buf_not_sent_truncated); RUN_TEST(will_qos1_queued_for_offline_persistent_subscriber); #endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) + RUN_TEST(queued_publish_expired_before_delivery_dropped); + RUN_TEST(queued_publish_expiry_reduced_by_wait); +#endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_will_scrub_after_failed_write); #ifdef WOLFMQTT_NONBLOCK From 6aa3feab7beba13ba20c7226721325fdc8fd251c Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 14:54:37 -0500 Subject: [PATCH 15/52] Doc changes --- src/mqtt_broker_persist.c | 22 +++++++++++++--------- src/mqtt_client.c | 26 ++++++++++++++------------ src/mqtt_packet.c | 24 +++++++++++++----------- src/mqtt_sn_packet.c | 3 ++- wolfmqtt/mqtt_broker.h | 24 +++++++++++++----------- wolfmqtt/mqtt_client.h | 31 ++++++++++++++++++------------- wolfmqtt/mqtt_packet.h | 19 +++++++++++-------- wolfmqtt/mqtt_sn_client.h | 14 ++++++++------ wolfmqtt/mqtt_sn_packet.h | 8 ++++---- 9 files changed, 96 insertions(+), 75 deletions(-) diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 54f674b1a..9a5582334 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -809,9 +809,10 @@ int BrokerPersist_PutSubs(MqttBroker* broker, const char* client_id) { size_t raw = XSTRLEN(client_id); /* word16 downcast - reject lengths that wouldn't fit instead - * of silently truncating. MQTT v3.1.1 caps client_id at 23 - * bytes; v5 caps at 65535 (i.e., fits in word16). Anything - * longer is malformed input from a caller. */ + * of silently truncating. MQTT strings are at most 65535 bytes + * (MQTT 3.1.1 section 1.5.3); the 23-byte Client ID limit in + * [MQTT-3.1.3-5] is a minimum a server must accept, not a cap. + * Anything longer is malformed input from a caller. */ if (raw == 0 || raw > 0xFFFFu) { return MQTT_CODE_ERROR_OUT_OF_BUFFER; } @@ -1859,7 +1860,9 @@ static int wmqb_purge_expired_retained_cb(const byte* key, word16 key_len, * every live copy and nothing at runtime can reach them again. Stages a * bounded batch, leaves the iterator, then deletes: a backend is not required * to tolerate mutation under an active iterator. Repeats until a scan finds - * nothing, so one restore drains a whole backlog at fixed memory cost. */ + * nothing or WMQB_RETAINED_PURGE_PASSES scans have run, so a large backlog + * can take more than one restore to drain. A key too long to stage is left + * in place. */ static void wmqb_restore_purge_expired_retained(MqttBroker* broker) { const MqttBrokerPersistHooks* h = broker->persist; @@ -2773,11 +2776,12 @@ int BrokerPersist_Restore(MqttBroker* broker) if (rc == MQTT_CODE_ERROR_MALFORMED_DATA || rc == MQTT_CODE_ERROR_BAD_ARG) { /* Schema or magic mismatch. Wipe-and-restart per the chosen - * policy: iterate every namespace, delete every record, - * restamp META. New records get written fresh as activity - * resumes. (BAD_ARG = header magic / schema version mismatch - * from wmqb_read_header; MALFORMED_DATA = decrypt or size - * failure from wmqb_kv_get_decrypt_meta.) */ + * policy: iterate every namespace, delete every record + * (dynamic memory builds only; static builds skip the wipe + * in wmqb_wipe_all), restamp META. New records get written + * fresh as activity resumes. (BAD_ARG = header magic / schema + * version mismatch from wmqb_read_header; MALFORMED_DATA = + * decrypt or size failure from wmqb_kv_get_decrypt_meta.) */ WMQB_LOG_ERR(broker, "broker: persist schema mismatch - wiping all records"); rc = wmqb_wipe_all(broker); diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 9b1828101..b449c61f7 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -47,7 +47,7 @@ WOLFMQTT_LOCAL void MqttClient_ForceZero(void* mem, word32 len) * flagged, so the other thread knows it completed. * * WOLFMQTT_NONBLOCK: Enabled transport support for returning WANT READ/WRITE, - * which becomes WOLFMQTT_CODE_CONTINUE. This prevents blocking if the + * which becomes MQTT_CODE_CONTINUE. This prevents blocking if the * transport (socket) has no data. * * WOLFMQTT_V5: Enables MQTT v5.0 support @@ -885,8 +885,9 @@ static void MqttClient_Replay_PubRelSentSafe(MqttClient* client, * dropped as soon as the waiting call returns - including on a timeout, where * no acknowledgement was processed at all. This table is the connection-level * record the rule actually asks for, so it is kept in every build. It is - * cleared when a Network Connection starts or ends, because the client holds - * no outbound session state across one. + * cleared when a Network Connection starts or ends. Unacknowledged messages + * kept for session replay have their identifiers reserved again when a + * resumed Session re-sends them. * * These are called from both the send and receive paths, so they take * client->lockClient themselves; no caller may already hold it. Every caller @@ -1067,8 +1068,8 @@ static void MqttClient_SendIdDisown(MqttClient* client, const void* owner) } /* Drop every reservation. The identifiers were only in use on the Network - * Connection that is starting or ending, and the client keeps no outbound - * session state across one. */ + * Connection that is starting or ending. Replayed messages reserve theirs + * again on a Session Present reconnect. */ static void MqttClient_SendIdsReset(MqttClient* client) { #ifdef WOLFMQTT_MULTITHREAD @@ -2774,8 +2775,8 @@ int MqttClient_Init(MqttClient *client, MqttNet* net, client->rx_buf_len = rx_buf_len; client->cmd_timeout_ms = cmd_timeout_ms; #ifdef WOLFMQTT_V5 - /* Initialize to this build's Maximum QoS. Handle_Props will narrow - * this if the server advertises a lower MQTT_PROP_MAX_QOS. */ + /* Initialize to this build's Maximum QoS. Handle_ConnectAck_Props will + * narrow this if the server advertises a lower MQTT_PROP_MAX_QOS. */ client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS; client->retain_avail = 1; client->protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL; @@ -3268,9 +3269,9 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) } /* Past the guard, so this really is a new handshake: start a fresh - * outbound Packet Identifier space. The client keeps no - * unacknowledged outbound PUBLISH/PUBREL across a Network Connection, - * so nothing from a previous one is still in flight [MQTT-2.3.1-3]. + * outbound Packet Identifier space. Identifiers from a previous + * Network Connection are not in flight on this one [MQTT-2.3.1-3]; + * replayed messages reserve theirs again if the Session resumes. * Must run after the guard above - a refused duplicate CONNECT would * otherwise wipe identifiers still in flight on the live connection - * and outside the WOLFMQTT_V5 block below, since the table exists in @@ -5646,8 +5647,9 @@ int MqttClient_NetDisconnect(MqttClient *client) } #endif - /* The Network Connection is going away and the client keeps no outbound - * session state across one, so nothing stays in flight [MQTT-2.3.1-3]. */ + /* The Network Connection is going away, so none of its identifiers stay + * in flight [MQTT-2.3.1-3]. Replayed messages reserve theirs again if a + * later Session resumes. */ MqttClient_SendIdsReset(client); rc = MqttSocket_Disconnect(client); diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 2a3cdf3a5..9b69b46b5 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -1094,7 +1094,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); break; } - /* Allocate a structure and add to head. */ + /* Allocate a structure and append it, keeping wire order. */ cur_prop = MqttProps_Add(props); if (cur_prop == NULL) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); @@ -3221,8 +3221,9 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) /* Preserve v5 No Local / Retain As Published / Retain Handling * (bits 2-5) so the full options byte round-trips. Reserved and * QoS bits were validated above; for a valid v3.1.1 packet these - * bits are 0. The broker stores these but does not yet enforce - * them on delivery. */ + * bits are 0. The broker applies Retain Handling when it sends + * retained messages for a new subscription; it does not store + * No Local or Retain As Published. */ topic->sub_options = (byte)(options & MQTT_SUBSCRIBE_OPTIONS_MASK); #endif subscribe->topic_count++; @@ -4310,8 +4311,8 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) return header_len + remain_len; } -/* Must be called once from a single thread before any concurrent access - * to MQTTv5 property functions. Not thread-safe if called concurrently. */ +/* Reference counted: each call must be paired with one MqttProps_ShutDown + * call. Calls must not run concurrently with each other. */ int MqttProps_Init(void) { int ret = MQTT_CODE_SUCCESS; @@ -4326,9 +4327,9 @@ int MqttProps_Init(void) return ret; } -/* Must be called once from a single thread after all concurrent access - * to MQTTv5 property functions has ceased. Not thread-safe if called - * concurrently. */ +/* Releases one MqttProps_Init reference and frees the shared property lock + * when the last one is released. Calls must not run concurrently with each + * other. */ int MqttProps_ShutDown(void) { int ret = MQTT_CODE_SUCCESS; @@ -4480,9 +4481,10 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) rc = MqttPacket_HandleNetError(client, rc); #ifndef WOLFMQTT_NO_TIME - /* Record when a full control packet leaves the wire so the keep-alive - * scheduler can measure how long the outbound link has been idle. Only - * needed while auto-ping is armed; skip the clock read otherwise. */ + /* Record each fully written buffer (a whole packet, or one segment of a + * streamed PUBLISH payload) so the keep-alive scheduler can measure how + * long the outbound link has been idle. Only needed while auto-ping is + * armed; skip the clock read otherwise. */ if (rc == tx_buf_len && client->keep_alive_sec != 0) { client->last_tx_time = WOLFMQTT_GET_TIME_S(); } diff --git a/src/mqtt_sn_packet.c b/src/mqtt_sn_packet.c index baba295d7..dc0266ad8 100644 --- a/src/mqtt_sn_packet.c +++ b/src/mqtt_sn_packet.c @@ -1264,7 +1264,8 @@ int SN_Encode_Publish(byte *tx_buf, int tx_buf_len, SN_Publish *publish) } if ((publish->topic_type == SN_TOPIC_ID_TYPE_SHORT) || (publish->topic_type == SN_TOPIC_ID_TYPE_PREDEF)) { - /* Short and predefined topic names are 2 chars */ + /* A short topic name is 2 chars; a predefined topic ID is a 2-byte + * identifier, not a name */ XMEMCPY(tx_payload, publish->topic_name, 2); tx_payload += 2; } diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index b42a2efdc..5cc0d99c0 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -149,8 +149,8 @@ #endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers - * any reasonable client; a misbehaving client that exceeds this gets a - * malformed-packet rejection. */ + * any reasonable client; a misbehaving client that exceeds this is + * disconnected (v5 reason Quota Exceeded 0x97). */ #ifndef BROKER_MAX_INBOUND_QOS2 #define BROKER_MAX_INBOUND_QOS2 16 #endif @@ -357,8 +357,9 @@ typedef struct MqttBrokerNet { /* The persistence layer is intentionally hook-based so the broker can run * on top of POSIX files, embedded flash, an external KV store, or an * in-RAM stub used by tests. Each hook returns 0 on success or a negative - * error code. Shadow-write failures are logged while in-memory state remains - * authoritative. Iterator failures during restore abort MqttBroker_Start. + * error code. Most shadow-write failures are not logged by the broker, so a + * hook should report its own errors; in-memory state remains authoritative. + * Iterator failures during restore abort MqttBroker_Start. * * The broker currently uses only the key/value API. At minimum kv_put, * kv_get, and kv_iter must be installed for sessions, subscriptions, @@ -646,7 +647,7 @@ typedef struct BrokerClient { * not yet PUBREL'd. A duplicate PUBLISH carrying one of these IDs is * acked again (PUBREC) but NOT re-fanned-out to subscribers. The * BROKER_MAX_INBOUND_QOS2 cap is enforced in both memory modes; a - * client that exceeds it is disconnected with malformed-packet error. + * client that exceeds it is disconnected with Quota Exceeded (0x97). * Compiled out for capped-QoS builds (WOLFMQTT_MAX_QOS < 2). */ #if WOLFMQTT_MAX_QOS >= 2 #ifdef WOLFMQTT_STATIC_MEMORY @@ -685,10 +686,10 @@ typedef struct BrokerClient { int out_q_pending_len; /* Encoded length of a direct broker response still being written. */ int direct_pending_len; - /* v5 Receive Maximum advertised by this client in CONNECT, or 65535 - * (per MQTT v5 sec 3.1.2.11.3) when the client did not include the - * property. For v3.1.1 clients this is left at 65535 - the cap - * comes from BROKER_MAX_INFLIGHT_PER_SUB alone. */ + /* v5 Receive Maximum advertised by this client in CONNECT, or 0 when + * the client did not include the property or is a v3.1.1 client. 0 + * means no per-client cap (the MQTT v5 section 3.1.2.11.3 default of + * 65535), so the cap comes from BROKER_MAX_INFLIGHT_PER_SUB alone. */ word16 client_receive_max; #endif /* !WOLFMQTT_STATIC_MEMORY */ word32 session_expiry_sec; @@ -994,8 +995,9 @@ WOLFMQTT_LOCAL int BrokerPersist_DelOutPub(MqttBroker* broker, WOLFMQTT_LOCAL int BrokerPersist_DelOutQueue(MqttBroker* broker, const char* client_id); /* Startup-time restore on a freshly initialized, empty broker. Rebuilds the - * in-memory tables before the first MqttBroker_Start accepts clients. Wipes - * and re-stamps META when the persisted schema version does not match. */ + * in-memory tables before the first MqttBroker_Start accepts clients. When the + * persisted schema version does not match it re-stamps META, first deleting + * every record in dynamic memory builds (static builds skip the wipe). */ WOLFMQTT_LOCAL int BrokerPersist_Restore(MqttBroker* broker); /* Discard state loaded by a failed startup restore. Defined in * mqtt_broker.c because it uses the broker's shared teardown helpers. */ diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index f4b04a67f..8b59eddf4 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -419,7 +419,7 @@ typedef struct _MqttClient { #endif MqttPkRead packet; /* publish packet state - protected by read lock */ - MqttPublishResp packetAck; /* publish ACK - protected by write lock */ + MqttPublishResp packetAck; /* publish ACK - protected by lockClient */ MqttSk read; /* read socket state - protected by read lock */ MqttSk write; /* write socket state - protected by write lock */ @@ -516,8 +516,8 @@ typedef struct _MqttClient { /* Outbound Packet Identifiers written on the current Network Connection * and not yet released by their acknowledgement. Cleared when a connection - * starts or ends, since the client keeps no outbound session state across - * one. */ + * starts or ends; messages kept in replay[] have their identifiers + * reserved again when a resumed Session re-sends them. */ MqttSendId send_inflight[MQTT_MAX_SEND_INFLIGHT]; #ifndef WOLFMQTT_NO_SESSION_REPLAY @@ -811,10 +811,12 @@ WOLFMQTT_API int MqttClient_Auth( /*! \brief Add a new property. - * Allocate a property structure and add it to the head of the list - pointed to by head. To be used prior to calling packet command. + * Allocate a property structure and append it to the tail of the list + pointed to by head; *head is set only when the list is empty. To be + used prior to calling packet command. * \param head Pointer-pointer to a property structure - * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_BAD_ARG + * \return Pointer to the new property, or NULL if head is NULL or no + property could be allocated */ WOLFMQTT_API MqttProp* MqttClient_PropsAdd( MqttProp **head); @@ -831,8 +833,9 @@ WOLFMQTT_API int MqttClient_PropsFree( /*! \brief Encodes and sends the MQTT Disconnect packet (no response) - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this @@ -844,8 +847,9 @@ WOLFMQTT_API int MqttClient_Disconnect( /*! \brief Encodes and sends the MQTT Disconnect packet (no response) - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \param disconnect Pointer to MqttDisconnect structure. NULL is valid. * \return MQTT_CODE_SUCCESS, @@ -897,7 +901,8 @@ WOLFMQTT_API int MqttClient_WaitMessage_ex( #if defined(WOLFMQTT_MULTITHREAD) || defined(WOLFMQTT_NONBLOCK) /*! \brief In a multi-threaded and non-blocking mode this allows you to cancel an MQTT object that was previously submitted. - * \note This is a blocking function that will wait for MqttNet.read + * \note This function performs no network I/O. In multi-threaded + builds it may wait to take the client locks. * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -914,8 +919,8 @@ WOLFMQTT_API int MqttClient_CancelMessage( * \note This function assumes caller owns the object * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure - * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* - (see enum MqttPacketResponseCodes) + * \return 1 if a read or write is pending, 0 if not, or a negative + MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_IsMessageActive( MqttClient *client, diff --git a/wolfmqtt/mqtt_packet.h b/wolfmqtt/mqtt_packet.h index 25efb2b0f..95075e991 100644 --- a/wolfmqtt/mqtt_packet.h +++ b/wolfmqtt/mqtt_packet.h @@ -204,9 +204,10 @@ typedef enum _MqttQoS { } MqttQoS; /* Maximum QoS supported by this build. Legal values: 0, 1, 2. Default 2. - * Define in user_settings.h to compile a QoS-capped client/broker. Wired - * into broker CONNACK Maximum QoS property emission today; broader code - * gating lives on the mqtt_qos_max branch. */ + * Define in user_settings.h to compile a QoS-capped client/broker. Sets the + * client's max_qos and the broker's CONNACK Maximum QoS, clamps granted + * subscription and Will QoS, rejects inbound PUBLISH above the cap, and + * compiles out the QoS 2 state when below 2. */ #ifndef WOLFMQTT_MAX_QOS #define WOLFMQTT_MAX_QOS 2 #endif @@ -903,12 +904,14 @@ WOLFMQTT_API int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf); WOLFMQTT_API int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* buf, word32 buf_len, word32 prop_len); -/* Must be called once from a single thread before any concurrent access - * to MQTTv5 property functions. Not thread-safe if called concurrently. */ +/* Reference counted: each call must be paired with one MqttProps_ShutDown + * call. MqttClient_Init and MqttClient_DeInit already make one pair per v5 + * client. Calls must not run concurrently with each other. */ WOLFMQTT_API int MqttProps_Init(void); -/* Must be called once from a single thread after all concurrent access - * to MQTTv5 property functions has ceased. Not thread-safe if called - * concurrently. */ +/* Releases one MqttProps_Init reference. The shared property lock is freed + * when the last reference is released, so call it only after property + * functions are no longer in use through that reference. Calls must not run + * concurrently with each other. */ WOLFMQTT_API int MqttProps_ShutDown(void); WOLFMQTT_API MqttProp* MqttProps_Add(MqttProp **head); WOLFMQTT_API int MqttProps_Free(MqttProp *head); diff --git a/wolfmqtt/mqtt_sn_client.h b/wolfmqtt/mqtt_sn_client.h index 985d0889b..3aef4c149 100644 --- a/wolfmqtt/mqtt_sn_client.h +++ b/wolfmqtt/mqtt_sn_client.h @@ -59,9 +59,9 @@ WOLFMQTT_API int SN_Client_SearchGW( /*! \brief Encodes and sends the Connect packet and waits for the Connect Acknowledgment packet. If Will is enabled, the gateway - prompts for LWT Topic and Message. Sending an empty will topic - indicates that the client wishes to delete the Will topic and - the Will message stored in the server. + prompts for LWT Topic and Message. The Will is always sent + with its flags, so an empty Will topic here does not use the + delete form (MQTT-SN 1.2 section 5.4.7). * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param connect Pointer to SN_Connect structure initialized @@ -96,7 +96,8 @@ WOLFMQTT_API int SN_Client_WillTopicUpdate(MqttClient *client, SN_Will *will); * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param will Pointer to SN_Will structure initialized - with topic and message parameters. NULL is valid. + with topic and message parameters. Must not be + NULL. * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ @@ -185,8 +186,9 @@ WOLFMQTT_API int SN_Client_Unsubscribe( /*! \brief Encodes and sends the MQTT-SN Disconnect packet. Client may send the disconnect with a duration to indicate the client is entering the "asleep" state. - * \note This is a non-blocking function that will try and send using - MqttNet.write + * \note This function does not wait for a response. It sends using + MqttNet.write and, unless WOLFMQTT_NONBLOCK is defined, + blocks until the packet is written or the write fails. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) diff --git a/wolfmqtt/mqtt_sn_packet.h b/wolfmqtt/mqtt_sn_packet.h index b74246525..f248892ea 100644 --- a/wolfmqtt/mqtt_sn_packet.h +++ b/wolfmqtt/mqtt_sn_packet.h @@ -44,10 +44,10 @@ #define WOLFMQTT_SN_MAXPACKET_SIZE 1024 #endif -/* The SN_GwAddr field has a variable length and contains the address of a GW. - Its depends on the network over which MQTT-SN operates and is indicated in - the first octet of this field. For example, in a ZigBee network the network - address is 2-octet long. */ +/* Gateway address. MQTT-SN 1.2 section 5.4.3 defines GwAdd as variable + length and network dependent, but this type holds only a fixed 2-octet + address (for example a ZigBee network address). SN_Decode_GWInfo copies at + most sizeof(SN_GwAddr) bytes. */ typedef word16 SN_GwAddr ; /* RETURN CODE values */ From 908c70df739b4c5707da95e822a542a57e0c0481 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 14:57:15 -0500 Subject: [PATCH 16/52] F-13808 - Reject a zero Receive Maximum in the property encoder --- src/mqtt_packet.c | 7 +++++-- tests/test_mqtt_packet.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 9b69b46b5..ada8c52fe 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -933,8 +933,11 @@ int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf) } case MQTT_DATA_TYPE_SHORT: { - /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. */ - if (cur_prop->type == MQTT_PROP_TOPIC_ALIAS && + /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. + * MQTT 5.0 sections 3.1.2.11.3 and 3.2.2.3.3: so is a + * Receive Maximum of 0. */ + if ((cur_prop->type == MQTT_PROP_TOPIC_ALIAS || + cur_prop->type == MQTT_PROP_RECEIVE_MAX) && cur_prop->data_short == 0) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); } diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 6e6b517cf..2d6bd5564 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -6903,6 +6903,39 @@ TEST(encode_props_boolean_byte_out_of_range_rejected) ASSERT_TRUE(rc > 0); } +/* MQTT 5.0 sections 3.1.2.11.3 and 3.2.2.3.3: a Receive Maximum of 0 is a + * Protocol Error in both CONNECT and CONNACK. The encoder must refuse it in + * the length pass (buf == NULL) and the output pass. */ +TEST(encode_props_receive_max_zero_rejected) +{ + MqttProp prop; + byte buf[8]; + /* Receive Maximum = 1: identifier 0x21, Two Byte Integer 0x0001 + * (MQTT 5.0 sections 2.2.2.2 and 1.5.2) */ + static const byte expect[] = { 0x21, 0x00, 0x01 }; + int rc; + + XMEMSET(&prop, 0, sizeof(prop)); + prop.type = MQTT_PROP_RECEIVE_MAX; + prop.data_short = 0; + prop.next = NULL; + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, NULL); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, buf); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT_ACK, &prop, NULL); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT_ACK, &prop, buf); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + + /* The smallest legal value still encodes. */ + prop.data_short = 1; + XMEMSET(buf, 0, sizeof(buf)); + rc = MqttEncode_Props(MQTT_PACKET_TYPE_CONNECT, &prop, buf); + ASSERT_EQ((int)sizeof(expect), rc); + ASSERT_EQ(0, XMEMCMP(buf, expect, sizeof(expect))); +} + /* ============================================================================ * MqttEncode/Decode_Auth roundtrip * @@ -7817,6 +7850,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_props_user_prop_invalid_utf8_rejected); RUN_TEST(encode_props_duplicate_repeatability); RUN_TEST(encode_props_boolean_byte_out_of_range_rejected); + RUN_TEST(encode_props_receive_max_zero_rejected); RUN_TEST(auth_v5_cont_auth_roundtrip); RUN_TEST(auth_v5_reauth_roundtrip); RUN_TEST(auth_v5_reauth_decodes_without_error); From b9695a0924060cf0ffdf594b8c2260aff7ea08f1 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 15:02:04 -0500 Subject: [PATCH 17/52] F-12452 - Reject reserved MQTT-SN TopicIdType values in encoders --- src/mqtt_sn_packet.c | 12 ++++++++ tests/test_mqtt_sn.c | 73 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/mqtt_sn_packet.c b/src/mqtt_sn_packet.c index dc0266ad8..3bc205eb5 100644 --- a/src/mqtt_sn_packet.c +++ b/src/mqtt_sn_packet.c @@ -1082,6 +1082,10 @@ int SN_Encode_Subscribe(byte *tx_buf, int tx_buf_len, SN_Subscribe *subscribe) subscribe->topicNameId == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (subscribe->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } /* Determine packet length */ if (subscribe->topic_type == SN_TOPIC_ID_TYPE_NORMAL) { @@ -1206,6 +1210,10 @@ int SN_Encode_Publish(byte *tx_buf, int tx_buf_len, SN_Publish *publish) if (tx_buf == NULL || publish == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (publish->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } if (tx_buf_len < 0) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } @@ -1497,6 +1505,10 @@ int SN_Encode_Unsubscribe(byte *tx_buf, int tx_buf_len, unsubscribe->topicNameId == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved */ + if (unsubscribe->topic_type > SN_TOPIC_ID_TYPE_SHORT) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } /* Determine packet length */ if (unsubscribe->topic_type == SN_TOPIC_ID_TYPE_NORMAL) { diff --git a/tests/test_mqtt_sn.c b/tests/test_mqtt_sn.c index 112e13cc5..3b78f042b 100644 --- a/tests/test_mqtt_sn.c +++ b/tests/test_mqtt_sn.c @@ -1055,6 +1055,32 @@ TEST(sn_encode_publish_short_topic_valid) ASSERT_EQ('i', tx_buf[8]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_publish_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + byte payload[2] = { 'h', 'i' }; + SN_Publish publish; + int rc; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.topic_type = 3; + publish.topic_name = topic; + publish.packet_id = 0x1234; + publish.buffer = payload; + publish.total_len = sizeof(payload); + + rc = SN_Encode_Publish(tx_buf, (int)sizeof(tx_buf), &publish); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + publish.topic_type = 4; + rc = SN_Encode_Publish(tx_buf, (int)sizeof(tx_buf), &publish); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + TEST(sn_encode_publish_ind_form_valid) { /* A 300-byte payload pushes the packet past SN_PACKET_MAX_SMALL_SIZE, so the @@ -1525,6 +1551,28 @@ TEST(sn_encode_subscribe_short_topic_valid) ASSERT_EQ('p', tx_buf[6]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_subscribe_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + SN_Subscribe subscribe; + int rc; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + subscribe.topic_type = 3; + subscribe.topicNameId = topic; + subscribe.packet_id = 0x1234; + + rc = SN_Encode_Subscribe(tx_buf, (int)sizeof(tx_buf), &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + subscribe.topic_type = 4; + rc = SN_Encode_Subscribe(tx_buf, (int)sizeof(tx_buf), &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + /* ============================================================================ * SN_Encode_Unsubscribe * @@ -1633,6 +1681,28 @@ TEST(sn_encode_unsubscribe_short_topic_valid) ASSERT_EQ('p', tx_buf[6]); } +/* MQTT-SN 1.2 section 5.3.4: TopicIdType 0b11 is reserved. A topic_type + * outside NORMAL/PREDEF/SHORT must be rejected, not masked into the flags. */ +TEST(sn_encode_unsubscribe_reserved_topic_type_rejected) +{ + byte tx_buf[32]; + char topic[2] = { 't', 'p' }; + SN_Unsubscribe unsubscribe; + int rc; + + XMEMSET(&unsubscribe, 0, sizeof(unsubscribe)); + unsubscribe.topic_type = 3; + unsubscribe.topicNameId = topic; + unsubscribe.packet_id = 0x1234; + + rc = SN_Encode_Unsubscribe(tx_buf, (int)sizeof(tx_buf), &unsubscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + + unsubscribe.topic_type = 4; + rc = SN_Encode_Unsubscribe(tx_buf, (int)sizeof(tx_buf), &unsubscribe); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); +} + /* ============================================================================ * SN_Encode_WillTopic * @@ -1971,6 +2041,7 @@ int main(int argc, char** argv) /* SN_Encode_Publish */ RUN_TEST(sn_encode_publish_short_topic_valid); + RUN_TEST(sn_encode_publish_reserved_topic_type_rejected); RUN_TEST(sn_encode_publish_ind_form_valid); RUN_TEST(sn_encode_publish_oversized_total_len_rejected); RUN_TEST(sn_encode_publish_total_len_int_max_rejected); @@ -1999,6 +2070,7 @@ int main(int argc, char** argv) RUN_TEST(sn_encode_subscribe_null_args_rejected); RUN_TEST(sn_encode_subscribe_normal_topic_valid); RUN_TEST(sn_encode_subscribe_short_topic_valid); + RUN_TEST(sn_encode_subscribe_reserved_topic_type_rejected); /* SN_Encode_Unsubscribe */ RUN_TEST(sn_encode_unsubscribe_null_topic_normal_rejected); @@ -2006,6 +2078,7 @@ int main(int argc, char** argv) RUN_TEST(sn_encode_unsubscribe_null_args_rejected); RUN_TEST(sn_encode_unsubscribe_normal_topic_valid); RUN_TEST(sn_encode_unsubscribe_short_topic_valid); + RUN_TEST(sn_encode_unsubscribe_reserved_topic_type_rejected); /* SN_Encode_WillTopic */ RUN_TEST(sn_encode_willtopic_null_topic_string_rejected); From e32ed3e5162c5a43724664a99d44b61ac3a26edd Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 15:05:47 -0500 Subject: [PATCH 18/52] F-14609 - Reject AUTH packets that omit the Authentication Method --- src/mqtt_packet.c | 13 +++++++++++++ tests/test_mqtt_packet.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index ada8c52fe..00a3e321e 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -4229,6 +4229,7 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) int header_len, remain_len, tmp; byte *rx_payload; word32 props_len = 0; + MqttProp* auth_prop; /* Validate required arguments */ @@ -4293,6 +4294,18 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) Remaining Length of 0 and never reaches this branch. */ return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } + /* MQTT 5.0 section 3.15.2.2.2: omitting the Authentication + * Method is a Protocol Error. */ + auth_prop = auth->props; + while (auth_prop != NULL && + auth_prop->type != MQTT_PROP_AUTH_METHOD) { + auth_prop = auth_prop->next; + } + if (auth_prop == NULL) { + (void)MqttProps_Free(auth->props); + auth->props = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } } else { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 2d6bd5564..362343bb2 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -2360,6 +2360,36 @@ TEST(decode_auth_v5_props_cannot_cross_packet_end) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(auth.props); } + +/* MQTT 5.0 section 3.15.2.2.2: it is a Protocol Error to omit the + * Authentication Method from an AUTH packet's properties. */ +TEST(decode_auth_v5_missing_auth_method_rejected) +{ + /* Remaining Length 6: Reason 0x18 (Continue authentication), Property + * Length 4, Reason String (0x1F) "r", no Authentication Method. */ + byte cont[] = { + 0xF0, 0x06, MQTT_REASON_CONT_AUTH, 0x04, + MQTT_PROP_REASON_STR, 0x00, 0x01, 'r' + }; + /* Same properties with Reason 0x00 (Success): the long form carries a + * property block, so the Authentication Method is still required. */ + byte success[] = { + 0xF0, 0x06, MQTT_REASON_SUCCESS, 0x04, + MQTT_PROP_REASON_STR, 0x00, 0x01, 'r' + }; + MqttAuth auth; + int rc; + + XMEMSET(&auth, 0, sizeof(auth)); + rc = MqttDecode_Auth(cont, (int)sizeof(cont), &auth); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(auth.props); + + XMEMSET(&auth, 0, sizeof(auth)); + rc = MqttDecode_Auth(success, (int)sizeof(success), &auth); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(auth.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-3.2.2-1] / [MQTT-3.2.2-4] CONNACK Flags receive-side validation. @@ -7560,6 +7590,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_disconnect_v5_props_cannot_cross_packet_end); RUN_TEST(decode_auth_v5_rejects_bytes_after_property_block); RUN_TEST(decode_auth_v5_props_cannot_cross_packet_end); + RUN_TEST(decode_auth_v5_missing_auth_method_rejected); #endif RUN_TEST(decode_connack_flags_session_present_accepted); RUN_TEST(decode_connack_flags_no_session_accepted); From a5f772984de74ee942410f05f9050619af1f29e5 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 16:26:50 -0500 Subject: [PATCH 19/52] F-14585 - Mark the client disconnected when msg_cb abandons a payload When the message callback returned an error while part of a PUBLISH payload was still unread, MqttClient_Publish_ReadPayload returned at once and left the remaining bytes in the transport. IS_CONNECTED stayed set, so the next read parsed payload bytes as a fixed header, contrary to the MqttMsgCb contract that a non-success return disconnects. Clear IS_CONNECTED on that path, as the fatal protocol error path does, and add client tests for a callback failing on the first and a later chunk. --- src/mqtt_client.c | 14 +++++++ tests/test_mqtt_client.c | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index b449c61f7..cbf5aa7ea 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -3732,6 +3732,18 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) return rc; } +/* The message callback refused a PUBLISH. With payload still unread the + * stream can no longer be parsed, so mark the connection unusable as the + * MqttMsgCb contract promises; the application tears the transport down via + * MqttClient_NetDisconnect. */ +static void MqttClient_Publish_AbandonPayload(MqttClient* client, + byte msg_done) +{ + if (!msg_done) { + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_IS_CONNECTED, 0); + } +} + static int MqttClient_Publish_ReadPayload(MqttClient* client, MqttPublish* publish, int timeout_ms) { @@ -3773,6 +3785,7 @@ static int MqttClient_Publish_ReadPayload(MqttClient* client, rc = client->msg_cb(client, publish, publish->buffer_new, msg_done); if (rc != MQTT_CODE_SUCCESS) { + MqttClient_Publish_AbandonPayload(client, msg_done); return rc; }; } @@ -3830,6 +3843,7 @@ static int MqttClient_Publish_ReadPayload(MqttClient* client, rc = client->msg_cb(client, publish, publish->buffer_new, msg_done); if (rc != MQTT_CODE_SUCCESS) { + MqttClient_Publish_AbandonPayload(client, msg_done); return rc; }; } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index d561384b0..5574cd172 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -6742,6 +6742,92 @@ TEST(wait_message_v5_props_null_msg_cb_frees_props) } #endif /* WOLFMQTT_V5 */ +/* Message callback that fails on call number g_msg_cb_fail_on (1-based). */ +static int g_msg_cb_fail_calls; +static int g_msg_cb_fail_on; +static int test_failing_message_cb(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; (void)msg; (void)msg_new; (void)msg_done; + g_msg_cb_fail_calls++; + return (g_msg_cb_fail_calls == g_msg_cb_fail_on) ? + MQTT_CODE_ERROR_CALLBACK : MQTT_CODE_SUCCESS; +} + +/* Drive a PUBLISH whose payload spans several rx_buf reads into a message + * callback that fails on call fail_on, with the transport marked connected. + * Returns the terminal WaitMessage result. */ +static int run_wait_message_cb_fails_mid_payload(int fail_on) +{ + /* rx_buf smaller than the payload, so it is delivered in chunks */ + static byte small_rx_buf[16]; + int rc; + int i; + /* v3.1.1 QoS 0 PUBLISH: type=0x30, Remaining Length 33, topic "a" + * (0x0001 'a'), 30-byte payload (no packet id for QoS 0). */ + static const byte publish_qos0[] = { + 0x30, 0x21, 0x00, 0x01, 'a', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + + rc = MqttClient_Init(&test_client, &test_net, test_failing_message_cb, + test_tx_buf, TEST_TX_BUF_SIZE, small_rx_buf, + (int)sizeof(small_rx_buf), TEST_CMD_TIMEOUT_MS); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + test_client_inited = 1; +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + + g_msg_cb_fail_calls = 0; + g_msg_cb_fail_on = fail_on; + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, publish_qos0, sizeof(publish_qos0)); + g_canned_len = (int)sizeof(publish_qos0); + g_canned_pos = 0; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + } + return rc; +} + +/* A message callback error with payload still unread leaves the rest of the + * PUBLISH in the transport, so the next read would parse payload bytes as a + * fixed header. MqttMsgCb documents that a non-success return causes a net + * disconnect: the client must stop reporting the connection as usable. */ +TEST(wait_message_cb_error_first_chunk_clears_connected) +{ + int rc; + + rc = run_wait_message_cb_fails_mid_payload(1); + + ASSERT_EQ(MQTT_CODE_ERROR_CALLBACK, rc); + ASSERT_EQ(1, g_msg_cb_fail_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +TEST(wait_message_cb_error_later_chunk_clears_connected) +{ + int rc; + + rc = run_wait_message_cb_fails_mid_payload(2); + + ASSERT_EQ(MQTT_CODE_ERROR_CALLBACK, rc); + ASSERT_EQ(2, g_msg_cb_fail_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + /* Counts deliveries for the positive-control test below. */ static int g_msg_cb_calls; static int test_accept_message_cb(MqttClient* client, MqttMessage* msg, @@ -8348,6 +8434,8 @@ void run_mqtt_client_tests(void) RUN_TEST(wait_message_qos1_null_msg_cb_errors_no_ack); RUN_TEST(wait_message_qos0_null_msg_cb_errors); RUN_TEST(wait_message_qos2_null_msg_cb_errors_no_ack); + RUN_TEST(wait_message_cb_error_first_chunk_clears_connected); + RUN_TEST(wait_message_cb_error_later_chunk_clears_connected); #if (WOLFMQTT_MAX_QOS >= 2) && (MQTT_MAX_RECV_QOS2 < 65535) RUN_TEST(wait_message_qos2_full_dedup_table_v311_errors_no_ack); #ifdef WOLFMQTT_V5 From d2ebb5cfa49b7919eb4c139da1841c7a0f50cd0b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 16:41:06 -0500 Subject: [PATCH 20/52] F-13834 - Reject v5 SUBACK and UNSUBACK without a Property Length --- src/mqtt_client.c | 2 ++ src/mqtt_packet.c | 14 +++++++++-- tests/test_mqtt_client.c | 50 ++++++++++++++++++++++++++++++++++++++++ tests/test_mqtt_packet.c | 39 +++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index cbf5aa7ea..b43ddb5b8 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -4631,6 +4631,8 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) * requested topic; too few would be read as granted QoS 0 (fail-open). */ if (subscribe->ack.return_code_count != subscribe->topic_count) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + (void)MqttClient_Flags(client, + MQTT_CLIENT_FLAG_IS_CONNECTED, 0); } else { for (i = 0; i < subscribe->topic_count && i < MAX_MQTT_TOPICS; i++) { diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 00a3e321e..0b8f47472 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -3279,6 +3279,14 @@ int MqttDecode_SubscribeAck(byte* rx_buf, int rx_buf_len, if (remain_len < MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } +#ifdef WOLFMQTT_V5 + /* MQTT 5.0 section 3.9.2: a v5 SUBACK always carries a Property Length + * after the Packet Identifier, even when it is zero. */ + if (subscribe_ack->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + remain_len <= MQTT_DATA_LEN_SIZE) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } +#endif rx_payload = &rx_buf[header_len]; @@ -3667,10 +3675,12 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, /* MQTT 3.1.1 section 3.11.1: the variable header is exactly the two-byte * Packet Identifier and there is no payload, so Remaining Length is fixed - * at 2. v5 section 3.11 appends Properties and Reason Codes. */ + * at 2. v5 section 3.11.2 appends a Property Length, which is present + * even when zero, so a v5 packet holding only the Packet Identifier is + * malformed. */ #ifdef WOLFMQTT_V5 if (unsubscribe_ack->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - if (remain_len < MQTT_DATA_LEN_SIZE) { + if (remain_len <= MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); } } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 5574cd172..6ff86a79d 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -2457,6 +2457,55 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) subscribe.topics[0].return_code); } +/* [MQTT-3.9.3-1] A SUBACK carries one return code per Topic Filter in the + * SUBSCRIBE. A count mismatch is a protocol violation, so the client must + * stop treating the connection as usable (MQTT 5.0 section 4.13.1), as + * MqttClient_Unsubscribe does for the matching UNSUBACK case. */ +TEST(subscribe_too_few_return_codes_clears_connected) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[2]; + /* SUBACK v3.1.1: type=0x90, remain=3, packet_id=46, one return code + * (granted QoS 0) for two requested filters. */ + static const byte suback[] = { 0x90, 0x03, 0x00, 0x2E, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED); + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, suback, sizeof(suback)); + g_canned_len = (int)sizeof(suback); + g_canned_pos = 0; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/one"; + topics[0].qos = MQTT_QOS_0; + topics[1].topic_filter = "test/two"; + topics[1].qos = MQTT_QOS_0; + subscribe.packet_id = 46; + subscribe.topic_count = 2; + subscribe.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + #ifdef WOLFMQTT_V5 /* A v5 UNSUBACK whose per-topic reason code has the high bit set means the * broker refused the unsubscribe; MqttClient_Unsubscribe must surface that as @@ -8330,6 +8379,7 @@ void run_mqtt_client_tests(void) RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); + RUN_TEST(subscribe_too_few_return_codes_clears_connected); #ifdef WOLFMQTT_V5 RUN_TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected); RUN_TEST(unsubscribe_too_few_reason_codes_is_malformed); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 362343bb2..b3766b888 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -5630,6 +5630,25 @@ TEST(decode_suback_v5_props_freed_on_payload_overrun) ASSERT_NULL(ack.props); } +/* MQTT 5.0 section 3.9.2: the v5 SUBACK variable header is the Packet + * Identifier followed by the Property Length, which is mandatory even when + * zero (section 2.2.2.1). A packet carrying only the Packet Identifier is + * malformed and must not decode as a SUBACK with zero reason codes. */ +TEST(decode_suback_v5_missing_props_len_rejected) +{ + byte buf[] = { + 0x90, 0x02, /* SUBACK, remain_len = 2 */ + 0x00, 0x01 /* packet_id, nothing after it */ + }; + MqttSubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_SubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); +} + /* Pin v5's broadened set via the helper. */ TEST(suback_return_code_v5_allowed_set) { @@ -6212,6 +6231,24 @@ TEST(decode_unsuback_v5_props_past_remain_len_rejected) rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); } + +/* MQTT 5.0 section 3.11.2: the v5 UNSUBACK variable header is the Packet + * Identifier followed by the mandatory Property Length (section 2.2.2.1). A + * packet carrying only the Packet Identifier is malformed. */ +TEST(decode_unsuback_v5_missing_props_len_rejected) +{ + byte buf[] = { + 0xB0, 0x02, /* UNSUBACK, remain_len = 2 */ + 0x00, 0x01 /* packet_id, nothing after it */ + }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); +} #endif /* WOLFMQTT_V5 */ #ifdef WOLFMQTT_BROKER @@ -7782,6 +7819,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_suback_v5_not_authorized_accepted); RUN_TEST(suback_return_code_v5_allowed_set); RUN_TEST(decode_suback_v5_props_freed_on_payload_overrun); + RUN_TEST(decode_suback_v5_missing_props_len_rejected); #endif /* MqttDecode_PublishResp */ @@ -7824,6 +7862,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_v5_longer_body_still_accepted); RUN_TEST(decode_unsuback_v5_reason_codes); RUN_TEST(decode_unsuback_v5_props_past_remain_len_rejected); + RUN_TEST(decode_unsuback_v5_missing_props_len_rejected); #endif #ifdef WOLFMQTT_BROKER RUN_TEST(encode_unsuback_packet_id_zero_rejected); From 8ce7681346f674197c36ac7c469263b6153452d1 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 19:07:08 -0500 Subject: [PATCH 21/52] F-12464 - Refuse a v5 PUBLISH when the quota lock cannot be taken --- src/mqtt_client.c | 25 ++++++--- tests/test_mqtt_props_init.c | 99 ++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index b43ddb5b8..a0feea3b0 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -448,14 +448,17 @@ WOLFMQTT_LOCAL void MqttReadStop(MqttClient* client, MqttMsgStat* stat) * duplicate, or unmatched ack cannot credit the quota twice. */ /* Atomically test-and-reserve one quota unit under lockClient (fixes the * unlocked exhaustion race). Returns 1 if a unit is held for this publish - * (already held, or newly reserved), 0 if the quota is exhausted. */ + * (already held, or newly reserved), 0 if the quota is exhausted, or a + * negative error code if lockClient could not be taken, in which case the + * quota was not checked and nothing is reserved. */ static int MqttClient_RecvQuotaReserve(MqttClient* client, MqttPublish* publish) { int reserved = 0; #ifdef WOLFMQTT_MULTITHREAD - if (wm_SemLock(&client->lockClient) != 0) { - return 1; /* lock failure: do not fail the publish on a quota race */ + int rc = wm_SemLock(&client->lockClient); + if (rc != 0) { + return rc; } #endif if (publish->stat.recvQuotaHeld) { @@ -4134,14 +4137,20 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, * non-blocking one keeps its pending response for the reader, so * both reserve. */ if (client->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && - publish->qos > MQTT_QOS_0 && + publish->qos > MQTT_QOS_0 #if defined(WOLFMQTT_MULTITHREAD) && !defined(WOLFMQTT_NONBLOCK) - !writeOnly && + && !writeOnly #endif - !MqttClient_RecvQuotaReserve(client, publish)) + ) { - MqttWriteStop(client, &publish->stat); - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + rc = MqttClient_RecvQuotaReserve(client, publish); + if (rc <= 0) { + MqttWriteStop(client, &publish->stat); + if (rc == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); + } + return rc; + } } #endif diff --git a/tests/test_mqtt_props_init.c b/tests/test_mqtt_props_init.c index 5b08a10ce..958972283 100644 --- a/tests/test_mqtt_props_init.c +++ b/tests/test_mqtt_props_init.c @@ -25,6 +25,10 @@ static wm_Sem* sem_watch_recv_lock; static MqttMsgStat* sem_watch_ping_stat; static int sem_stale_ping_locks; static int sem_watched_recv_locks; +static wm_Sem* sem_fail_lock; +static wm_Sem* sem_fail_lock_while_held; +static int sem_fail_lock_skip; +static int quota_write_calls; #ifdef ENABLE_MQTT_CURL #define TEST_CLIENT_LOCK_COUNT 4 @@ -52,6 +56,10 @@ static void reset_sem_state(void) sem_watch_ping_stat = NULL; sem_stale_ping_locks = 0; sem_watched_recv_locks = 0; + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + sem_fail_lock_skip = 0; + quota_write_calls = 0; } int wm_SemInit(wm_Sem* sem) @@ -83,6 +91,20 @@ int wm_SemFree(wm_Sem* sem) int wm_SemLock(wm_Sem* sem) { + /* Fail one acquisition of sem_fail_lock made while + * sem_fail_lock_while_held is owned, after skipping sem_fail_lock_skip + * such acquisitions, so a test can target a single lock inside a larger + * operation. */ + if (sem == sem_fail_lock && sem_fail_lock_while_held != NULL && + sem_fail_lock_while_held->lock_count > 0) { + if (sem_fail_lock_skip > 0) { + sem_fail_lock_skip--; + } + else { + sem_fail_lock = NULL; + return MQTT_CODE_ERROR_SYSTEM; + } + } sem->lock_count++; if (sem == sem_watch_recv_lock) { sem_watched_recv_locks++; @@ -479,6 +501,80 @@ static int test_failed_property_free_retried(void) return 0; } +static int quota_net_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; + (void)buf; + (void)timeout_ms; + quota_write_calls++; + return buf_len; +} + +/* [MQTT-3.3.4-9] The Client MUST NOT send more QoS 1/2 PUBLISH packets than + * the server's Receive Maximum allows. If lockClient cannot be taken to + * reserve a unit, the quota was never checked, so the PUBLISH must be refused + * rather than sent. The quota is exhausted here so any send exceeds it. */ +static int test_recv_quota_lock_failure_refuses_publish(void) +{ + int rc; + MqttClient client; + MqttNet net; + MqttPublish publish; + byte tx_buf[64]; + byte rx_buf[64]; + byte payload[1] = { 'x' }; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + XMEMSET(&publish, 0, sizeof(publish)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = quota_net_write; + net.disconnect = ping_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "quota client initialization returned %d\n", rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, + MQTT_CLIENT_FLAG_CONNECT_SENT | MQTT_CLIENT_FLAG_IS_CONNECTED); + client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + client.server_recv_max_negotiated = 1; + client.server_recv_max = 0; + + publish.qos = MQTT_QOS_1; + publish.packet_id = 1; + publish.topic_name = "t"; + publish.buffer = payload; + publish.total_len = (word32)sizeof(payload); + + /* With lockSend held, MqttWriteStart first takes lockClient to mark the + * writer active; the next lockClient acquisition is the Receive Maximum + * reservation, which is the one that fails. */ + sem_fail_lock = &client.lockClient; + sem_fail_lock_while_held = &client.lockSend; + sem_fail_lock_skip = 1; + rc = MqttClient_Publish(&client, &publish); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + + if (rc != MQTT_CODE_ERROR_SYSTEM || quota_write_calls != 0 || + publish.stat.recvQuotaHeld != 0 || client.server_recv_max != 0) { + fprintf(stderr, + "quota lock failure returned %d, writes=%d held=%d quota=%u\n", + rc, quota_write_calls, publish.stat.recvQuotaHeld, + (unsigned)client.server_recv_max); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + int main(void) { if (test_sn_null_ping_resets_terminal_read_state() != 0) { @@ -502,5 +598,8 @@ int main(void) if (test_repeated_client_deinit_preserves_property_owner() != 0) { return 1; } + if (test_recv_quota_lock_failure_refuses_publish() != 0) { + return 1; + } return 0; } From ff139fb0123827fd95d24cdc39ebd241d02beb52 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 19 Oct 2026 19:10:44 -0500 Subject: [PATCH 22/52] F-12468 - Let SN_Client_WillTopicUpdate send the empty Will delete --- src/mqtt_sn_client.c | 5 +++-- tests/test_mqtt_sn_client.c | 33 +++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_sn_client.h | 10 ++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 4158e4a64..c505e0bb2 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -1527,9 +1527,10 @@ int SN_Client_WillTopicUpdate(MqttClient *client, SN_Will *will) return rc; } - /* Encode Will Topic Update */ + /* Encode Will Topic Update. With no topic, send the empty form that + * deletes the stored Will (MQTT-SN 1.2 section 5.4.19). */ rc = SN_Encode_WillTopicUpdate(client->tx_buf, - client->tx_buf_len, will); + client->tx_buf_len, (will->willTopic != NULL) ? will : NULL); #ifdef WOLFMQTT_DEBUG_CLIENT PRINTF("MqttClient_EncodePacket: Len %d, Type %s (%d)", rc, SN_Packet_TypeDesc(SN_MSG_TYPE_WILLTOPICUPD), diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 366c5c065..22ffa566e 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -924,6 +924,38 @@ TEST(sn_willmsgupd_payload_scrubbed_after_send) sn_will_msg_update_scrub_check(0 /* no CONTINUE */); } +/* MQTT-SN 1.2 section 5.4.19: an empty WILLTOPICUPD (no Flags, no WillTopic) + * asks the gateway to delete the stored Will topic and Will message. + * SN_Client_WillTopicUpdate must send that form when the SN_Will carries no + * topic, then complete on the WILLTOPICRESP (section 5.4.20). */ +TEST(sn_willtopicupd_null_topic_sends_delete) +{ + SN_Will will; + int rc = MQTT_CODE_CONTINUE; + int i; + /* WILLTOPICUPD: Length=2, MsgType=0x1A, nothing else. */ + static const byte expect[] = { 0x02, SN_MSG_TYPE_WILLTOPICUPD }; + /* WILLTOPICRESP: Length=3, MsgType=0x1B, ReturnCode=accepted. */ + static const byte resp[] = { 0x03, SN_MSG_TYPE_WILLTOPICRESP, + SN_RC_ACCEPTED }; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + mock_net_push(&g_mock, resp, (int)sizeof(resp)); + + XMEMSET(&will, 0, sizeof(will)); + will.willTopic = NULL; + + for (i = 0; i < 50 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WillTopicUpdate(&g_client, &will); + } + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(SN_RC_ACCEPTED, will.resp.topicResp.return_code); + ASSERT_EQ((int)sizeof(expect), g_mock.out_len); + ASSERT_EQ(0, XMEMCMP(expect, g_mock.out, sizeof(expect))); + ASSERT_NO_PENDRESP(); +} + /* #3138 error-path coverage. The success-path tests above never reach the * rc != xfer branch because mock_write() always accepts the whole buffer, so * the error-path CLIENT_FORCE_ZERO (and the pendResp removal beside it) would go @@ -3297,6 +3329,7 @@ int main(int argc, char** argv) RUN_TEST(sn_connect_refused); RUN_TEST(sn_will_payload_scrubbed_after_send); RUN_TEST(sn_willmsgupd_payload_scrubbed_after_send); + RUN_TEST(sn_willtopicupd_null_topic_sends_delete); RUN_TEST(sn_willmsgupd_payload_scrubbed_on_write_error); #ifdef WOLFMQTT_MULTITHREAD RUN_TEST(sn_willmsgupd_scrubbed_on_response_registration_error); diff --git a/wolfmqtt/mqtt_sn_client.h b/wolfmqtt/mqtt_sn_client.h index 3aef4c149..99a19ba74 100644 --- a/wolfmqtt/mqtt_sn_client.h +++ b/wolfmqtt/mqtt_sn_client.h @@ -80,13 +80,15 @@ WOLFMQTT_API int SN_Client_Connect( MqttClient *client, SN_Connect *connect); -/*! \brief Encodes and sends the MQTT-SN Will Topic Update packet. Sending - a NULL 'will' indicates that the client wishes to delete the - Will topic and the Will message stored in the server. +/*! \brief Encodes and sends the MQTT-SN Will Topic Update packet. Setting + will->willTopic to NULL sends an empty Will Topic Update, + which asks the server to delete the stored Will topic and + Will message. * \note This is a blocking function that will wait for MqttNet.read * \param client Pointer to MqttClient structure * \param will Pointer to SN_Will structure initialized - with topic and message parameters. NULL is valid. + with topic and message parameters. Must not be + NULL; it also holds the request state. * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ From 9e4748e7cc46c2943176bbf9832caf7c7c3507f3 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 07:04:31 -0500 Subject: [PATCH 23/52] Restore MqttProps_FindType and require an AUTH method with properties --- src/mqtt_packet.c | 21 +++++++++++++-- tests/test_mqtt_packet.c | 58 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 0b8f47472..485e9b36e 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -4145,9 +4145,13 @@ int MqttEncode_Auth(byte *tx_buf, int tx_buf_len, MqttAuth *auth) } /* [MQTT-4.12] AUTH (REAUTH/CONT_AUTH) must carry an Authentication Method - * property; reject if the list is empty or lacks it. */ + * property; reject if the list is empty or lacks it. A Success AUTH may + * omit Properties entirely (MQTT 5.0 section 3.15.2.1), but once it has + * any, the Authentication Method is required too (section 3.15.2.2.2). */ if ((auth->reason_code == MQTT_REASON_CONT_AUTH) || - (auth->reason_code == MQTT_REASON_REAUTH)) { + (auth->reason_code == MQTT_REASON_REAUTH) || + ((auth->reason_code == MQTT_REASON_SUCCESS) && + (auth->props != NULL))) { auth_prop = auth->props; while (auth_prop != NULL && auth_prop->type != MQTT_PROP_AUTH_METHOD) { auth_prop = auth_prop->next; @@ -4468,6 +4472,19 @@ int MqttProps_Free(MqttProp *head) return ret; } +/* Find the first property of a specific type. Returns NULL if none */ +MqttProp* MqttProps_FindType(MqttProp *head, MqttPropertyType type) +{ + while (head != NULL) { + if (head->type == type) { + break; + } + head = head->next; + } + + return head; +} + #endif /* WOLFMQTT_V5 */ int MqttPacket_HandleNetError(MqttClient *client, int rc) diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index b3766b888..407136e67 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1781,6 +1781,35 @@ TEST(encode_publish_v5_response_topic_wildcard_rejected) MqttProps_Free(pub.props); } +/* MqttProps_FindType is public API: it must return the first property of the + * requested type, NULL when the list has none, and NULL for an empty list. */ +TEST(props_find_type) +{ + MqttProp* head = NULL; + MqttProp* first; + MqttProp* second; + MqttProp* third; + + ASSERT_NULL(MqttProps_FindType(NULL, MQTT_PROP_USER_PROP)); + + first = MqttProps_Add(&head); + ASSERT_TRUE(first != NULL); + first->type = MQTT_PROP_CONTENT_TYPE; + second = MqttProps_Add(&head); + ASSERT_TRUE(second != NULL); + second->type = MQTT_PROP_USER_PROP; + third = MqttProps_Add(&head); + ASSERT_TRUE(third != NULL); + third->type = MQTT_PROP_USER_PROP; + + ASSERT_TRUE(MqttProps_FindType(head, MQTT_PROP_CONTENT_TYPE) == first); + /* The first match wins when a type repeats. */ + ASSERT_TRUE(MqttProps_FindType(head, MQTT_PROP_USER_PROP) == second); + ASSERT_NULL(MqttProps_FindType(head, MQTT_PROP_RESP_TOPIC)); + + MqttProps_Free(head); +} + /* A single message may not carry more than the internal * MQTT_MAX_PROPS (default 30) properties; otherwise a peer can saturate the * shared property pool. 40 User Property entries exceeds that cap. The cap @@ -7129,6 +7158,33 @@ TEST(auth_v5_success_no_props_roundtrip) ASSERT_NULL(dec.props); } +/* MQTT 5.0 section 3.15.2.2.2: an AUTH that carries Properties must include + * the Authentication Method; omitting it is a Protocol Error. The short form + * of section 3.15.2.1 applies only to a Success AUTH with no Properties, so a + * Success AUTH with only a Reason String must be refused, not encoded. */ +TEST(encode_auth_v5_success_props_without_method_rejected) +{ + byte buf[32]; + MqttAuth enc; + MqttProp prop; + int rc; + char reason[] = "ok"; + + XMEMSET(&enc, 0, sizeof(enc)); + XMEMSET(&prop, 0, sizeof(prop)); + XMEMSET(buf, 0xA5, sizeof(buf)); + prop.type = MQTT_PROP_REASON_STR; + prop.data_str.str = reason; + prop.data_str.len = (word16)XSTRLEN(reason); + prop.next = NULL; + enc.reason_code = MQTT_REASON_SUCCESS; + enc.props = ∝ + + rc = MqttEncode_Auth(buf, (int)sizeof(buf), &enc); + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, rc); + ASSERT_EQ(0xA5, buf[0]); +} + TEST(auth_v5_invalid_reason_code_rejected) { /* A reason code outside {SUCCESS, CONT_AUTH, REAUTH} must not decode @@ -7590,6 +7646,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_byte_property_out_of_range_rejected); RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); RUN_TEST(encode_publish_v5_response_topic_wildcard_rejected); + RUN_TEST(props_find_type); RUN_TEST(decode_publish_v5_property_count_capped); RUN_TEST(decode_publish_v5_duplicate_singleton_prop_rejected); RUN_TEST(decode_publish_v5_duplicate_subscription_id_accepted); @@ -7926,6 +7983,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(auth_v5_reauth_decodes_without_error); RUN_TEST(auth_v5_success_remaining_length_zero); RUN_TEST(auth_v5_success_no_props_roundtrip); + RUN_TEST(encode_auth_v5_success_props_without_method_rejected); RUN_TEST(auth_v5_invalid_reason_code_rejected); RUN_TEST(decode_auth_v5_reason_code_past_buf_rejected); #endif From 233cec35514df76d15cef91d4fb23d9026c78b65 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 07:25:47 -0500 Subject: [PATCH 24/52] F-15375 - Accept only lowercase hex in POSIX persist record names --- src/mqtt_broker_persist_posix.c | 7 ++-- tests/test_broker_connect.c | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker_persist_posix.c b/src/mqtt_broker_persist_posix.c index 39c2a3b04..640c53ea9 100644 --- a/src/mqtt_broker_persist_posix.c +++ b/src/mqtt_broker_persist_posix.c @@ -120,7 +120,10 @@ static void wmqb_hex_encode(char* out, const byte* in, word16 in_len) } /* hex decode a NUL-terminated hex string into out. Returns the byte - * length on success, -1 on malformed input. */ + * length on success, -1 on malformed input. Only lowercase is accepted, the + * form wmqb_hex_encode writes: an uppercase name decodes to a key whose + * get/del path is the lowercase name, so iterating it would yield a record + * that can never be deleted. */ static int wmqb_hex_decode(const char* in, byte* out, word16 out_cap) { word16 n; @@ -145,12 +148,10 @@ static int wmqb_hex_decode(const char* in, byte* out, word16 out_cap) char c = in[2 * i]; if (c >= '0' && c <= '9') hi = (byte)(c - '0'); else if (c >= 'a' && c <= 'f') hi = (byte)(10 + c - 'a'); - else if (c >= 'A' && c <= 'F') hi = (byte)(10 + c - 'A'); else return -1; c = in[2 * i + 1]; if (c >= '0' && c <= '9') lo = (byte)(c - '0'); else if (c >= 'a' && c <= 'f') lo = (byte)(10 + c - 'a'); - else if (c >= 'A' && c <= 'F') lo = (byte)(10 + c - 'A'); else return -1; out[i] = (byte)((hi << 4) | lo); } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 92b882d14..8ad5c13e3 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -12210,6 +12210,71 @@ TEST(persist_iter_skips_fifo_without_blocking) ASSERT_EQ(0, WEXITSTATUS(status)); } +/* kv_iter and kv_del take the same opaque key bytes, so any key the POSIX + * backend yields while iterating must be removable through kv_del; the static + * DelOutQueue loop repeats iterate-and-delete until nothing matches and would + * otherwise never finish. The broker names records in lowercase hex, so a + * record file named in uppercase hex (created outside the broker) must not + * be yielded as a key that kv_del then cannot find. */ +TEST(persist_iter_keys_are_deletable) +{ + static const byte blob[] = "x"; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + char rec_path[680]; + MqttBrokerPersistHooks hooks; + PersistIterCapture capture; + char* root; + FILE* f; + int paths_ok = 0; + int init_rc; + int iter1_rc = MQTT_CODE_ERROR_SYSTEM; + int del_rc = MQTT_CODE_SUCCESS; + int iter2_rc = MQTT_CODE_ERROR_SYSTEM; + int iter2_calls = -1; + + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&capture, 0, sizeof(capture)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && + snprintf(rec_path, sizeof(rec_path), "%s/ABCD.bin", + ns_path) > 0) { + paths_ok = 1; + } + ASSERT_TRUE(paths_ok); + ASSERT_EQ(0, mkdir(ns_path, 0700)); + f = fopen(rec_path, "wb"); + ASSERT_NOT_NULL(f); + ASSERT_EQ(sizeof(blob), fwrite(blob, 1, sizeof(blob), f)); + ASSERT_EQ(0, fclose(f)); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + iter1_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, + &capture); + if (capture.calls > 0) { + del_rc = hooks.kv_del(hooks.ctx, 7, capture.key, + capture.key_len); + } + XMEMSET(&capture, 0, sizeof(capture)); + iter2_rc = hooks.kv_iter(hooks.ctx, 7, persist_test_iter_cb, + &capture); + iter2_calls = capture.calls; + MqttBrokerNet_PersistPosix_Free(&hooks); + } + + (void)unlink(rec_path); + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, iter1_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, del_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, iter2_rc); + ASSERT_EQ(0, iter2_calls); +} + #ifdef __MACH__ /* macOS exposes /tmp as a trusted operating-system symlink to /private/tmp. * A persistence root below that standard alias must initialize without @@ -13895,6 +13960,7 @@ int main(int argc, char** argv) #endif RUN_TEST(persist_parent_component_rejected_as_bad_argument); RUN_TEST(persist_iter_skips_fifo_without_blocking); + RUN_TEST(persist_iter_keys_are_deletable); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(persist_restore_packet_id_wrap_preserves_fifo); #ifdef WOLFMQTT_NONBLOCK From 0daebf182da73e1a69dfde704d4b5670e455572b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 07:29:46 -0500 Subject: [PATCH 25/52] F-12450 - Check the property identifier against the Property Length --- src/mqtt_packet.c | 5 +++++ tests/test_mqtt_packet.c | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 485e9b36e..886d9c0a1 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -1118,6 +1118,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, } cur_prop->type = (MqttPropertyType)prop_type; tmp = rc; + /* The Identifier must fit in the remaining Property Length */ + if ((word32)tmp > prop_len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } buf += tmp; total += tmp; prop_len -= tmp; diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 407136e67..95e14ad71 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -7520,6 +7520,24 @@ TEST(decode_props_auth_data_without_method_rejected) ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); ASSERT_NULL(props); } + +/* MQTT 5.0 section 2.2.2.1: the Property Length counts every byte of the + * properties, and section 2.2.2.2 encodes each Identifier as a Variable Byte + * Integer. An Identifier whose encoding runs past the declared Property + * Length is a Malformed Packet, and must be rejected before the remaining + * length is charged for it. Wire: Property Length 1, Identifier 0x80 0x01 + * (two bytes, value 128). */ +TEST(decode_props_identifier_past_prop_len_rejected) +{ + byte buf[] = { 0x80, 0x01 }; + MqttProp* props = NULL; + int rc; + + rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &props, buf, + (word32)sizeof(buf), 1); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(props); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -7971,6 +7989,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_pubcomp_v5_reason_code_out_of_table_rejected); RUN_TEST(decode_props_connack_max_packet_size_zero_rejected); RUN_TEST(decode_props_auth_data_without_method_rejected); + RUN_TEST(decode_props_identifier_past_prop_len_rejected); /* MqttEncode/Decode_Auth */ RUN_TEST(encode_props_string_invalid_utf8_rejected); From d3bdc0c5d84e8c3972c2cdf825bfed41680bc70c Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 07:34:03 -0500 Subject: [PATCH 26/52] F-12449 - Read client flags under lockClient in KeepAlive and WaitType --- src/mqtt_client.c | 13 +++++--- tests/test_mqtt_props_init.c | 61 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index a0feea3b0..2e6bc7d69 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2718,8 +2718,7 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, * race a concurrent publisher inside wolfSSL_write in MT builds. Gated * on recvFatal so a local ack-encode failure (e.g. an out-of-table * Reason Code) does not disconnect an otherwise healthy peer. */ - if (recvFatal && MqttClient_IsFatalProtoError(rc) && - (client->flags & MQTT_CLIENT_FLAG_IS_CONNECTED) != 0) { + if (recvFatal && MqttClient_IsFatalProtoError(rc)) { (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_IS_CONNECTED, 0); } return rc; @@ -5313,14 +5312,20 @@ static int MqttClient_KeepAlive(MqttClient *client, MqttObject* msg) int rc = MQTT_CODE_SUCCESS; int mid_transfer; word32 now, elapsed, threshold; + word32 flags = 0; if (client == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } /* Disabled at runtime by the application (e.g. it schedules its own ping), - * independent of the keep-alive negotiated with the broker. */ - if ((client->flags & MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE) != 0) { + * independent of the keep-alive negotiated with the broker. Read under + * lockClient, since another thread may change the flags. */ + rc = MqttClient_ReadFlags(client, &flags); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + if ((flags & MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE) != 0) { return MQTT_CODE_SUCCESS; } diff --git a/tests/test_mqtt_props_init.c b/tests/test_mqtt_props_init.c index 958972283..6fc3c5a89 100644 --- a/tests/test_mqtt_props_init.c +++ b/tests/test_mqtt_props_init.c @@ -29,6 +29,8 @@ static wm_Sem* sem_fail_lock; static wm_Sem* sem_fail_lock_while_held; static int sem_fail_lock_skip; static int quota_write_calls; +static MqttClient* sem_flag_client; +static word32 sem_flag_bits; #ifdef ENABLE_MQTT_CURL #define TEST_CLIENT_LOCK_COUNT 4 @@ -60,6 +62,8 @@ static void reset_sem_state(void) sem_fail_lock_while_held = NULL; sem_fail_lock_skip = 0; quota_write_calls = 0; + sem_flag_client = NULL; + sem_flag_bits = 0; } int wm_SemInit(wm_Sem* sem) @@ -106,6 +110,11 @@ int wm_SemLock(wm_Sem* sem) } } sem->lock_count++; + /* Model another thread that updated client->flags under lockClient and + * released it just as this acquisition succeeded. */ + if (sem_flag_client != NULL && sem == &sem_flag_client->lockClient) { + sem_flag_client->flags |= sem_flag_bits; + } if (sem == sem_watch_recv_lock) { sem_watched_recv_locks++; } @@ -575,6 +584,55 @@ static int test_recv_quota_lock_failure_refuses_publish(void) return 0; } +/* client->flags is shared with other threads and guarded by lockClient in + * multithread builds, so the keep-alive check of NO_AUTO_KEEPALIVE must read + * it under that lock. Here another thread disables automatic keep-alive under + * lockClient; a read that bypasses the lock misses that update and sends a + * PINGREQ the application has turned off. The ping is otherwise due: + * keep-alive is negotiated and nothing has been sent since time 0. */ +static int test_keepalive_flag_read_takes_client_lock(void) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = quota_net_write; + net.disconnect = ping_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "keep-alive client initialization returned %d\n", rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_CONNECT_SENT | + MQTT_CLIENT_FLAG_IS_CONNECTED); + client.keep_alive_sec = 60; + client.last_tx_time = 0; + + sem_flag_client = &client; + sem_flag_bits = MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE; + rc = MqttClient_WaitMessage_ex(&client, &client.msg, 1000); + sem_flag_client = NULL; + + if (quota_write_calls != 0) { + fprintf(stderr, + "keep-alive ignored a locked flag update: rc=%d writes=%d\n", + rc, quota_write_calls); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + int main(void) { if (test_sn_null_ping_resets_terminal_read_state() != 0) { @@ -601,5 +659,8 @@ int main(void) if (test_recv_quota_lock_failure_refuses_publish() != 0) { return 1; } + if (test_keepalive_flag_read_takes_client_lock() != 0) { + return 1; + } return 0; } From 27fcb81f21f051c681d9ab0e3a96462559ea3e3c Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 08:00:18 -0500 Subject: [PATCH 27/52] F-13805 - Report native semaphore init failures from wm_SemInit --- .gitignore | 1 + src/mqtt_client.c | 21 ++++- tests/include.am | 17 +++- tests/test_mqtt_sem_config.h | 23 +++++ tests/test_mqtt_sem_init.c | 165 +++++++++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 tests/test_mqtt_sem_config.h create mode 100644 tests/test_mqtt_sem_init.c diff --git a/.gitignore b/.gitignore index 6c79ba464..3b0b3ac6a 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,5 @@ tests/test_mqtt_sn_client_mt_only tests/test_mqtt_sn_client_nodata tests/unit_tests tests/test_mqtt_props_init +tests/test_mqtt_sem_init tests/test_mqtt_curl_init diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 2e6bc7d69..1b3d6e3d2 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -137,11 +137,16 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); #elif defined(WOLFMQTT_POSIX_SEMAPHORES) /* Posix style semaphore */ int wm_SemInit(wm_Sem *s) { + if (pthread_mutex_init(&s->mutex, NULL) != 0) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } #ifndef WOLFMQTT_NO_COND_SIGNAL s->lockCount = 0; - pthread_cond_init(&s->cond, NULL); + if (pthread_cond_init(&s->cond, NULL) != 0) { + (void)pthread_mutex_destroy(&s->mutex); + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } #endif - pthread_mutex_init(&s->mutex, NULL); return 0; } int wm_SemFree(wm_Sem *s) { @@ -176,7 +181,14 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); /* FreeRTOS binary semaphore */ int wm_SemInit(wm_Sem *s) { *s = xSemaphoreCreateBinary(); - xSemaphoreGive(*s); + if (*s == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + if (xSemaphoreGive(*s) != pdTRUE) { + vSemaphoreDelete(*s); + *s = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } return 0; } int wm_SemFree(wm_Sem *s) { @@ -196,6 +208,9 @@ static int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg); /* Windows semaphore object */ int wm_SemInit(wm_Sem *s) { *s = CreateSemaphoreW( NULL, 1, 1, NULL); + if (*s == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } return 0; } int wm_SemFree(wm_Sem *s) { diff --git a/tests/include.am b/tests/include.am index 9d79bef16..6c8612e1e 100644 --- a/tests/include.am +++ b/tests/include.am @@ -41,6 +41,20 @@ tests_test_mqtt_props_init_SOURCES = \ tests_test_mqtt_props_init_CPPFLAGS = -I$(top_srcdir) \ -include $(top_srcdir)/tests/test_mqtt_props_config.h $(AM_CPPFLAGS) +# Native semaphore initialization tests. Build mqtt_client.c directly with +# the POSIX mutex and condition-variable primitives renamed so their +# initialization failures are deterministic. +check_PROGRAMS += tests/test_mqtt_sem_init +tests_test_mqtt_sem_init_SOURCES = \ + tests/test_mqtt_sem_init.c \ + src/mqtt_client.c \ + src/mqtt_packet.c \ + src/mqtt_socket.c \ + src/mqtt_sn_client.c \ + src/mqtt_sn_packet.c +tests_test_mqtt_sem_init_CPPFLAGS = -I$(top_srcdir) \ + -include $(top_srcdir)/tests/test_mqtt_sem_config.h $(AM_CPPFLAGS) + # libcurl global lifecycle tests. Compile mqtt_socket.c directly so the test # can inject deterministic curl_global_init/cleanup results. if HAVE_LIBCURL @@ -54,7 +68,8 @@ endif # Test framework headers noinst_HEADERS += tests/unit_test.h tests/test_broker_alloc.h \ - tests/test_mqtt_props_config.h tests/test_mqtt_curl_config.h + tests/test_mqtt_props_config.h tests/test_mqtt_curl_config.h \ + tests/test_mqtt_sem_config.h # MQTT-SN packet decoder tests. The SN_Decode_* routines are WOLFMQTT_LOCAL # (hidden) so they cannot be linked from libwolfmqtt; src/mqtt_sn_packet.c is diff --git a/tests/test_mqtt_sem_config.h b/tests/test_mqtt_sem_config.h new file mode 100644 index 000000000..b5e876a20 --- /dev/null +++ b/tests/test_mqtt_sem_config.h @@ -0,0 +1,23 @@ +/* Force the multithreaded client and rename the POSIX primitives behind + * wm_SemInit/wm_SemFree so the dedicated semaphore test can make their + * initialization fail deterministically. */ +#include "wolfmqtt/options.h" + +#ifndef WOLFMQTT_MULTITHREAD + #define WOLFMQTT_MULTITHREAD +#endif + +#if defined(__FreeBSD__) || defined(__linux__) || defined(__QNX__) + /* Declare the real primitives before renaming them. */ + #include + int wolfmqtt_test_pthread_mutex_init(pthread_mutex_t* m, + const pthread_mutexattr_t* a); + int wolfmqtt_test_pthread_mutex_destroy(pthread_mutex_t* m); + int wolfmqtt_test_pthread_cond_init(pthread_cond_t* c, + const pthread_condattr_t* a); + int wolfmqtt_test_pthread_cond_destroy(pthread_cond_t* c); + #define pthread_mutex_init wolfmqtt_test_pthread_mutex_init + #define pthread_mutex_destroy wolfmqtt_test_pthread_mutex_destroy + #define pthread_cond_init wolfmqtt_test_pthread_cond_init + #define pthread_cond_destroy wolfmqtt_test_pthread_cond_destroy +#endif diff --git a/tests/test_mqtt_sem_init.c b/tests/test_mqtt_sem_init.c new file mode 100644 index 000000000..7a62a89b9 --- /dev/null +++ b/tests/test_mqtt_sem_init.c @@ -0,0 +1,165 @@ +/* Native semaphore initialization failure tests with deterministic POSIX + * primitive injection. */ + +#include +#include + +#include "wolfmqtt/mqtt_client.h" + +#ifdef WOLFMQTT_POSIX_SEMAPHORES + +/* The config header renames these in every translation unit; call the real + * primitives from the hooks below. */ +#undef pthread_mutex_init +#undef pthread_mutex_destroy +#undef pthread_cond_init +#undef pthread_cond_destroy + +static int mutex_fail_on; /* fail the Nth mutex init, 0 = never */ +static int cond_fail_on; /* fail the Nth cond init, 0 = never */ +static int mutex_inits; +static int cond_inits; +static int mutex_live; +static int cond_live; + +static void reset_counts(void) +{ + mutex_fail_on = 0; + cond_fail_on = 0; + mutex_inits = 0; + cond_inits = 0; + mutex_live = 0; + cond_live = 0; +} + +int wolfmqtt_test_pthread_mutex_init(pthread_mutex_t* m, + const pthread_mutexattr_t* a) +{ + int rc; + if (++mutex_inits == mutex_fail_on) { + return EAGAIN; + } + rc = pthread_mutex_init(m, a); + if (rc == 0) { + mutex_live++; + } + return rc; +} + +int wolfmqtt_test_pthread_mutex_destroy(pthread_mutex_t* m) +{ + mutex_live--; + return pthread_mutex_destroy(m); +} + +int wolfmqtt_test_pthread_cond_init(pthread_cond_t* c, + const pthread_condattr_t* a) +{ + int rc; + if (++cond_inits == cond_fail_on) { + return ENOMEM; + } + rc = pthread_cond_init(c, a); + if (rc == 0) { + cond_live++; + } + return rc; +} + +int wolfmqtt_test_pthread_cond_destroy(pthread_cond_t* c) +{ + cond_live--; + return pthread_cond_destroy(c); +} + +static int test_connect(void* context, const char* host, word16 port, + int timeout_ms) +{ + (void)context; + (void)host; + (void)port; + (void)timeout_ms; + return MQTT_CODE_SUCCESS; +} + +static int test_io(void* context, byte* buf, int buf_len, int timeout_ms) +{ + (void)context; + (void)buf; + (void)buf_len; + (void)timeout_ms; + return MQTT_CODE_ERROR_TIMEOUT; +} + +static int test_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; + (void)buf; + (void)timeout_ms; + return buf_len; +} + +static int test_disconnect(void* context) +{ + (void)context; + return MQTT_CODE_SUCCESS; +} + +/* A failed native mutex or condition-variable initialization must fail + * MqttClient_Init rather than mark the lock initialized, and must leave no + * native object behind once the failed init has cleaned up. */ +static int run_init_failure(const char* name, int fail_mutex, int fail_cond) +{ + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + int rc; + + reset_counts(); + mutex_fail_on = fail_mutex; + cond_fail_on = fail_cond; + XMEMSET(&net, 0, sizeof(net)); + net.connect = test_connect; + net.read = test_io; + net.write = test_write; + net.disconnect = test_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc == MQTT_CODE_SUCCESS) { + MqttClient_DeInit(&client); + fprintf(stderr, "%s: MqttClient_Init reported success\n", name); + return 1; + } + if (mutex_live != 0 || cond_live != 0) { + fprintf(stderr, "%s: rc=%d leaked mutexes=%d conds=%d\n", + name, rc, mutex_live, cond_live); + return 1; + } + return 0; +} + +int main(void) +{ + int fail = 0; + + /* First and a later lock: the first fails before anything else is set + * up, the later one after earlier locks were initialized. */ + fail |= run_init_failure("first mutex", 1, 0); + fail |= run_init_failure("second mutex", 2, 0); + fail |= run_init_failure("first cond", 0, 1); + fail |= run_init_failure("second cond", 0, 2); + return fail; +} + +#else + +int main(void) +{ + /* Only the POSIX semaphore backend is exercised here. */ + return 77; +} + +#endif /* WOLFMQTT_POSIX_SEMAPHORES */ From fb8db6e4b1d79f0cbc8b9a3128285fd6fa801e6e Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 08:05:22 -0500 Subject: [PATCH 28/52] F-14358 - Reset SN_Client_Connect state when the Will exchange fails --- src/mqtt_sn_client.c | 23 ++++++++++++++++++ tests/test_mqtt_sn_client.c | 48 ++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index c505e0bb2..0718cb520 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -1395,6 +1395,23 @@ enum { SN_WILL_DONE_TOPIC = 2 /* will topic sent, message still pending */ }; +/* The Will exchange after CONNECT failed. Release the CONNACK pending + * response and rewind the state so a retry with the same SN_Connect sends a + * new CONNECT; the gateway only requests the Will in reply to one. */ +static void SN_Client_ConnectReset(MqttClient *client, SN_Connect *mc_connect) +{ +#ifdef WOLFMQTT_MULTITHREAD + if (wm_SemLock(&client->lockClient) == 0) { + MqttClient_RespList_Remove(client, &mc_connect->pendResp); + wm_SemUnlock(&client->lockClient); + } +#else + (void)client; +#endif + mc_connect->stat.write = MQTT_MSG_BEGIN; + mc_connect->will_done = SN_WILL_DONE_NONE; +} + int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) { int rc = 0; @@ -1469,6 +1486,9 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) if (mc_connect->will_done == SN_WILL_DONE_NONE) { rc = SN_WillTopic(client, &mc_connect->will); if (rc != 0) { + if (rc != MQTT_CODE_CONTINUE) { + SN_Client_ConnectReset(client, mc_connect); + } return rc; } mc_connect->will_done = SN_WILL_DONE_TOPIC; @@ -1476,6 +1496,9 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) rc = SN_WillMessage(client, &mc_connect->will); if (rc != 0) { + if (rc != MQTT_CODE_CONTINUE) { + SN_Client_ConnectReset(client, mc_connect); + } return rc; } mc_connect->will_done = SN_WILL_DONE_ALL; diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 22ffa566e..8825bb6f5 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -101,6 +101,8 @@ typedef struct MockNet { int write_fail_rc; /* if nonzero, write() returns this instead of * accepting the buffer (simulate short/failed * write) */ + int write_fail_type; /* fail only this MQTT-SN packet type, 0 = + * every write */ int write_chunk; /* maximum bytes accepted per write */ int write_zero_count; /* zero-progress writes before accepting */ int read_zero_count; /* zero-progress reads before delivering */ @@ -233,7 +235,8 @@ static int mock_write(void *ctx, const byte* buf, int buf_len, int timeout_ms) #endif net->write_calls++; - if (net->write_fail_rc != 0) { + if (net->write_fail_rc != 0 && (net->write_fail_type == 0 || + (buf_len > 1 && buf[1] == net->write_fail_type))) { /* Simulate a short/failed write so the caller's rc != xfer error path is * exercised. The buffer is left as the caller encoded it (not captured), * matching a real transport that errored mid-send. */ @@ -861,6 +864,48 @@ TEST(sn_connect_lwt_no_continue) * sn_subscribe_rejected. Without the guard this connect would report * MQTT_CODE_SUCCESS over a link the gateway actually refused. Runs in every SN * build. */ +/* SN_Client_Connect is resumable and keyed off mc_connect->stat.write. When + * the Will exchange after CONNECT fails, the call must leave the object ready + * to start over: write state back at BEGIN and, in multithread builds, the + * CONNACK pending response unlinked. A retry with the same SN_Connect then + * sends a new CONNECT; the gateway only issues WILLTOPICREQ in reply to one + * (MQTT-SN 1.2 section 6.3). The first attempt fails because the WILLTOPIC + * write fails at the transport. */ +TEST(sn_connect_will_error_allows_retry) +{ + SN_Connect mc; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + g_mock.write_fail_rc = MQTT_CODE_ERROR_NETWORK; + g_mock.write_fail_type = SN_MSG_TYPE_WILLTOPIC; + + sn_will_setup_connect(&mc); + rc = sn_connect_pump(&mc, NULL); + + ASSERT_TRUE(rc < 0); + ASSERT_TRUE(rc != MQTT_CODE_CONTINUE); + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_NO_PENDRESP(); + + /* Retry with the same object: CONNECT goes out again first. */ + g_mock.write_fail_rc = 0; + g_mock.write_fail_type = 0; + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + g_mock.out_len = 0; + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(SN_RC_ACCEPTED, mc.ack.return_code); + ASSERT_TRUE(g_mock.out_len >= 2); + ASSERT_EQ(SN_MSG_TYPE_CONNECT, g_mock.out[1]); + ASSERT_NO_PENDRESP(); +} + TEST(sn_connect_refused) { SN_Connect mc; @@ -3327,6 +3372,7 @@ int main(int argc, char** argv) /* Happy path runs in every SN build (blocking and non-blocking). */ RUN_TEST(sn_connect_lwt_no_continue); RUN_TEST(sn_connect_refused); + RUN_TEST(sn_connect_will_error_allows_retry); RUN_TEST(sn_will_payload_scrubbed_after_send); RUN_TEST(sn_willmsgupd_payload_scrubbed_after_send); RUN_TEST(sn_willtopicupd_null_topic_sends_delete); From 2837eaf7c110dfb8e9415fa254c8338e33ad3cc2 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 08:11:52 -0500 Subject: [PATCH 29/52] F-15374 - Reset ack wait state after Subscribe, Unsubscribe and Publish --- src/mqtt_client.c | 13 +++- tests/test_mqtt_client.c | 138 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 1b3d6e3d2..18078bca1 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -4494,7 +4494,10 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, #endif ) { + /* Reset the response wait state too (see MqttClient_Subscribe) */ publish->stat.write = MQTT_MSG_BEGIN; + publish->resp.stat.read = MQTT_MSG_BEGIN; + publish->resp.stat.ack = MQTT_MSG_BEGIN; } if (rc > 0) { rc = MQTT_CODE_SUCCESS; @@ -4671,8 +4674,12 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) } } - /* reset state */ + /* reset state. The ack wait state is reset too, as in + * MqttClient_Connect: left at MQTT_MSG_PAYLOAD, a reuse of this object + * would skip the read and dispatch stale rx_buf contents. */ subscribe->stat.write = MQTT_MSG_BEGIN; + subscribe->ack.stat.read = MQTT_MSG_BEGIN; + subscribe->ack.stat.ack = MQTT_MSG_BEGIN; return rc; } @@ -4833,8 +4840,10 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) } #endif - /* reset state */ + /* reset state, including the ack wait state (see MqttClient_Subscribe) */ unsubscribe->stat.write = MQTT_MSG_BEGIN; + unsubscribe->ack.stat.read = MQTT_MSG_BEGIN; + unsubscribe->ack.stat.ack = MQTT_MSG_BEGIN; return rc; } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 6ff86a79d..839144ed5 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -2457,6 +2457,141 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) subscribe.topics[0].return_code); } +/* MqttClient_Connect resets its ack wait state so one object can be reused + * across exchanges; Subscribe, Unsubscribe and Publish leave their ack wait + * state at PAYLOAD after a completed exchange. A second request on the same + * completed object must read its own ack from the network instead of + * re-dispatching what the first exchange left in rx_buf. Both acks are queued + * up front (v3.1.1 wire forms, MQTT 3.1.1 sections 3.4, 3.9 and 3.11); the + * second must be consumed and must be the one reported. */ +static void reuse_setup(const byte* acks, int acks_len) +{ + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, acks, (size_t)acks_len); + g_canned_len = acks_len; + g_canned_pos = 0; +} + +TEST(subscribe_reused_object_reads_new_suback) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[1]; + /* SUBACK id 1 granting QoS 0, then SUBACK id 2 granting QoS 1. */ + static const byte acks[] = { + 0x90, 0x03, 0x00, 0x01, 0x00, + 0x90, 0x03, 0x00, 0x02, 0x01 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + topics[0].qos = MQTT_QOS_1; + subscribe.topic_count = 1; + subscribe.topics = topics; + + subscribe.packet_id = 1; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(0x00, topics[0].return_code); + + subscribe.packet_id = 2; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(2, subscribe.ack.packet_id); + ASSERT_EQ(0x01, topics[0].return_code); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + +TEST(unsubscribe_reused_object_reads_new_unsuback) +{ + int rc; + int i; + MqttUnsubscribe unsub; + MqttTopic topics[1]; + /* UNSUBACK id 3, then UNSUBACK id 4. */ + static const byte acks[] = { + 0xB0, 0x02, 0x00, 0x03, + 0xB0, 0x02, 0x00, 0x04 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + unsub.topic_count = 1; + unsub.topics = topics; + + unsub.packet_id = 3; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + unsub.packet_id = 4; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, unsub.ack.packet_id); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + +TEST(publish_qos1_reused_object_reads_new_puback) +{ + int rc; + int i; + MqttPublish publish; + byte payload[1] = { 'x' }; + /* PUBACK id 5, then PUBACK id 6. */ + static const byte acks[] = { + 0x40, 0x02, 0x00, 0x05, + 0x40, 0x02, 0x00, 0x06 + }; + + reuse_setup(acks, (int)sizeof(acks)); + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_1; + publish.topic_name = "test/topic"; + publish.buffer = payload; + publish.total_len = (word32)sizeof(payload); + + publish.packet_id = 5; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + publish.packet_id = 6; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(6, publish.resp.packet_id); + ASSERT_EQ(g_canned_len, g_canned_pos); +} + /* [MQTT-3.9.3-1] A SUBACK carries one return code per Topic Filter in the * SUBSCRIBE. A count mismatch is a protocol violation, so the client must * stop treating the connection as usable (MQTT 5.0 section 4.13.1), as @@ -8380,6 +8515,9 @@ void run_mqtt_client_tests(void) RUN_TEST(subscribe_null_subscribe); RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); RUN_TEST(subscribe_too_few_return_codes_clears_connected); + RUN_TEST(subscribe_reused_object_reads_new_suback); + RUN_TEST(unsubscribe_reused_object_reads_new_unsuback); + RUN_TEST(publish_qos1_reused_object_reads_new_puback); #ifdef WOLFMQTT_V5 RUN_TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected); RUN_TEST(unsubscribe_too_few_reason_codes_is_malformed); From bf7b5c87db8179d768c5f07e7e501690124a25fd Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 08:55:25 -0500 Subject: [PATCH 30/52] F-16042 - Record PUBREL replay only for the client's own QoS 2 publishes --- src/mqtt_client.c | 17 ++++++- tests/test_mqtt_client.c | 102 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 18078bca1..968748106 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -77,6 +77,7 @@ WOLFMQTT_LOCAL void MqttClient_ForceZero(void* mem, word32 len) /* forward declarations */ static int MqttClient_Publish_ReadPayload(MqttClient* client, MqttPublish* publish, int timeout_ms); +static int MqttClient_SendIds_Find(const MqttClient* client, word16 packet_id); #ifdef WOLFMQTT_V5 static int MqttClient_AuthEx(MqttClient *client, MqttAuth* auth, MqttMsgStat* stat, @@ -755,16 +756,27 @@ static void MqttClient_Replay_Add(MqttClient* client, MqttPublish* publish) /* QoS 2 advanced past PUBREC, so [MQTT-4.4.0-1] replays the PUBREL rather * than the PUBLISH. Recorded even when no copy was retained: a PUBREL needs - * only the Packet Identifier. */ + * only the Packet Identifier. Only this client's own QoS 2 PUBLISH counts: a + * PUBREC for an identifier it never published, or for a QoS 1 PUBLISH, is not + * Session state (MQTT 5.0 section 3.6.2.1, Packet Identifier not found) and + * must neither take a slot nor turn a QoS 1 replay into a PUBREL. A QoS 2 + * PUBLISH sent without a slot (e.g. no topic to retain) is recognised by its + * in-flight reservation awaiting PUBCOMP. */ static void MqttClient_Replay_PubRelSent(MqttClient* client, word16 packet_id) { MqttReplayMsg* slot; + int i; if (packet_id == 0) { return; } slot = MqttClient_Replay_Find(client, packet_id); if (slot == NULL) { + i = MqttClient_SendIds_Find(client, packet_id); + if (i < 0 || client->send_inflight[i].ack_type != + (byte)MQTT_PACKET_TYPE_PUBLISH_COMP) { + return; + } slot = MqttClient_Replay_Find(client, 0); if (slot == NULL) { return; @@ -773,6 +785,9 @@ static void MqttClient_Replay_PubRelSent(MqttClient* client, word16 packet_id) slot->packet_id = packet_id; slot->qos = MQTT_QOS_2; } + else if (slot->qos != MQTT_QOS_2) { + return; + } slot->pubrelSent = 1; } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 839144ed5..b8132558d 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -4475,6 +4475,106 @@ TEST(publish_refused_when_replay_pool_full_on_persistent_session) ASSERT_EQ(0, frames); } +/* Feed fixed v3.1.1 PUBREC frames (MQTT 3.1.1 section 3.5: 0x50, Remaining + * Length 2, Packet Identifier) through MqttClient_WaitMessage until all are + * consumed. */ +static int run_wait_pubrecs(const byte* frames, int frames_len) +{ + int rc = MQTT_CODE_SUCCESS; + int i; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, frames, (size_t)frames_len); + g_canned_len = frames_len; + g_canned_pos = 0; + for (i = 0; i < 40 && g_canned_pos < g_canned_len; i++) { + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + if (rc != MQTT_CODE_SUCCESS && rc != MQTT_CODE_CONTINUE) { + break; + } + } + return rc; +} + +/* The replay pool holds this client's own unacknowledged QoS > 0 PUBLISHes + * [MQTT-4.4.0-1]. A PUBREC naming an identifier the client never published + * is not part of its Session state (MQTT 5.0 section 3.6.2.1 calls it Packet + * Identifier not found), so it must not take a pool slot: a server sending + * one per slot would otherwise make every later QoS > 0 publish on this + * persistent Session fail for lack of room. */ +TEST(unsolicited_pubrec_does_not_fill_replay_pool) +{ + int rc; + int i; + MqttConnect connect; + MqttPublish publish; + byte pubrecs[4 * MQTT_MAX_REPLAY_MSGS]; + static byte payload[] = "hello"; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + ASSERT_EQ(MQTT_CODE_SUCCESS, run_initial_connect(&connect)); + + ASSERT_TRUE((int)sizeof(pubrecs) <= (int)sizeof(g_canned_buf)); + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + pubrecs[4 * i] = 0x50; + pubrecs[4 * i + 1] = 0x02; + pubrecs[4 * i + 2] = 0x02; + pubrecs[4 * i + 3] = (byte)(0x01 + i); /* ids 0x0201.. */ + } + rc = run_wait_pubrecs(pubrecs, (int)sizeof(pubrecs)); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(g_canned_len, g_canned_pos); + + init_qos_publish(&publish, MQTT_QOS_1, 0x0300, "t", payload, + (word32)(sizeof(payload) - 1)); + rc = run_publish_unacked(&publish); + /* Sent and left unacknowledged, not refused for lack of a slot. */ + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); +} + +/* A PUBREC is the QoS 2 response; for the identifier of an outstanding QoS 1 + * PUBLISH it does not advance that exchange. The QoS 1 message is still + * unacknowledged, so a resumed Session must re-send the PUBLISH + * [MQTT-4.4.0-1], not a PUBREL. */ +TEST(pubrec_for_qos1_id_keeps_publish_replay) +{ + int rc; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + /* PUBREC for Packet Identifier 0x0110, the QoS 1 publish below. */ + static const byte pubrec[] = { 0x50, 0x02, 0x01, 0x10 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + ASSERT_EQ(MQTT_CODE_SUCCESS, run_initial_connect(&connect)); + + init_qos_publish(&publish, MQTT_QOS_1, 0x0110, "sensor/temp", payload, + (word32)(sizeof(payload) - 1)); + rc = run_publish_unacked(&publish); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + + (void)run_wait_pubrecs(pubrec, (int)sizeof(pubrec)); + ASSERT_EQ(g_canned_len, g_canned_pos); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_NetDisconnect(&test_client)); + replay_mock_reset(); + rc = run_reconnect_with(&connect, 1, replay_mock_write); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + ASSERT_EQ(2, g_replay_frames); + ASSERT_EQ(MQTT_PACKET_TYPE_CONNECT, g_replay_types[0]); + ASSERT_EQ(MQTT_PACKET_TYPE_PUBLISH, g_replay_types[1]); +} + #ifdef WOLFMQTT_V5 /* MQTT 5.0 section 3.2.2.3.2: a Session Expiry Interval in CONNACK replaces * the one requested in CONNECT. When the server negotiates it down to 0 the @@ -8471,6 +8571,8 @@ void run_mqtt_client_tests(void) #if !defined(WOLFMQTT_NO_SESSION_REPLAY) && WOLFMQTT_MAX_QOS >= 1 RUN_TEST(reconnect_replays_unacked_qos1_publish); RUN_TEST(publish_refused_when_replay_pool_full_on_persistent_session); + RUN_TEST(unsolicited_pubrec_does_not_fill_replay_pool); + RUN_TEST(pubrec_for_qos1_id_keeps_publish_replay); RUN_TEST(publish_allowed_past_replay_pool_on_clean_session); #ifdef WOLFMQTT_V5 RUN_TEST(publish_allowed_past_replay_pool_when_connack_ends_session); From f152cc78740d5b6f2ea2bd432bb4e893e11a9395 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 09:01:16 -0500 Subject: [PATCH 31/52] F-15403 - Send CONNACK 0x01 when an unsupported-level CONNECT won't decode --- src/mqtt_broker.c | 13 ++++++- tests/test_broker_connect.c | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 4d5721964..3b8c84e6d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -6645,7 +6645,18 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: CONNECT recv sock=%d len=%d", (int)bc->sock, rx_len); rc = MqttDecode_Connect(bc->rx_buf, rx_len, &mc); - if (rc < 0) { + /* The decoder records the Protocol Level once the Protocol Name checks + * out, before the rest of the packet, whose layout depends on that + * level. A CONNECT for a level this build does not support may then fail + * to decode, yet [MQTT-3.1.2-2] still calls for CONNACK 0x01: let it + * reach the unsupported-level refusal below. Level 0 means the decoder + * stopped before reading it. */ + if (rc < 0 && (mc.protocol_level == 0 || + mc.protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_4 + #ifdef WOLFMQTT_V5 + || mc.protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_5 + #endif + )) { WBLOG_ERR(broker, "broker: CONNECT decode failed rc=%d", rc); #ifdef WOLFMQTT_V5 if (mc.props) { (void)MqttProps_Free(mc.props); } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 8ad5c13e3..9b3f7ddbd 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1129,6 +1129,71 @@ TEST(connect_unsupported_level_127_refused) run_unsupported_level(0x7F); } +/* The body of a CONNECT is laid out by its Protocol Level, so a level this + * build does not speak may not decode at all. [MQTT-3.1.2-2] still requires + * CONNACK 0x01 rather than a silent close, which is also what lets an MQTT 5 + * client fall back to 3.1.1 against a broker built without v5. */ +static void run_undecodable_unsupported_level(const byte* connect, + size_t connect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, connect_len); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 4); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(0x02, g_out_buf[1]); + ASSERT_EQ(0x00, g_out_buf[2]); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_PROTO, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +TEST(connect_unsupported_level_undecodable_refused) +{ + /* Level 6, ClientId length 5 with one byte present: malformed whether + * the body is read with or without a v5 Property Length. */ + static const byte connect[] = { + 0x10, 13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x06, /* unsupported protocol level */ + 0x02, /* CleanSession=1 */ + 0x00, 0x3C, + 0x00, 0x05, 'A' + }; + + run_undecodable_unsupported_level(connect, sizeof(connect)); +} + +#ifndef WOLFMQTT_V5 +TEST(connect_v5_to_non_v5_broker_refused) +{ + /* A well-formed MQTT 5 CONNECT (MQTT 5.0 section 3.1.2) with a Session + * Expiry Interval property. Read as 3.1.1, the Property Length 0x05 and + * the property identifier 0x11 become a 0x0511-byte ClientId length. */ + static const byte connect[] = { + 0x10, 19, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, /* MQTT 5.0 */ + 0x02, /* CleanStart=1 */ + 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, /* Session Expiry = 60 */ + 0x00, 0x01, 'A' + }; + + run_undecodable_unsupported_level(connect, sizeof(connect)); +} +#endif + #ifdef WOLFMQTT_BROKER_AUTH /* Regression: [MQTT-3.1.3.5] Password is Binary Data and may legally * contain 0x00. The broker must not use XSTRLEN-based length recovery on @@ -13617,6 +13682,10 @@ int main(int argc, char** argv) RUN_TEST(connect_unsupported_level_3_refused); RUN_TEST(connect_unsupported_level_6_refused); RUN_TEST(connect_unsupported_level_127_refused); + RUN_TEST(connect_unsupported_level_undecodable_refused); +#ifndef WOLFMQTT_V5 + RUN_TEST(connect_v5_to_non_v5_broker_refused); +#endif #ifdef WOLFMQTT_BROKER_AUTH RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); RUN_TEST(connect_v311_binary_password_exact_match_accepted); From 82ff725c7f8812a81ea9e9539bf4cf1da29feb8b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 09:08:01 -0500 Subject: [PATCH 32/52] F-13809 - Reject unassigned CONNACK return and reason codes in the client --- src/mqtt_packet.c | 97 +++++++++++++++++++++++----------------- tests/test_mqtt_client.c | 8 ++-- tests/test_mqtt_packet.c | 35 +++++++++++++++ 3 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 886d9c0a1..48fb7f97d 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2087,6 +2087,49 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) } #endif /* WOLFMQTT_BROKER */ +/* Validate against MQTT v3.1.1 section 3.2.2.3 or the MQTT 5.0 CONNACK + * Reason Code table in section 3.2.2.2. Used by the decoder and, in broker + * builds, the encoder. */ +static int MqttPacket_ConnectAckReturnCodeValid(byte code, + byte protocol_level) +{ +#ifdef WOLFMQTT_V5 + if (protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + switch (code) { + case MQTT_REASON_SUCCESS: + case MQTT_REASON_UNSPECIFIED_ERR: + case MQTT_REASON_MALFORMED_PACKET: + case MQTT_REASON_PROTOCOL_ERR: + case MQTT_REASON_IMPL_SPECIFIC_ERR: + case MQTT_REASON_UNSUP_PROTO_VER: + case MQTT_REASON_CLIENT_ID_NOT_VALID: + case MQTT_REASON_BAD_USER_OR_PASS: + case MQTT_REASON_NOT_AUTHORIZED: + case MQTT_REASON_SERVER_UNAVAILABLE: + case MQTT_REASON_SERVER_BUSY: + case MQTT_REASON_BANNED: + case MQTT_REASON_BAD_AUTH_METHOD: + case MQTT_REASON_TOPIC_NAME_INVALID: + case MQTT_REASON_PACKET_TOO_LARGE: + case MQTT_REASON_QUOTA_EXCEEDED: + case MQTT_REASON_PAYLOAD_FORMAT_INVALID: + case MQTT_REASON_RETAIN_NOT_SUPPORTED: + case MQTT_REASON_QOS_NOT_SUPPORTED: + case MQTT_REASON_USE_ANOTHER_SERVER: + case MQTT_REASON_SERVER_MOVED: + case MQTT_REASON_CON_RATE_EXCEED: + return 1; + default: + return 0; + } + } +#else + (void)protocol_level; +#endif + + return code <= MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH; +} + int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, MqttConnectAck *connect_ack) { @@ -2138,6 +2181,18 @@ int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, connect_ack->flags = *rx_payload++; connect_ack->return_code = *rx_payload++; + /* A code outside the table for this protocol version is a protocol + * violation, not an ordinary refusal. */ + #ifdef WOLFMQTT_V5 + if (!MqttPacket_ConnectAckReturnCodeValid(connect_ack->return_code, + connect_ack->protocol_level)) { + #else + if (!MqttPacket_ConnectAckReturnCodeValid(connect_ack->return_code, + MQTT_CONNECT_PROTOCOL_LEVEL_4)) { + #endif + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + /* [MQTT-3.2.2-1] Bits 7-1 of the Connect Acknowledge Flags byte are * reserved and MUST be 0. Any other value is a protocol violation; * [MQTT-4.8.0-1] requires the receiver to close the connection. */ @@ -2201,48 +2256,6 @@ int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, } #ifdef WOLFMQTT_BROKER -/* Validate against MQTT v3.1.1 section 3.2.2.3 or the MQTT 5.0 CONNACK - * Reason Code table in section 3.2.2.2. */ -static int MqttPacket_ConnectAckReturnCodeValid(byte code, - byte protocol_level) -{ -#ifdef WOLFMQTT_V5 - if (protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - switch (code) { - case MQTT_REASON_SUCCESS: - case MQTT_REASON_UNSPECIFIED_ERR: - case MQTT_REASON_MALFORMED_PACKET: - case MQTT_REASON_PROTOCOL_ERR: - case MQTT_REASON_IMPL_SPECIFIC_ERR: - case MQTT_REASON_UNSUP_PROTO_VER: - case MQTT_REASON_CLIENT_ID_NOT_VALID: - case MQTT_REASON_BAD_USER_OR_PASS: - case MQTT_REASON_NOT_AUTHORIZED: - case MQTT_REASON_SERVER_UNAVAILABLE: - case MQTT_REASON_SERVER_BUSY: - case MQTT_REASON_BANNED: - case MQTT_REASON_BAD_AUTH_METHOD: - case MQTT_REASON_TOPIC_NAME_INVALID: - case MQTT_REASON_PACKET_TOO_LARGE: - case MQTT_REASON_QUOTA_EXCEEDED: - case MQTT_REASON_PAYLOAD_FORMAT_INVALID: - case MQTT_REASON_RETAIN_NOT_SUPPORTED: - case MQTT_REASON_QOS_NOT_SUPPORTED: - case MQTT_REASON_USE_ANOTHER_SERVER: - case MQTT_REASON_SERVER_MOVED: - case MQTT_REASON_CON_RATE_EXCEED: - return 1; - default: - return 0; - } - } -#else - (void)protocol_level; -#endif - - return code <= MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH; -} - int MqttEncode_ConnectAck(byte *tx_buf, int tx_buf_len, MqttConnectAck *connect_ack) { diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index b8132558d..db64b94e9 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -1470,10 +1470,10 @@ TEST(connect_refused_connack_preserves_v5_defaults) int rc; int i; MqttConnect connect; - /* Same properties as the acceptance test, but return_code=0x05 - * (not authorized). */ + /* Same properties as the acceptance test, but reason code 0x87 (Not + * authorized, MQTT 5.0 section 3.2.2.2). */ static const byte connack[] = { - 0x20, 0x0C, 0x00, 0x05, 0x09, + 0x20, 0x0C, 0x00, 0x87, 0x09, 0x24, 0x01, 0x25, 0x00, 0x27, 0x00, 0x00, 0x04, 0x00 @@ -1500,7 +1500,7 @@ TEST(connect_refused_connack_preserves_v5_defaults) } ASSERT_EQ(MQTT_CODE_ERROR_CONNECT_REFUSED, rc); - ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH, connect.ack.return_code); + ASSERT_EQ(MQTT_REASON_NOT_AUTHORIZED, connect.ack.return_code); /* Pre-CONNACK defaults established by MqttClient_Connect must be intact. */ ASSERT_EQ((byte)WOLFMQTT_MAX_QOS, test_client.max_qos); ASSERT_EQ(1, test_client.retain_avail); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 95e14ad71..c2d050063 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -2501,6 +2501,37 @@ TEST(decode_connack_refused_without_session_present_accepted) ASSERT_TRUE(rc > 0); } +/* MQTT 3.1.1 section 3.2.2.3 assigns return codes 0-5 and reserves 6-255, so + * a CONNACK carrying a reserved code is a protocol violation, not an + * ordinary refusal. */ +TEST(decode_connack_v311_reserved_return_code_rejected) +{ + ASSERT_TRUE(decode_connack_flags(0x00, + MQTT_CONNECT_ACK_CODE_REFUSED_NOT_AUTH) > 0); /* 0x05, last assigned */ + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, + decode_connack_flags(0x00, 0x06)); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, + decode_connack_flags(0x00, 0xFF)); +} + +#ifdef WOLFMQTT_V5 +/* MQTT 5.0 section 3.2.2.2 lists the CONNACK Reason Codes; 0x01 is not one + * of them. Wire: CONNACK, Remaining Length 3, flags 0, reason 0x01, + * Property Length 0. */ +TEST(decode_connack_v5_unassigned_reason_code_rejected) +{ + byte buf[] = { 0x20, 0x03, 0x00, 0x01, 0x00 }; + MqttConnectAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_ConnectAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(ack.props); +} +#endif + #ifdef WOLFMQTT_BROKER /* [MQTT-3.2.2-4] Session Present must be zero when a connection is refused. * The encoder must not produce a CONNACK that the matching decoder rejects. */ @@ -7712,6 +7743,10 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_connack_flags_all_bits_rejected); RUN_TEST(decode_connack_refused_with_session_present_rejected); RUN_TEST(decode_connack_refused_without_session_present_accepted); + RUN_TEST(decode_connack_v311_reserved_return_code_rejected); +#ifdef WOLFMQTT_V5 + RUN_TEST(decode_connack_v5_unassigned_reason_code_rejected); +#endif #ifdef WOLFMQTT_BROKER RUN_TEST(encode_connack_refused_with_session_present_rejected); #endif From d862777ad0182508089bdbb2c1e2df1c9ea6eac9 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 09:44:31 -0500 Subject: [PATCH 33/52] F-14621 - Escape client IDs in persistence restore log messages --- .gitignore | 1 + src/mqtt_broker.c | 2 +- src/mqtt_broker_persist.c | 4 +- tests/include.am | 19 ++- tests/test_broker_log.c | 255 +++++++++++++++++++++++++++++++++ tests/test_broker_log_config.h | 10 ++ wolfmqtt/mqtt_broker.h | 6 + 7 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 tests/test_broker_log.c create mode 100644 tests/test_broker_log_config.h diff --git a/.gitignore b/.gitignore index 3b0b3ac6a..2f1a989a1 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,5 @@ tests/test_mqtt_sn_client_nodata tests/unit_tests tests/test_mqtt_props_init tests/test_mqtt_sem_init +tests/test_broker_log tests/test_mqtt_curl_init diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 3b8c84e6d..78dc7a121 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -133,7 +133,7 @@ typedef char wolfmqtt_broker_time_t_must_be_unsigned[ * arguments can appear in one log statement; the broker log path is single * threaded (as is PRINTF itself). Output is NUL-terminated and truncated to * fit. NULL src yields "(null)". */ -static const char* BrokerLog_Sanitize(const char* src) +WOLFMQTT_LOCAL const char* BrokerLog_Sanitize(const char* src) { static const char hex_digits[] = "0123456789abcdef"; static char pool[BROKER_LOG_SAN_POOL][BROKER_LOG_SAN_SZ]; diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 9a5582334..df3f5f722 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -2410,7 +2410,7 @@ static void wmqb_restore_drop_ended(MqttBroker* broker, if (rc != 0) { WMQB_LOG_ERR(broker, "broker: persist drop ended session failed " - "client_id=%s rc=%d", cid, rc); + "client_id=%s rc=%d", BrokerLog_Sanitize(cid), rc); } WOLFMQTT_FREE(cid); } @@ -2597,7 +2597,7 @@ static void wmqb_restore_expiry_sweep(MqttBroker* broker) WMQB_LOG_INFO(broker, "broker: persist expired session client_id=%s " "(expiry=%us)", - cur->client_id == NULL ? "(null)" : cur->client_id, + BrokerLog_Sanitize(cur->client_id), (unsigned)cur->session_expiry_sec); /* Shared teardown lives in mqtt_broker.c so the eviction * path and this expiry-sweep path can't drift. Drops diff --git a/tests/include.am b/tests/include.am index 6c8612e1e..6f27a93a6 100644 --- a/tests/include.am +++ b/tests/include.am @@ -69,7 +69,7 @@ endif # Test framework headers noinst_HEADERS += tests/unit_test.h tests/test_broker_alloc.h \ tests/test_mqtt_props_config.h tests/test_mqtt_curl_config.h \ - tests/test_mqtt_sem_config.h + tests/test_mqtt_sem_config.h tests/test_broker_log_config.h # MQTT-SN packet decoder tests. The SN_Decode_* routines are WOLFMQTT_LOCAL # (hidden) so they cannot be linked from libwolfmqtt; src/mqtt_sn_packet.c is @@ -170,6 +170,23 @@ tests_test_broker_connect_CPPFLAGS = -I$(top_srcdir) \ tests_test_broker_connect_LDADD = src/libwolfmqtt.la $(LIB_STATIC_ADD) tests_test_broker_connect_DEPENDENCIES = src/libwolfmqtt.la +# Broker log sanitization tests: logging enabled, with PRINTF redirected into +# a capture buffer in the test. +check_PROGRAMS += tests/test_broker_log +tests_test_broker_log_SOURCES = \ + tests/test_broker_log.c \ + src/mqtt_broker.c \ + src/mqtt_broker_persist.c \ + src/mqtt_broker_persist_posix.c +tests_test_broker_log_CFLAGS = -DWOLFMQTT_BROKER -DWOLFMQTT_BROKER_CUSTOM_NET \ + -DNO_MAIN_DRIVER '-DPRINTF(...)=wolfmqtt_test_log(__VA_ARGS__)' \ + '-DWOLFMQTT_BROKER_GET_TIME_S()=wolfmqtt_test_broker_time_s()' \ + '-DBROKER_SLEEP_MS(ms)=do{}while(0)' $(AM_CFLAGS) +tests_test_broker_log_CPPFLAGS = -I$(top_srcdir) \ + -include $(top_srcdir)/tests/test_broker_log_config.h $(AM_CPPFLAGS) +tests_test_broker_log_LDADD = src/libwolfmqtt.la $(LIB_STATIC_ADD) +tests_test_broker_log_DEPENDENCIES = src/libwolfmqtt.la + check_PROGRAMS += tests/test_broker_connect_static tests_test_broker_connect_static_SOURCES = \ tests/test_broker_connect.c \ diff --git a/tests/test_broker_log.c b/tests/test_broker_log.c new file mode 100644 index 000000000..b803e8b57 --- /dev/null +++ b/tests/test_broker_log.c @@ -0,0 +1,255 @@ +/* Broker log sanitization tests. The broker is built with logging enabled and + * PRINTF redirected into a capture buffer, so a test can check what reaches + * the log sink. */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include +#include +#include + +#include "wolfmqtt/mqtt_client.h" +#include "wolfmqtt/mqtt_broker.h" + +/* The broker sources call these in every configuration. */ +static char g_log[4096]; +static size_t g_log_len; +static unsigned long g_time_s; + +void wolfmqtt_test_log(const char* fmt, ...) +{ + va_list ap; + int n; + + if (g_log_len >= sizeof(g_log) - 1) { + return; + } + va_start(ap, fmt); + n = vsnprintf(g_log + g_log_len, sizeof(g_log) - g_log_len, fmt, ap); + va_end(ap); + if (n > 0) { + g_log_len += (size_t)n; + if (g_log_len > sizeof(g_log) - 1) { + g_log_len = sizeof(g_log) - 1; + } + } +} + +unsigned long wolfmqtt_test_broker_time_s(void) +{ + return g_time_s; +} + +#if defined(WOLFMQTT_BROKER_PERSIST) && !defined(WOLFMQTT_STATIC_MEMORY) && \ + !defined(WOLFMQTT_BROKER_NO_LOG) + +#include +#include +#include + +static int net_listen(void* ctx, BROKER_SOCKET_T* sock, word16 port, + int backlog) +{ + (void)ctx; (void)sock; (void)port; (void)backlog; + return MQTT_CODE_SUCCESS; +} + +static int net_accept(void* ctx, BROKER_SOCKET_T listen_sock, + BROKER_SOCKET_T* client_sock) +{ + (void)ctx; (void)listen_sock; (void)client_sock; + return MQTT_CODE_CONTINUE; +} + +static int net_read(void* ctx, BROKER_SOCKET_T sock, byte* buf, int buf_len, + int timeout_ms) +{ + (void)ctx; (void)sock; (void)buf; (void)buf_len; (void)timeout_ms; + return MQTT_CODE_CONTINUE; +} + +static int net_write(void* ctx, BROKER_SOCKET_T sock, const byte* buf, + int buf_len, int timeout_ms) +{ + (void)ctx; (void)sock; (void)buf; (void)timeout_ms; + return buf_len; +} + +static int net_close(void* ctx, BROKER_SOCKET_T sock) +{ + (void)ctx; (void)sock; + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int test_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* Remove root// as laid out by the POSIX backend. */ +static void remove_store(const char* root) +{ + char path[512]; + char sub[1024]; + DIR* d; + DIR* sd; + struct dirent* e; + struct dirent* se; + + d = opendir(root); + if (d == NULL) { + return; + } + while ((e = readdir(d)) != NULL) { + if (e->d_name[0] == '.') { + continue; + } + (void)snprintf(path, sizeof(path), "%s/%s", root, e->d_name); + sd = opendir(path); + if (sd != NULL) { + while ((se = readdir(sd)) != NULL) { + if (se->d_name[0] == '.') { + continue; + } + (void)snprintf(sub, sizeof(sub), "%s/%s", path, se->d_name); + (void)unlink(sub); + } + (void)closedir(sd); + (void)rmdir(path); + } + else { + (void)unlink(path); + } + } + (void)closedir(d); + (void)rmdir(root); +} + +static int init_broker(MqttBroker* broker, MqttBrokerNet* net, + MqttBrokerPersistHooks* hooks) +{ + int rc; + + XMEMSET(broker, 0, sizeof(*broker)); + rc = MqttBroker_Init(broker, net); + if (rc == MQTT_CODE_SUCCESS) { + rc = MqttBroker_SetPersistHooks(broker, hooks); + } + return rc; +} + +/* A persisted Session whose expiry passed while the broker was down is + * logged when the restore sweep drops it. The Client Identifier is chosen by + * the remote client and may hold control bytes (MQTT 5.0 section 1.5.4 only + * forbids U+0000), so it must reach the log escaped: a raw CR/LF would forge + * log lines and a raw ESC would drive the operator's terminal. */ +static int test_restore_expired_session_logs_escaped_client_id(void) +{ + static const char cid[] = "x\x1b[31m\ny"; + char root_template[] = "./wolfmqtt-log-XXXXXX"; + MqttBroker first; + MqttBroker second; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + char* root; + int rc; + int fail = 0; + + XMEMSET(&net, 0, sizeof(net)); + net.listen = net_listen; + net.accept = net_accept; + net.read = net_read; + net.write = net_write; + net.close = net_close; + XMEMSET(&hooks, 0, sizeof(hooks)); + + root = mkdtemp(root_template); + if (root == NULL) { + fprintf(stderr, "mkdtemp failed\n"); + return 1; + } + rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "persist init returned %d\n", rc); + remove_store(root); + return 1; + } +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = test_derive_key; +#endif + + /* First run: stamp META and persist an orphan Session that expires one + * second after it was orphaned at time 1. */ + g_time_s = 1; + rc = init_broker(&first, &net, &hooks); + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&first); + } + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_PutOrphanSession(&first, cid, + MQTT_CONNECT_PROTOCOL_LEVEL_5, 1, (WOLFMQTT_BROKER_TIME_T)1); + } + MqttBroker_Free(&first); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "first broker setup returned %d\n", rc); + fail = 1; + } + + /* Restart well after the expiry. */ + if (!fail) { + g_time_s = 100; + rc = init_broker(&second, &net, &hooks); + g_log_len = 0; + g_log[0] = '\0'; + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&second); + } + MqttBroker_Free(&second); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "restore returned %d\n", rc); + fail = 1; + } + } + + if (!fail) { + if (strstr(g_log, "expired session") == NULL) { + fprintf(stderr, "expiry not logged: %s\n", g_log); + fail = 1; + } + else if (strchr(g_log, 0x1b) != NULL || + strchr(g_log, '\n') != NULL) { + fprintf(stderr, "raw control bytes reached the log\n"); + fail = 1; + } + else if (strstr(g_log, "client_id=x\\e[31m\\ny") == NULL) { + fprintf(stderr, "client_id not escaped as expected: %s\n", + g_log); + fail = 1; + } + } + + MqttBrokerNet_PersistPosix_Free(&hooks); + remove_store(root); + return fail; +} + +int main(void) +{ + return test_restore_expired_session_logs_escaped_client_id(); +} + +#else + +int main(void) +{ + /* Needs the dynamic-memory persistence restore with logging enabled. */ + return 77; +} + +#endif diff --git a/tests/test_broker_log_config.h b/tests/test_broker_log_config.h new file mode 100644 index 000000000..df22fb774 --- /dev/null +++ b/tests/test_broker_log_config.h @@ -0,0 +1,10 @@ +/* Prototypes for the hooks test_broker_log builds the broker against: PRINTF + * is redirected to wolfmqtt_test_log and the broker clock to + * wolfmqtt_test_broker_time_s. */ +#ifndef WOLFMQTT_TEST_BROKER_LOG_CONFIG_H +#define WOLFMQTT_TEST_BROKER_LOG_CONFIG_H + +void wolfmqtt_test_log(const char* fmt, ...); +unsigned long wolfmqtt_test_broker_time_s(void); + +#endif diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 5cc0d99c0..2ac15730a 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -1017,6 +1017,12 @@ WOLFMQTT_LOCAL void BrokerOrphan_DropFull(MqttBroker* broker, BrokerOrphanSession* o); #endif +#ifndef WOLFMQTT_BROKER_NO_LOG +/* Escape control bytes in a peer-controlled string before it is logged. + * Defined in mqtt_broker.c; shared with the persistence layer. */ +WOLFMQTT_LOCAL const char* BrokerLog_Sanitize(const char* src); +#endif + /* CLI wrapper interface */ WOLFMQTT_API int wolfmqtt_broker(int argc, char** argv); From 0095f7e95aacfd7e55efbafe1f404e7a9a68c79e Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 09:47:45 -0500 Subject: [PATCH 34/52] F-12451 - Check restored OUTQ packet IDs against the record key --- src/mqtt_broker_persist.c | 5 +++ tests/test_broker_connect.c | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index df3f5f722..2d178dce1 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -2480,6 +2480,11 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, return MQTT_CODE_ERROR_MALFORMED_DATA; } packet_id = wmqb_r_u16(p); p += 2; + /* The key repeats the Packet Identifier; both copies must agree, and a + * queued QoS > 0 message never has identifier 0 [MQTT-2.2.1-3]. */ + if (packet_id == 0 || packet_id != wmqb_r_u16(&key[cid_len + 1])) { + return MQTT_CODE_ERROR_MALFORMED_DATA; + } enq_time = wmqb_r_u64(p); p += 8; enqueue_seq = wmqb_r_u64(p); p += 8; if (enqueue_seq == 0) { diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 9b3f7ddbd..ffb81940c 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -11751,6 +11751,86 @@ TEST(persist_restore_packet_id_wrap_preserves_fifo) MqttBroker_Free(&source); } +/* An OUTQ record is keyed by client_id || 0x00 || Packet Identifier and also + * carries the identifier in its body. Restore must only load a record whose + * two copies agree and are non-zero ([MQTT-2.2.1-3]: a QoS > 0 PUBLISH has a + * non-zero Packet Identifier). A record whose key disagrees with its body + * could not be deleted by the identifier it would be acknowledged under, and + * could collide with another queued entry. */ +TEST(persist_restore_rejects_outq_packet_id_mismatch) +{ + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistOrderStore store; + BrokerOutPub out; + BrokerOrphanSession* orphan; + PersistOrderRecord* rec; + char topic[] = "t"; + + install_mock_net(&net); + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&restored, 0, sizeof(restored)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&store, 0, sizeof(store)); + hooks.kv_put = persist_order_put; + hooks.kv_get = persist_order_get; + hooks.kv_iter = persist_order_iter; + hooks.ctx = &store; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&source, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&source, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&source)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_PutOrphanSession(&source, + "cid", MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1)); + + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.qos = MQTT_QOS_1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + /* Packet Identifier 1: valid. */ + out.enqueue_seq = 1; + out.packet_id = 1; + ASSERT_EQ(MQTT_CODE_SUCCESS, + BrokerPersist_PutOutPub(&source, "cid", &out)); + /* Packet Identifier 2, then its key retagged to 3. */ + out.enqueue_seq = 2; + out.packet_id = 2; + ASSERT_EQ(MQTT_CODE_SUCCESS, + BrokerPersist_PutOutPub(&source, "cid", &out)); + /* Packet Identifier 0 in both key and body. */ + out.enqueue_seq = 3; + out.packet_id = 0; + (void)BrokerPersist_PutOutPub(&source, "cid", &out); + ASSERT_EQ(3, store.outq_count); + + rec = &store.outq[1]; + ASSERT_EQ(3 + 3, (int)rec->key_len); /* "cid" 0x00 pid_hi pid_lo */ + ASSERT_EQ(0x02, rec->key[5]); + rec->key[5] = 0x03; + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&restored, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, + MqttBroker_SetPersistHooks(&restored, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&restored)); + + orphan = restored.orphan_sessions; + ASSERT_NOT_NULL(orphan); + ASSERT_EQ(1, orphan->out_q_count); + ASSERT_NOT_NULL(orphan->out_q_head); + ASSERT_EQ(1, orphan->out_q_head->packet_id); + + MqttBroker_Free(&restored); + MqttBroker_Free(&source); +} + #ifdef WOLFMQTT_NONBLOCK typedef struct PersistSubsCount { int puts; @@ -14032,6 +14112,7 @@ int main(int argc, char** argv) RUN_TEST(persist_iter_keys_are_deletable); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(persist_restore_packet_id_wrap_preserves_fifo); + RUN_TEST(persist_restore_rejects_outq_packet_id_mismatch); #ifdef WOLFMQTT_NONBLOCK RUN_TEST(persist_subs_written_when_ack_deferred); RUN_TEST(persist_partial_publish_restart_keeps_dup); From 8c23557f9cc41299c79a435535d9fb4ac2cb4328 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:09:01 -0500 Subject: [PATCH 35/52] F-11491 - Report oversized POSIX persist records instead of truncating --- src/mqtt_broker_persist_posix.c | 14 ++++++++ tests/test_broker_connect.c | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/mqtt_broker_persist_posix.c b/src/mqtt_broker_persist_posix.c index 640c53ea9..f19324732 100644 --- a/src/mqtt_broker_persist_posix.c +++ b/src/mqtt_broker_persist_posix.c @@ -456,6 +456,8 @@ static int wmqb_posix_get(void* ctx, byte ns, const byte* key, ssize_t r; word32 cap; word32 read_total = 0; + byte probe; + int probe_eintr = 0; if (c == NULL || key == NULL || inout_len == NULL) { return MQTT_CODE_ERROR_BAD_ARG; @@ -507,6 +509,18 @@ static int wmqb_posix_get(void* ctx, byte ns, const byte* key, read_total += (word32)r; } } + /* A full buffer need not be the whole record. Probe for one more byte + * so a larger record is reported, not returned truncated. */ + if (read_total == cap) { + do { + r = read(fd, &probe, 1); + } while (r < 0 && errno == EINTR && probe_eintr++ < 16); + if (r != 0) { + (void)close(fd); + return (r > 0) ? MQTT_CODE_ERROR_OUT_OF_BUFFER : + MQTT_CODE_ERROR_SYSTEM; + } + } (void)close(fd); *inout_len = read_total; return 0; diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ffb81940c..ae2c415c1 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -12478,6 +12478,64 @@ TEST(persist_root_writable_permissions_rejected) ASSERT_EQ(MQTT_CODE_ERROR_SYSTEM, persist_test_init_root_mode(0777)); } +/* kv_get fills at most *inout_len bytes. A record larger than that must be + * reported as MQTT_CODE_ERROR_OUT_OF_BUFFER rather than returned truncated as + * if it were the whole record; the META check relies on that result to treat + * an oversized record as a schema mismatch. */ +TEST(persist_get_reports_record_larger_than_buffer) +{ + static const byte key[] = { 0x9a }; + static const byte value[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + char rec_path[680]; + MqttBrokerPersistHooks hooks; + byte out[sizeof(value)]; + word32 out_len; + char* root; + int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int short_rc = MQTT_CODE_SUCCESS; + int exact_rc = MQTT_CODE_ERROR_SYSTEM; + word32 exact_len = 0; + int paths_ok = 0; + + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + if (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0 && + snprintf(rec_path, sizeof(rec_path), "%s/9a.bin", + ns_path) > 0) { + paths_ok = 1; + } + ASSERT_TRUE(paths_ok); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)sizeof(key), + value, (word32)sizeof(value)); + out_len = 4; + short_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)sizeof(key), + out, &out_len); + out_len = (word32)sizeof(out); + exact_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)sizeof(key), + out, &out_len); + exact_len = out_len; + MqttBrokerNet_PersistPosix_Free(&hooks); + } + + (void)unlink(rec_path); + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, short_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, exact_rc); + ASSERT_EQ((word32)sizeof(value), exact_len); + ASSERT_EQ(0, XMEMCMP(out, value, sizeof(value))); +} + /* A rename or unlink is not durable until its namespace directory is * flushed, so the POSIX backend must report a failed directory fsync rather * than claim the update was committed. */ @@ -14129,6 +14187,7 @@ int main(int argc, char** argv) RUN_TEST(persist_root_writable_permissions_rejected); RUN_TEST(persist_put_rejects_existing_temp_symlink); RUN_TEST(persist_put_del_report_namespace_fsync_failure); + RUN_TEST(persist_get_reports_record_larger_than_buffer); RUN_TEST(persist_iter_reports_unreadable_record); RUN_TEST(persist_iter_skips_socket_and_symlink_entries); #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT From c48c5c77280673d197386f6ece2dc27b7e2442f5 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:12:45 -0500 Subject: [PATCH 36/52] F-14195 - Send a v5 DISCONNECT reason before closing on broker errors --- src/mqtt_broker.c | 34 +++++++++++-- tests/test_broker_connect.c | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 78dc7a121..de175f45d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -8885,6 +8885,32 @@ static int BrokerClient_ResumeDirect(MqttBroker* broker, BrokerClient* bc) /* -------------------------------------------------------------------------- */ /* Per-client processing (called from Step) */ /* -------------------------------------------------------------------------- */ +/* MQTT 5.0 section 4.13.1: after a Malformed Packet (malformed != 0) or a + * Protocol Error in a packet other than CONNECT, a v5 Server SHOULD send + * DISCONNECT with the Reason Code before closing the Network Connection. + * BrokerSend_Disconnect sends nothing below v5, so this then only closes. */ +static void BrokerClient_ErrorClose(MqttBroker* broker, BrokerClient* bc, + int malformed) +{ +#ifdef WOLFMQTT_V5 + byte reason = (byte)(malformed ? MQTT_REASON_MALFORMED_PACKET : + MQTT_REASON_PROTOCOL_ERR); + + #ifndef WOLFMQTT_STATIC_MEMORY + if (BrokerSend_Disconnect(bc, reason) == MQTT_CODE_CONTINUE) { + /* Finish writing the DISCONNECT on a later step before closing. */ + bc->close_after_direct = 1; + return; + } + #else + (void)BrokerSend_Disconnect(bc, reason); + #endif +#else + (void)malformed; +#endif + BrokerClient_AbnormalClose(broker, bc); +} + static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) { int rc; @@ -9033,7 +9059,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) "sock=%d [MQTT-2.2.2-2]", type, bc->rx_buf[0], (int)bc->sock); if (bc->connected) { - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 1); } else { BrokerSubs_RemoveClient(broker, bc); @@ -9055,7 +9081,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) WBLOG_ERR(broker, "broker: second CONNECT on sock=%d [MQTT-3.1.0-2]", (int)bc->sock); - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 0); return 0; } switch (type) { @@ -9208,7 +9234,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) * only - Remaining Length MUST be 0. Reject malformed * PINGREQ before sending PINGRESP. */ if (bc->client.packet.remain_len != 0) { - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 1); return 0; } /* A failed PINGRESP write is an I/O error like any other @@ -9348,7 +9374,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) WBLOG_ERR(broker, "broker: unhandled packet type %u sock=%d", type, (int)bc->sock); - BrokerClient_AbnormalClose(broker, bc); + BrokerClient_ErrorClose(broker, bc, 0); return 0; } #ifdef ENABLE_MQTT_WEBSOCKET diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ae2c415c1..031a60652 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -9773,6 +9773,99 @@ TEST(disconnect_v5_session_expiry_raise_sends_protocol_error) MqttBroker_Free(&broker); } +/* MQTT 5.0 section 4.13.1: a Server that detects a Malformed Packet or + * Protocol Error in a packet other than CONNECT SHOULD send DISCONNECT with + * the Reason Code before closing. Connect a v5 client, send one bad packet, + * and return the Reason Code of the DISCONNECT the broker sent (-1 if none); + * *closed reports whether the connection was closed. */ +static int v5_bad_packet_disconnect_reason(const byte* bad, size_t bad_len, + int* closed) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int reason; + /* v5 CONNECT, CleanStart, keep alive 60, no properties, ClientId "A". */ + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + *closed = 0; + return -2; + } + reset_mock_state(connect, sizeof(connect)); + mock_client_input_append(0, bad, bad_len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + reason = first_disconnect_reason(g_out_buf, g_out_len); + *closed = g_client_closed; + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return reason; +} + +/* [MQTT-2.2.2-2] PUBACK fixed-header flags must be 0000: Malformed Packet. */ +TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect) +{ + static const byte puback[] = { 0x41, 0x02, 0x00, 0x01 }; + int closed = 0; + + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + v5_bad_packet_disconnect_reason(puback, sizeof(puback), &closed)); + ASSERT_TRUE(closed); +} + +/* [MQTT-3.1.0-2] A second CONNECT is a Protocol Error. */ +TEST(v5_second_connect_sends_protocol_error_disconnect) +{ + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + int closed = 0; + + ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + v5_bad_packet_disconnect_reason(connect, sizeof(connect), &closed)); + ASSERT_TRUE(closed); +} + +/* MQTT 5.0 section 3.12: PINGREQ has no variable header or payload, so a + * non-zero Remaining Length is a Malformed Packet. */ +TEST(v5_pingreq_with_body_sends_malformed_disconnect) +{ + static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; + int closed = 0; + + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + v5_bad_packet_disconnect_reason(pingreq, sizeof(pingreq), &closed)); + ASSERT_TRUE(closed); +} + +/* MQTT 5.0 section 4.12: without an Authentication Method in CONNECT the + * Client must not send AUTH, so an AUTH here is a Protocol Error. Wire: AUTH + * with Remaining Length 0 (Success, section 3.15.2.1). */ +TEST(v5_unexpected_auth_sends_protocol_error_disconnect) +{ + static const byte auth[] = { 0xF0, 0x00 }; + int closed = 0; + + ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + v5_bad_packet_disconnect_reason(auth, sizeof(auth), &closed)); + ASSERT_TRUE(closed); +} + /* v5 CONNECT with Clean Start=0, no Session Expiry: must default to 0, * not the v3.1.1 0xFFFFFFFF "never" sentinel. */ TEST(connect_v5_clean0_no_se_prop_orphan_expiry_zero) @@ -14063,6 +14156,10 @@ int main(int argc, char** argv) #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_v5_receive_max_zero_protocol_error); RUN_TEST(disconnect_v5_session_expiry_raise_sends_protocol_error); + RUN_TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect); + RUN_TEST(v5_second_connect_sends_protocol_error_disconnect); + RUN_TEST(v5_pingreq_with_body_sends_malformed_disconnect); + RUN_TEST(v5_unexpected_auth_sends_protocol_error_disconnect); #endif #endif #ifdef WOLFMQTT_V5 From cb85c9a0263ee6b8872bbfba247c522567a50392 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:19:05 -0500 Subject: [PATCH 37/52] F-13257 - Wipe restored topic copies freed on malformed persist records --- src/mqtt_broker_persist.c | 34 +++-- tests/test_broker_connect.c | 280 +++++++++++++++++++++++++++++++++++- 2 files changed, 299 insertions(+), 15 deletions(-) diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 2d178dce1..fa4413416 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -211,8 +211,23 @@ static INLINE int wmqb_read_header(const byte* buf, word32 buf_len, return 0; } +#if defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) || \ + !defined(WOLFMQTT_STATIC_MEMORY) +/* Volatile wipe the compiler cannot elide, for plaintext about to be freed. */ +static void wmqb_force_zero(void* mem, word32 len) +{ + volatile byte* p = (volatile byte*)mem; + if (p == NULL) { + return; + } + while (len > 0) { + *p++ = 0; + len--; + } +} +#endif + #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT -static void wmqb_force_zero(void* mem, word32 len); /* Lazy-init key cache. Single-threaded broker - no lock needed. The * application-provided derive_key hook fills 32 bytes on first request. @@ -255,19 +270,6 @@ static int wmqb_get_key(MqttBroker* broker) * rekeying only when persist_key_cache changes would amortize the AES * key schedule. Requires deciding the threading model first - today * the broker is single-threaded so a single shared context is safe. */ -/* Volatile wipe the compiler cannot elide, for plaintext about to be freed. */ -static void wmqb_force_zero(void* mem, word32 len) -{ - volatile byte* p = (volatile byte*)mem; - if (p == NULL) { - return; - } - while (len > 0) { - *p++ = 0; - len--; - } -} - /* AES-GCM additional data: the record header followed by where the record * is stored, encoded unambiguously as namespace(1) || key_len(2, big * endian) || key. A ciphertext moved to another namespace or storage key @@ -1767,6 +1769,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, payload_len = wmqb_r_u32(p); p += 4; if ((word32)(end - p) < payload_len) { + wmqb_force_zero(m->topic, (word32)topic_len + 1); WOLFMQTT_FREE(m->topic); WOLFMQTT_FREE(m); return MQTT_CODE_ERROR_MALFORMED_DATA; @@ -1774,6 +1777,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, if (payload_len > 0) { m->payload = (byte*)WOLFMQTT_MALLOC(payload_len); if (m->payload == NULL) { + wmqb_force_zero(m->topic, (word32)topic_len + 1); WOLFMQTT_FREE(m->topic); WOLFMQTT_FREE(m); return MQTT_CODE_ERROR_MEMORY; @@ -2520,6 +2524,7 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, payload_len = wmqb_r_u32(p); p += 4; if ((word32)(end - p) < payload_len) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); WOLFMQTT_FREE(e->topic); WOLFMQTT_FREE(e); return MQTT_CODE_ERROR_MALFORMED_DATA; @@ -2527,6 +2532,7 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, if (payload_len > 0) { e->payload = (byte*)WOLFMQTT_MALLOC(payload_len); if (e->payload == NULL) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); WOLFMQTT_FREE(e->topic); WOLFMQTT_FREE(e); return MQTT_CODE_ERROR_MEMORY; diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 031a60652..2c638ef68 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -45,6 +45,7 @@ #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT #include #endif + #include #include #include #include @@ -144,7 +145,8 @@ static void broker_test_disable_alloc_failure(void) } #endif -#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +#if defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) || \ + (defined(WOLFMQTT_BROKER_PERSIST) && !defined(WOLFMQTT_STATIC_MEMORY)) static void broker_test_capture_free(size_t allocation_size) { g_capture_alloc_size = allocation_size; @@ -12571,6 +12573,277 @@ TEST(persist_root_writable_permissions_rejected) ASSERT_EQ(MQTT_CODE_ERROR_SYSTEM, persist_test_init_root_mode(0777)); } +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + !defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) +/* Drop the last cut bytes of the only record in root/ and lower the + * header's body length to match (record header: magic(4) version(2) kind(1) + * wrap(1) body_len(4, big-endian)), so the record stays self-consistent and + * only the payload length inside the body overruns. Returns 0 on success. */ +static int persist_truncate_only_record(const char* root, byte ns, + long cut) +{ + char dir_path[640]; + char rec_path[1024]; + byte hdr[12]; + struct stat st; + struct dirent* ent; + DIR* d; + FILE* f; + word32 body_len; + int rc = -1; + + if (snprintf(dir_path, sizeof(dir_path), "%s/%u", root, + (unsigned)ns) <= 0) { + return -1; + } + d = opendir(dir_path); + if (d == NULL) { + return -1; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] != '.') { + break; + } + } + if (ent != NULL && + snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, + ent->d_name) > 0 && + stat(rec_path, &st) == 0 && st.st_size > cut + (long)sizeof(hdr)) { + f = fopen(rec_path, "r+b"); + if (f != NULL) { + if (fread(hdr, 1, sizeof(hdr), f) == sizeof(hdr)) { + body_len = ((word32)hdr[8] << 24) | ((word32)hdr[9] << 16) | + ((word32)hdr[10] << 8) | (word32)hdr[11]; + body_len -= (word32)cut; + hdr[8] = (byte)(body_len >> 24); + hdr[9] = (byte)(body_len >> 16); + hdr[10] = (byte)(body_len >> 8); + hdr[11] = (byte)body_len; + if (fseek(f, 0, SEEK_SET) == 0 && + fwrite(hdr, 1, sizeof(hdr), f) == sizeof(hdr)) { + rc = 0; + } + } + if (fclose(f) != 0) { + rc = -1; + } + } + if (rc == 0 && truncate(rec_path, st.st_size - cut) != 0) { + rc = -1; + } + } + (void)closedir(d); + return rc; +} + +/* Remove root// left by the POSIX backend. */ +static void persist_remove_tree(const char* root) +{ + char dir_path[640]; + char rec_path[1024]; + struct dirent* ent; + struct dirent* sub; + DIR* d; + DIR* sd; + + d = opendir(root); + if (d == NULL) { + return; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') { + continue; + } + if (snprintf(dir_path, sizeof(dir_path), "%s/%s", root, + ent->d_name) <= 0) { + continue; + } + sd = opendir(dir_path); + if (sd == NULL) { + (void)unlink(dir_path); + continue; + } + while ((sub = readdir(sd)) != NULL) { + if (sub->d_name[0] != '.' && + snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, + sub->d_name) > 0) { + (void)unlink(rec_path); + } + } + (void)closedir(sd); + (void)rmdir(dir_path); + } + (void)closedir(d); + (void)rmdir(root); +} + +/* Whether the captured freed allocation was all zero. */ +static int persist_captured_free_zeroed(void) +{ + size_t i; + + for (i = 0; i < g_capture_freed_len; i++) { + if (g_capture_freed[i] != 0) { + return 0; + } + } + return 1; +} + +/* A 40-character topic: its heap copy is 41 bytes, a size nothing else + * allocates during these restores, so the free capture sees only it. */ +#define PERSIST_SCRUB_TOPIC "secret/plant-7/valve-control/setpoint-01" + +/* Restore copies a persisted topic to the heap before it reads the payload. + * When the record turns out to be malformed the copy is freed; topic strings + * are application data the broker scrubs before freeing elsewhere, so it must + * be wiped first. The record is cut short on disk so its declared payload + * overruns the file. */ +TEST(persist_restore_malformed_retained_scrubs_topic) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + BrokerRetainedMsg rm; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[8] = { 'p', 'a', 'y', 'l', 'o', 'a', 'd', '!' }; + char* root; + int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int cut_rc = -1; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int seen = 0; + int zeroed = 0; + int retained_empty = 0; + + install_mock_net(&net); + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&rm, 0, sizeof(rm)); + rm.topic = topic; + rm.payload = payload; + rm.payload_len = (word32)sizeof(payload); + rm.qos = MQTT_QOS_0; + rm.store_time = (WOLFMQTT_BROKER_TIME_T)1; + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutRetained(&source, &rm); + } + MqttBroker_Free(&source); + cut_rc = persist_truncate_only_record(root, + BROKER_PERSIST_NS_RETAINED, (long)sizeof(payload)); + + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + retained_empty = (restored.retained == NULL); + MqttBroker_Free(&restored); + MqttBrokerNet_PersistPosix_Free(&hooks); + } + persist_remove_tree(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(0, cut_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, restore_rc); + ASSERT_TRUE(retained_empty); + ASSERT_TRUE(seen); + ASSERT_TRUE(zeroed); +} + +/* The same for a queued outbound message of an offline Session. */ +TEST(persist_restore_malformed_outq_scrubs_topic) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + BrokerOutPub out; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[8] = { 'p', 'a', 'y', 'l', 'o', 'a', 'd', '!' }; + char* root; + int init_rc; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int cut_rc = -1; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int seen = 0; + int zeroed = 0; + int queue_len = -1; + + install_mock_net(&net); + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS && + BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + MqttBroker_Free(&source); + cut_rc = persist_truncate_only_record(root, BROKER_PERSIST_NS_OUTQ, + (long)sizeof(payload)); + + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + if (restored.orphan_sessions != NULL) { + queue_len = restored.orphan_sessions->out_q_count; + } + MqttBroker_Free(&restored); + MqttBrokerNet_PersistPosix_Free(&hooks); + } + persist_remove_tree(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, put_rc); + ASSERT_EQ(0, cut_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, restore_rc); + ASSERT_EQ(0, queue_len); + ASSERT_TRUE(seen); + ASSERT_TRUE(zeroed); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ + /* kv_get fills at most *inout_len bytes. A record larger than that must be * reported as MQTT_CODE_ERROR_OUT_OF_BUFFER rather than returned truncated as * if it were the whole record; the META check relies on that result to treat @@ -14285,6 +14558,11 @@ int main(int argc, char** argv) RUN_TEST(persist_put_rejects_existing_temp_symlink); RUN_TEST(persist_put_del_report_namespace_fsync_failure); RUN_TEST(persist_get_reports_record_larger_than_buffer); +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + !defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) + RUN_TEST(persist_restore_malformed_retained_scrubs_topic); + RUN_TEST(persist_restore_malformed_outq_scrubs_topic); +#endif RUN_TEST(persist_iter_reports_unreadable_record); RUN_TEST(persist_iter_skips_socket_and_symlink_entries); #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT From ebdb03b7758c0117d0fc19c7b8f46f20947a2672 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:21:43 -0500 Subject: [PATCH 38/52] F-13875 - Wipe CONNECT credentials when a refused CONNACK is deferred --- src/mqtt_broker.c | 44 +++++++++++++++++------------ tests/test_broker_connect.c | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index de175f45d..0077ede6e 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -6605,6 +6605,28 @@ static int BrokerSend_Disconnect(BrokerClient* bc, byte reason_code) /* Packet handlers */ /* -------------------------------------------------------------------------- */ +/* Wipe the plaintext CONNECT in bc->rx_buf (the decoded username, password + * and v5 authentication data point into it) and the bc->username / + * bc->password copies. Nothing reads them once CONNECT is handled. */ +static void BrokerClient_ScrubConnect(BrokerClient* bc, int rx_len) +{ + BROKER_FORCE_ZERO(bc->rx_buf, (word32)rx_len); +#ifdef WOLFMQTT_BROKER_AUTH +#ifdef WOLFMQTT_STATIC_MEMORY + BROKER_FORCE_ZERO(bc->username, BROKER_MAX_USERNAME_LEN); + BROKER_FORCE_ZERO(bc->password, BROKER_MAX_PASSWORD_LEN); +#else + if (bc->username != NULL) { + BROKER_FORCE_ZERO(bc->username, XSTRLEN(bc->username) + 1); + } + if (bc->password != NULL) { + BROKER_FORCE_ZERO(bc->password, (size_t)bc->password_len + 1); + } +#endif + bc->password_len = 0; +#endif +} + /* Returns: > 0 success, 0 auth rejected (CONNACK sent with refused), * < 0 error */ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, @@ -7617,9 +7639,11 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, * Closing now would deliver only the bytes already written, so the * client could never read why it was refused. Keep the socket open * until BrokerClient_ResumeConnAck finishes the write; it then - * performs the close. */ + * performs the close. The credentials are not needed for that, + * so wipe them now instead of when the client is freed. */ bc->connack_pending_len = ack_len; bc->connack_refused = 1; + BrokerClient_ScrubConnect(bc, rx_len); } #endif return 0; @@ -7691,23 +7715,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, * re-auth path. Scrub them all now so credentials reside in memory only * for the duration of CONNECT processing rather than the whole session. * Refused/failed CONNECTs return earlier and are wiped when the caller - * frees the client. Mirrors the client-side CLIENT_FORCE_ZERO hardening + * frees the client, or above if their CONNACK is deferred. Mirrors the client-side CLIENT_FORCE_ZERO hardening * in mqtt_client.c. */ - BROKER_FORCE_ZERO(bc->rx_buf, (word32)rx_len); -#ifdef WOLFMQTT_BROKER_AUTH -#ifdef WOLFMQTT_STATIC_MEMORY - BROKER_FORCE_ZERO(bc->username, BROKER_MAX_USERNAME_LEN); - BROKER_FORCE_ZERO(bc->password, BROKER_MAX_PASSWORD_LEN); -#else - if (bc->username != NULL) { - BROKER_FORCE_ZERO(bc->username, XSTRLEN(bc->username) + 1); - } - if (bc->password != NULL) { - BROKER_FORCE_ZERO(bc->password, (size_t)bc->password_len + 1); - } -#endif - bc->password_len = 0; -#endif + BrokerClient_ScrubConnect(bc, rx_len); return rc; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 2c638ef68..36d9eb19c 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -2102,6 +2102,60 @@ TEST(connect_credentials_scrubbed_after_accept) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* A refused CONNECT whose CONNACK cannot be written at once keeps the client + * until the write finishes or times out. Its credentials are not needed for + * that, so they must be wiped as on the accepted path rather than stay in + * rx_buf and the username/password copies for the whole deferred write. */ +TEST(connect_refused_deferred_connack_scrubs_credentials) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* bc; + /* CONNECT v3.1.1, ClientId "id", Username "alice", Password "wrongPW1" + * (the broker expects "s3cr3tPW"). connect_flags 0xC2 = username + + * password + clean session. */ + static const byte connect[] = { + 0x10, 31, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, + 0xC2, + 0x00, 0x3C, + 0x00, 0x02, 'i', 'd', + 0x00, 0x05, 'a', 'l', 'i', 'c', 'e', + 0x00, 0x08, 'w', 'r', 'o', 'n', 'g', 'P', 'W', '1' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + broker.auth_user = "alice"; + broker.auth_pass = "s3cr3tPW"; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + /* Only one CONNACK byte goes out, so the refusal is deferred. */ + g_clients[0].write_limit_once = 1; + run_broker_one_connect(&broker); + + bc = broker.clients; + ASSERT_NOT_NULL(bc); + ASSERT_EQ(1, (int)bc->connack_refused); + ASSERT_EQ(0, (int)bc->connected); + + ASSERT_EQ(0, (int)bc->password_len); + if (bc->password != NULL) { + ASSERT_EQ(0, region_contains((const byte*)bc->password, 8, + "wrongPW1", 8)); + } + ASSERT_EQ(0, region_contains(bc->rx_buf, (int)sizeof(connect), + "wrongPW1", 8)); + ASSERT_EQ(0, region_contains(bc->rx_buf, (int)sizeof(connect), + "alice", 5)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} #endif /* WOLFMQTT_BROKER_AUTH */ #ifdef WOLFMQTT_V5 @@ -14206,6 +14260,7 @@ int main(int argc, char** argv) RUN_TEST(broker_set_auth_pass_shorter_second_no_residue); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_credentials_scrubbed_after_accept); + RUN_TEST(connect_refused_deferred_connack_scrubs_credentials); #endif #endif #ifdef ENABLE_MQTT_TLS From 445666dccc41a1f19de5f9bb6e38d4c5e385f985 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:24:20 -0500 Subject: [PATCH 39/52] F-13258 - Wipe the client tx and rx buffers in MqttClient_DeInit --- src/mqtt_client.c | 8 ++++++++ tests/test_mqtt_client.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 968748106..a00bd5d33 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2903,6 +2903,14 @@ void MqttClient_DeInit(MqttClient *client) } } #endif + /* The caller's buffers can still hold a serialized CONNECT or AUTH + * from an abandoned operation; the client no longer uses them. */ + if (client->tx_buf != NULL && client->tx_buf_len > 0) { + CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); + } + if (client->rx_buf != NULL && client->rx_buf_len > 0) { + CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); + } } } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index db64b94e9..eb72637f4 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -309,6 +309,39 @@ TEST(deinit_after_init) ASSERT_TRUE(1); } +/* The caller-provided buffers can still hold a serialized CONNECT (username, + * password, v5 authentication data) from an abandoned operation. Once the + * client is torn down the library no longer uses them, so DeInit must wipe + * them rather than hand them back to the application with that content. */ +TEST(deinit_scrubs_tx_rx_buffers) +{ + int rc; + int i; + int tx_clear = 1; + int rx_clear = 1; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + XMEMSET(test_tx_buf, 0x5A, sizeof(test_tx_buf)); + XMEMSET(test_rx_buf, 0x5A, sizeof(test_rx_buf)); + + MqttClient_DeInit(&test_client); + test_client_inited = 0; + + for (i = 0; i < (int)sizeof(test_tx_buf); i++) { + if (test_tx_buf[i] != 0) { + tx_clear = 0; + } + } + for (i = 0; i < (int)sizeof(test_rx_buf); i++) { + if (test_rx_buf[i] != 0) { + rx_clear = 0; + } + } + ASSERT_TRUE(tx_clear); + ASSERT_TRUE(rx_clear); +} + /* ============================================================================ * MqttClient_Connect Tests * ============================================================================ */ @@ -8493,6 +8526,7 @@ void run_mqtt_client_tests(void) /* MqttClient_DeInit tests */ RUN_TEST(deinit_null_client); RUN_TEST(deinit_after_init); + RUN_TEST(deinit_scrubs_tx_rx_buffers); /* MqttClient_Connect tests */ RUN_TEST(connect_null_client); From 6ceeb807bf5fda8521e7f755349c447cf09492d5 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:28:27 -0500 Subject: [PATCH 40/52] F-14386 - Wipe the AES-GCM context after persist record crypto --- src/mqtt_broker_persist.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index fa4413416..ebfddc3b4 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -349,6 +349,7 @@ static int wmqb_encrypt_blob(MqttBroker* broker, byte ns, if (wc_AesGcmSetKey(&aes, broker->persist_key_cache, (word32)sizeof(broker->persist_key_cache)) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_SYSTEM; @@ -361,11 +362,15 @@ static int wmqb_encrypt_blob(MqttBroker* broker, byte ns, WMQB_GCM_TAG_LEN, aad, aad_len) != 0) { /* aad = header + location */ wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_SYSTEM; } + /* The Aes holds the expanded persistence key. wc_AesFree only clears + * it in newer wolfSSL releases, so wipe it here on every path. */ wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); *ct_out = out; return 0; @@ -418,6 +423,7 @@ static int wmqb_decrypt_blob(MqttBroker* broker, byte ns, if (wc_AesGcmSetKey(&aes, broker->persist_key_cache, (word32)sizeof(broker->persist_key_cache)) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); wmqb_force_zero(out, *plain_out_len); WOLFMQTT_FREE(out); @@ -431,12 +437,14 @@ static int wmqb_decrypt_blob(MqttBroker* broker, byte ns, WMQB_GCM_TAG_LEN, aad, aad_len) != 0) { wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); wmqb_force_zero(out, *plain_out_len); WOLFMQTT_FREE(out); return MQTT_CODE_ERROR_MALFORMED_DATA; } wc_AesFree(&aes); + wmqb_force_zero(&aes, (word32)sizeof(aes)); WOLFMQTT_FREE(aad); *plain_out = out; return 0; From 89f377f18d00abbc544082ba190ecd6dcf797ed6 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:30:07 -0500 Subject: [PATCH 41/52] F-13875 - Run the deferred refused CONNACK test only in nonblock builds --- tests/test_broker_connect.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 36d9eb19c..043755ebf 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -2103,10 +2103,12 @@ TEST(connect_credentials_scrubbed_after_accept) MqttBroker_Free(&broker); } +#ifdef WOLFMQTT_NONBLOCK /* A refused CONNECT whose CONNACK cannot be written at once keeps the client * until the write finishes or times out. Its credentials are not needed for * that, so they must be wiped as on the accepted path rather than stay in - * rx_buf and the username/password copies for the whole deferred write. */ + * rx_buf and the username/password copies for the whole deferred write. + * Only nonblocking builds defer the write; a blocking write completes it. */ TEST(connect_refused_deferred_connack_scrubs_credentials) { MqttBroker broker; @@ -2156,6 +2158,7 @@ TEST(connect_refused_deferred_connack_scrubs_credentials) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } +#endif /* WOLFMQTT_NONBLOCK */ #endif /* WOLFMQTT_BROKER_AUTH */ #ifdef WOLFMQTT_V5 @@ -14260,9 +14263,11 @@ int main(int argc, char** argv) RUN_TEST(broker_set_auth_pass_shorter_second_no_residue); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_credentials_scrubbed_after_accept); +#ifdef WOLFMQTT_NONBLOCK RUN_TEST(connect_refused_deferred_connack_scrubs_credentials); #endif #endif +#endif #ifdef ENABLE_MQTT_TLS RUN_TEST(broker_start_partial_listener_failure_cleans_up); RUN_TEST(broker_add_failure_closes_socket_once); From 7e0929017b51088574ed01091b91adb70d2f0eac Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:31:49 -0500 Subject: [PATCH 42/52] F-14183 - Wipe the whole tx_buf when a WILLMSG cannot be encoded --- src/mqtt_sn_client.c | 4 +++- tests/test_mqtt_sn_client.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 0718cb520..f14eee2a4 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -1320,7 +1320,9 @@ static int SN_WillMessage(MqttClient *client, SN_Will *will) will->stat.write = MQTT_MSG_PAYLOAD; } else { - CLIENT_FORCE_ZERO(client->tx_buf, client->write.len); + /* write.len was not set for this packet; wipe the whole + * buffer as the other encode-failure paths do. */ + CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); MqttWriteStop(client, &will->stat); will->stat.write = MQTT_MSG_BEGIN; break; diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 8825bb6f5..9c9d4e904 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -952,6 +952,39 @@ TEST(sn_will_payload_scrubbed_after_send) sn_will_scrub_check(0 /* no CONTINUE */); } +/* When the WILLMSG cannot be encoded (here the Will Message is larger than + * tx_buf), SN_WillMessage releases the writer after wiping tx_buf, like the + * other encode-failure paths. The wipe must cover the whole buffer: tx_buf is + * pre-filled to stand in for earlier traffic, and none of it may remain. */ +TEST(sn_willmsg_encode_failure_scrubs_tx_buf) +{ + SN_Connect mc; + int rc, i; + static byte big_msg[600]; /* larger than the 512-byte test tx_buf */ + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + XMEMSET(g_client.tx_buf, 0x5A, (size_t)g_client.tx_buf_len); + XMEMSET(big_msg, 'w', sizeof(big_msg)); + + sn_will_setup_connect(&mc); + mc.will.willMsg = big_msg; + mc.will.willMsgLen = (word16)sizeof(big_msg); + ASSERT_TRUE((int)sizeof(big_msg) > g_client.tx_buf_len); + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_TRUE(rc < 0); + ASSERT_TRUE(rc != MQTT_CODE_CONTINUE); + for (i = 0; i < g_client.tx_buf_len; i++) { + if (g_client.tx_buf[i] != 0) { + FAIL("tx_buf not wiped after WILLMSG encode failure"); + } + } + ASSERT_NO_PENDRESP(); +} + /* ============================================================================ * SN will-message-update scrub test (#3138) * @@ -3374,6 +3407,7 @@ int main(int argc, char** argv) RUN_TEST(sn_connect_refused); RUN_TEST(sn_connect_will_error_allows_retry); RUN_TEST(sn_will_payload_scrubbed_after_send); + RUN_TEST(sn_willmsg_encode_failure_scrubs_tx_buf); RUN_TEST(sn_willmsgupd_payload_scrubbed_after_send); RUN_TEST(sn_willtopicupd_null_topic_sends_delete); RUN_TEST(sn_willmsgupd_payload_scrubbed_on_write_error); From 8ec6e592d1f754f967df15d6e1d2312d9b367c56 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 10:36:44 -0500 Subject: [PATCH 43/52] F-13803 - Reject POSIX persist keys longer than the file name limit --- BROKER.md | 10 ++++++ src/mqtt_broker_persist_posix.c | 13 +++++++- tests/test_broker_connect.c | 58 +++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 5 +-- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/BROKER.md b/BROKER.md index 97711b68b..d9de1f972 100644 --- a/BROKER.md +++ b/BROKER.md @@ -165,6 +165,16 @@ script or installer inherits the ambient umask - under a relaxed umask such as mkdir -p /var/lib/wolfmqtt && chmod 0700 /var/lib/wolfmqtt ``` +The POSIX backend names each record after its key in hex, so a key can be at +most `(NAME_MAX - 8) / 2` bytes: 123 on filesystems with the usual 255-byte +`NAME_MAX`. Sessions and subscriptions are keyed by client ID, retained +messages by topic, and offline-queue entries by client ID plus 3 bytes. With +this backend a client ID longer than 120 bytes loses its queued messages across +a restart, one longer than 123 bytes loses its whole session, and a retained +message whose topic is longer than 123 bytes is not kept. The broker still +serves them from memory while it runs. Use a custom backend through +`MqttBroker_SetPersistHooks()` if longer identifiers must persist. + | Macro | Default | Description | |---|---|---| | `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions retained across restarts | diff --git a/src/mqtt_broker_persist_posix.c b/src/mqtt_broker_persist_posix.c index f19324732..2bff8c663 100644 --- a/src/mqtt_broker_persist_posix.c +++ b/src/mqtt_broker_persist_posix.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -310,6 +311,16 @@ static int wmqb_open_ns(const WmqbPosixCtx* c, byte ns, int create, return 0; } +/* Longest key this backend can store. A record is named ".bin" and + * written through ".bin.tmp", so 2 * key_len + 8 must fit in one + * file name component (NAME_MAX, 255 on common filesystems: 123 bytes). */ +#ifdef NAME_MAX + #define WMQB_NAME_MAX NAME_MAX +#else + #define WMQB_NAME_MAX 255 +#endif +#define WMQB_MAX_KEY_LEN ((WMQB_NAME_MAX - 8) / 2) + static int wmqb_rec_name(const byte* key, word16 key_len, char* out, size_t out_cap) { @@ -319,7 +330,7 @@ static int wmqb_rec_name(const byte* key, word16 key_len, char* out, if (key == NULL || out == NULL) { return MQTT_CODE_ERROR_BAD_ARG; } - if (key_len > 256) { + if (key_len > 256 || key_len > WMQB_MAX_KEY_LEN) { return MQTT_CODE_ERROR_OUT_OF_BUFFER; } wmqb_hex_encode(hex, key, key_len); diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 043755ebf..f35cd89e0 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -47,6 +47,7 @@ #endif #include #include + #include #include #include #include @@ -12901,6 +12902,62 @@ TEST(persist_restore_malformed_outq_scrubs_topic) } #endif /* !WOLFMQTT_STATIC_MEMORY && !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ +/* The POSIX backend names a record ".bin" and writes it through + * ".bin.tmp", so a key can be at most (NAME_MAX - 8) / 2 bytes. A + * longer key must be refused up front as MQTT_CODE_ERROR_OUT_OF_BUFFER, the + * backend's documented "key too long" result, not surface later as a generic + * system error from the filesystem. */ +TEST(persist_key_longer_than_name_limit_rejected) +{ + byte key[2 * 256]; + static const byte value[] = "v"; + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + char ns_path[640]; + MqttBrokerPersistHooks hooks; + byte out[8]; + word32 out_len; + char* root; + int max_len = (NAME_MAX - 8) / 2; + int init_rc; + int fit_put_rc = MQTT_CODE_ERROR_SYSTEM; + int fit_del_rc = MQTT_CODE_ERROR_SYSTEM; + int long_put_rc = MQTT_CODE_SUCCESS; + int long_get_rc = MQTT_CODE_SUCCESS; + int long_del_rc = MQTT_CODE_SUCCESS; + int path_ok; + + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(key, 'k', sizeof(key)); + ASSERT_TRUE(max_len + 1 <= (int)sizeof(key)); + root = mkdtemp(root_template); + ASSERT_NOT_NULL(root); + path_ok = (snprintf(ns_path, sizeof(ns_path), "%s/7", root) > 0); + ASSERT_TRUE(path_ok); + + init_rc = MqttBrokerNet_PersistPosix_Init(&hooks, root); + if (init_rc == MQTT_CODE_SUCCESS) { + fit_put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)max_len, + value, (word32)sizeof(value)); + fit_del_rc = hooks.kv_del(hooks.ctx, 7, key, (word16)max_len); + long_put_rc = hooks.kv_put(hooks.ctx, 7, key, (word16)(max_len + 1), + value, (word32)sizeof(value)); + out_len = (word32)sizeof(out); + long_get_rc = hooks.kv_get(hooks.ctx, 7, key, (word16)(max_len + 1), + out, &out_len); + long_del_rc = hooks.kv_del(hooks.ctx, 7, key, (word16)(max_len + 1)); + MqttBrokerNet_PersistPosix_Free(&hooks); + } + (void)rmdir(ns_path); + (void)rmdir(root); + + ASSERT_EQ(MQTT_CODE_SUCCESS, init_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, fit_put_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, fit_del_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_put_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_get_rc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, long_del_rc); +} + /* kv_get fills at most *inout_len bytes. A record larger than that must be * reported as MQTT_CODE_ERROR_OUT_OF_BUFFER rather than returned truncated as * if it were the whole record; the META check relies on that result to treat @@ -14618,6 +14675,7 @@ int main(int argc, char** argv) RUN_TEST(persist_put_rejects_existing_temp_symlink); RUN_TEST(persist_put_del_report_namespace_fsync_failure); RUN_TEST(persist_get_reports_record_larger_than_buffer); + RUN_TEST(persist_key_longer_than_name_limit_rejected); #if !defined(WOLFMQTT_STATIC_MEMORY) && \ !defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) RUN_TEST(persist_restore_malformed_retained_scrubs_topic); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 2ac15730a..a94238690 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -381,8 +381,9 @@ typedef struct MqttBrokerPersistHooks { * implementations must honor key_len. Keys are derived from * peer-supplied client_ids and topics, which dynamic-memory builds do * not cap. A backend with a maximum key length (the POSIX backend - * accepts up to 256 bytes) must reject a longer key with an error - * rather than truncate it. */ + * accepts up to (NAME_MAX - 8) / 2 bytes, 123 with the usual 255-byte + * NAME_MAX) must reject a longer key with an error rather than + * truncate it. */ int (*kv_put)(void* ctx, byte ns, const byte* key, word16 key_len, const byte* blob, word32 blob_len); int (*kv_get)(void* ctx, byte ns, const byte* key, word16 key_len, From 5488ccde4d97dbad9d275cda1500bc5a701acc51 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:02:16 -0500 Subject: [PATCH 44/52] F-13249 - Deliver MQTT-SN QoS 2 retransmits to the application once --- src/mqtt_client.c | 13 +++--- src/mqtt_sn_client.c | 39 ++++++++++++++++++ tests/test_mqtt_sn_client.c | 82 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_client.h | 11 +++++ 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index a00bd5d33..0c61ff591 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -567,8 +567,9 @@ static void MqttClient_SessionIdRecord(MqttClient* client, * QoS 2 PUBLISH to the application and answered with PUBREC records its packet * id until the matching PUBREL arrives, so a retransmitted PUBLISH is * acknowledged again without a second delivery [MQTT-4.3.3-10]. These run on - * the receive path (under lockRecv), so no extra locking is required. */ -static int MqttClient_RecvQos2_Contains(const MqttClient* client, + * the receive path (under lockRecv), so no extra locking is required. The + * MQTT-SN client shares them for its QoS 2 flow (MQTT-SN 1.2 section 6.7). */ +WOLFMQTT_LOCAL int MqttClient_RecvQos2_Contains(const MqttClient* client, word16 packet_id) { int i; @@ -584,7 +585,7 @@ static int MqttClient_RecvQos2_Contains(const MqttClient* client, return 0; } -static int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) +WOLFMQTT_LOCAL int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) { int i; @@ -599,7 +600,8 @@ static int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client) return 0; } -static void MqttClient_RecvQos2_Add(MqttClient* client, word16 packet_id) +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Add(MqttClient* client, + word16 packet_id) { int i; @@ -617,7 +619,8 @@ static void MqttClient_RecvQos2_Add(MqttClient* client, word16 packet_id) } } -static void MqttClient_RecvQos2_Remove(MqttClient* client, word16 packet_id) +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Remove(MqttClient* client, + word16 packet_id) { int i; diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index f14eee2a4..856edd157 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -262,6 +262,24 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, * the callback may safely call a client send API. MQTT_MSG_WAIT * records successful delivery across a busy-writer retry. */ if (wait_stat->ack == MQTT_MSG_BEGIN) { + #if WOLFMQTT_MAX_QOS >= 2 + /* MQTT-SN 1.2 section 6.7: a QoS 2 PUBLISH is delivered once. + * A retransmit of one still awaiting its PUBREL is answered + * with PUBREC again but not delivered a second time. */ + if (p_pub->qos == MQTT_QOS_2 && + MqttClient_RecvQos2_Contains(client, + p_pub->packet_id)) { + wait_stat->ack = MQTT_MSG_WAIT; + } + else if (p_pub->qos == MQTT_QOS_2 && + !MqttClient_RecvQos2_HasFreeSlot(client)) { + /* A new id that cannot be tracked must not be delivered, + * or its retransmit would be delivered again. MQTT-SN + * cannot refuse it in-band, so fail without PUBREC. */ + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PACKET_ID); + } + else + #endif if (client->msg_cb) { /* if using the temp publish message buffer, then populate message context with client context */ @@ -271,6 +289,12 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, if (rc != MQTT_CODE_SUCCESS) { return rc; } + #if WOLFMQTT_MAX_QOS >= 2 + if (p_pub->qos == MQTT_QOS_2) { + /* Delivered; remember the id until its PUBREL. */ + MqttClient_RecvQos2_Add(client, p_pub->packet_id); + } + #endif wait_stat->ack = MQTT_MSG_WAIT; } else { @@ -354,6 +378,14 @@ static int SN_Client_HandlePacket(MqttClient* client, SN_MsgType packet_type, } packet_id = p_publish_resp->packet_id; + #if WOLFMQTT_MAX_QOS >= 2 + /* PUBREL completes the inbound QoS 2 flow: drop the id so a later + * PUBLISH may reuse it. */ + if (packet_type == SN_MSG_TYPE_PUBREL) { + MqttClient_RecvQos2_Remove(client, packet_id); + } + #endif + /* If Qos then send response */ if (packet_type == SN_MSG_TYPE_PUBREC || packet_type == SN_MSG_TYPE_PUBREL) { @@ -1531,6 +1563,13 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) mc_connect->ack.return_code != SN_RC_ACCEPTED) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_CONNECT_REFUSED); } +#if WOLFMQTT_MAX_QOS >= 2 + /* A clean session starts with no inbound QoS 2 flows in progress. */ + if (rc == MQTT_CODE_SUCCESS && mc_connect->clean_session) { + XMEMSET(client->recv_qos2_pending, 0, + sizeof(client->recv_qos2_pending)); + } +#endif return rc; } diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 9c9d4e904..276785749 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -1386,6 +1386,85 @@ TEST(sn_publish_incoming_null_msg_cb_errors_no_ack) ASSERT_EQ(0, g_mock.write_calls); } +#if WOLFMQTT_MAX_QOS >= 2 +static int g_sn_qos2_deliveries; + +static int sn_count_qos2_delivery(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg; + (void)msg_new; + (void)msg_done; + g_sn_qos2_deliveries++; + return MQTT_CODE_SUCCESS; +} + +/* Count short-form (1-byte length) MQTT-SN frames of a type in the capture. */ +static int sn_out_count(const MockNet* net, byte packet_type) +{ + int count = 0; + int pos = 0; + + while (pos + 1 < net->out_len && net->out[pos] >= 2) { + if (net->out[pos + 1] == packet_type) { + count++; + } + pos += net->out[pos]; + } + return count; +} + +/* MQTT-SN 1.2 section 6.7: a QoS 2 PUBLISH is answered with PUBREC and its + * delivery completes with PUBREL/PUBCOMP. A retransmitted PUBLISH (DUP set) + * for an id still awaiting PUBREL must be acknowledged again but not handed + * to the application a second time. Once PUBREL has released the id, a new + * PUBLISH may reuse it and is delivered. Frames: PUBLISH QoS 2 id 7 (flags + * 0x40), its DUP retransmit (0xC0), PUBREL id 7, a new PUBLISH id 7. */ +TEST(sn_qos2_retransmit_not_delivered_twice) +{ + static const byte pub[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x00, 0x07, 0x01, 0x02 }; + static const byte dup[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0xC0, 0x00, 0x0A, + 0x00, 0x07, 0x01, 0x02 }; + static const byte rel[] = { 0x04, SN_MSG_TYPE_PUBREL, 0x00, 0x07 }; + static const byte pub2[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x00, 0x07, 0x03, 0x04 }; + int rc = MQTT_CODE_SUCCESS; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_count_qos2_delivery; + g_sn_qos2_deliveries = 0; + mock_net_push(&g_mock, pub, (int)sizeof(pub)); + mock_net_push(&g_mock, dup, (int)sizeof(dup)); + mock_net_push(&g_mock, rel, (int)sizeof(rel)); + mock_net_push(&g_mock, pub2, (int)sizeof(pub2)); + + /* Each frame is waited for on a fresh wait object, as in + * sn_will_response_reset_clears_return_code: a completed wait leaves + * client->msgSN at its payload state. */ + for (i = 0; i < 40 && g_mock.in_idx < g_mock.in_count; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&g_client.msgSN, 0, sizeof(g_client.msgSN)); + } + else if (rc != MQTT_CODE_CONTINUE) { + FAIL("SN_Client_WaitMessage failed"); + } + } + /* Let a pending response from the last frame finish. */ + for (i = 0; i < 4 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + + ASSERT_EQ(g_mock.in_count, g_mock.in_idx); + ASSERT_EQ(2, g_sn_qos2_deliveries); + ASSERT_EQ(3, sn_out_count(&g_mock, SN_MSG_TYPE_PUBREC)); + ASSERT_EQ(1, sn_out_count(&g_mock, SN_MSG_TYPE_PUBCOMP)); +} +#endif /* WOLFMQTT_MAX_QOS >= 2 */ + #ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS static int sn_mt_only_packet_count(const MockNet* net, byte packet_type) { @@ -3431,6 +3510,9 @@ int main(int argc, char** argv) #endif RUN_TEST(sn_unsubscribe_no_continue); RUN_TEST(sn_publish_incoming_null_msg_cb_errors_no_ack); +#if WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(sn_qos2_retransmit_not_delivered_twice); +#endif RUN_TEST(sn_ping_no_continue); RUN_TEST(sn_ping_null_no_continue); RUN_TEST(sn_ping_null_consecutive_success); diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 8b59eddf4..dacb13295 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -1008,6 +1008,17 @@ WOLFMQTT_LOCAL int MqttClient_CheckPendResp(MqttClient *client, byte wait_type, word16 wait_packet_id); #endif WOLFMQTT_LOCAL int MqttPacket_HandleNetError(MqttClient *client, int rc); +#if WOLFMQTT_MAX_QOS >= 2 +/* Inbound QoS 2 de-duplication table (recv_qos2_pending), shared by the MQTT + * and MQTT-SN clients. */ +WOLFMQTT_LOCAL int MqttClient_RecvQos2_Contains(const MqttClient* client, + word16 packet_id); +WOLFMQTT_LOCAL int MqttClient_RecvQos2_HasFreeSlot(const MqttClient* client); +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Add(MqttClient* client, + word16 packet_id); +WOLFMQTT_LOCAL void MqttClient_RecvQos2_Remove(MqttClient* client, + word16 packet_id); +#endif /* Securely zero `len` bytes of `mem` (e.g. client->tx_buf) to scrub plaintext * such as credentials or will payloads before releasing the send lock. Uses a From 3fd5afd949d2dc74c6f6002fe4c8b179f8c96e60 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:05:11 -0500 Subject: [PATCH 45/52] Reset the MQTT-SN wait state once a packet has been handled --- src/mqtt_sn_client.c | 11 ++++---- tests/test_mqtt_sn_client.c | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 856edd157..61d2255da 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -914,12 +914,11 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, client->sn_wait_packet_type = SN_MSG_TYPE_RESERVED; client->sn_wait_packet_id = 0; MqttReadStop(client, mms_stat); - if (rc == MQTT_CODE_CONTINUE) { - /* CONTINUE from an application callback is a terminal callback result, - * not an internal automatic-response retry. Consume the packet so a - * later wait cannot replay the callback. */ - mms_stat->read = MQTT_MSG_BEGIN; - } + /* The packet is finished: handled, failed, or (for CONTINUE from an + * application callback, a terminal callback result rather than an + * automatic-response retry) consumed. Rewind so the next wait on this + * object reads a new packet instead of handling this one again. */ + mms_stat->read = MQTT_MSG_BEGIN; #ifdef WOLFMQTT_NONBLOCK #ifdef WOLFMQTT_DEBUG_CLIENT diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 276785749..41b563b31 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -1386,6 +1386,61 @@ TEST(sn_publish_incoming_null_msg_cb_errors_no_ack) ASSERT_EQ(0, g_mock.write_calls); } +static int g_sn_wait_deliveries; +static byte g_sn_wait_last_byte; + +static int sn_record_delivery(MqttClient* client, MqttMessage* msg, + byte msg_new, byte msg_done) +{ + (void)client; + (void)msg_new; + (void)msg_done; + g_sn_wait_deliveries++; + if (msg->buffer != NULL && msg->buffer_len > 0) { + g_sn_wait_last_byte = msg->buffer[0]; + } + return MQTT_CODE_SUCCESS; +} + +/* SN_Client_WaitMessage waits on the client's shared msgSN object. Once a + * packet has been handled the next call must read a new one, not handle the + * previous packet again from its leftover payload state. Two QoS 0 PUBLISH + * frames (MQTT-SN 1.2 section 5.4.12: Length, MsgType, Flags, TopicId, + * MsgId, Data) with different data, waited for back to back. */ +TEST(sn_wait_message_reads_each_packet_once) +{ + static const byte pub1[] = { 0x08, SN_MSG_TYPE_PUBLISH, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x11 }; + static const byte pub2[] = { 0x08, SN_MSG_TYPE_PUBLISH, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x22 }; + int rc; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_record_delivery; + g_sn_wait_deliveries = 0; + g_sn_wait_last_byte = 0; + mock_net_push(&g_mock, pub1, (int)sizeof(pub1)); + mock_net_push(&g_mock, pub2, (int)sizeof(pub2)); + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(1, g_sn_wait_deliveries); + ASSERT_EQ(0x11, g_sn_wait_last_byte); + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(2, g_mock.in_idx); + ASSERT_EQ(2, g_sn_wait_deliveries); + ASSERT_EQ(0x22, g_sn_wait_last_byte); +} + #if WOLFMQTT_MAX_QOS >= 2 static int g_sn_qos2_deliveries; @@ -3510,6 +3565,7 @@ int main(int argc, char** argv) #endif RUN_TEST(sn_unsubscribe_no_continue); RUN_TEST(sn_publish_incoming_null_msg_cb_errors_no_ack); + RUN_TEST(sn_wait_message_reads_each_packet_once); #if WOLFMQTT_MAX_QOS >= 2 RUN_TEST(sn_qos2_retransmit_not_delivered_twice); #endif From 8097a45dfa5fc916fb5737fe62da0c583bd5ff6c Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:06:24 -0500 Subject: [PATCH 46/52] F-13249 - Drop the msgSN reset workaround from the SN QoS 2 test --- tests/test_mqtt_sn_client.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 41b563b31..95cbdba39 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -1496,15 +1496,9 @@ TEST(sn_qos2_retransmit_not_delivered_twice) mock_net_push(&g_mock, rel, (int)sizeof(rel)); mock_net_push(&g_mock, pub2, (int)sizeof(pub2)); - /* Each frame is waited for on a fresh wait object, as in - * sn_will_response_reset_clears_return_code: a completed wait leaves - * client->msgSN at its payload state. */ for (i = 0; i < 40 && g_mock.in_idx < g_mock.in_count; i++) { rc = SN_Client_WaitMessage(&g_client, 1000); - if (rc == MQTT_CODE_SUCCESS) { - XMEMSET(&g_client.msgSN, 0, sizeof(g_client.msgSN)); - } - else if (rc != MQTT_CODE_CONTINUE) { + if (rc != MQTT_CODE_SUCCESS && rc != MQTT_CODE_CONTINUE) { FAIL("SN_Client_WaitMessage failed"); } } From b21f2c0ed57a3f825be57532a310f481bfe7061f Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:18:00 -0500 Subject: [PATCH 47/52] F-14584 - Fail a blocking wait that times out partway through a packet --- src/mqtt_client.c | 14 ++++++ tests/test_mqtt_client.c | 94 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 0c61ff591..bc7113c9e 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2270,6 +2270,20 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, /* advance state, since we received some data */ mms_stat->read = MQTT_MSG_HEADER; } + #else + /* A blocking read cannot resume a packet: the partial state + * is discarded below, but its bytes have already left the + * transport, so the next read would parse the rest of this + * packet as a new one. A timeout after part of a packet + * arrived therefore ends the connection rather than reading + * as an idle wait. */ + if (rc == MQTT_CODE_ERROR_TIMEOUT && + (client->packet.stat > MQTT_PK_BEGIN || + client->read.pos > 0)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_NETWORK); + (void)MqttClient_Flags(client, + MQTT_CLIENT_FLAG_IS_CONNECTED, 0); + } #endif break; } diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index eb72637f4..77aa3532b 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -7230,6 +7230,96 @@ TEST(wait_message_timeout_preserves_partial_vbi) } #endif /* WOLFMQTT_NONBLOCK */ +#ifndef WOLFMQTT_NONBLOCK +/* Blocking-build read mock: hands out g_timeout_frame up to g_timeout_after + * bytes in total, then reports MQTT_CODE_ERROR_TIMEOUT. */ +static const byte* g_timeout_frame; +static int g_timeout_after; +static int g_timeout_pos; + +static int mock_net_read_then_timeout(void *context, byte* buf, int buf_len, + int timeout_ms) +{ + int n; + + (void)context; + (void)timeout_ms; + n = g_timeout_after - g_timeout_pos; + if (n <= 0) { + return MQTT_CODE_ERROR_TIMEOUT; + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_timeout_frame + g_timeout_pos, (size_t)n); + g_timeout_pos += n; + return n; +} + +static int run_wait_timeout_after(const byte* frame, int after) +{ + int rc; + + rc = test_init_client(); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = test_accept_message_cb; + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + test_net.read = mock_net_read_then_timeout; + g_timeout_frame = frame; + g_timeout_after = after; + g_timeout_pos = 0; + g_msg_cb_calls = 0; + return MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); +} + +/* v3.1.1 QoS 0 PUBLISH: Remaining Length 5, topic "ab", payload "x". */ +static const byte g_timeout_publish[] = { + 0x30, 0x05, 0x00, 0x02, 'a', 'b', 'x' +}; + +/* A blocking read cannot resume a packet: once it times out after consuming + * part of one, the rest of that packet would be read as a new packet. The + * wait must fail the connection, not report an ordinary idle timeout. Here + * one byte of the fixed header arrives before the timeout. */ +TEST(wait_message_blocking_timeout_mid_header_fails_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 1); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(0, g_msg_cb_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* The same once the fixed header is complete and part of the body is in. */ +TEST(wait_message_blocking_timeout_mid_body_fails_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 4); + + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, rc); + ASSERT_EQ(0, g_msg_cb_calls); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} + +/* A timeout before any byte of a packet arrives is an ordinary idle wait and + * leaves the connection up. */ +TEST(wait_message_blocking_idle_timeout_keeps_connection) +{ + int rc = run_wait_timeout_after(g_timeout_publish, 0); + + ASSERT_EQ(MQTT_CODE_ERROR_TIMEOUT, rc); + ASSERT_NE(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED)); +} +#endif /* !WOLFMQTT_NONBLOCK */ + #ifdef WOLFMQTT_V5 /* The client advertises Topic Alias Maximum 0 and keeps no inbound alias table, * so an empty Topic Name paired with a Topic Alias is a non-conforming server @@ -8779,6 +8869,10 @@ void run_mqtt_client_tests(void) #endif #ifdef WOLFMQTT_NONBLOCK RUN_TEST(wait_message_timeout_preserves_partial_vbi); +#else + RUN_TEST(wait_message_blocking_timeout_mid_header_fails_connection); + RUN_TEST(wait_message_blocking_timeout_mid_body_fails_connection); + RUN_TEST(wait_message_blocking_idle_timeout_keeps_connection); #endif RUN_TEST(wait_message_qos0_publish_no_ack); RUN_TEST(wait_message_qos1_publish_acks_puback); From 96f3a8ff722ae67f512ac9a6cd16e1ad56c0903a Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:24:18 -0500 Subject: [PATCH 48/52] F-14610 - Hold back other packets until an enhanced-auth CONNACK --- src/mqtt_client.c | 49 ++++++++++++++-- src/mqtt_socket.c | 6 +- tests/test_mqtt_client.c | 123 +++++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_client.h | 8 ++- 4 files changed, 179 insertions(+), 7 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index bc7113c9e..6ca493f53 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -1772,6 +1772,10 @@ static int MqttClient_HandlePacket(MqttClient* client, rc = MqttClient_DecodePacket(client, client->rx_buf, client->packet.buf_len, packet_obj, &packet_type, &packet_qos, &packet_id, 1); + #ifdef WOLFMQTT_V5 + /* The CONNACK ends the [MQTT-3.1.2-30] authentication window. */ + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, 0); + #endif break; } case MQTT_PACKET_TYPE_PUBLISH: @@ -3205,6 +3209,30 @@ static int MqttClient_CheckConnectSent(MqttClient *client) return MQTT_CODE_SUCCESS; } +/* Gate for PUBLISH, SUBSCRIBE, UNSUBSCRIBE and PINGREQ: CONNECT must have been + * sent, and [MQTT-3.1.2-30] forbids them while a CONNECT with an + * Authentication Method still awaits its CONNACK. DISCONNECT and AUTH stay + * allowed in that window, so they use MqttClient_CheckConnectSent alone. */ +static int MqttClient_CheckSendAllowed(MqttClient *client) +{ + int rc; +#ifdef WOLFMQTT_V5 + word32 flags = 0; +#endif + + rc = MqttClient_CheckConnectSent(client); +#ifdef WOLFMQTT_V5 + if (rc == MQTT_CODE_SUCCESS) { + rc = MqttClient_ReadFlags(client, &flags); + } + if (rc == MQTT_CODE_SUCCESS && + (flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } +#endif + return rc; +} + #ifndef WOLFMQTT_NO_TIME /* Apply the auto keep-alive decision once a connect attempt has reached a * final result. Shared by the normal handshake tail and by the @@ -3503,6 +3531,14 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) * out. Resuming this same write re-enters past MQTT_MSG_BEGIN, so * the once-per-connection guard does not see it. */ (void)MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_CONNECT_SENT); + #ifdef WOLFMQTT_V5 + /* [MQTT-3.1.2-30] With an Authentication Method, only AUTH or + * DISCONNECT may follow until the CONNACK. */ + if (client->auth_method_set) { + (void)MqttClient_Flags(client, 0, + MQTT_CLIENT_FLAG_AUTH_PENDING); + } + #endif #ifndef WOLFMQTT_NO_SESSION_REPLAY if (MqttClient_ConnectIsPersistent(mc_connect)) { (void)MqttClient_Flags(client, 0, @@ -4130,7 +4166,7 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4577,7 +4613,7 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4737,7 +4773,7 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -4926,7 +4962,7 @@ int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping) } /* [MQTT-3.1.0-1] CONNECT must be the first packet on the connection. */ - rc = MqttClient_CheckConnectSent(client); + rc = MqttClient_CheckSendAllowed(client); if (rc != MQTT_CODE_SUCCESS) { return rc; } @@ -5392,6 +5428,11 @@ static int MqttClient_KeepAlive(MqttClient *client, MqttObject* msg) if ((flags & MQTT_CLIENT_FLAG_NO_AUTO_KEEPALIVE) != 0) { return MQTT_CODE_SUCCESS; } + /* No PINGREQ while an authentication exchange awaits its CONNACK + * [MQTT-3.1.2-30]. */ + if ((flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { + return MQTT_CODE_SUCCESS; + } /* Disabled until a non-zero keep-alive has been negotiated in CONNECT. */ if (client->keep_alive_sec == 0) { diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index 2f028f941..cb6fd699a 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -521,7 +521,8 @@ int MqttSocket_Connect(MqttClient *client, const char* host, word16 port, * MqttClient_NetDisconnect. */ MqttClient_Flags(client, (MQTT_CLIENT_FLAG_CONNECT_SENT | - MQTT_CLIENT_FLAG_DISCONNECT_SENT), + MQTT_CLIENT_FLAG_DISCONNECT_SENT | + MQTT_CLIENT_FLAG_AUTH_PENDING), MQTT_CLIENT_FLAG_IS_CONNECTED); } @@ -736,7 +737,8 @@ int MqttSocket_Disconnect(MqttClient *client) * the next one must start with its own CONNECT [MQTT-3.1.0-1]. */ MqttClient_Flags(client, (MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT | - MQTT_CLIENT_FLAG_DISCONNECT_SENT), 0); + MQTT_CLIENT_FLAG_DISCONNECT_SENT | + MQTT_CLIENT_FLAG_AUTH_PENDING), 0); #ifdef ENABLE_MQTT_CURL if (client->curl_initialized) { diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 77aa3532b..ab8418852 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -1281,6 +1281,125 @@ TEST(auth_with_connect_method_allowed) MqttClient_PropsFree(auth.props); } +#ifdef WOLFMQTT_NONBLOCK +/* Start a v5 CONNECT that carries an Authentication Method and return once it + * is on the wire but no CONNACK has arrived (the canned reader has nothing to + * deliver yet). */ +static int start_connect_v5_auth_pending(MqttConnect* connect, + MqttProp* method_prop) +{ + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + g_canned_len = 0; + g_canned_pos = 0; + + XMEMSET(connect, 0, sizeof(*connect)); + connect->keep_alive_sec = 60; + connect->clean_session = 1; + connect->client_id = "test_client"; + XMEMSET(method_prop, 0, sizeof(*method_prop)); + method_prop->type = MQTT_PROP_AUTH_METHOD; + method_prop->data_str.str = (char*)"SCRAM-SHA-256"; + method_prop->data_str.len = (word16)XSTRLEN("SCRAM-SHA-256"); + connect->props = method_prop; + + return MqttClient_Connect(&test_client, connect); +} + +/* [MQTT-3.1.2-30] After a CONNECT with an Authentication Method the Client + * MUST NOT send any packet other than AUTH or DISCONNECT until it receives + * CONNACK. A nonblocking MqttClient_Connect returns to the caller in that + * window, so PUBLISH, SUBSCRIBE and PINGREQ must be refused there, and + * allowed again once the CONNACK has been processed. */ +TEST(connect_auth_pending_blocks_other_packets) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + MqttPublish publish; + MqttSubscribe subscribe; + MqttTopic topic; + MqttPing ping; + /* v5 CONNACK: accepted, Session Present = 0, no properties. */ + static const byte connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + ASSERT_NE(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_CONNECT_SENT)); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MqttClient_Publish(&test_client, &publish); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(&topic, 0, sizeof(topic)); + topic.topic_filter = "t"; + subscribe.packet_id = 1; + subscribe.topic_count = 1; + subscribe.topics = &topic; + rc = MqttClient_Subscribe(&test_client, &subscribe); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + XMEMSET(&ping, 0, sizeof(ping)); + rc = MqttClient_Ping_ex(&test_client, &ping); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + ASSERT_EQ(0, g_frames_written); + + /* The CONNACK arrives: the handshake completes and PUBLISH is allowed. */ + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} + +/* DISCONNECT stays allowed while the authentication exchange is pending + * [MQTT-3.1.2-30]. */ +TEST(connect_auth_pending_allows_disconnect) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + g_frames_written = 0; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Disconnect(&test_client); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} +#endif /* WOLFMQTT_NONBLOCK */ + /* MQTT 3.1.1 section 2.2.1 lists packet type 15 as Reserved/Forbidden; AUTH * exists only from v5. A session negotiated at protocol level 4 must treat a * received type-15 packet as a protocol violation [MQTT-4.8.0-1] even in a @@ -8643,6 +8762,10 @@ void run_mqtt_client_tests(void) RUN_TEST(connect_v5_scrubs_connack_auth_data_from_rx_buf); RUN_TEST(auth_without_connect_method_rejected); RUN_TEST(auth_with_connect_method_allowed); +#ifdef WOLFMQTT_NONBLOCK + RUN_TEST(connect_auth_pending_blocks_other_packets); + RUN_TEST(connect_auth_pending_allows_disconnect); +#endif RUN_TEST(wait_message_v311_rejects_auth_packet_type); RUN_TEST(wait_message_v5_accepts_auth_packet_type); RUN_TEST(wait_message_v311_rejects_disconnect_packet_type); diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index dacb13295..96776aae0 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -197,7 +197,13 @@ enum MqttClientFlags { * section 3.2.2.3.2). Managed by the library - set or cleared by * MqttClient_Connect and updated from the CONNACK. Applications must * not set or clear it through MqttClient_Flags. */ - MQTT_CLIENT_FLAG_PERSISTENT_SESSION = 0x01 << 10 + MQTT_CLIENT_FLAG_PERSISTENT_SESSION = 0x01 << 10, + /* A v5 CONNECT carrying an Authentication Method has been written and no + * CONNACK has arrived yet. [MQTT-3.1.2-30] allows only AUTH or DISCONNECT + * in that window, so the other send APIs refuse. Managed by the library - + * set by MqttClient_Connect, cleared when the CONNACK is processed and by + * MqttSocket_Connect and MqttSocket_Disconnect. */ + MQTT_CLIENT_FLAG_AUTH_PENDING = 0x01 << 11 }; /*! \brief Sets flags in the MqttClient structure. To be used from the application before calling MqttClient_NetConnect. From a1f0df5a41dbf31a1c47b32d75e8a3566fda5c74 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:33:29 -0500 Subject: [PATCH 49/52] F-16041 - Refuse new persistent sessions at the dynamic broker cap --- BROKER.md | 2 +- src/mqtt_broker.c | 51 ++++++++++++++++++++- tests/test_broker_connect.c | 89 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 9 ++-- 4 files changed, 145 insertions(+), 6 deletions(-) diff --git a/BROKER.md b/BROKER.md index d9de1f972..03f3b5032 100644 --- a/BROKER.md +++ b/BROKER.md @@ -177,7 +177,7 @@ serves them from memory while it runs. Use a custom backend through | Macro | Default | Description | |---|---|---| -| `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions retained across restarts | +| `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions (connected and offline); a new persistent CONNECT beyond it is refused, existing sessions are never evicted | | `BROKER_MAX_OFFLINE_MSGS_PER_SUB` | 32 | Offline queue depth per session | | `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 5 | On-disk record schema version | diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 0077ede6e..1e88ebc55 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3648,6 +3648,32 @@ static BrokerOrphanSession* BrokerOrphan_Find(MqttBroker* broker, return NULL; } +/* Persistent Sessions held by ClientIds other than client_id: saved offline + * Sessions plus established connections whose Session outlives them. A + * Session client_id already holds is excluded, since resuming or taking it + * over does not add one. */ +static int BrokerSession_CountOthers(MqttBroker* broker, + const char* client_id) +{ + BrokerOrphanSession* o; + BrokerClient* c; + int count = 0; + + for (o = broker->orphan_sessions; o != NULL; o = o->next) { + if (o->client_id != NULL && XSTRCMP(o->client_id, client_id) != 0) { + count++; + } + } + for (c = broker->clients; c != NULL; c = c->next) { + if (c->session_established && c->session_expiry_sec != 0 && + BROKER_STR_VALID(c->client_id) && + XSTRCMP(c->client_id, client_id) != 0) { + count++; + } + } + return count; +} + /* Free everything an orphan owns (queue entries + client_id) but do * NOT unlink from broker->orphan_sessions; the caller does that. */ static void BrokerOrphan_FreeContents(BrokerOrphanSession* o) @@ -3880,7 +3906,8 @@ static BrokerOrphanSession* BrokerOrphan_Take(MqttBroker* broker, #endif } - /* Cap check; evict oldest if at limit. */ + /* Backstop only: BrokerHandle_Connect refuses a new persistent Session at + * BROKER_MAX_PERSIST_SESSIONS, so the cap is not normally reached here. */ while (broker->orphan_session_count >= BROKER_MAX_PERSIST_SESSIONS) { if (!BrokerOrphan_EvictOldest(broker)) { break; @@ -7394,6 +7421,28 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, orphan = BrokerStaticOrphan_Find(broker, bc->client_id); had_static_session = (orphan != NULL); } +#else + /* Bound persistent Sessions the same way: at BROKER_MAX_PERSIST_SESSIONS + * a new one is refused here, rather than admitted and later made room for + * by evicting another client's saved Session when this one goes offline. + * Resuming or taking over this ClientId's own Session is not counted. */ + if (bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id) && + BrokerSession_CountOthers(broker, bc->client_id) >= + BROKER_MAX_PERSIST_SESSIONS) { + WBLOG_ERR(broker, + "broker: persistent-session quota exceeded sock=%d", + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_QUOTA_EXCEEDED; + } + else + #endif + { + ack.return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL; + } + goto send_connack; + } #endif bc->session_established = 1; diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index f35cd89e0..b108f307a 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -2162,6 +2162,92 @@ TEST(connect_refused_deferred_connack_scrubs_credentials) #endif /* WOLFMQTT_NONBLOCK */ #endif /* WOLFMQTT_BROKER_AUTH */ +#ifndef WOLFMQTT_STATIC_MEMORY +/* One v3.1.1 CONNECT with CleanSession 0 for ClientId "c" (MQTT 3.1.1 + * section 3.1), optionally followed by DISCONNECT, through mock client 0. + * Returns the CONNACK return code, or -1 if none was written. */ +static int session_cap_connect(MqttBroker* broker, int n, int disconnect) +{ + static const byte disc[] = { 0xE0, 0x00 }; + byte connect[] = { + 0x10, 16, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, /* level 4, CleanSession 0 */ + 0x00, 0x04, 'c', '0', '0', '0' + }; + int i; + + connect[15] = (byte)('0' + (n / 100) % 10); + connect[16] = (byte)('0' + (n / 10) % 10); + connect[17] = (byte)('0' + n % 10); + reset_mock_state(connect, sizeof(connect)); + if (disconnect) { + mock_client_input_append(0, disc, sizeof(disc)); + } + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(broker); + if (g_client_closed) { + break; + } + } + if (g_out_len < 4 || g_out_buf[0] != 0x20) { + return -1; + } + return g_out_buf[3]; +} + +static int session_cap_has_orphan(MqttBroker* broker, const char* client_id) +{ + BrokerOrphanSession* o; + + for (o = broker->orphan_sessions; o != NULL; o = o->next) { + if (o->client_id != NULL && XSTRCMP(o->client_id, client_id) == 0) { + return 1; + } + } + return 0; +} + +/* With BROKER_MAX_PERSIST_SESSIONS persistent Sessions held, a CONNECT asking + * for another one must be refused (CONNACK 0x03 Server unavailable, MQTT + * 3.1.1 section 3.2.2.3) rather than accepted and later admitted by evicting + * another client's saved Session. A client resuming its own Session does not + * add one and is still accepted. */ +TEST(connect_persistent_session_cap_refuses_new_session) +{ + MqttBroker broker; + MqttBrokerNet net; + int n; + int rc; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + for (n = 0; n < BROKER_MAX_PERSIST_SESSIONS; n++) { + rc = session_cap_connect(&broker, n, 1); + if (rc != MQTT_CONNECT_ACK_CODE_ACCEPTED) { + FAIL("persistent session below the cap was not accepted"); + } + } + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS, broker.orphan_session_count); + + /* A new ClientId would be one Session too many. */ + rc = session_cap_connect(&broker, BROKER_MAX_PERSIST_SESSIONS, 1); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, rc); + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS, broker.orphan_session_count); + ASSERT_TRUE(session_cap_has_orphan(&broker, "c000")); + + /* The oldest client resumes its own Session at the cap. */ + rc = session_cap_connect(&broker, 0, 0); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, rc); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + #ifdef WOLFMQTT_V5 /* Return the Reason Code byte of the first DISCONNECT packet in a captured * stream, or -1 if none carries a reason code. A v5 DISCONNECT with a @@ -14301,6 +14387,9 @@ int main(int argc, char** argv) RUN_TEST(connect_unsupported_level_6_refused); RUN_TEST(connect_unsupported_level_127_refused); RUN_TEST(connect_unsupported_level_undecodable_refused); +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_persistent_session_cap_refuses_new_session); +#endif #ifndef WOLFMQTT_V5 RUN_TEST(connect_v5_to_non_v5_broker_refused); #endif diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index a94238690..04bab4af6 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -866,10 +866,11 @@ typedef struct MqttBroker { #ifndef WOLFMQTT_STATIC_MEMORY /* Linked list of disconnected persistent sessions. Each entry holds * its own outbound queue + identity so messages published while the - * owning client is offline are retained until reconnect (or - * BROKER_MAX_PERSIST_SESSIONS forces drop-oldest eviction). Subs - * pointing at orphaned sessions keep sub->client=NULL; fan-out - * branches on that to look up the orphan by client_id. */ + * owning client is offline are retained until reconnect. CONNECT refuses + * a new persistent Session once BROKER_MAX_PERSIST_SESSIONS are held, so + * existing ones are not evicted to make room. Subs pointing at orphaned + * sessions keep sub->client=NULL; fan-out branches on that to look up + * the orphan by client_id. */ BrokerOrphanSession* orphan_sessions; int orphan_session_count; #endif From 774e19fd8259798c36d1e10b98741b5bb3f32ea7 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 20 Oct 2026 11:40:20 -0500 Subject: [PATCH 50/52] F-13806 - Add MqttClient_NetDisconnect_Wait and document the race --- .github/workflows/broker-check.yml | 4 + BROKER.md | 18 + README.md | 1 + src/mqtt_broker.c | 78 +- src/mqtt_client.c | 72 +- src/mqtt_packet.c | 8 +- tests/test_broker_connect.c | 1448 +++++++++++++++++++++------- tests/test_broker_log_config.h | 7 + tests/test_mqtt_client.c | 469 +++++++-- tests/test_mqtt_props_init.c | 142 ++- tests/test_mqtt_sn_client.c | 77 ++ wolfmqtt/mqtt_broker.h | 9 + wolfmqtt/mqtt_client.h | 65 +- wolfmqtt/mqtt_packet.h | 16 + 14 files changed, 1902 insertions(+), 512 deletions(-) diff --git a/.github/workflows/broker-check.yml b/.github/workflows/broker-check.yml index 1c653ec54..8806de06f 100644 --- a/.github/workflows/broker-check.yml +++ b/.github/workflows/broker-check.yml @@ -115,6 +115,10 @@ jobs: cflags: "" wolfmqtt_opts: "--enable-broker --enable-v5 --enable-broker-persist --disable-broker-retained" check_units: "yes" + - name: "Broker with persist + nonblock" + cflags: "" + wolfmqtt_opts: "--enable-broker --enable-v5 --enable-broker-persist --enable-nonblock" + check_units: "yes" steps: - name: Install dependencies diff --git a/BROKER.md b/BROKER.md index 03f3b5032..3d8bdb088 100644 --- a/BROKER.md +++ b/BROKER.md @@ -175,12 +175,30 @@ message whose topic is longer than 123 bytes is not kept. The broker still serves them from memory while it runs. Use a custom backend through `MqttBroker_SetPersistHooks()` if longer identifiers must persist. +A custom backend must follow the same contract as the POSIX one. `kv_get` +treats `*inout_len` as the buffer capacity on input and sets the record length +on success, returns `MQTT_CODE_ERROR_NOT_FOUND` for a missing key, and returns +`MQTT_CODE_ERROR_OUT_OF_BUFFER` - never a truncated success - for a record +larger than the buffer. Every key `kv_iter` hands to its callback must be +accepted unchanged by `kv_get` and `kv_del`; the broker deletes records it +finds while iterating and repeats until none match. + | Macro | Default | Description | |---|---|---| | `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions (connected and offline); a new persistent CONNECT beyond it is refused, existing sessions are never evicted | | `BROKER_MAX_OFFLINE_MSGS_PER_SUB` | 32 | Offline queue depth per session | | `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 5 | On-disk record schema version | +Because existing persistent sessions are never evicted, any client allowed to +connect can use up `BROKER_MAX_PERSIST_SESSIONS` by opening persistent sessions +under fresh client IDs (a v3.1.1 CleanSession 0 session never expires); after +that, persistent CONNECTs with new client IDs are refused until sessions end, +and with persistence enabled the sessions survive a restart. Clients that +resume their own session are unaffected. Configure broker credentials +(`-u`/`-P`), size the cap for the deployment, and prefer bounded v5 Session +Expiry Intervals where this matters. The previous drop-oldest +policy let the same client destroy other clients' saved sessions instead. + ### Encryption at rest Add `--enable-broker-persist-encrypt` (requires `--enable-broker-persist`) to wrap persisted records with wolfCrypt AES-GCM. The key is provided by a `derive_key` callback that real deployments install via `MqttBroker_SetPersistHooks()` before starting the broker. diff --git a/README.md b/README.md index 4b6a71102..a15fd589c 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ These are the network connect / disconnect interfaces that wrap the MqttNet call * `int MqttClient_NetConnect(MqttClient *client, const char* host, word16 port, int timeout_ms, int use_tls, MqttTlsCb cb);` * `int MqttClient_NetDisconnect(MqttClient *client);` +* `int MqttClient_NetDisconnect_Wait(MqttClient *client);` - like `MqttClient_NetDisconnect`, but in `WOLFMQTT_MULTITHREAD` builds waits for other threads' network I/O on the client to finish first. Do not call it from a message callback. Helper functions: diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 1e88ebc55..387eed23a 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2135,16 +2135,6 @@ static void BrokerClient_FreeOutQueue(BrokerClient* bc) bc->out_q_pending_len = 0; } -/* Send as many QUEUED entries from out_q as the inflight cap allows. - * - * Ordering: walks from out_q_head, never reorders. Already-sent entries - * (state != QUEUED) are stepped over - their PUBLISH already hit the - * wire in publish order. The cap stops the drain at the first QUEUED - * QoS>0 entry that would exceed BROKER_MAX_INFLIGHT_PER_SUB (or the v5 - * client's Receive Maximum, whichever is smaller). [MQTT-4.6.0-3] is - * preserved: even a QUEUED QoS 0 behind a capped QoS>0 stays put. Returns the - * number of completed writes, MQTT_CODE_CONTINUE for an in-progress write, - * or a negative error for a terminal write failure. */ /* Unlink cur (preceded by prev) from bc's out_q, drop any persisted copy and * free it. Returns the entry that followed cur. */ static BrokerOutPub* BrokerClient_OutQRemove(BrokerClient* bc, @@ -2175,6 +2165,16 @@ static BrokerOutPub* BrokerClient_OutQRemove(BrokerClient* bc, return next; } +/* Send as many QUEUED entries from out_q as the inflight cap allows. + * + * Ordering: walks from out_q_head, never reorders. Already-sent entries + * (state != QUEUED) are stepped over - their PUBLISH already hit the + * wire in publish order. The cap stops the drain at the first QUEUED + * QoS>0 entry that would exceed BROKER_MAX_INFLIGHT_PER_SUB (or the v5 + * client's Receive Maximum, whichever is smaller). [MQTT-4.6.0-3] is + * preserved: even a QUEUED QoS 0 behind a capped QoS>0 stays put. Returns the + * number of completed writes, MQTT_CODE_CONTINUE for an in-progress write, + * or a negative error for a terminal write failure. */ static int BrokerClient_DrainOutQueue(BrokerClient* bc) { BrokerOutPub* cur; @@ -7473,12 +7473,12 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, BrokerClient_PublishWill(broker, old); } else { - BrokerSend_Disconnect(old, - MQTT_REASON_SESSION_TAKEN_OVER); #ifdef WOLFMQTT_BROKER_WILL /* MQTT 5.0 section 3.1.4: a resumed Session suppresses a * delayed Will [MQTT-3.1.3-9]; an ending Session or a zero - * Will Delay publishes it now. */ + * Will Delay publishes it now. Done before the DISCONNECT so + * a copy for the old connection itself (subscribed to its + * own Will topic) is not written after it [MQTT-3.14.4-1]. */ if (old->will_delay_sec > 0 && !mc.clean_session && old->session_expiry_sec != 0) { BrokerClient_ClearWill(old); @@ -7488,6 +7488,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, BrokerClient_PublishWill(broker, old); } #endif + BrokerSend_Disconnect(old, + MQTT_REASON_SESSION_TAKEN_OVER); } #else BrokerClient_PublishWill(broker, old); @@ -7764,8 +7766,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, * re-auth path. Scrub them all now so credentials reside in memory only * for the duration of CONNECT processing rather than the whole session. * Refused/failed CONNECTs return earlier and are wiped when the caller - * frees the client, or above if their CONNACK is deferred. Mirrors the client-side CLIENT_FORCE_ZERO hardening - * in mqtt_client.c. */ + * frees the client, or above if their CONNACK is deferred. Mirrors the + * client-side CLIENT_FORCE_ZERO hardening in mqtt_client.c. */ BrokerClient_ScrubConnect(bc, rx_len); return rc; @@ -7776,6 +7778,9 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, { int rc; int i; +#ifdef WOLFMQTT_BROKER_PERSIST + int subs_changed = 0; +#endif MqttSubscribe sub; MqttTopic topic_buf[MAX_MQTT_TOPICS]; byte return_codes[MAX_MQTT_TOPICS]; @@ -7785,6 +7790,7 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, sub.protocol_level = bc->protocol_level; #endif XMEMSET(topic_buf, 0, sizeof(topic_buf)); + XMEMSET(return_codes, 0, sizeof(return_codes)); sub.topics = topic_buf; WBLOG_INFO(broker, "broker: SUBSCRIBE recv sock=%d len=%d", (int)bc->sock, rx_len); @@ -7847,6 +7853,11 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, #endif { sub_rc = BrokerSubs_Add(broker, bc, f, flen, topic_qos); + #ifdef WOLFMQTT_BROKER_PERSIST + if (sub_rc >= 0) { + subs_changed = 1; + } + #endif } if (sub_rc < 0) { granted_qos = (MqttQoS)fail_code; @@ -7930,21 +7941,21 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, * avoid reading past the end of the return_codes array */ rc = BrokerSend_SubAck(bc, sub.packet_id, return_codes, i); +#ifdef WOLFMQTT_BROKER_RETAINED +subscribe_cleanup: +#endif #ifdef WOLFMQTT_BROKER_PERSIST /* Shadow-write the full subscription list for this client. Only * meaningful for clean_session=0 sessions; the persist layer no-ops - * when no hooks are installed. A deferred SUBACK still committed the - * subscriptions, so it is persisted too. */ - if ((rc > 0 || rc == MQTT_CODE_CONTINUE) && - bc->session_expiry_sec != 0 && + * when no hooks are installed. Keyed on the in-memory list having + * changed, not on the SUBACK write: a deferred or failed SUBACK, or a + * retained delivery that failed after some filters were added, leaves + * those subscriptions in the Session all the same. */ + if (subs_changed && bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } #endif - -#ifdef WOLFMQTT_BROKER_RETAINED -subscribe_cleanup: -#endif #ifdef WOLFMQTT_V5 if (sub.props) { (void)MqttProps_Free(sub.props); @@ -7958,6 +7969,9 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, { int rc; int i; +#ifdef WOLFMQTT_BROKER_PERSIST + int subs_changed = 0; +#endif MqttUnsubscribe unsub; MqttUnsubscribeAck ack; MqttTopic topic_buf[MAX_MQTT_TOPICS]; @@ -7988,16 +8002,19 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, #endif if (f && MqttDecode_Num((byte*)f - MQTT_DATA_LEN_SIZE, &flen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { -#ifdef WOLFMQTT_V5 if (BrokerSubs_Remove(broker, bc, f, flen)) { + #ifdef WOLFMQTT_V5 reasons[i] = MQTT_REASON_SUCCESS; + #endif + #ifdef WOLFMQTT_BROKER_PERSIST + subs_changed = 1; + #endif } + #ifdef WOLFMQTT_V5 else { reasons[i] = MQTT_REASON_NO_SUB_EXIST; } -#else - BrokerSubs_Remove(broker, bc, f, flen); -#endif + #endif } } @@ -8024,10 +8041,9 @@ static int BrokerHandle_Unsubscribe(BrokerClient* bc, int rx_len, } #ifdef WOLFMQTT_BROKER_PERSIST - /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs), including - * when the UNSUBACK write is deferred. */ - if ((rc > 0 || rc == MQTT_CODE_CONTINUE) && - bc->session_expiry_sec != 0 && + /* Re-snapshot subs (PutSubs converts count=0 into a DelSubs) whenever a + * subscription was removed, whatever the UNSUBACK write returned. */ + if (subs_changed && bc->session_expiry_sec != 0 && BROKER_STR_VALID(bc->client_id)) { (void)BrokerPersist_PutSubs(broker, bc->client_id); } diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 6ca493f53..c4f4694d9 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2795,6 +2795,14 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, } #endif + /* The matched packet is finished. A packet that needed no reply leaves + * the wait at its payload state; rewind it so the next wait on this + * object (MqttClient_WaitMessage always uses client->msg) reads a new + * packet instead of handling this one again. */ + if (mms_stat->read == MQTT_MSG_PAYLOAD || + mms_stat->read == MQTT_MSG_PAYLOAD2) { + mms_stat->read = MQTT_MSG_BEGIN; + } return rc; } @@ -2924,14 +2932,9 @@ void MqttClient_DeInit(MqttClient *client) } } #endif - /* The caller's buffers can still hold a serialized CONNECT or AUTH - * from an abandoned operation; the client no longer uses them. */ - if (client->tx_buf != NULL && client->tx_buf_len > 0) { - CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); - } - if (client->rx_buf != NULL && client->rx_buf_len > 0) { - CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); - } + /* tx_buf and rx_buf belong to the caller, who may already have freed + * them (the bundled examples do), so they are not touched here. + * MqttClient_NetDisconnect scrubs them while they are still in use. */ } } @@ -3215,22 +3218,20 @@ static int MqttClient_CheckConnectSent(MqttClient *client) * allowed in that window, so they use MqttClient_CheckConnectSent alone. */ static int MqttClient_CheckSendAllowed(MqttClient *client) { - int rc; -#ifdef WOLFMQTT_V5 word32 flags = 0; -#endif + int rc; - rc = MqttClient_CheckConnectSent(client); -#ifdef WOLFMQTT_V5 - if (rc == MQTT_CODE_SUCCESS) { - rc = MqttClient_ReadFlags(client, &flags); + /* One snapshot of the flags for every condition. */ + rc = MqttClient_ReadFlags(client, &flags); + if (rc != MQTT_CODE_SUCCESS) { + return rc; } - if (rc == MQTT_CODE_SUCCESS && + if ((flags & MQTT_CLIENT_FLAG_CONNECT_SENT) == 0 || + (flags & MQTT_CLIENT_FLAG_DISCONNECT_SENT) != 0 || (flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { - rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); } -#endif - return rc; + return MQTT_CODE_SUCCESS; } #ifndef WOLFMQTT_NO_TIME @@ -3507,6 +3508,16 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) } #endif + #ifdef WOLFMQTT_V5 + /* [MQTT-3.1.2-30] With an Authentication Method, only AUTH or + * DISCONNECT may follow until the CONNACK. Raised before any CONNECT + * byte can reach the peer, so neither another sender nor a reader + * that processes the CONNACK can see the window open late. */ + if (client->auth_method_set) { + (void)MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_AUTH_PENDING); + } + #endif + mc_connect->stat.write = MQTT_MSG_HEADER; } if (mc_connect->stat.write == MQTT_MSG_HEADER) { @@ -3531,14 +3542,6 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) * out. Resuming this same write re-enters past MQTT_MSG_BEGIN, so * the once-per-connection guard does not see it. */ (void)MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_CONNECT_SENT); - #ifdef WOLFMQTT_V5 - /* [MQTT-3.1.2-30] With an Authentication Method, only AUTH or - * DISCONNECT may follow until the CONNACK. */ - if (client->auth_method_set) { - (void)MqttClient_Flags(client, 0, - MQTT_CLIENT_FLAG_AUTH_PENDING); - } - #endif #ifndef WOLFMQTT_NO_SESSION_REPLAY if (MqttClient_ConnectIsPersistent(mc_connect)) { (void)MqttClient_Flags(client, 0, @@ -3572,6 +3575,13 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) /* The handshake state set above stands or not on whether bytes * reached the peer, so a retry is refused only when some of this * CONNECT is already out there. */ + #ifdef WOLFMQTT_V5 + if (wrote == 0) { + /* No CONNECT reached the peer, so no CONNACK is coming. */ + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, + 0); + } + #endif MqttClient_CancelMessage(client, (MqttObject*)mc_connect); return rc; } @@ -4924,11 +4934,15 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) return rc; } -static int MqttClient_PingTimeoutDisconnect(MqttClient *client) +/* Also used when a keep-alive ping times out, see MqttClient_Ping_ex. */ +int MqttClient_NetDisconnect_Wait(MqttClient *client) { #ifdef WOLFMQTT_MULTITHREAD int rc; + if (client == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } /* A message callback can hold lockRecv while starting a publish, so take * the I/O locks in that same direction. Once both are held, no read or * write callback can still reference socket, TLS, or curl state while the @@ -5051,7 +5065,7 @@ int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping) * direct Ping callers while the automatic keep-alive path maps it to a * network error. */ if (rc == MQTT_CODE_ERROR_TIMEOUT) { - (void)MqttClient_PingTimeoutDisconnect(client); + (void)MqttClient_NetDisconnect_Wait(client); } return rc; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 48fb7f97d..d79e49598 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -1859,11 +1859,9 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect) * Corner case: a peer claiming level 6 but sending a v5-shape wire * (extra properties-length VBI present) will misparse on the v3.1.1 * path and the strict tail-consumption check below returns - * MALFORMED_DATA, which the broker translates to a silent socket - * close - CONNACK 0x01 is emitted only when the v3.1.1-shape decode - * succeeds. This is a best-effort spec compliance trade-off; clients - * that misrepresent their protocol level should not expect the broker - * to reverse-engineer the wire shape. */ + * MALFORMED_DATA. protocol_level is already recorded at that point, so + * the broker still answers any unsupported level with CONNACK 0x01 + * [MQTT-3.1.2-2], whether or not the body decodes. */ if (mc_connect->protocol_level == MQTT_CONNECT_PROTOCOL_LEVEL_5) { /* Decode Length of Properties */ if (rx_buf_len < (rx_payload - rx_buf)) { diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index b108f307a..9ada370ae 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -71,6 +71,7 @@ static void* g_capture_alloc_ptr; static byte g_capture_freed[64]; static size_t g_capture_freed_len; static int g_capture_free_seen; +static size_t g_alloc_fail_size; /* fail the next malloc of exactly this size */ void* wolfmqtt_test_broker_malloc(size_t size) { @@ -81,6 +82,11 @@ void* wolfmqtt_test_broker_malloc(size_t size) g_alloc_failure_count++; return NULL; } + if (g_alloc_fail_size != 0 && size == g_alloc_fail_size) { + g_alloc_fail_size = 0; + g_alloc_failure_count++; + return NULL; + } if (g_alloc_fail_after > 0) { g_alloc_fail_after--; } @@ -2246,6 +2252,107 @@ TEST(connect_persistent_session_cap_refuses_new_session) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* Build a CleanSession 0 CONNECT for ClientId "c" into out: v3.1.1 + * (MQTT 3.1.1 section 3.1), or at level 5 a v5 CONNECT with Session Expiry + * Interval 60 (MQTT 5.0 section 3.1.2.11.2). Returns its length. */ +static size_t session_cap_build_connect(byte* out, int n, int v5) +{ + size_t len = 0; + byte rl = (byte)(v5 ? 22 : 16); + + out[len++] = 0x10; + out[len++] = rl; + out[len++] = 0x00; out[len++] = 0x04; + out[len++] = 'M'; out[len++] = 'Q'; out[len++] = 'T'; out[len++] = 'T'; + out[len++] = (byte)(v5 ? 0x05 : 0x04); + out[len++] = 0x00; /* CleanSession / Clean Start 0 */ + out[len++] = 0x00; out[len++] = 0x3C; /* keep alive 60 */ + if (v5) { + out[len++] = 0x05; /* Properties length */ + out[len++] = 0x11; /* Session Expiry Interval */ + out[len++] = 0x00; out[len++] = 0x00; + out[len++] = 0x00; out[len++] = 0x3C; + } + out[len++] = 0x00; out[len++] = 0x04; + out[len++] = 'c'; + out[len++] = (byte)('0' + (n / 100) % 10); + out[len++] = (byte)('0' + (n / 10) % 10); + out[len++] = (byte)('0' + n % 10); + return len; +} + +/* The cap counts connected persistent clients as well as offline Sessions. + * With BROKER_MAX_PERSIST_SESSIONS - 1 offline Sessions and one connected + * persistent client, a new ClientId is refused - with Quota Exceeded 0x97 + * for a v5 client (MQTT 5.0 section 3.2.2.2) - while a second connection + * taking over the connected client's own Session is still accepted. */ +TEST(connect_persistent_session_cap_counts_live_sessions) +{ + MqttBroker broker; + MqttBrokerNet net; + byte connect[32]; + size_t len; + int n; + int i; + int rc; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + for (n = 0; n < BROKER_MAX_PERSIST_SESSIONS - 1; n++) { + rc = session_cap_connect(&broker, n, 1); + if (rc != MQTT_CONNECT_ACK_CODE_ACCEPTED) { + FAIL("persistent session below the cap was not accepted"); + } + } + ASSERT_EQ(BROKER_MAX_PERSIST_SESSIONS - 1, broker.orphan_session_count); + + reset_mock_clients(3); + /* Client 0 takes the last slot and stays connected. */ + len = session_cap_build_connect(connect, 200, 0); + mock_client_input_append(0, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[0].out_buf[3]); + + /* Client 1, a new ClientId, is one Session too many. */ +#ifdef WOLFMQTT_V5 + len = session_cap_build_connect(connect, 201, 1); +#else + len = session_cap_build_connect(connect, 201, 0); +#endif + mock_client_input_append(1, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].out_len >= 4); +#ifdef WOLFMQTT_V5 + ASSERT_EQ(MQTT_REASON_QUOTA_EXCEEDED, g_clients[1].out_buf[3]); +#else + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, g_clients[1].out_buf[3]); +#endif + ASSERT_TRUE(g_clients[1].closed); + + /* Client 2 takes over client 0's Session ("c200") at the cap. */ + len = session_cap_build_connect(connect, 200, 0); + mock_client_input_append(2, connect, len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[2].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[2].out_buf[3]); + ASSERT_TRUE(g_clients[0].closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + #endif /* !WOLFMQTT_STATIC_MEMORY */ #ifdef WOLFMQTT_V5 @@ -2288,7 +2395,6 @@ static int first_disconnect_reason(const byte* buf, size_t len) return -1; } #endif /* WOLFMQTT_V5 */ -#endif /* !WOLFMQTT_STATIC_MEMORY */ /* [MQTT-4.3.3] / Method B: when the broker receives a duplicate QoS 2 * PUBLISH carrying a packet ID that's still awaiting PUBREL, it MUST send @@ -5825,6 +5931,111 @@ static int takeover_v5_wills(word32 session_expiry, word32 will_delay, return publishes; } +/* Number of packets in the capture after the first packet of type `after` + * that have type `target`, or -1 if no `after` packet is present. */ +static int count_packets_after(const byte* buf, size_t len, byte after, + byte target) +{ + size_t pos = 0; + int seen = 0; + int count = 0; + + while (pos < len) { + byte type = (byte)((buf[pos] >> 4) & 0x0F); + size_t remain = 0; + size_t mult = 1; + size_t hdr_len = 1; + int vbi_complete = 0; + + while (pos + hdr_len < len && hdr_len <= 5) { + byte b = buf[pos + hdr_len]; + remain += (size_t)(b & 0x7F) * mult; + hdr_len++; + if ((b & 0x80) == 0) { + vbi_complete = 1; + break; + } + mult *= 128; + } + if (!vbi_complete || remain > len - pos - hdr_len) { + break; + } + if (seen && type == target) { + count++; + } + if (type == after) { + seen = 1; + } + pos += hdr_len + remain; + } + return seen ? count : -1; +} + +/* [MQTT-3.14.4-1] Nothing may be sent on a connection after its DISCONNECT. + * On a v5 takeover the broker sends the old connection DISCONNECT 0x8E and + * publishes its Will; when the old client subscribes to its own Will topic, + * that copy must not reach it after the DISCONNECT. Will Delay 0 and Session + * Expiry 0, so the Will is published at the takeover. */ +TEST(takeover_v5_own_will_not_sent_after_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* v5 CONNECT for "P" with Will "lwt"/"bye", Session Expiry 0, Will + * Delay 0 (layout as in takeover_v5_wills). */ + static const byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE (MQTT 5.0 section 3.8): packet id 1, Properties length + * 0, filter "lwt", options 0 (QoS 0). Remaining Length 9. */ + static const byte old_subscribe[] = { + 0x82, 0x09, + 0x00, 0x01, + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT for "P", Clean Start 1, no Will, no properties. */ + static const byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[0].closed); + ASSERT_EQ(0, count_packets_after(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_DISCONNECT, + MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + /* MQTT 5.0 section 3.1.4: with a zero Will Delay the old Will is published * because its Network Connection is closed, even though the Session * survives the takeover. */ @@ -6000,109 +6211,254 @@ TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will) #endif /* !WOLFMQTT_DYN_PROP */ #endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ -#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) -/* Leave a v5 Session for "P" with Session Expiry 60, resume it with - * reconnect, and return the Session Expiry Interval the resumed client - * ends up with (0 on setup failure). */ -static word32 reclaim_session_expiry(const byte* reconnect, - size_t reconnect_len) +#ifdef WOLFMQTT_V5 +/* MQTT 5.0 section 4.13.1: a Server that detects a Malformed Packet or + * Protocol Error in a packet other than CONNECT SHOULD send DISCONNECT with + * the Reason Code before closing. Connect a v5 client, send one bad packet, + * and return the Reason Code of the DISCONNECT the broker sent (-1 if none); + * *closed reports whether the connection was closed. */ +static int v5_bad_packet_disconnect_reason(const byte* bad, size_t bad_len, + int* closed) { MqttBroker broker; MqttBrokerNet net; - BrokerClient* bc; - word32 expiry = 0; int i; - /* v5 CONNECT (MQTT 5.0 section 3.1): Clean Start 0, keep alive 60, - * Properties length 5 holding Session Expiry Interval 60 (0x11), - * ClientId "P". Remaining Length 19. */ - static const byte connect_60[] = { - 0x10, 0x13, + int reason; + /* v5 CONNECT, CleanStart, keep alive 60, no properties, ClientId "A". */ + static const byte connect[] = { + 0x10, 14, 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x05, 0x00, 0x00, 0x3C, - 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, - 0x00, 0x01, 'P' + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' }; - /* v5 DISCONNECT, Remaining Length 0: Reason Code 0x00 */ - static const byte disconnect[] = { 0xE0, 0x00 }; install_mock_net(&net); XMEMSET(&broker, 0, sizeof(broker)); if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { - return 0; - } - - reset_mock_clients(2); - mock_client_input_append(0, connect_60, sizeof(connect_60)); - mock_client_input_append(0, disconnect, sizeof(disconnect)); - for (i = 0; i < 16; i++) { - MqttBroker_Step(&broker); + *closed = 0; + return -2; } - if (broker.orphan_sessions != NULL && - broker.orphan_sessions->session_expiry_sec == 60) { - mock_client_input_append(1, reconnect, reconnect_len); - for (i = 0; i < 16; i++) { - MqttBroker_Step(&broker); - } - bc = find_broker_client(&broker, "P"); - if (bc != NULL && broker.orphan_sessions == NULL) { - expiry = bc->session_expiry_sec; - } + reset_mock_state(connect, sizeof(connect)); + mock_client_input_append(0, bad, bad_len); + for (i = 0; i < 8; i++) { + (void)MqttBroker_Step(&broker); } - + reason = first_disconnect_reason(g_out_buf, g_out_len); + *closed = g_client_closed; MqttBroker_Stop(&broker); MqttBroker_Free(&broker); - return expiry; + return reason; } -/* [MQTT-3.1.2.11.2] The resuming CONNECT's explicit 0xFFFFFFFF means the - * Session never expires; it must not be replaced by the old interval. */ -TEST(reclaim_keeps_explicit_v5_never_expire) +/* [MQTT-2.2.2-2] PUBACK fixed-header flags must be 0000: Malformed Packet. */ +TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect) { - /* As connect_60 in reclaim_session_expiry, with Session Expiry - * 0xFFFFFFFF */ - static const byte connect_never[] = { - 0x10, 0x13, - 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x05, 0x00, 0x00, 0x3C, - 0x05, 0x11, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x01, 'P' - }; + static const byte puback[] = { 0x41, 0x02, 0x00, 0x01 }; + int closed = 0; - ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_never, - sizeof(connect_never))); + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + v5_bad_packet_disconnect_reason(puback, sizeof(puback), &closed)); + ASSERT_TRUE(closed); } -/* A v3.1.1 CleanSession=0 Session never expires (MQTT 3.1.1 section - * 3.1.2.4), even when it resumes a Session created over v5. */ -TEST(reclaim_v311_resume_never_expires) +/* [MQTT-3.1.0-2] A second CONNECT is a Protocol Error. */ +TEST(v5_second_connect_sends_protocol_error_disconnect) { - static const byte connect_v311[] = { - 0x10, 0x0D, + static const byte connect[] = { + 0x10, 14, 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x04, 0x00, 0x00, 0x3C, - 0x00, 0x01, 'P' + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' }; + int closed = 0; - ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_v311, - sizeof(connect_v311))); + ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + v5_bad_packet_disconnect_reason(connect, sizeof(connect), &closed)); + ASSERT_TRUE(closed); } -#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ -#ifdef WOLFMQTT_V5 -/* Build a v3.1.1 QoS 0 PUBLISH to "x" that exactly fills the broker receive - * buffer (MQTT 3.1.1 section 3.3): fixed header byte, two byte Remaining - * Length, Topic Name "x", then payload. Forwarded to a v5 subscriber it gains - * a Property Length byte and no longer fits the transmit buffer. */ -static size_t build_full_rx_publish(byte* buf, size_t buf_sz, byte retain) +/* MQTT 5.0 section 3.12: PINGREQ has no variable header or payload, so a + * non-zero Remaining Length is a Malformed Packet. */ +TEST(v5_pingreq_with_body_sends_malformed_disconnect) { - word32 remain_len = (word32)BROKER_RX_BUF_SZ - 3; - size_t i; + static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; + int closed = 0; - if (buf_sz < (size_t)BROKER_RX_BUF_SZ || remain_len < 128 || - remain_len > 16383) { - return 0; - } + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + v5_bad_packet_disconnect_reason(pingreq, sizeof(pingreq), &closed)); + ASSERT_TRUE(closed); +} + +/* MQTT 5.0 section 4.12: without an Authentication Method in CONNECT the + * Client must not send AUTH, so an AUTH here is a Protocol Error. Wire: AUTH + * with Remaining Length 0 (Success, section 3.15.2.1). */ +TEST(v5_unexpected_auth_sends_protocol_error_disconnect) +{ + static const byte auth[] = { 0xF0, 0x00 }; + int closed = 0; + + ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + v5_bad_packet_disconnect_reason(auth, sizeof(auth), &closed)); + ASSERT_TRUE(closed); +} + +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) +/* When the DISCONNECT before an error close cannot be written at once, the + * connection stays open until it is fully written and is closed then, so the + * Reason Code still reaches the client. Every write is held back with + * MQTT_CODE_CONTINUE once the CONNACK is out, then released. */ +TEST(v5_error_close_waits_for_deferred_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + /* PINGREQ with a body: a Malformed Packet (MQTT 5.0 section 3.12). */ + static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_out_buf[3]); + + g_clients[0].write_continue = 1; + mock_client_input_append(0, pingreq, sizeof(pingreq)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_FALSE(g_client_closed); + ASSERT_EQ(-1, first_disconnect_reason(g_out_buf, g_out_len)); + + g_clients[0].write_continue = 0; + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + first_disconnect_reason(g_out_buf, g_out_len)); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK && !WOLFMQTT_STATIC_MEMORY */ +#endif /* WOLFMQTT_V5 */ + +#if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) +/* Leave a v5 Session for "P" with Session Expiry 60, resume it with + * reconnect, and return the Session Expiry Interval the resumed client + * ends up with (0 on setup failure). */ +static word32 reclaim_session_expiry(const byte* reconnect, + size_t reconnect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerClient* bc; + word32 expiry = 0; + int i; + /* v5 CONNECT (MQTT 5.0 section 3.1): Clean Start 0, keep alive 60, + * Properties length 5 holding Session Expiry Interval 60 (0x11), + * ClientId "P". Remaining Length 19. */ + static const byte connect_60[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + /* v5 DISCONNECT, Remaining Length 0: Reason Code 0x00 */ + static const byte disconnect[] = { 0xE0, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return 0; + } + + reset_mock_clients(2); + mock_client_input_append(0, connect_60, sizeof(connect_60)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + if (broker.orphan_sessions != NULL && + broker.orphan_sessions->session_expiry_sec == 60) { + mock_client_input_append(1, reconnect, reconnect_len); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + bc = find_broker_client(&broker, "P"); + if (bc != NULL && broker.orphan_sessions == NULL) { + expiry = bc->session_expiry_sec; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return expiry; +} + +/* [MQTT-3.1.2.11.2] The resuming CONNECT's explicit 0xFFFFFFFF means the + * Session never expires; it must not be replaced by the old interval. */ +TEST(reclaim_keeps_explicit_v5_never_expire) +{ + /* As connect_60 in reclaim_session_expiry, with Session Expiry + * 0xFFFFFFFF */ + static const byte connect_never[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_never, + sizeof(connect_never))); +} + +/* A v3.1.1 CleanSession=0 Session never expires (MQTT 3.1.1 section + * 3.1.2.4), even when it resumes a Session created over v5. */ +TEST(reclaim_v311_resume_never_expires) +{ + static const byte connect_v311[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P' + }; + + ASSERT_EQ(0xFFFFFFFFu, reclaim_session_expiry(connect_v311, + sizeof(connect_v311))); +} +#endif /* WOLFMQTT_V5 && !WOLFMQTT_STATIC_MEMORY */ + +#ifdef WOLFMQTT_V5 +/* Build a v3.1.1 QoS 0 PUBLISH to "x" that exactly fills the broker receive + * buffer (MQTT 3.1.1 section 3.3): fixed header byte, two byte Remaining + * Length, Topic Name "x", then payload. Forwarded to a v5 subscriber it gains + * a Property Length byte and no longer fits the transmit buffer. */ +static size_t build_full_rx_publish(byte* buf, size_t buf_sz, byte retain) +{ + word32 remain_len = (word32)BROKER_RX_BUF_SZ - 3; + size_t i; + + if (buf_sz < (size_t)BROKER_RX_BUF_SZ || remain_len < 128 || + remain_len > 16383) { + return 0; + } buf[0] = (byte)(0x30 | (retain ? 0x01 : 0x00)); buf[1] = (byte)((remain_len & 0x7F) | 0x80); buf[2] = (byte)(remain_len >> 7); @@ -6429,94 +6785,389 @@ static int queued_publish_after_wait(word32 wait_sec, long* wire_expiry) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); g_broker_time_s = 0; - return publishes; -} - -/* [MQTT-3.3.2-5] A queued copy whose Message Expiry Interval passed before - * onward delivery started is deleted, not delivered. */ -TEST(queued_publish_expired_before_delivery_dropped) -{ - long wire_expiry; - - ASSERT_EQ(0, queued_publish_after_wait(11, &wire_expiry)); + return publishes; +} + +/* [MQTT-3.3.2-5] A queued copy whose Message Expiry Interval passed before + * onward delivery started is deleted, not delivered. */ +TEST(queued_publish_expired_before_delivery_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_publish_after_wait(11, &wire_expiry)); +} + +/* [MQTT-3.3.2-6] A queued copy is delivered with the received Message Expiry + * Interval minus the time it waited in the Server. */ +TEST(queued_publish_expiry_reduced_by_wait) +{ + long wire_expiry; + + ASSERT_EQ(1, queued_publish_after_wait(4, &wire_expiry)); + ASSERT_EQ(6, wire_expiry); +} +#endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ + +/* Parsed details of a single PUBLISH packet on the wire. found = 0 means + * no PUBLISH was present in the buffer; subsequent fields are zero. */ +typedef struct { + int found; + byte first_byte; /* type | DUP | QoS | RETAIN */ + size_t remain_len; /* fixed-header Remaining Length value */ + word16 packet_id; /* 0 if QoS 0 (no packet identifier on wire) */ +} PublishInfo; + +/* Find the first PUBLISH packet (high nibble = 0x3) in `buf` and return + * its first byte plus parsed remaining-length and packet identifier. + * Mirrors count_packets_of_type's VBI walk so any malformed input stops + * cleanly instead of overrunning. */ +static PublishInfo first_publish_info(const byte* buf, size_t len) +{ + PublishInfo info; + size_t pos = 0; + XMEMSET(&info, 0, sizeof(info)); + while (pos < len) { + byte type = (byte)((buf[pos] >> 4) & 0x0F); + size_t remain = 0; + size_t mult = 1; + size_t hdr_len = 1; + int vbi_complete = 0; + while (pos + hdr_len < len && hdr_len <= 5) { + byte b = buf[pos + hdr_len]; + remain += (size_t)(b & 0x7F) * mult; + hdr_len++; + if ((b & 0x80) == 0) { + vbi_complete = 1; + break; + } + mult *= 128; + } + if (!vbi_complete) { + break; + } + if (type == MQTT_PACKET_TYPE_PUBLISH) { + byte qos = (byte)((buf[pos] >> 1) & 0x03); + info.found = 1; + info.first_byte = buf[pos]; + info.remain_len = remain; + if (qos >= MQTT_QOS_1) { + /* Skip the 2-byte topic length + topic to reach the + * 2-byte packet identifier that follows for QoS >= 1. */ + size_t var_off = pos + hdr_len; + if (var_off + 2 <= len) { + word16 topic_len = + (word16)((buf[var_off] << 8) | buf[var_off + 1]); + size_t pid_off = var_off + 2 + topic_len; + if (pid_off + 2 <= len) { + info.packet_id = (word16)( + (buf[pid_off] << 8) | buf[pid_off + 1]); + } + } + } + return info; + } + if (remain > len - pos - hdr_len) { + break; + } + pos += hdr_len + remain; + } + return info; +} + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) +/* v5 PUBLISH QoS 1 on "x" with Message Expiry Interval 10 (MQTT 5.0 + * section 3.3.2.3.3), packet id `pid`, payload "hi". Remaining Length 13. */ +static void expiry_publish_frame(byte* out, byte pid) +{ + static const byte frame[] = { + 0x32, 0x0D, + 0x00, 0x01, 'x', + 0x00, 0x01, + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 'h', 'i' + }; + XMEMCPY(out, frame, sizeof(frame)); + out[6] = pid; +} + +/* A connected v5 subscriber with Receive Maximum 1 holds one message in + * flight; a second queued behind it expires before the window opens again, + * so after the PUBACK it is deleted instead of delivered [MQTT-3.3.2-5]. */ +TEST(queued_publish_expired_behind_inflight_cap_dropped) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo first; + byte publish[15]; + byte puback[4]; + int i; + /* v5 CONNECT, Clean Start 1, Properties length 3 holding Receive + * Maximum 1 (0x21), ClientId "R". Remaining Length 17. */ + static const byte connect_sub[] = { + 0x10, 0x11, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x03, 0x21, 0x00, 0x01, + 0x00, 0x01, 'R' + }; + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + g_broker_time_s = 100; + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + expiry_publish_frame(publish, 2); + mock_client_input_append(1, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + first = first_publish_info(g_clients[0].out_buf, g_clients[0].out_len); + ASSERT_TRUE(first.found); + + /* The queued copy expires, then the window opens. */ + g_broker_time_s += 11; + puback[0] = 0x40; + puback[1] = 0x02; + puback[2] = (byte)(first.packet_id >> 8); + puback[3] = (byte)first.packet_id; + mock_client_input_append(0, puback, sizeof(puback)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; +} + +/* [MQTT-4.4.0-1] An unacknowledged QoS 1 PUBLISH is re-sent with DUP set when + * the Session resumes. It was already delivered once, so its Message Expiry + * no longer applies: the retransmission goes out even after the interval has + * passed. */ +TEST(inflight_publish_resent_after_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo resent; + byte publish[15]; + int i; + /* v5 CONNECT, Clean Start 0, Session Expiry Interval 300, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Delivered once, left unacknowledged, then the subscriber leaves. */ + ASSERT_EQ(1, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += 11; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + resent = first_publish_info(g_clients[2].out_buf, g_clients[2].out_len); + ASSERT_TRUE(resent.found); + ASSERT_NE(0, resent.first_byte & 0x08); /* DUP */ + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; +} + +#ifdef WOLFMQTT_BROKER_PERSIST +static int g_expiry_outq_dels; + +static int expiry_kv_put(void* ctx, byte ns, const byte* key, word16 key_len, + const byte* blob, word32 blob_len) +{ + (void)ctx; (void)ns; (void)key; (void)key_len; (void)blob; + (void)blob_len; + return MQTT_CODE_SUCCESS; +} + +static int expiry_kv_get(void* ctx, byte ns, const byte* key, word16 key_len, + byte* out, word32* inout_len) +{ + (void)ctx; (void)ns; (void)key; (void)key_len; (void)out; + (void)inout_len; + return MQTT_CODE_ERROR_NOT_FOUND; +} + +static int expiry_kv_del(void* ctx, byte ns, const byte* key, word16 key_len) +{ + (void)ctx; (void)key; (void)key_len; + if (ns == BROKER_PERSIST_NS_OUTQ) { + g_expiry_outq_dels++; + } + return MQTT_CODE_SUCCESS; +} + +static int expiry_kv_iter(void* ctx, byte ns, MqttBrokerPersist_IterCb cb, + void* cb_ctx) +{ + (void)ctx; (void)ns; (void)cb; (void)cb_ctx; + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int expiry_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* Queue a QoS 1 copy with Message Expiry 10 for an offline persistent + * subscriber, let wait_sec pass and resume the Session. Returns the number + * of OUTQ records the broker deleted. */ +static int expiry_outq_dels_after_wait(int wait_sec) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + byte publish[15]; + int i; + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + hooks.kv_put = expiry_kv_put; + hooks.kv_get = expiry_kv_get; + hooks.kv_del = expiry_kv_del; + hooks.kv_iter = expiry_kv_iter; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = expiry_derive_key; +#endif + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_SetPersistHooks(&broker, &hooks) != + MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + expiry_publish_frame(publish, 1); + mock_client_input_append(1, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + g_expiry_outq_dels = 0; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return g_expiry_outq_dels; } -/* [MQTT-3.3.2-6] A queued copy is delivered with the received Message Expiry - * Interval minus the time it waited in the Server. */ -TEST(queued_publish_expiry_reduced_by_wait) +/* An expired queued copy is deleted from the persistent store too, so a + * restart cannot replay it [MQTT-3.3.2-5]. Measured against the same flow + * where the copy has not expired and is only delivered. */ +TEST(queued_publish_expired_deletes_persisted_record) { - long wire_expiry; + int expired_dels = expiry_outq_dels_after_wait(11); + int live_dels = expiry_outq_dels_after_wait(4); - ASSERT_EQ(1, queued_publish_after_wait(4, &wire_expiry)); - ASSERT_EQ(6, wire_expiry); + ASSERT_TRUE(expired_dels >= 0); + ASSERT_TRUE(live_dels >= 0); + ASSERT_TRUE(expired_dels > live_dels); } +#endif /* WOLFMQTT_BROKER_PERSIST */ #endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ -/* Parsed details of a single PUBLISH packet on the wire. found = 0 means - * no PUBLISH was present in the buffer; subsequent fields are zero. */ -typedef struct { - int found; - byte first_byte; /* type | DUP | QoS | RETAIN */ - size_t remain_len; /* fixed-header Remaining Length value */ - word16 packet_id; /* 0 if QoS 0 (no packet identifier on wire) */ -} PublishInfo; - -/* Find the first PUBLISH packet (high nibble = 0x3) in `buf` and return - * its first byte plus parsed remaining-length and packet identifier. - * Mirrors count_packets_of_type's VBI walk so any malformed input stops - * cleanly instead of overrunning. */ -static PublishInfo first_publish_info(const byte* buf, size_t len) -{ - PublishInfo info; - size_t pos = 0; - XMEMSET(&info, 0, sizeof(info)); - while (pos < len) { - byte type = (byte)((buf[pos] >> 4) & 0x0F); - size_t remain = 0; - size_t mult = 1; - size_t hdr_len = 1; - int vbi_complete = 0; - while (pos + hdr_len < len && hdr_len <= 5) { - byte b = buf[pos + hdr_len]; - remain += (size_t)(b & 0x7F) * mult; - hdr_len++; - if ((b & 0x80) == 0) { - vbi_complete = 1; - break; - } - mult *= 128; - } - if (!vbi_complete) { - break; - } - if (type == MQTT_PACKET_TYPE_PUBLISH) { - byte qos = (byte)((buf[pos] >> 1) & 0x03); - info.found = 1; - info.first_byte = buf[pos]; - info.remain_len = remain; - if (qos >= MQTT_QOS_1) { - /* Skip the 2-byte topic length + topic to reach the - * 2-byte packet identifier that follows for QoS >= 1. */ - size_t var_off = pos + hdr_len; - if (var_off + 2 <= len) { - word16 topic_len = - (word16)((buf[var_off] << 8) | buf[var_off + 1]); - size_t pid_off = var_off + 2 + topic_len; - if (pid_off + 2 <= len) { - info.packet_id = (word16)( - (buf[pid_off] << 8) | buf[pid_off + 1]); - } - } - } - return info; - } - if (remain > len - pos - hdr_len) { - break; - } - pos += hdr_len + remain; - } - return info; -} #ifdef WOLFMQTT_STATIC_MEMORY /* Reads the broker's outbound in-flight table directly; the library helper @@ -9919,98 +10570,6 @@ TEST(disconnect_v5_session_expiry_raise_sends_protocol_error) MqttBroker_Free(&broker); } -/* MQTT 5.0 section 4.13.1: a Server that detects a Malformed Packet or - * Protocol Error in a packet other than CONNECT SHOULD send DISCONNECT with - * the Reason Code before closing. Connect a v5 client, send one bad packet, - * and return the Reason Code of the DISCONNECT the broker sent (-1 if none); - * *closed reports whether the connection was closed. */ -static int v5_bad_packet_disconnect_reason(const byte* bad, size_t bad_len, - int* closed) -{ - MqttBroker broker; - MqttBrokerNet net; - int i; - int reason; - /* v5 CONNECT, CleanStart, keep alive 60, no properties, ClientId "A". */ - static const byte connect[] = { - 0x10, 14, - 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x05, 0x02, 0x00, 0x3C, - 0x00, - 0x00, 0x01, 'A' - }; - - install_mock_net(&net); - XMEMSET(&broker, 0, sizeof(broker)); - if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || - MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { - *closed = 0; - return -2; - } - reset_mock_state(connect, sizeof(connect)); - mock_client_input_append(0, bad, bad_len); - for (i = 0; i < 8; i++) { - (void)MqttBroker_Step(&broker); - } - reason = first_disconnect_reason(g_out_buf, g_out_len); - *closed = g_client_closed; - MqttBroker_Stop(&broker); - MqttBroker_Free(&broker); - return reason; -} - -/* [MQTT-2.2.2-2] PUBACK fixed-header flags must be 0000: Malformed Packet. */ -TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect) -{ - static const byte puback[] = { 0x41, 0x02, 0x00, 0x01 }; - int closed = 0; - - ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, - v5_bad_packet_disconnect_reason(puback, sizeof(puback), &closed)); - ASSERT_TRUE(closed); -} - -/* [MQTT-3.1.0-2] A second CONNECT is a Protocol Error. */ -TEST(v5_second_connect_sends_protocol_error_disconnect) -{ - static const byte connect[] = { - 0x10, 14, - 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x05, 0x02, 0x00, 0x3C, - 0x00, - 0x00, 0x01, 'A' - }; - int closed = 0; - - ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, - v5_bad_packet_disconnect_reason(connect, sizeof(connect), &closed)); - ASSERT_TRUE(closed); -} - -/* MQTT 5.0 section 3.12: PINGREQ has no variable header or payload, so a - * non-zero Remaining Length is a Malformed Packet. */ -TEST(v5_pingreq_with_body_sends_malformed_disconnect) -{ - static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; - int closed = 0; - - ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, - v5_bad_packet_disconnect_reason(pingreq, sizeof(pingreq), &closed)); - ASSERT_TRUE(closed); -} - -/* MQTT 5.0 section 4.12: without an Authentication Method in CONNECT the - * Client must not send AUTH, so an AUTH here is a Protocol Error. Wire: AUTH - * with Remaining Length 0 (Success, section 3.15.2.1). */ -TEST(v5_unexpected_auth_sends_protocol_error_disconnect) -{ - static const byte auth[] = { 0xF0, 0x00 }; - int closed = 0; - - ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, - v5_bad_packet_disconnect_reason(auth, sizeof(auth), &closed)); - ASSERT_TRUE(closed); -} /* v5 CONNECT with Clean Start=0, no Session Expiry: must default to 0, * not the v3.1.1 0xFFFFFFFF "never" sentinel. */ @@ -12070,7 +12629,6 @@ TEST(persist_restore_rejects_outq_packet_id_mismatch) MqttBroker_Free(&source); } -#ifdef WOLFMQTT_NONBLOCK typedef struct PersistSubsCount { int puts; int dels; @@ -12089,39 +12647,99 @@ static int persist_subs_count_put(void* ctx, byte ns, const byte* key, return MQTT_CODE_SUCCESS; } -static int persist_subs_count_get(void* ctx, byte ns, const byte* key, - word16 key_len, byte* out, word32* inout_len) -{ - (void)ctx; - (void)ns; - (void)key; - (void)key_len; - (void)out; - (void)inout_len; - return MQTT_CODE_ERROR_NOT_FOUND; -} +static int persist_subs_count_get(void* ctx, byte ns, const byte* key, + word16 key_len, byte* out, word32* inout_len) +{ + (void)ctx; + (void)ns; + (void)key; + (void)key_len; + (void)out; + (void)inout_len; + return MQTT_CODE_ERROR_NOT_FOUND; +} + +static int persist_subs_count_del(void* ctx, byte ns, const byte* key, + word16 key_len) +{ + (void)key; + (void)key_len; + if (ns == BROKER_PERSIST_NS_SUBS) { + ((PersistSubsCount*)ctx)->dels++; + } + return MQTT_CODE_SUCCESS; +} + +static int persist_subs_count_iter(void* ctx, byte ns, + MqttBrokerPersist_IterCb cb, void* cb_ctx) +{ + (void)ctx; + (void)ns; + (void)cb; + (void)cb_ctx; + return MQTT_CODE_SUCCESS; +} + +/* SUBSCRIBE and UNSUBSCRIBE change the Session's subscription list before + * the SUBACK or UNSUBACK is written, so the persisted list must follow the + * change even when that write fails and the connection closes; otherwise a + * restart restores the list from before the change. */ +TEST(persist_subs_written_when_ack_write_fails) +{ + MqttBroker broker; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistSubsCount count; + int i; + int puts_before; + /* v3.1.1 CONNECT, CleanSession=0, keep alive 60, ClientId "S" */ + static const byte connect_s[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "x", QoS 1 (MQTT 3.1.1 section 3.8) */ + static const byte subscribe_x[] = { + 0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x01 + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&count, 0, sizeof(count)); + hooks.kv_put = persist_subs_count_put; + hooks.kv_get = persist_subs_count_get; + hooks.kv_del = persist_subs_count_del; + hooks.kv_iter = persist_subs_count_iter; + hooks.ctx = &count; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&broker, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); -static int persist_subs_count_del(void* ctx, byte ns, const byte* key, - word16 key_len) -{ - (void)key; - (void)key_len; - if (ns == BROKER_PERSIST_NS_SUBS) { - ((PersistSubsCount*)ctx)->dels++; + reset_mock_state(connect_s, sizeof(connect_s)); + run_broker_one_connect(&broker); + ASSERT_EQ(1, count_packets_of_type(g_out_buf, g_out_len, + MQTT_PACKET_TYPE_CONNECT_ACK)); + + /* The SUBACK write fails and the connection closes. */ + puts_before = count.puts; + g_clients[0].write_err_pending = 1; + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); } - return MQTT_CODE_SUCCESS; -} + ASSERT_TRUE(count.puts > puts_before); -static int persist_subs_count_iter(void* ctx, byte ns, - MqttBrokerPersist_IterCb cb, void* cb_ctx) -{ - (void)ctx; - (void)ns; - (void)cb; - (void)cb_ctx; - return MQTT_CODE_SUCCESS; + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); } +#ifdef WOLFMQTT_NONBLOCK /* A SUBACK or UNSUBACK whose write is deferred must still shadow-write the * persistent session's subscription list; otherwise a restart restores the * list from before the change. */ @@ -12717,8 +13335,194 @@ TEST(persist_root_writable_permissions_rejected) ASSERT_EQ(MQTT_CODE_ERROR_SYSTEM, persist_test_init_root_mode(0777)); } -#if !defined(WOLFMQTT_STATIC_MEMORY) && \ - !defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) +#ifndef WOLFMQTT_STATIC_MEMORY +/* Remove root// left by the POSIX backend. */ +static void persist_remove_tree(const char* root) +{ + char dir_path[640]; + char rec_path[1024]; + struct dirent* ent; + struct dirent* sub; + DIR* d; + DIR* sd; + + d = opendir(root); + if (d == NULL) { + return; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') { + continue; + } + if (snprintf(dir_path, sizeof(dir_path), "%s/%s", root, + ent->d_name) <= 0) { + continue; + } + sd = opendir(dir_path); + if (sd == NULL) { + (void)unlink(dir_path); + continue; + } + while ((sub = readdir(sd)) != NULL) { + if (sub->d_name[0] != '.' && + snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, + sub->d_name) > 0) { + (void)unlink(rec_path); + } + } + (void)closedir(sd); + (void)rmdir(dir_path); + } + (void)closedir(d); + (void)rmdir(root); +} + +/* Whether the captured freed allocation was all zero. */ +static int persist_captured_free_zeroed(void) +{ + size_t i; + + for (i = 0; i < g_capture_freed_len; i++) { + if (g_capture_freed[i] != 0) { + return 0; + } + } + return 1; +} + +/* A 40-character topic: its heap copy is 41 bytes, a size nothing else + * allocates during these restores, so the free capture sees only it. */ +#define PERSIST_SCRUB_TOPIC "secret/plant-7/valve-control/setpoint-01" + +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT +static int scrub_derive_key(void* ctx, byte* out_key, word32 key_len) +{ + (void)ctx; + XMEMSET(out_key, 0x11, key_len); + return MQTT_CODE_SUCCESS; +} +#endif + +/* A 37-byte payload: an allocation size nothing else uses in these restores, + * so the targeted allocation failure hits the payload copy. */ +#define PERSIST_SCRUB_PAYLOAD_LEN 37 + +/* Persist one record through `put`, then restore it with the payload copy's + * allocation failing and the freed topic copy captured. Returns 1 if the + * topic copy was freed and all zero, 0 if it was freed with data left, -1 if + * the flow did not reach that point. */ +static int persist_restore_alloc_failure_scrub(int outq) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; +#ifdef WOLFMQTT_BROKER_RETAINED + BrokerRetainedMsg rm; +#endif + BrokerOutPub out; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[PERSIST_SCRUB_PAYLOAD_LEN]; + char* root; + int rc; + int result = -1; + + install_mock_net(&net); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(payload, 'p', sizeof(payload)); + root = mkdtemp(root_template); + if (root == NULL) { + return -1; + } + if (MqttBrokerNet_PersistPosix_Init(&hooks, root) != MQTT_CODE_SUCCESS) { + persist_remove_tree(root); + return -1; + } +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = scrub_derive_key; +#endif + + XMEMSET(&source, 0, sizeof(source)); + rc = MqttBroker_Init(&source, &net); + if (rc == MQTT_CODE_SUCCESS) { + rc = MqttBroker_SetPersistHooks(&source, &hooks); + } + if (rc == MQTT_CODE_SUCCESS) { + rc = BrokerPersist_Restore(&source); + } + if (rc == MQTT_CODE_SUCCESS && outq) { + rc = BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_4, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1); + if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&out, 0, sizeof(out)); + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + } +#ifdef WOLFMQTT_BROKER_RETAINED + else if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&rm, 0, sizeof(rm)); + rm.topic = topic; + rm.payload = payload; + rm.payload_len = (word32)sizeof(payload); + rm.qos = MQTT_QOS_0; + rm.store_time = (WOLFMQTT_BROKER_TIME_T)1; + rc = BrokerPersist_PutRetained(&source, &rm); + } +#else + else { + rc = MQTT_CODE_ERROR_BAD_ARG; /* no retained store in this build */ + } +#endif + MqttBroker_Free(&source); + + if (rc == MQTT_CODE_SUCCESS) { + XMEMSET(&restored, 0, sizeof(restored)); + broker_test_capture_free(sizeof(topic)); + g_alloc_fail_size = sizeof(payload); + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + (void)BrokerPersist_Restore(&restored); + if (g_capture_free_seen) { + result = persist_captured_free_zeroed(); + } + } + g_alloc_fail_size = 0; + broker_test_disable_free_capture(); + MqttBroker_Free(&restored); + } + MqttBrokerNet_PersistPosix_Free(&hooks); + persist_remove_tree(root); + return result; +} + +#ifdef WOLFMQTT_BROKER_RETAINED +/* When the payload copy cannot be allocated while restoring a retained + * message, the topic copy already made is wiped before it is freed. */ +TEST(persist_restore_retained_alloc_failure_scrubs_topic) +{ + ASSERT_EQ(1, persist_restore_alloc_failure_scrub(0)); +} +#endif + +/* The same for a queued outbound message of an offline Session. */ +TEST(persist_restore_outq_alloc_failure_scrubs_topic) +{ + ASSERT_EQ(1, persist_restore_alloc_failure_scrub(1)); +} + +#ifndef WOLFMQTT_BROKER_PERSIST_ENCRYPT /* Drop the last cut bytes of the only record in root/ and lower the * header's body length to match (record header: magic(4) version(2) kind(1) * wrap(1) body_len(4, big-endian)), so the record stays self-consistent and @@ -12780,64 +13584,7 @@ static int persist_truncate_only_record(const char* root, byte ns, return rc; } -/* Remove root// left by the POSIX backend. */ -static void persist_remove_tree(const char* root) -{ - char dir_path[640]; - char rec_path[1024]; - struct dirent* ent; - struct dirent* sub; - DIR* d; - DIR* sd; - - d = opendir(root); - if (d == NULL) { - return; - } - while ((ent = readdir(d)) != NULL) { - if (ent->d_name[0] == '.') { - continue; - } - if (snprintf(dir_path, sizeof(dir_path), "%s/%s", root, - ent->d_name) <= 0) { - continue; - } - sd = opendir(dir_path); - if (sd == NULL) { - (void)unlink(dir_path); - continue; - } - while ((sub = readdir(sd)) != NULL) { - if (sub->d_name[0] != '.' && - snprintf(rec_path, sizeof(rec_path), "%s/%s", dir_path, - sub->d_name) > 0) { - (void)unlink(rec_path); - } - } - (void)closedir(sd); - (void)rmdir(dir_path); - } - (void)closedir(d); - (void)rmdir(root); -} - -/* Whether the captured freed allocation was all zero. */ -static int persist_captured_free_zeroed(void) -{ - size_t i; - - for (i = 0; i < g_capture_freed_len; i++) { - if (g_capture_freed[i] != 0) { - return 0; - } - } - return 1; -} - -/* A 40-character topic: its heap copy is 41 bytes, a size nothing else - * allocates during these restores, so the free capture sees only it. */ -#define PERSIST_SCRUB_TOPIC "secret/plant-7/valve-control/setpoint-01" - +#ifdef WOLFMQTT_BROKER_RETAINED /* Restore copies a persisted topic to the heap before it reads the payload. * When the record turns out to be malformed the copy is freed; topic strings * are application data the broker scrubs before freeing elsewhere, so it must @@ -12910,6 +13657,8 @@ TEST(persist_restore_malformed_retained_scrubs_topic) ASSERT_TRUE(zeroed); } +#endif /* WOLFMQTT_BROKER_RETAINED */ + /* The same for a queued outbound message of an offline Session. */ TEST(persist_restore_malformed_outq_scrubs_topic) { @@ -12986,7 +13735,8 @@ TEST(persist_restore_malformed_outq_scrubs_topic) ASSERT_TRUE(seen); ASSERT_TRUE(zeroed); } -#endif /* !WOLFMQTT_STATIC_MEMORY && !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ +#endif /* !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ +#endif /* !WOLFMQTT_STATIC_MEMORY */ /* The POSIX backend names a record ".bin" and writes it through * ".bin.tmp", so a key can be at most (NAME_MAX - 8) / 2 bytes. A @@ -14389,6 +15139,7 @@ int main(int argc, char** argv) RUN_TEST(connect_unsupported_level_undecodable_refused); #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_persistent_session_cap_refuses_new_session); + RUN_TEST(connect_persistent_session_cap_counts_live_sessions); #endif #ifndef WOLFMQTT_V5 RUN_TEST(connect_v5_to_non_v5_broker_refused); @@ -14510,6 +15261,7 @@ int main(int argc, char** argv) RUN_TEST(disconnect_v5_reason_4_publishes_will); RUN_TEST(disconnect_v5_reason_error_publishes_will); RUN_TEST(takeover_v5_zero_will_delay_publishes_will); + RUN_TEST(takeover_v5_own_will_not_sent_after_disconnect); RUN_TEST(takeover_v5_will_delay_resumed_session_discards_will); RUN_TEST(takeover_v5_clean_start_publishes_will); RUN_TEST(takeover_v5_zero_session_expiry_publishes_will); @@ -14534,6 +15286,11 @@ int main(int argc, char** argv) #if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) RUN_TEST(queued_publish_expired_before_delivery_dropped); RUN_TEST(queued_publish_expiry_reduced_by_wait); + RUN_TEST(queued_publish_expired_behind_inflight_cap_dropped); + RUN_TEST(inflight_publish_resent_after_expiry); +#ifdef WOLFMQTT_BROKER_PERSIST + RUN_TEST(queued_publish_expired_deletes_persisted_record); +#endif #endif #if defined(WOLFMQTT_BROKER_WILL) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_will_scrub_after_failed_write); @@ -14635,10 +15392,13 @@ int main(int argc, char** argv) #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(connect_v5_receive_max_zero_protocol_error); RUN_TEST(disconnect_v5_session_expiry_raise_sends_protocol_error); +#endif RUN_TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect); RUN_TEST(v5_second_connect_sends_protocol_error_disconnect); RUN_TEST(v5_pingreq_with_body_sends_malformed_disconnect); RUN_TEST(v5_unexpected_auth_sends_protocol_error_disconnect); +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(v5_error_close_waits_for_deferred_disconnect); #endif #endif #ifdef WOLFMQTT_V5 @@ -14747,6 +15507,7 @@ int main(int argc, char** argv) #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(persist_restore_packet_id_wrap_preserves_fifo); RUN_TEST(persist_restore_rejects_outq_packet_id_mismatch); + RUN_TEST(persist_subs_written_when_ack_write_fails); #ifdef WOLFMQTT_NONBLOCK RUN_TEST(persist_subs_written_when_ack_deferred); RUN_TEST(persist_partial_publish_restart_keeps_dup); @@ -14765,10 +15526,17 @@ int main(int argc, char** argv) RUN_TEST(persist_put_del_report_namespace_fsync_failure); RUN_TEST(persist_get_reports_record_larger_than_buffer); RUN_TEST(persist_key_longer_than_name_limit_rejected); -#if !defined(WOLFMQTT_STATIC_MEMORY) && \ - !defined(WOLFMQTT_BROKER_PERSIST_ENCRYPT) +#ifndef WOLFMQTT_STATIC_MEMORY +#ifdef WOLFMQTT_BROKER_RETAINED + RUN_TEST(persist_restore_retained_alloc_failure_scrubs_topic); +#endif + RUN_TEST(persist_restore_outq_alloc_failure_scrubs_topic); +#ifndef WOLFMQTT_BROKER_PERSIST_ENCRYPT +#ifdef WOLFMQTT_BROKER_RETAINED RUN_TEST(persist_restore_malformed_retained_scrubs_topic); +#endif RUN_TEST(persist_restore_malformed_outq_scrubs_topic); +#endif #endif RUN_TEST(persist_iter_reports_unreadable_record); RUN_TEST(persist_iter_skips_socket_and_symlink_entries); diff --git a/tests/test_broker_log_config.h b/tests/test_broker_log_config.h index df22fb774..00c2a8933 100644 --- a/tests/test_broker_log_config.h +++ b/tests/test_broker_log_config.h @@ -4,7 +4,14 @@ #ifndef WOLFMQTT_TEST_BROKER_LOG_CONFIG_H #define WOLFMQTT_TEST_BROKER_LOG_CONFIG_H +/* Declared printf-like so a non-literal fmt can be passed on to vsnprintf + * (clang -Wformat-nonliteral) and the broker's PRINTF calls are checked. */ +#if defined(__GNUC__) || defined(__clang__) +void wolfmqtt_test_log(const char* fmt, ...) + __attribute__((format(printf, 1, 2))); +#else void wolfmqtt_test_log(const char* fmt, ...); +#endif unsigned long wolfmqtt_test_broker_time_s(void); #endif diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index ab8418852..cd8003cc7 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -309,16 +309,37 @@ TEST(deinit_after_init) ASSERT_TRUE(1); } -/* The caller-provided buffers can still hold a serialized CONNECT (username, - * password, v5 authentication data) from an abandoned operation. Once the - * client is torn down the library no longer uses them, so DeInit must wipe - * them rather than hand them back to the application with that content. */ -TEST(deinit_scrubs_tx_rx_buffers) +TEST(net_disconnect_wait_null_client) +{ + ASSERT_EQ(MQTT_CODE_ERROR_BAD_ARG, MqttClient_NetDisconnect_Wait(NULL)); +} + +/* Without other threads the waiting disconnect closes the transport like + * MqttClient_NetDisconnect. */ +TEST(net_disconnect_wait_closes_transport) +{ + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + + rc = MqttClient_NetDisconnect_Wait(&test_client); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + (MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT))); +} + +/* tx_buf and rx_buf belong to the caller, and the bundled examples free them + * before calling MqttClient_DeInit, so DeInit must not write to them: doing + * so would write into freed heap memory. Marker bytes must survive DeInit. */ +TEST(deinit_leaves_caller_buffers_untouched) { int rc; int i; - int tx_clear = 1; - int rx_clear = 1; + int tx_kept = 1; + int rx_kept = 1; rc = test_init_client(); ASSERT_EQ(MQTT_CODE_SUCCESS, rc); @@ -329,17 +350,17 @@ TEST(deinit_scrubs_tx_rx_buffers) test_client_inited = 0; for (i = 0; i < (int)sizeof(test_tx_buf); i++) { - if (test_tx_buf[i] != 0) { - tx_clear = 0; + if (test_tx_buf[i] != 0x5A) { + tx_kept = 0; } } for (i = 0; i < (int)sizeof(test_rx_buf); i++) { - if (test_rx_buf[i] != 0) { - rx_clear = 0; + if (test_rx_buf[i] != 0x5A) { + rx_kept = 0; } } - ASSERT_TRUE(tx_clear); - ASSERT_TRUE(rx_clear); + ASSERT_TRUE(tx_kept); + ASSERT_TRUE(rx_kept); } /* ============================================================================ @@ -1176,7 +1197,8 @@ TEST(auth_without_connect_method_rejected) * Authentication Method property. Exercises MqttConnect_HasAuthMethod and its * per-connection assignment in MqttClient_Connect rather than poking * client->auth_method_set directly. */ -static int run_connect_v5_with_auth_method(int with_method) +static int run_connect_v5_with_auth_method_write(int with_method, + MqttNetWriteCb write_cb) { int rc; int i; @@ -1185,7 +1207,7 @@ static int run_connect_v5_with_auth_method(int with_method) /* v5 CONNACK: accepted, Session Present = 0, no properties. */ static const byte connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; - test_net.write = mock_net_write_accept; + test_net.write = write_cb; test_net.read = mock_net_read_canned; XMEMCPY(g_canned_buf, connack, sizeof(connack)); g_canned_len = (int)sizeof(connack); @@ -1210,6 +1232,12 @@ static int run_connect_v5_with_auth_method(int with_method) return rc; } +static int run_connect_v5_with_auth_method(int with_method) +{ + return run_connect_v5_with_auth_method_write(with_method, + mock_net_write_accept); +} + /* 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. The @@ -1398,8 +1426,141 @@ TEST(connect_auth_pending_allows_disconnect) ASSERT_EQ(MQTT_CODE_SUCCESS, rc); ASSERT_TRUE(g_frames_written > 0); } + +/* UNSUBSCRIBE is refused in the [MQTT-3.1.2-30] window like PUBLISH, and the + * automatic keep-alive does not send a PINGREQ there even when one is due. */ +TEST(connect_auth_pending_blocks_unsubscribe_and_keepalive) +{ + int rc; + MqttConnect connect; + MqttProp method_prop; + MqttUnsubscribe unsub; + MqttTopic topic; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + g_frames_written = 0; + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(&topic, 0, sizeof(topic)); + topic.topic_filter = "t"; + unsub.packet_id = 2; + unsub.topic_count = 1; + unsub.topics = &topic; + rc = MqttClient_Unsubscribe(&test_client, &unsub); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, rc); + + /* A keep-alive ping is due: nothing has been sent since time 0. */ + test_client.keep_alive_sec = 60; + test_client.last_tx_time = 0; + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(0, g_frames_written); +} + +/* AUTH stays allowed in the window [MQTT-3.1.2-30]: it is how the exchange + * proceeds. */ +TEST(connect_auth_pending_allows_auth) +{ + int rc; + MqttConnect connect; + MqttProp method_prop; + MqttAuth auth; + MqttProp* prop; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + XMEMSET(&auth, 0, sizeof(auth)); + auth.reason_code = MQTT_REASON_CONT_AUTH; + prop = MqttClient_PropsAdd(&auth.props); + ASSERT_NOT_NULL(prop); + prop->type = MQTT_PROP_AUTH_METHOD; + prop->data_str.str = (char*)"SCRAM-SHA-256"; + prop->data_str.len = (word16)XSTRLEN("SCRAM-SHA-256"); + + g_frames_written = 0; + rc = MqttClient_Auth(&test_client, &auth); + ASSERT_NE(MQTT_CODE_ERROR_STAT, rc); + ASSERT_TRUE(g_frames_written > 0); + + MqttClient_PropsFree(auth.props); +} + +/* The window belongs to its Network Connection: after one that never got its + * CONNACK is closed, a new connection whose CONNECT has no Authentication + * Method is not held back by it. */ +TEST(connect_auth_pending_cleared_by_reconnect) +{ + int rc; + int i; + MqttConnect connect; + MqttProp method_prop; + MqttPublish publish; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = start_connect_v5_auth_pending(&connect, &method_prop); + ASSERT_EQ(MQTT_CODE_CONTINUE, rc); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_NetDisconnect(&test_client)); + rc = run_connect_v5_with_auth_method(0); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + g_frames_written = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_frames_written > 0); +} #endif /* WOLFMQTT_NONBLOCK */ +static int g_auth_pending_at_connect_write; + +static int mock_net_write_note_auth_pending(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + if (buf != NULL && buf_len > 0 && + ((buf[0] >> 4) & 0x0F) == MQTT_PACKET_TYPE_CONNECT) { + g_auth_pending_at_connect_write = + (test_client.flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0; + } + return mock_net_write_accept(context, buf, buf_len, timeout_ms); +} + +/* [MQTT-3.1.2-30] The window must already be closed to other senders when + * the CONNECT reaches the peer; raising it only after the write would let + * another thread send in between, or leave it stuck if a reader processed the + * CONNACK first. The write callback records the flag as the CONNECT goes out, + * and the CONNACK then clears it. */ +TEST(connect_auth_pending_raised_before_connect_write) +{ + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + g_auth_pending_at_connect_write = 0; + + rc = run_connect_v5_with_auth_method_write(1, + mock_net_write_note_auth_pending); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(1, g_auth_pending_at_connect_write); + ASSERT_EQ(0, (int)(MqttClient_Flags(&test_client, 0, 0) & + MQTT_CLIENT_FLAG_AUTH_PENDING)); +} + /* MQTT 3.1.1 section 2.2.1 lists packet type 15 as Reserved/Forbidden; AUTH * exists only from v5. A session negotiated at protocol level 4 must treat a * received type-15 packet as a protocol violation [MQTT-4.8.0-1] even in a @@ -4779,6 +4940,73 @@ TEST(publish_allowed_past_replay_pool_when_connack_ends_session) } #endif /* WOLFMQTT_V5 */ +#if WOLFMQTT_MAX_QOS >= 2 +/* A QoS 2 PUBLISH can leave without a replay slot (here a CleanSession 1 + * connection sends it unretained while the pool is full). When its PUBREC + * arrives after a slot has freed, the PUBREL state must still be recorded + * for it [MQTT-4.4.0-1]: the client's own in-flight reservation awaiting + * PUBCOMP identifies it as this client's QoS 2 message. */ +TEST(pubrec_for_unslotted_qos2_records_pubrel) +{ + int rc; + int i; + int found = 0; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + /* PUBACK for id 1 (MQTT 3.1.1 section 3.4), then PUBREC for id + * MQTT_MAX_REPLAY_MSGS + 1 (section 3.5). */ + byte acks[8] = { 0x40, 0x02, 0x00, 0x01, 0x50, 0x02, 0x00, 0x00 }; + word16 qos2_id = (word16)(MQTT_MAX_REPLAY_MSGS + 1); + + acks[6] = (byte)(qos2_id >> 8); + acks[7] = (byte)qos2_id; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "replay_client"; + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 20 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + /* Fill the pool with QoS 1 messages, then send the QoS 2 one. */ + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + init_qos_publish(&publish, MQTT_QOS_1, (word16)(i + 1), "t", + payload, (word32)(sizeof(payload) - 1)); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, run_publish_unacked(&publish)); + } + init_qos_publish(&publish, MQTT_QOS_2, qos2_id, "t", payload, + (word32)(sizeof(payload) - 1)); + ASSERT_EQ(MQTT_CODE_ERROR_NETWORK, run_publish_unacked(&publish)); + + rc = run_wait_pubrecs(acks, (int)sizeof(acks)); + ASSERT_EQ(g_canned_len, g_canned_pos); + + for (i = 0; i < MQTT_MAX_REPLAY_MSGS; i++) { + if (test_client.replay[i].packet_id == qos2_id && + test_client.replay[i].qos == MQTT_QOS_2 && + test_client.replay[i].pubrelSent) { + found = 1; + } + } + ASSERT_TRUE(found); +} +#endif /* WOLFMQTT_MAX_QOS >= 2 */ + /* A CleanSession 1 Session ends with the Network Connection, so nothing is * ever replayed and a full pool must not limit how many messages are in * flight. */ @@ -5419,6 +5647,86 @@ TEST(publish_stream_cb_nonblock_resume_no_tail_drop) } #endif /* WOLFMQTT_NONBLOCK */ +static byte g_connect_wire[512]; +static int g_connect_wire_len; + +static int mock_net_write_capture(void *context, const byte* buf, int buf_len, + int timeout_ms) +{ + (void)context; (void)timeout_ms; + if (buf != NULL && buf_len > 0 && + g_connect_wire_len + buf_len <= (int)sizeof(g_connect_wire)) { + XMEMCPY(g_connect_wire + g_connect_wire_len, buf, (size_t)buf_len); + g_connect_wire_len += buf_len; + } + return buf_len; /* accept the whole chunk */ +} + +/* A PUBLISH whose fixed and variable header exactly fill tx_buf leaves no room + * for payload in the first write. The whole payload must then follow in later + * writes instead of the client issuing an empty write after the header. */ +TEST(publish_header_fills_tx_buf_sends_payload) +{ + int rc; + int i; + MqttConnect connect; + MqttPublish publish; + static byte payload[] = "hello"; + /* CONNACK v3.1.1: type=0x20, remain=2, flags=0x00, return_code=0x00. */ + static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; + /* MQTT 3.1.1 section 3.3: PUBLISH QoS 0, Remaining Length 8, Topic Name + * "t" (length 1), no Packet Identifier, payload "hello". */ + static const byte expected[] = { + 0x30, 0x08, 0x00, 0x01, 't', 'h', 'e', 'l', 'l', 'o' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, connack, sizeof(connack)); + g_canned_len = (int)sizeof(connack); + g_canned_pos = 0; + + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "test_client"; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Connect(&test_client, &connect); + } + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + /* Fixed header (2) plus Topic Name (3) exactly fill tx_buf */ + test_client.tx_buf_len = 5; + test_net.write = mock_net_write_capture; + g_connect_wire_len = 0; + + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + publish.buffer = payload; + publish.total_len = (word32)(sizeof(payload) - 1); + publish.buffer_len = publish.total_len; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && (rc == MQTT_CODE_CONTINUE || + rc == MQTT_CODE_PUB_CONTINUE); i++) { + rc = MqttClient_Publish(&test_client, &publish); + } + test_client.tx_buf_len = TEST_TX_BUF_SIZE; + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ((int)sizeof(expected), g_connect_wire_len); + ASSERT_EQ(0, XMEMCMP(expected, g_connect_wire, sizeof(expected))); +} + #if defined(WOLFMQTT_V5) && WOLFMQTT_MAX_QOS >= 1 /* Drives one QoS>0 publish to completion against the canned-response mock and * returns the final MqttClient_Publish result. The caller stages the broker's @@ -5987,21 +6295,6 @@ TEST(connect_resume_declined_clears_qos2_dedup) ASSERT_EQ(0, test_client.recv_qos2_pending[0]); } -static byte g_connect_wire[512]; -static int g_connect_wire_len; - -static int mock_net_write_capture(void *context, const byte* buf, int buf_len, - int timeout_ms) -{ - (void)context; (void)timeout_ms; - if (buf != NULL && buf_len > 0 && - g_connect_wire_len + buf_len <= (int)sizeof(g_connect_wire)) { - XMEMCPY(g_connect_wire + g_connect_wire_len, buf, (size_t)buf_len); - g_connect_wire_len += buf_len; - } - return buf_len; /* accept the whole chunk */ -} - /* Point *props at the property block of the captured v5 CONNECT and return its * length, or -1 if the frame is truncated. Layout: type byte, Remaining Length * (VBI), 10-byte variable header ("MQTT" + level + flags + keep alive), then @@ -6033,70 +6326,6 @@ static int connect_wire_props(const byte* wire, int len, const byte** props) return prop_len; } -/* A PUBLISH whose fixed and variable header exactly fill tx_buf leaves no room - * for payload in the first write. The whole payload must then follow in later - * writes instead of the client issuing an empty write after the header. */ -TEST(publish_header_fills_tx_buf_sends_payload) -{ - int rc; - int i; - MqttConnect connect; - MqttPublish publish; - static byte payload[] = "hello"; - /* CONNACK v3.1.1: type=0x20, remain=2, flags=0x00, return_code=0x00. */ - static const byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - /* MQTT 3.1.1 section 3.3: PUBLISH QoS 0, Remaining Length 8, Topic Name - * "t" (length 1), no Packet Identifier, payload "hello". */ - static const byte expected[] = { - 0x30, 0x08, 0x00, 0x01, 't', 'h', 'e', 'l', 'l', 'o' - }; - - rc = test_init_client(); - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); -#ifdef WOLFMQTT_V5 - test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; -#endif - - test_net.write = mock_net_write_accept; - test_net.read = mock_net_read_canned; - XMEMCPY(g_canned_buf, connack, sizeof(connack)); - g_canned_len = (int)sizeof(connack); - g_canned_pos = 0; - - XMEMSET(&connect, 0, sizeof(connect)); - connect.keep_alive_sec = 60; - connect.clean_session = 1; - connect.client_id = "test_client"; - - rc = MQTT_CODE_CONTINUE; - for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { - rc = MqttClient_Connect(&test_client, &connect); - } - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); - - /* Fixed header (2) plus Topic Name (3) exactly fill tx_buf */ - test_client.tx_buf_len = 5; - test_net.write = mock_net_write_capture; - g_connect_wire_len = 0; - - XMEMSET(&publish, 0, sizeof(publish)); - publish.qos = MQTT_QOS_0; - publish.topic_name = "t"; - publish.buffer = payload; - publish.total_len = (word32)(sizeof(payload) - 1); - publish.buffer_len = publish.total_len; - - rc = MQTT_CODE_CONTINUE; - for (i = 0; i < 10 && (rc == MQTT_CODE_CONTINUE || - rc == MQTT_CODE_PUB_CONTINUE); i++) { - rc = MqttClient_Publish(&test_client, &publish); - } - test_client.tx_buf_len = TEST_TX_BUF_SIZE; - - ASSERT_EQ(MQTT_CODE_SUCCESS, rc); - ASSERT_EQ((int)sizeof(expected), g_connect_wire_len); - ASSERT_EQ(0, XMEMCMP(expected, g_connect_wire, sizeof(expected))); -} /* [MQTT-3.3.4] The inbound QoS 2 dedup table holds MQTT_MAX_RECV_QOS2 packet * ids; without an advertised Receive Maximum a server may keep more QoS 2 @@ -7274,6 +7503,43 @@ static int test_accept_message_cb(MqttClient* client, MqttMessage* msg, return MQTT_CODE_SUCCESS; } +/* MqttClient_WaitMessage waits on the client's shared msg object. Once a + * packet that needs no reply (here an unsolicited PUBACK, MQTT 3.1.1 section + * 3.4) has been handled, the next call must read the next packet, not handle + * the PUBACK again. Frames: PUBACK id 9, then a QoS 0 PUBLISH on "t". */ +TEST(wait_message_reads_each_packet_once) +{ + int rc; + static const byte frames[] = { + 0x40, 0x02, 0x00, 0x09, + 0x30, 0x04, 0x00, 0x01, 't', 'x' + }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif + test_client.msg_cb = test_accept_message_cb; + g_msg_cb_calls = 0; + (void)MqttClient_Flags(&test_client, 0, + MQTT_CLIENT_FLAG_IS_CONNECTED | MQTT_CLIENT_FLAG_CONNECT_SENT); + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, frames, sizeof(frames)); + g_canned_len = (int)sizeof(frames); + g_canned_pos = 0; + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(4, g_canned_pos); + + rc = MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_EQ(g_canned_len, g_canned_pos); + ASSERT_EQ(1, g_msg_cb_calls); +} + #ifdef WOLFMQTT_NONBLOCK static int g_partial_vbi_read_step; static byte g_partial_vbi_publish[131]; @@ -7445,6 +7711,7 @@ TEST(wait_message_blocking_idle_timeout_keeps_connection) * PUBLISH that cannot be resolved; it must be rejected, not delivered to the * callback with a zero-length topic. This fixed wire fixture reaches the * preliminary packet decode used by MqttClient_WaitMessage. */ + TEST(wait_message_v5_empty_topic_with_alias_rejected) { int rc; @@ -8735,7 +9002,9 @@ void run_mqtt_client_tests(void) /* MqttClient_DeInit tests */ RUN_TEST(deinit_null_client); RUN_TEST(deinit_after_init); - RUN_TEST(deinit_scrubs_tx_rx_buffers); + RUN_TEST(deinit_leaves_caller_buffers_untouched); + RUN_TEST(net_disconnect_wait_null_client); + RUN_TEST(net_disconnect_wait_closes_transport); /* MqttClient_Connect tests */ RUN_TEST(connect_null_client); @@ -8765,7 +9034,11 @@ void run_mqtt_client_tests(void) #ifdef WOLFMQTT_NONBLOCK RUN_TEST(connect_auth_pending_blocks_other_packets); RUN_TEST(connect_auth_pending_allows_disconnect); + RUN_TEST(connect_auth_pending_blocks_unsubscribe_and_keepalive); + RUN_TEST(connect_auth_pending_allows_auth); + RUN_TEST(connect_auth_pending_cleared_by_reconnect); #endif + RUN_TEST(connect_auth_pending_raised_before_connect_write); RUN_TEST(wait_message_v311_rejects_auth_packet_type); RUN_TEST(wait_message_v5_accepts_auth_packet_type); RUN_TEST(wait_message_v311_rejects_disconnect_packet_type); @@ -8820,6 +9093,9 @@ void run_mqtt_client_tests(void) RUN_TEST(publish_refused_when_replay_pool_full_on_persistent_session); RUN_TEST(unsolicited_pubrec_does_not_fill_replay_pool); RUN_TEST(pubrec_for_qos1_id_keeps_publish_replay); +#if WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(pubrec_for_unslotted_qos2_records_pubrel); +#endif RUN_TEST(publish_allowed_past_replay_pool_on_clean_session); #ifdef WOLFMQTT_V5 RUN_TEST(publish_allowed_past_replay_pool_when_connack_ends_session); @@ -8909,7 +9185,6 @@ void run_mqtt_client_tests(void) RUN_TEST(connect_clientid_change_clears_qos2_dedup); RUN_TEST(connect_clean1_clears_qos2_dedup); RUN_TEST(connect_resume_declined_clears_qos2_dedup); - RUN_TEST(publish_header_fills_tx_buf_sends_payload); RUN_TEST(connect_v5_advertises_receive_max); RUN_TEST(connect_v5_app_receive_max_preserved); #endif @@ -8997,6 +9272,8 @@ void run_mqtt_client_tests(void) RUN_TEST(wait_message_blocking_timeout_mid_body_fails_connection); RUN_TEST(wait_message_blocking_idle_timeout_keeps_connection); #endif + RUN_TEST(wait_message_reads_each_packet_once); + RUN_TEST(publish_header_fills_tx_buf_sends_payload); RUN_TEST(wait_message_qos0_publish_no_ack); RUN_TEST(wait_message_qos1_publish_acks_puback); RUN_TEST(wait_message_qos2_publish_acks_pubrec); diff --git a/tests/test_mqtt_props_init.c b/tests/test_mqtt_props_init.c index 6fc3c5a89..8b3c0ac34 100644 --- a/tests/test_mqtt_props_init.c +++ b/tests/test_mqtt_props_init.c @@ -31,6 +31,8 @@ static int sem_fail_lock_skip; static int quota_write_calls; static MqttClient* sem_flag_client; static word32 sem_flag_bits; +static MqttClient* io_lock_client; +static int io_locks_held_at_close; #ifdef ENABLE_MQTT_CURL #define TEST_CLIENT_LOCK_COUNT 4 @@ -96,11 +98,11 @@ int wm_SemFree(wm_Sem* sem) int wm_SemLock(wm_Sem* sem) { /* Fail one acquisition of sem_fail_lock made while - * sem_fail_lock_while_held is owned, after skipping sem_fail_lock_skip - * such acquisitions, so a test can target a single lock inside a larger - * operation. */ - if (sem == sem_fail_lock && sem_fail_lock_while_held != NULL && - sem_fail_lock_while_held->lock_count > 0) { + * sem_fail_lock_while_held is owned (or any acquisition when it is NULL), + * after skipping sem_fail_lock_skip such acquisitions, so a test can + * target a single lock inside a larger operation. */ + if (sem == sem_fail_lock && (sem_fail_lock_while_held == NULL || + sem_fail_lock_while_held->lock_count > 0)) { if (sem_fail_lock_skip > 0) { sem_fail_lock_skip--; } @@ -633,6 +635,127 @@ static int test_keepalive_flag_read_takes_client_lock(void) return 0; } +static int io_lock_disconnect_calls; + +static int io_lock_net_disconnect(void* context) +{ + (void)context; + io_lock_disconnect_calls++; + if (io_lock_client != NULL) { + io_locks_held_at_close = + (io_lock_client->lockRecv.lock_count > 0 && + io_lock_client->lockSend.lock_count > 0); + } + return MQTT_CODE_SUCCESS; +} + +/* MqttClient_NetDisconnect_Wait releases the transport only while holding + * lockRecv and lockSend, so no read or write on another thread can still be + * using socket or TLS state when it is freed. The transport's disconnect + * callback records whether both I/O locks were held when it ran. */ +static int test_net_disconnect_wait_holds_io_locks(void) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = ping_net_write; + net.disconnect = io_lock_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "disconnect-wait client initialization returned %d\n", + rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + io_lock_client = &client; + io_locks_held_at_close = 0; + + rc = MqttClient_NetDisconnect_Wait(&client); + io_lock_client = NULL; + + if (rc != MQTT_CODE_SUCCESS || !io_locks_held_at_close || + client.lockRecv.lock_count != 0 || + client.lockSend.lock_count != 0 || + (MqttClient_Flags(&client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED) != 0) { + fprintf(stderr, + "disconnect-wait rc=%d locks_held=%d recv=%d send=%d\n", + rc, io_locks_held_at_close, client.lockRecv.lock_count, + client.lockSend.lock_count); + MqttClient_DeInit(&client); + return 1; + } + + MqttClient_DeInit(&client); + return 0; +} + +/* If lockSend cannot be taken, MqttClient_NetDisconnect_Wait must release + * the lockRecv it already holds and leave the transport alone; if lockRecv + * cannot be taken, it must not touch the transport either. */ +static int run_net_disconnect_wait_lock_failure(int fail_send) +{ + int rc; + MqttClient client; + MqttNet net; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + XMEMSET(&net, 0, sizeof(net)); + net.connect = ping_net_connect; + net.read = ping_net_read; + net.write = ping_net_write; + net.disconnect = io_lock_net_disconnect; + + rc = MqttClient_Init(&client, &net, NULL, tx_buf, sizeof(tx_buf), + rx_buf, sizeof(rx_buf), 1000); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "lock-failure client initialization returned %d\n", + rc); + return 1; + } + (void)MqttClient_Flags(&client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + io_lock_disconnect_calls = 0; + if (fail_send) { + sem_fail_lock = &client.lockSend; + sem_fail_lock_while_held = &client.lockRecv; + } + else { + sem_fail_lock = &client.lockRecv; + sem_fail_lock_while_held = NULL; + } + + rc = MqttClient_NetDisconnect_Wait(&client); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + + if (rc != MQTT_CODE_ERROR_SYSTEM || io_lock_disconnect_calls != 0 || + client.lockRecv.lock_count != 0 || + client.lockSend.lock_count != 0 || + (MqttClient_Flags(&client, 0, 0) & + MQTT_CLIENT_FLAG_IS_CONNECTED) == 0) { + fprintf(stderr, + "disconnect-wait fail_send=%d rc=%d closes=%d recv=%d send=%d\n", + fail_send, rc, io_lock_disconnect_calls, + client.lockRecv.lock_count, client.lockSend.lock_count); + fail = 1; + } + + MqttClient_DeInit(&client); + return fail; +} + int main(void) { if (test_sn_null_ping_resets_terminal_read_state() != 0) { @@ -662,5 +785,14 @@ int main(void) if (test_keepalive_flag_read_takes_client_lock() != 0) { return 1; } + if (test_net_disconnect_wait_holds_io_locks() != 0) { + return 1; + } + if (run_net_disconnect_wait_lock_failure(1) != 0) { + return 1; + } + if (run_net_disconnect_wait_lock_failure(0) != 0) { + return 1; + } return 0; } diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 95cbdba39..7dcc0964e 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -983,6 +983,24 @@ TEST(sn_willmsg_encode_failure_scrubs_tx_buf) } } ASSERT_NO_PENDRESP(); + + /* The connect state is rewound, so a retry with the same SN_Connect and + * a Will Message that fits sends a new CONNECT and completes. */ + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_EQ(0, mc.will_done); /* SN_WILL_DONE_NONE, private to the client */ + mc.will.willMsg = (byte*)"offline"; + mc.will.willMsgLen = 7; + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + mock_net_push(&g_mock, WILLMSGREQ_FRAME, (int)sizeof(WILLMSGREQ_FRAME)); + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + g_mock.out_len = 0; + + rc = sn_connect_pump(&mc, NULL); + + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_TRUE(g_mock.out_len >= 2); + ASSERT_EQ(SN_MSG_TYPE_CONNECT, g_mock.out[1]); + ASSERT_NO_PENDRESP(); } /* ============================================================================ @@ -1512,6 +1530,63 @@ TEST(sn_qos2_retransmit_not_delivered_twice) ASSERT_EQ(3, sn_out_count(&g_mock, SN_MSG_TYPE_PUBREC)); ASSERT_EQ(1, sn_out_count(&g_mock, SN_MSG_TYPE_PUBCOMP)); } + +/* A new QoS 2 id that does not fit the de-duplication table cannot be + * tracked, so it is refused with MQTT_CODE_ERROR_PACKET_ID rather than + * delivered: delivered untracked, its retransmit would reach the application + * again (MQTT-SN 1.2 section 6.7). No PUBREC is sent. */ +TEST(sn_qos2_full_table_refuses_new_id) +{ + static const byte pub[] = { 0x09, SN_MSG_TYPE_PUBLISH, 0x40, 0x00, 0x0A, + 0x01, 0x00, 0x01, 0x02 }; /* id 0x0100 */ + int rc = MQTT_CODE_CONTINUE; + int i; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + g_client.msg_cb = sn_count_qos2_delivery; + g_sn_qos2_deliveries = 0; + for (i = 0; i < MQTT_MAX_RECV_QOS2; i++) { + g_client.recv_qos2_pending[i] = (word16)(i + 1); + } + mock_net_push(&g_mock, pub, (int)sizeof(pub)); + + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = SN_Client_WaitMessage(&g_client, 1000); + } + + ASSERT_EQ(MQTT_CODE_ERROR_PACKET_ID, rc); + ASSERT_EQ(0, g_sn_qos2_deliveries); + ASSERT_EQ(0, sn_out_count(&g_mock, SN_MSG_TYPE_PUBREC)); +} + +/* A clean-session CONNECT starts with no inbound QoS 2 flows, so the + * de-duplication table is cleared once the gateway accepts it; a + * non-clean-session CONNECT resumes and keeps it. */ +static int sn_connect_keeps_qos2_id(int clean_session) +{ + SN_Connect mc; + int rc; + + if (sn_client_init(0) != MQTT_CODE_SUCCESS) { + return -1; + } + g_client.recv_qos2_pending[0] = 0x0042; + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + sn_will_setup_connect(&mc); + mc.enable_lwt = 0; + mc.clean_session = (byte)clean_session; + rc = sn_connect_pump(&mc, NULL); + if (rc != MQTT_CODE_SUCCESS) { + return -1; + } + return g_client.recv_qos2_pending[0] == 0x0042; +} + +TEST(sn_qos2_table_cleared_by_clean_session_connect) +{ + ASSERT_EQ(0, sn_connect_keeps_qos2_id(1)); + ASSERT_EQ(1, sn_connect_keeps_qos2_id(0)); +} #endif /* WOLFMQTT_MAX_QOS >= 2 */ #ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS @@ -3562,6 +3637,8 @@ int main(int argc, char** argv) RUN_TEST(sn_wait_message_reads_each_packet_once); #if WOLFMQTT_MAX_QOS >= 2 RUN_TEST(sn_qos2_retransmit_not_delivered_twice); + RUN_TEST(sn_qos2_full_table_refuses_new_id); + RUN_TEST(sn_qos2_table_cleared_by_clean_session_connect); #endif RUN_TEST(sn_ping_no_continue); RUN_TEST(sn_ping_null_no_continue); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 04bab4af6..21f30ff7d 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -386,9 +386,18 @@ typedef struct MqttBrokerPersistHooks { * truncate it. */ int (*kv_put)(void* ctx, byte ns, const byte* key, word16 key_len, const byte* blob, word32 blob_len); + /* On input *inout_len is the capacity of out; on success it is set to + * the record length. Return MQTT_CODE_ERROR_NOT_FOUND for a missing key + * and MQTT_CODE_ERROR_OUT_OF_BUFFER when the record is larger than the + * buffer - never a truncated success, since restore treats an oversized + * META record as a schema mismatch. */ int (*kv_get)(void* ctx, byte ns, const byte* key, word16 key_len, byte* out, word32* inout_len); int (*kv_del)(void* ctx, byte ns, const byte* key, word16 key_len); + /* Calls cb for each record in namespace ns. Every key passed to cb must + * be accepted unchanged by kv_get and kv_del: the broker deletes records + * it finds while iterating and repeats until none match, so a key that + * kv_del cannot remove makes that loop spin. */ int (*kv_iter)(void* ctx, byte ns, MqttBrokerPersist_IterCb cb, void* cb_ctx); diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 96776aae0..947706b7d 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -576,6 +576,10 @@ WOLFMQTT_API int MqttClient_Init( int cmd_timeout_ms); /*! \brief Cleans up resources allocated to the MqttClient structure + * \note Does not access the tx_buf and rx_buf passed to + MqttClient_Init, so the caller may free them before or after + this call. MqttClient_NetDisconnect scrubs both buffers when + the transport is closed. * \param client Pointer to MqttClient structure */ WOLFMQTT_API void MqttClient_DeInit(MqttClient *client); @@ -655,7 +659,10 @@ WOLFMQTT_API int MqttClient_Connect( Maximum QoS, Retain unavailable, Topic Alias above the server maximum, or Receive Maximum quota exhausted), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -723,7 +730,10 @@ WOLFMQTT_API int MqttClient_Publish_ex( broker-rejection detection for QoS>0 is required. * \return MQTT_CODE_SUCCESS, MQTT_CODE_CONTINUE (for non-blocking), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -750,7 +760,10 @@ WOLFMQTT_API int MqttClient_Publish_WriteOnly( v3.1.1 rejection is 0x80, v5 rejection is any reason_code >= 0x80), MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or another MQTT_CODE_ERROR_* for transport/protocol @@ -768,7 +781,10 @@ WOLFMQTT_API int MqttClient_Subscribe( with topic list. * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), MQTT_CODE_ERROR_PACKET_ID if the Packet Identifier is still awaiting its acknowledgement [MQTT-2.3.1-2], or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -783,7 +799,10 @@ WOLFMQTT_API int MqttClient_Unsubscribe( * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_Ping( @@ -797,7 +816,10 @@ WOLFMQTT_API int MqttClient_Ping( * \param ping Pointer to MqttPing structure * \return MQTT_CODE_SUCCESS, MQTT_CODE_ERROR_STAT if CONNECT has not been sent on this - Network Connection [MQTT-3.1.0-1], + Network Connection [MQTT-3.1.0-1], or while a v5 CONNECT + carrying an Authentication Method awaits its CONNACK + [MQTT-3.1.2-30] (only MqttClient_Auth and + MqttClient_Disconnect may be called then), or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) */ WOLFMQTT_API int MqttClient_Ping_ex(MqttClient *client, MqttPing* ping); @@ -876,6 +898,13 @@ WOLFMQTT_API int MqttClient_Disconnect_ex( connection disconnected (clears the connected flag); the application should call MqttClient_NetDisconnect to tear down the transport. + * \note In builds without WOLFMQTT_NONBLOCK a partly received packet + cannot be resumed, so a read that times out after part of a + packet arrived returns MQTT_CODE_ERROR_NETWORK and marks the + connection disconnected, rather than MQTT_CODE_ERROR_TIMEOUT. + Use a timeout_ms long enough for one packet to arrive on the + link. A timeout before any byte of a packet still returns + MQTT_CODE_ERROR_TIMEOUT. * \param client Pointer to MqttClient structure * \param timeout_ms Milliseconds until read timeout * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -966,6 +995,13 @@ WOLFMQTT_API int MqttClient_NetConnect( WOLFMQTT_MULTITHREAD builds a buffer whose read or write is still active is left to its owner; release an abandoned operation with MqttClient_CancelMessage, which scrubs it. + * \note In WOLFMQTT_MULTITHREAD builds this does not wait for other + threads: no other thread may be inside a client call that + reads or writes the network (for example blocked in + MqttClient_WaitMessage), or it could use transport or TLS + state while it is freed. Stop those threads first, or use + MqttClient_NetDisconnect_Wait. This function may be called + from a message callback. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -973,6 +1009,23 @@ WOLFMQTT_API int MqttClient_NetConnect( WOLFMQTT_API int MqttClient_NetDisconnect( MqttClient *client); +/*! \brief Performs a network disconnect after other threads' network I/O + on this client has finished + * \note In WOLFMQTT_MULTITHREAD builds this takes the receive lock and + then the send lock before releasing the transport, so a read + or write in progress on another thread completes (or times + out) first and nothing can use transport or TLS state while it + is freed. It must not be called from a message callback or + other code already holding those locks, which would deadlock; + use MqttClient_NetDisconnect there. In other builds it is the + same as MqttClient_NetDisconnect. + * \param client Pointer to MqttClient structure + * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* + (see enum MqttPacketResponseCodes) + */ +WOLFMQTT_API int MqttClient_NetDisconnect_Wait( + MqttClient *client); + /*! \brief Gets number version of connected protocol version * \param client Pointer to MqttClient structure * \return 4 (v3.1.1) or 5 (v5) diff --git a/wolfmqtt/mqtt_packet.h b/wolfmqtt/mqtt_packet.h index 95075e991..c1899cbec 100644 --- a/wolfmqtt/mqtt_packet.h +++ b/wolfmqtt/mqtt_packet.h @@ -845,6 +845,11 @@ WOLFMQTT_API int MqttEncode_Vbi(byte *buf, word32 x); /* Packet Encoders/Decoders */ WOLFMQTT_API int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *connect); +/* Decode a CONNACK. In v5 builds set connect_ack->protocol_level first: it + * selects the v5 form (Properties after the return code) and which return + * codes are valid. A code outside the MQTT 3.1.1 section 3.2.2.3 table, or + * the MQTT 5.0 section 3.2.2.2 table at level 5, is rejected with + * MQTT_CODE_ERROR_MALFORMED_DATA. */ WOLFMQTT_API int MqttDecode_ConnectAck(byte *rx_buf, int rx_buf_len, MqttConnectAck *connect_ack); WOLFMQTT_API int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, @@ -900,6 +905,12 @@ WOLFMQTT_API int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth); WOLFMQTT_API int MqttEncode_Auth(byte *tx_buf, int tx_buf_len, MqttAuth *auth); +/* Encode or decode a v5 property list. packet selects the allow-list the + * properties are checked against (MQTT 5.0 section 2.2.2.2). Will Properties + * have their own list (section 3.1.3.2), selected with + * MQTT_PACKET_TYPE_RESERVED; MQTT_PACKET_TYPE_CONNECT covers only the CONNECT + * Properties, so Will Delay Interval and Message Expiry Interval are rejected + * there. */ WOLFMQTT_API int MqttEncode_Props(MqttPacketType packet, MqttProp* props, byte* buf); WOLFMQTT_API int MqttDecode_Props(MqttPacketType packet, MqttProp** props, @@ -913,8 +924,13 @@ WOLFMQTT_API int MqttProps_Init(void); * functions are no longer in use through that reference. Calls must not run * concurrently with each other. */ WOLFMQTT_API int MqttProps_ShutDown(void); +/* Append a cleared property to the end of the list at *head and return it, + * or NULL if no property can be allocated. */ WOLFMQTT_API MqttProp* MqttProps_Add(MqttProp **head); +/* Release every property in the list starting at head. */ WOLFMQTT_API int MqttProps_Free(MqttProp *head); +/* Return the first property in the list starting at head whose type is + * type, or NULL if the list is empty or holds none. */ WOLFMQTT_API MqttProp* MqttProps_FindType(MqttProp *head, MqttPropertyType type); #endif From 3e56ca7c6828c5bbb2c2c8a3cc86d4a370335d74 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 22 Oct 2026 12:03:25 -0500 Subject: [PATCH 51/52] F-5117 - Wait for an in-flight reader before unlinking a pending response --- BROKER.md | 2 +- README.md | 4 +- src/mqtt_broker.c | 465 ++++++++++- src/mqtt_broker_persist.c | 62 +- src/mqtt_client.c | 347 ++++++-- src/mqtt_sn_client.c | 58 +- tests/include.am | 1 + tests/test_broker_connect.c | 1459 ++++++++++++++++++++++++++++++++-- tests/test_mqtt_client.c | 784 ++++++++++++++++++ tests/test_mqtt_props_init.c | 268 +++++++ tests/test_mqtt_sn_client.c | 166 +++- wolfmqtt/mqtt_broker.h | 44 +- wolfmqtt/mqtt_client.h | 42 +- wolfmqtt/mqtt_types.h | 11 +- 14 files changed, 3551 insertions(+), 162 deletions(-) diff --git a/BROKER.md b/BROKER.md index 3d8bdb088..65974a6e5 100644 --- a/BROKER.md +++ b/BROKER.md @@ -187,7 +187,7 @@ finds while iterating and repeats until none match. |---|---|---| | `BROKER_MAX_PERSIST_SESSIONS` | 64 | Dynamic-memory persistent sessions (connected and offline); a new persistent CONNECT beyond it is refused, existing sessions are never evicted | | `BROKER_MAX_OFFLINE_MSGS_PER_SUB` | 32 | Offline queue depth per session | -| `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 5 | On-disk record schema version | +| `WOLFMQTT_BROKER_PERSIST_SCHEMA_VER` | 6 | On-disk record schema version | Because existing persistent sessions are never evicted, any client allowed to connect can use up `BROKER_MAX_PERSIST_SESSIONS` by opening persistent sessions diff --git a/README.md b/README.md index a15fd589c..bb52b2353 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ These are the network connect / disconnect interfaces that wrap the MqttNet call * `int MqttClient_NetConnect(MqttClient *client, const char* host, word16 port, int timeout_ms, int use_tls, MqttTlsCb cb);` * `int MqttClient_NetDisconnect(MqttClient *client);` -* `int MqttClient_NetDisconnect_Wait(MqttClient *client);` - like `MqttClient_NetDisconnect`, but in `WOLFMQTT_MULTITHREAD` builds waits for other threads' network I/O on the client to finish first. Do not call it from a message callback. +* `int MqttClient_NetDisconnect_Wait(MqttClient *client);` - like `MqttClient_NetDisconnect`, but in `WOLFMQTT_MULTITHREAD` builds waits for other threads' network I/O on the client to finish first. Do not call it from a message, property or publish payload callback; where thread identities are available it returns `MQTT_CODE_ERROR_STAT` there instead of deadlocking. Helper functions: @@ -226,6 +226,8 @@ The multi-threading feature can also be used with the non-blocking socket (--ena If you are having issues with thread synchronization on Linux consider using not the conditional signal (`WOLFMQTT_NO_COND_SIGNAL`). +With `WOLFMQTT_NO_COND_SIGNAL`, or with `WOLFMQTT_USER_THREADING` unless `WOLFMQTT_SEM_SIGNAL` is defined, a write-only publish or `MqttClient_CancelMessage` whose response another thread is decoding waits for that thread to release the receive lock instead of being signalled. A thread that blocks in `MqttClient_WaitMessage` can then hold them up until its read returns. + ### Atomic publish and subscribe examples In the `examples/pub-sub` folder, there are two simple client examples: * mqtt-pub - publishes to a topic diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 387eed23a..b7d36137d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1950,8 +1950,100 @@ static void BrokerProps_FreeClone(MqttProp* head) head = next; } } + +/* Encode e->props as PUBLISH Properties (no length prefix) into buf, or only + * size them when buf is NULL. Returns the length or a negative error. */ +WOLFMQTT_LOCAL int BrokerOutPub_EncodeProps(const BrokerOutPub* e, byte* buf) +{ + if (e == NULL) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } + if (e->props == NULL) { + return 0; + } + return MqttEncode_Props(MQTT_PACKET_TYPE_PUBLISH, e->props, buf); +} + +/* Decode PUBLISH Properties written by BrokerOutPub_EncodeProps into a + * broker-owned list on e. The whole buffer must decode, and a Message Expiry + * Interval must be present exactly when e->has_expiry is set, as + * BrokerOutPub_Alloc records it. */ +WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, + word32 len) +{ + MqttProp* decoded = NULL; + const MqttProp* prop; + int rc; + int shut_rc; + int expiry_found = 0; + + if (e == NULL || e->props != NULL || (buf == NULL && len > 0)) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); + } + if (len == 0) { + return e->has_expiry ? MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA) + : 0; + } + /* Restore runs before any client exists, so nothing else holds the + * shared property pool lock yet. */ + rc = MqttProps_Init(); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &decoded, (byte*)buf, + len, len); + if (rc >= 0 && (word32)rc != len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (rc >= 0) { + for (prop = decoded; prop != NULL; prop = prop->next) { + if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + expiry_found = 1; + break; + } + } + if (expiry_found != (e->has_expiry ? 1 : 0)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + } + if (rc >= 0) { + /* The decoded list points into buf, so keep a deep copy. */ + e->props = BrokerProps_Clone(decoded); + if (e->props == NULL) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + } + if (decoded != NULL) { + (void)MqttProps_Free(decoded); + } + shut_rc = MqttProps_ShutDown(); + if (rc >= 0 && shut_rc != MQTT_CODE_SUCCESS) { + BrokerProps_FreeClone(e->props); + e->props = NULL; + rc = shut_rc; + } + return (rc < 0) ? rc : 0; +} #endif /* WOLFMQTT_V5 */ +/* MQTT 5.0 section 3.3.2.3.3: has the copy's Message Expiry Interval passed? + * Time is in whole seconds, so an explicit zero interval has passed once a + * second has: a copy whose delivery starts at once still goes out. */ +static int BrokerOutPub_Expired(const BrokerOutPub* e, + WOLFMQTT_BROKER_TIME_T now) +{ + WOLFMQTT_BROKER_TIME_T elapsed; + + if (!e->has_expiry || now < e->enq_time) { + return 0; + } + elapsed = now - e->enq_time; + if (e->expiry_sec == 0) { + return (elapsed > 0) ? 1 : 0; + } + return (elapsed >= e->expiry_sec) ? 1 : 0; +} + /* Free a single queue entry (topic, payload, props, the entry itself). */ static void BrokerOutPub_Free(BrokerOutPub* e) { @@ -2085,6 +2177,7 @@ static BrokerOutPub* BrokerOutPub_Alloc(const char* topic, * was queued. */ for (prop = e->props; prop != NULL; prop = prop->next) { if (prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + e->has_expiry = 1; e->expiry_sec = prop->data_int; break; } @@ -2251,11 +2344,10 @@ static int BrokerClient_DrainOutQueue(BrokerClient* bc) } /* Expiry applies until onward delivery starts; a deferred write or a * DUP retransmission reuses the interval it was first sent with. */ - if (cur->expiry_sec > 0 && bc->out_q_pending_len == 0 && + if (cur->has_expiry && bc->out_q_pending_len == 0 && !cur->retransmit_dup && !cur->partial_send) { now = WOLFMQTT_BROKER_GET_TIME_S(); - if (now >= cur->enq_time && - now - cur->enq_time >= cur->expiry_sec) { + if (BrokerOutPub_Expired(cur, now)) { /* [MQTT-3.3.2-5] Delete the expired copy */ WBLOG_DBG(bc->broker, "broker: drain dropped expired PUBLISH sock=%d topic=%s", @@ -2703,6 +2795,12 @@ static void BrokerClient_Free(BrokerClient* bc) BROKER_FORCE_ZERO(bc->will_payload, bc->will_payload_len); WOLFMQTT_FREE(bc->will_payload); } +#ifdef WOLFMQTT_V5 + if (bc->will_props) { + BROKER_FORCE_ZERO(bc->will_props, bc->will_props_len); + WOLFMQTT_FREE(bc->will_props); + } +#endif #endif if (bc->tx_buf) { BROKER_FORCE_ZERO(bc->tx_buf, bc->tx_buf_len); @@ -5377,6 +5475,82 @@ static int BrokerTopicMatch(const char* filter, const char* topic); /* LWT (Last Will and Testament) helpers */ /* -------------------------------------------------------------------------- */ #ifdef WOLFMQTT_BROKER_WILL +#ifdef WOLFMQTT_V5 +/* Will Properties carried onto the Will PUBLISH. The Will Delay Interval only + * schedules it (MQTT 5.0 section 3.1.3.2). */ +static int BrokerWill_IsPublishProp(const MqttProp* prop) +{ + return (prop->type == MQTT_PROP_PAYLOAD_FORMAT_IND || + prop->type == MQTT_PROP_MSG_EXPIRY_INTERVAL || + prop->type == MQTT_PROP_CONTENT_TYPE || + prop->type == MQTT_PROP_RESP_TOPIC || + prop->type == MQTT_PROP_CORRELATION_DATA || + prop->type == MQTT_PROP_USER_PROP); +} + +/* Encode the publishable Will Properties, keeping their order + * [MQTT-3.1.3-10], into buf, or only size them when buf is NULL. Returns the + * encoded length or a negative error. */ +static int BrokerWill_EncodeProps(const MqttProp* props, byte* buf) +{ + MqttProp one; + int len = 0; + int rc = 0; + + for (; props != NULL && rc >= 0; props = props->next) { + if (BrokerWill_IsPublishProp(props)) { + one = *props; + one.next = NULL; + rc = MqttEncode_Props(MQTT_PACKET_TYPE_PUBLISH, &one, + (buf != NULL) ? &buf[len] : NULL); + if (rc >= 0) { + len += rc; + } + } + } + if (rc >= 0 && len > 0xFFFF) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } + return (rc < 0) ? rc : len; +} + +/* Keep the publishable Will Properties until the Will is published. */ +static int BrokerClient_StoreWillProps(BrokerClient* bc, + const MqttProp* props) +{ + int len; + int rc = MQTT_CODE_SUCCESS; + + len = BrokerWill_EncodeProps(props, NULL); + if (len < 0) { + rc = len; + } +#ifdef WOLFMQTT_STATIC_MEMORY + else if (len > BROKER_MAX_WILL_PROPS_LEN) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } +#else + else if (len > 0) { + bc->will_props = (byte*)WOLFMQTT_MALLOC((size_t)len); + if (bc->will_props == NULL) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + } +#endif + if (rc == MQTT_CODE_SUCCESS && len > 0) { + rc = BrokerWill_EncodeProps(props, bc->will_props); + if (rc == len) { + bc->will_props_len = (word16)len; + rc = MQTT_CODE_SUCCESS; + } + else if (rc >= 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } + } + return rc; +} +#endif /* WOLFMQTT_V5 */ + static void BrokerClient_ClearWill(BrokerClient* bc) { if (bc == NULL) { @@ -5386,6 +5560,18 @@ static void BrokerClient_ClearWill(BrokerClient* bc) bc->will_qos = MQTT_QOS_0; bc->will_retain = 0; bc->will_delay_sec = 0; +#ifdef WOLFMQTT_V5 +#ifdef WOLFMQTT_STATIC_MEMORY + BROKER_FORCE_ZERO(bc->will_props, sizeof(bc->will_props)); +#else + if (bc->will_props != NULL) { + BROKER_FORCE_ZERO(bc->will_props, bc->will_props_len); + WOLFMQTT_FREE(bc->will_props); + bc->will_props = NULL; + } +#endif + bc->will_props_len = 0; +#endif #ifdef WOLFMQTT_STATIC_MEMORY BROKER_FORCE_ZERO(bc->will_topic, sizeof(bc->will_topic)); BROKER_FORCE_ZERO(bc->will_payload, sizeof(bc->will_payload)); @@ -5409,6 +5595,23 @@ static void BrokerClient_ClearWill(BrokerClient* bc) /* Pending will management (v5 Will Delay Interval) */ /* -------------------------------------------------------------------------- */ +#ifndef WOLFMQTT_STATIC_MEMORY +/* Scrub and free a pending Will's stored properties. */ +static void BrokerPendingWill_FreeProps(BrokerPendingWill* pw) +{ +#ifdef WOLFMQTT_V5 + if (pw->props != NULL) { + BROKER_FORCE_ZERO(pw->props, pw->props_len); + WOLFMQTT_FREE(pw->props); + pw->props = NULL; + } + pw->props_len = 0; +#else + (void)pw; +#endif +} +#endif + /* Add a pending will to be published after delay expires */ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) { @@ -5457,6 +5660,12 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) bc->will_payload_len); pw->payload_len = bc->will_payload_len; } + #ifdef WOLFMQTT_V5 + if (bc->will_props_len > 0) { + XMEMCPY(pw->props, bc->will_props, bc->will_props_len); + pw->props_len = bc->will_props_len; + } + #endif } } #else @@ -5505,6 +5714,18 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) rc = MQTT_CODE_ERROR_MEMORY; } } + #ifdef WOLFMQTT_V5 + if (rc == MQTT_CODE_SUCCESS && bc->will_props_len > 0) { + pw->props = (byte*)WOLFMQTT_MALLOC(bc->will_props_len); + if (pw->props != NULL) { + XMEMCPY(pw->props, bc->will_props, bc->will_props_len); + pw->props_len = bc->will_props_len; + } + else { + rc = MQTT_CODE_ERROR_MEMORY; + } + } + #endif } if (rc == MQTT_CODE_SUCCESS) { pw->next = broker->pending_wills; @@ -5522,6 +5743,7 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); } #endif @@ -5606,6 +5828,7 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); return; } @@ -5639,6 +5862,7 @@ static void BrokerPendingWill_FreeAll(MqttBroker* broker) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); pw = next; } @@ -5646,9 +5870,16 @@ static void BrokerPendingWill_FreeAll(MqttBroker* broker) #endif } +#ifdef WOLFMQTT_V5 +static MqttProp* BrokerProps_Find(MqttProp* head, MqttPropertyType type); +#endif static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, - MqttQoS qos, byte retain); + MqttQoS qos, byte retain +#ifdef WOLFMQTT_V5 + , byte* props_buf, word16 props_len +#endif + ); /* Process pending wills - publish any that have expired their delay */ static int BrokerPendingWill_Process(MqttBroker* broker) @@ -5678,7 +5909,11 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, - pw->payload, pw->payload_len, pw->qos, pw->retain); + pw->payload, pw->payload_len, pw->qos, pw->retain + #ifdef WOLFMQTT_V5 + , pw->props, pw->props_len + #endif + ); BROKER_FORCE_ZERO(pw, sizeof(BrokerPendingWill)); activity = 1; } @@ -5693,7 +5928,11 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, - pw->payload, pw->payload_len, pw->qos, pw->retain); + pw->payload, pw->payload_len, pw->qos, pw->retain + #ifdef WOLFMQTT_V5 + , pw->props, pw->props_len + #endif + ); if (prev) { prev->next = next; } @@ -5722,6 +5961,7 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BROKER_FORCE_ZERO(pw->payload, pw->payload_len); WOLFMQTT_FREE(pw->payload); } + BrokerPendingWill_FreeProps(pw); WOLFMQTT_FREE(pw); activity = 1; } @@ -6117,14 +6357,75 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) BrokerClient_PublishWillImmediate(broker, bc->will_topic, bc->will_payload, bc->will_payload_len, bc->will_qos, - bc->will_retain); + bc->will_retain + #ifdef WOLFMQTT_V5 + , bc->will_props, bc->will_props_len + #endif + ); BrokerClient_ClearWill(bc); } +/* A persistent subscription whose client is offline. */ +static int BrokerSub_IsOffline(const BrokerSub* sub) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + return (sub->in_use && + (sub->client == NULL || !sub->client->connected) && + BROKER_STR_VALID(sub->client_id)); +#else + return (sub->client == NULL && sub->client_id != NULL); +#endif +} + +/* An offline Session gets one queued copy at the highest QoS of all of its + * subscriptions matching topic [MQTT-3.3.4-2]; a copy per subscription could + * let its bounded queue evict the highest-QoS one. Returns 1 when sub is the + * Session's first matching subscription, setting *eff_qos to that QoS capped + * at qos, or 0 when an earlier subscription already covers the Session. */ +static int BrokerSubs_OfflineFirstMatch(MqttBroker* broker, + const BrokerSub* sub, const char* topic, MqttQoS qos, MqttQoS* eff_qos) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + int i; +#endif + const BrokerSub* s; + MqttQoS max_qos = MQTT_QOS_0; + int first = -1; + +#ifdef WOLFMQTT_STATIC_MEMORY + for (i = 0; i < BROKER_MAX_SUBS; i++) { + s = &broker->subs[i]; +#else + for (s = broker->subs; s != NULL; s = s->next) { +#endif + if (!BrokerSub_IsOffline(s) || !BROKER_STR_VALID(s->filter) || + XSTRCMP(s->client_id, sub->client_id) != 0 || + !BrokerTopicMatch(s->filter, topic)) { + continue; + } + if (first < 0) { + first = (s == sub) ? 1 : 0; + if (first == 0) { + /* An earlier subscription queues this Session's copy. */ + break; + } + } + if (s->qos > max_qos) { + max_qos = s->qos; + } + } + *eff_qos = (qos < max_qos) ? qos : max_qos; + return (first == 1); +} + /* Publish a will message immediately (shared by direct and deferred paths) */ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, - MqttQoS qos, byte retain) + MqttQoS qos, byte retain +#ifdef WOLFMQTT_V5 + , byte* props_buf, word16 props_len +#endif + ) { #ifdef WOLFMQTT_STATIC_MEMORY int i; @@ -6135,11 +6436,53 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, word32 clients_gen_snapshot; int t; #endif +#ifdef WOLFMQTT_V5 + MqttProp* props = NULL; + MqttProp* expiry_prop; + word32 expiry = 0; + int prop_rc; + int props_ref = 0; +#endif if (broker == NULL || topic == NULL) { return; } +#ifdef WOLFMQTT_V5 + /* The Will Properties travel with the Will on every delivery path + * (MQTT 5.0 section 3.1.3.2). */ + if (props_buf != NULL && props_len > 0) { + /* A delayed Will can fire after the last client was freed, so hold + * the shared property pool lock for the decoded list's lifetime. */ + prop_rc = MqttProps_Init(); + if (prop_rc != MQTT_CODE_SUCCESS) { + WBLOG_ERR(broker, + "broker: will property pool init failed rc=%d topic=%s", + prop_rc, BrokerLog_Sanitize(topic)); + return; + } + props_ref = 1; + prop_rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &props, + props_buf, props_len, props_len); + if (prop_rc < 0) { + WBLOG_ERR(broker, + "broker: will properties decode failed rc=%d topic=%s", + prop_rc, BrokerLog_Sanitize(topic)); + if (props != NULL) { + (void)MqttProps_Free(props); + props = NULL; + } + } + expiry_prop = BrokerProps_Find(props, MQTT_PROP_MSG_EXPIRY_INTERVAL); + if (expiry_prop != NULL) { + expiry = expiry_prop->data_int; + } + } + #ifndef WOLFMQTT_BROKER_RETAINED + (void)expiry; /* only the retained store uses it */ + #endif +#endif + /* Handle retain flag on will message */ if (retain) { if (payload_len == 0) { @@ -6147,7 +6490,13 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } else { int ret_rc = BrokerRetained_Store(broker, topic, payload, - payload_len, qos, 0); + payload_len, qos, + #ifdef WOLFMQTT_V5 + expiry + #else + 0 + #endif + ); if (ret_rc != MQTT_CODE_SUCCESS) { WBLOG_ERR(broker, "Retained store failed: %s", MqttClient_ReturnCodeToString(ret_rc)); @@ -6191,7 +6540,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, enqueue_rc = BrokerStaticOrphan_Enqueue(broker, orphan, topic, payload, payload_len, eff_qos, 0, 0 #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (enqueue_rc != MQTT_CODE_SUCCESS) { @@ -6228,6 +6577,9 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } #ifdef WOLFMQTT_V5 out_pub.protocol_level = wc->protocol_level; + if (wc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + out_pub.props = props; + } #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); @@ -6264,15 +6616,16 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BROKER_STR_VALID(sub->client_id) && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { - MqttQoS eff_qos = (qos < sub->qos) ? qos : sub->qos; - if (eff_qos >= MQTT_QOS_1) { + MqttQoS eff_qos; + if (BrokerSubs_OfflineFirstMatch(broker, sub, topic, qos, + &eff_qos) && eff_qos >= MQTT_QOS_1) { BrokerStaticOrphanSession* orphan = BrokerStaticOrphan_Find(broker, sub->client_id); if (orphan != NULL) { int enqueue_rc = BrokerStaticOrphan_Enqueue(broker, orphan, topic, payload, payload_len, eff_qos, 0, 0 #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (enqueue_rc != MQTT_CODE_SUCCESS) { @@ -6307,14 +6660,15 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, else if (sub->client == NULL && sub->client_id != NULL && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { - MqttQoS eff_qos = (qos < sub->qos) ? qos : sub->qos; - if (eff_qos > MQTT_QOS_0) { + MqttQoS eff_qos; + if (BrokerSubs_OfflineFirstMatch(broker, sub, topic, qos, + &eff_qos) && eff_qos > MQTT_QOS_0) { BrokerOrphanSession* o = BrokerOrphan_Find(broker, sub->client_id); if (o != NULL && BrokerOrphan_Enqueue(broker, o, topic, payload, payload_len, eff_qos, 0 #ifdef WOLFMQTT_V5 - , NULL + , props #endif ) < 0) { WBLOG_ERR(broker, @@ -6343,6 +6697,12 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, !BrokerClients_StillLinked(broker, wc)) { continue; } + /* A write started on a connection being taken over could leave its + * Session Taken Over DISCONNECT unsent. QoS 0 to it is dropped and + * QoS 1/2 is only queued, for the Session if it is resumed. */ + if (wc->takeover_closing && eff_qos == MQTT_QOS_0) { + continue; + } if (eff_qos >= MQTT_QOS_1 || wc->connack_pending_len != 0 || wc->out_q_pending_len != 0 || @@ -6375,7 +6735,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BrokerOutPub* e = BrokerOutPub_Alloc(topic, (payload_len > 0) ? payload : NULL, payload_len #ifdef WOLFMQTT_V5 - , NULL + , props #endif ); if (e == NULL) { @@ -6402,7 +6762,9 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, e->protocol_level = wc->protocol_level; #endif BrokerClient_EnqueueOutPub(wc, e); - BrokerClient_DrainOutQueue(wc); + if (!wc->takeover_closing) { + BrokerClient_DrainOutQueue(wc); + } } } } @@ -6420,6 +6782,9 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, out_pub.total_len = payload_len; #ifdef WOLFMQTT_V5 out_pub.protocol_level = wc->protocol_level; + if (wc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + out_pub.props = props; + } #endif enc_rc = MqttEncode_Publish(wc->tx_buf, BROKER_CLIENT_TX_SZ(wc), &out_pub, 0); @@ -6440,6 +6805,18 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } BrokerFanout_Free(&targets); #endif +#ifdef WOLFMQTT_V5 + if (props != NULL) { + (void)MqttProps_Free(props); + } + if (props_ref) { + prop_rc = MqttProps_ShutDown(); + if (prop_rc != MQTT_CODE_SUCCESS) { + WBLOG_ERR(broker, "broker: will property pool release failed " + "rc=%d", prop_rc); + } + } +#endif } #endif /* WOLFMQTT_BROKER_WILL */ @@ -6682,6 +7059,10 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #ifdef WOLFMQTT_V5 int auto_assigned = 0; #endif +#if defined(WOLFMQTT_STATIC_MEMORY) && \ + (defined(WOLFMQTT_V5) || WOLFMQTT_MAX_QOS >= 1) + int will_props_len = 0; +#endif XMEMSET(&mc, 0, sizeof(mc)); XMEMSET(&ack, 0, sizeof(ack)); @@ -7151,6 +7532,40 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } goto send_connack; } +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_STATIC_MEMORY) + /* Will Properties are v5 only. Refuse rather than drop any. */ + will_props_len = BrokerWill_EncodeProps(mc.lwt_msg->props, NULL); + if (will_props_len > BROKER_MAX_WILL_PROPS_LEN) { + WBLOG_ERR(broker, + "broker: LWT properties too large (max %d) sock=%d", + BROKER_MAX_WILL_PROPS_LEN, (int)bc->sock); + ack.return_code = MQTT_REASON_PACKET_TOO_LARGE; + goto send_connack; + } +#endif +#if defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 + /* A Will at QoS 1/2 after the WOLFMQTT_MAX_QOS limit may be queued + * for an offline Session, so payload and properties must fit one + * offline slot together. */ + if (mc.lwt_msg->qos > MQTT_QOS_0 && + (word32)will_props_len + mc.lwt_msg->total_len > + BROKER_MAX_STATIC_OFFLINE_DATA_LEN) { + WBLOG_ERR(broker, + "broker: LWT payload and properties too large (max %d) " + "sock=%d", BROKER_MAX_STATIC_OFFLINE_DATA_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_PACKET_TOO_LARGE; + } + else + #endif + { + ack.return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL; + } + goto send_connack; + } +#endif #ifdef WOLFMQTT_V5 /* [MQTT-3.2.2-11] A v5 Server that caps Maximum QoS below the requested * Will QoS must refuse the connection, not silently downgrade the Will @@ -7216,6 +7631,12 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #endif bc->will_payload_len = wp_len; } +#ifdef WOLFMQTT_V5 + if (will_stored && BrokerClient_StoreWillProps(bc, + mc.lwt_msg->props) != MQTT_CODE_SUCCESS) { + will_stored = 0; + } +#endif if (!will_stored) { /* A Will missing its topic or payload would be dropped or * published altered, so refuse the connection instead. */ @@ -7458,6 +7879,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " "old sock=%d", BrokerLog_Sanitize(bc->client_id), (int)old->sock); +#ifndef WOLFMQTT_STATIC_MEMORY + old->takeover_closing = 1; +#endif #ifdef ENABLE_MQTT_WEBSOCKET /* Guard old across its takeover fan-out: a re-entrant * LWS_CALLBACK_CLOSED for old's dropped socket must take the @@ -8977,6 +9401,11 @@ static void BrokerClient_ErrorClose(MqttBroker* broker, BrokerClient* bc, bc->close_after_direct = 1; return; } + #elif defined(WOLFMQTT_NONBLOCK) + /* Static clients cannot resume a deferred write, and closing part way + * through one would leave a truncated DISCONNECT on the wire, so close + * without it. The DISCONNECT is a SHOULD, not a MUST. */ + (void)reason; #else (void)BrokerSend_Disconnect(bc, reason); #endif diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index ebfddc3b4..f11f80621 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1032,7 +1032,7 @@ static int wmqb_outq_build_key(const char* client_id, word16 packet_id, * 2 1 retain * 3 1 protocol_level * 4 1 retransmit_dup - * 5 1 _reserved + * 5 1 flags (bit 0: Message Expiry Interval present) * 6 2 packet_id (redundant with key, simplifies decode) * 8 8 enq_time * 16 8 enqueue_seq @@ -1040,7 +1040,9 @@ static int wmqb_outq_build_key(const char* client_id, word16 packet_id, * 28 2 topic_len * 30 N topic * 30+N 4 payload_len - * 34+N M payload */ + * 34+N M payload + * 34+N+M 4 props_len + * 38+N+M P props (v5 PUBLISH Properties, no length prefix) */ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, const struct BrokerOutPub* e) { @@ -1062,6 +1064,7 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, byte key[256 + 3]; word16 key_len; int rc; + int props_len = 0; if (broker == NULL || broker->persist == NULL || client_id == NULL || p_e == NULL) { @@ -1086,8 +1089,14 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, if (rc != 0) { return rc; } +#ifdef WOLFMQTT_V5 + props_len = BrokerOutPub_EncodeProps(p_e, NULL); + if (props_len < 0) { + return props_len; + } +#endif body_len = 1 + 1 + 1 + 1 + 2 + 2 + 8 + 8 + 4 + 2 + topic_len + 4 - + payload_len; + + payload_len + 4 + (word32)props_len; total_len = WMQB_HDR_LEN + body_len; buf = (byte*)WOLFMQTT_MALLOC(total_len); if (buf == NULL) { @@ -1100,7 +1109,7 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, *bp++ = p_e->retain; *bp++ = p_e->protocol_level; *bp++ = p_e->retransmit_dup; - *bp++ = 0; /* _reserved */ + *bp++ = p_e->has_expiry ? 1 : 0; wmqb_w_u16(bp, p_e->packet_id); bp += 2; wmqb_w_u64(bp, (word64)p_e->enq_time); bp += 8; wmqb_w_u64(bp, p_e->enqueue_seq); bp += 8; @@ -1111,9 +1120,18 @@ int BrokerPersist_PutOutPub(MqttBroker* broker, const char* client_id, if (payload_len > 0 && p_e->payload != NULL) { XMEMCPY(bp, p_e->payload, payload_len); } + bp += payload_len; + wmqb_w_u32(bp, (word32)props_len); bp += 4; +#ifdef WOLFMQTT_V5 + if (props_len > 0 && BrokerOutPub_EncodeProps(p_e, bp) != props_len) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + } +#endif - rc = wmqb_kv_put_commit(broker, BROKER_PERSIST_NS_OUTQ, - key, key_len, buf, total_len); + if (rc == 0) { + rc = wmqb_kv_put_commit(broker, BROKER_PERSIST_NS_OUTQ, + key, key_len, buf, total_len); + } #ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT wmqb_force_zero(buf, total_len); #endif @@ -2458,10 +2476,12 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, byte retain; byte protocol_level; byte retransmit_dup; + byte flags; word16 packet_id; word64 enq_time; word64 enqueue_seq; word32 expiry_sec; + word32 props_len; BrokerOutPub** prev_link; BrokerOutPub* iter; @@ -2487,8 +2507,8 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, retain = *p++; protocol_level = *p++; retransmit_dup = *p++; - p++; /* _reserved */ - if (retransmit_dup > 1) { + flags = *p++; + if (retransmit_dup > 1 || (flags & ~0x01) != 0) { return MQTT_CODE_ERROR_MALFORMED_DATA; } packet_id = wmqb_r_u16(p); p += 2; @@ -2531,7 +2551,15 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, p += topic_len; payload_len = wmqb_r_u32(p); p += 4; - if ((word32)(end - p) < payload_len) { + if ((word32)(end - p) < payload_len || + (word32)(end - p) - payload_len < 4) { + wmqb_force_zero(e->topic, (word32)topic_len + 1); + WOLFMQTT_FREE(e->topic); + WOLFMQTT_FREE(e); + return MQTT_CODE_ERROR_MALFORMED_DATA; + } + props_len = wmqb_r_u32(p + payload_len); + if ((word32)(end - p) - payload_len - 4 != props_len) { wmqb_force_zero(e->topic, (word32)topic_len + 1); WOLFMQTT_FREE(e->topic); WOLFMQTT_FREE(e); @@ -2548,6 +2576,22 @@ static int wmqb_decode_and_insert_outq(MqttBroker* broker, XMEMCPY(e->payload, p, payload_len); } e->payload_len = payload_len; + e->has_expiry = (byte)(flags & 0x01); + p += payload_len + 4; +#ifdef WOLFMQTT_V5 + rc = BrokerOutPub_DecodeProps(e, p, props_len); + if (rc != 0) { + if (e->payload != NULL) { + wmqb_force_zero(e->payload, payload_len); + WOLFMQTT_FREE(e->payload); + } + wmqb_force_zero(e->topic, (word32)topic_len + 1); + WOLFMQTT_FREE(e->topic); + WOLFMQTT_FREE(e); + return (rc == MQTT_CODE_ERROR_MEMORY) ? rc : + MQTT_CODE_ERROR_MALFORMED_DATA; + } +#endif e->qos = (MqttQoS)qos; e->packet_id = packet_id; e->retain = retain; diff --git a/src/mqtt_client.c b/src/mqtt_client.c index c4f4694d9..711884ad3 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -72,6 +72,7 @@ WOLFMQTT_LOCAL void MqttClient_ForceZero(void* mem, word32 len) #define MQTT_CLIENT_INIT_LOCK_RECV 0x02U #define MQTT_CLIENT_INIT_LOCK_CLIENT 0x04U #define MQTT_CLIENT_INIT_LOCK_CURL 0x08U + #define MQTT_CLIENT_INIT_DECODE_DONE 0x10U #endif /* forward declarations */ @@ -413,6 +414,10 @@ WOLFMQTT_LOCAL int MqttReadStart(MqttClient* client, MqttMsgStat* stat) { /* mark read active */ client->read.isActive = 1; + #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + client->read_owner = WOLFMQTT_THREAD_SELF(); + client->read_owner_valid = 1; + #endif /* reset the packet state used by MqttPacket_Read */ client->packet.stat = MQTT_PK_BEGIN; @@ -437,11 +442,16 @@ WOLFMQTT_LOCAL void MqttReadStop(MqttClient* client, MqttMsgStat* stat) #endif #ifdef WOLFMQTT_MULTITHREAD - if (wm_SemLock(&client->lockClient) == 0) + /* Only the lockRecv holder may reset the read state. A repeat stop after + * the release would clobber another reader that has since taken it. */ + if (stat->isReadActive && wm_SemLock(&client->lockClient) == 0) #endif { /* reset read */ XMEMSET(&client->read, 0, sizeof(client->read)); + #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + client->read_owner_valid = 0; + #endif #ifdef WOLFMQTT_MULTITHREAD wm_SemUnlock(&client->lockClient); #endif @@ -525,7 +535,7 @@ static void MqttClient_RecvQuotaRelease(MqttClient* client, MqttMsgStat* stat) * digest of it would let two different ClientIds that happen to share one * inherit each other's replay entries and QoS 2 pending ids. Returns 0 when no * Session is recorded, so a first connect never matches. */ -static int MqttClient_SessionIdMatches(const MqttClient* client, +WOLFMQTT_LOCAL int MqttClient_SessionIdMatches(const MqttClient* client, const char* client_id) { size_t len; @@ -544,7 +554,7 @@ static int MqttClient_SessionIdMatches(const MqttClient* client, * too long for the buffer is not recorded, so the next Session Present is * treated as a different Session and the state is dropped rather than * inherited on a partial match. */ -static void MqttClient_SessionIdRecord(MqttClient* client, +WOLFMQTT_LOCAL void MqttClient_SessionIdRecord(MqttClient* client, const char* client_id) { size_t len; @@ -1237,6 +1247,135 @@ void MqttClient_RespList_Remove(MqttClient *client, MqttPendResp *rmResp) #endif } +/* Returns 1 if the calling thread holds lockRecv, or lockSend when check_write + * is set. Waiting on either would then self-deadlock. Without thread + * identities this cannot be told and returns 0. Caller must hold lockClient. */ +static int MqttClient_OwnsIoLock(MqttClient *client, int check_write) +{ + int owns = 0; + +#ifdef WOLFMQTT_THREAD_ID_T + if (client->read.isActive && client->read_owner_valid && + WOLFMQTT_THREAD_EQUAL(client->read_owner, + WOLFMQTT_THREAD_SELF())) { + owns = 1; + } + if (check_write && client->write.isActive && client->write_owner_valid && + WOLFMQTT_THREAD_EQUAL(client->write_owner, + WOLFMQTT_THREAD_SELF())) { + owns = 1; + } +#else + (void)client; + (void)check_write; +#endif + + return owns; +} + +/* Mark a reader done with resp and wake any thread waiting to unlink it. + * Caller must hold lockClient. */ +void MqttClient_RespList_EndProcessing(MqttClient *client, MqttPendResp *resp) +{ + resp->packetProcessing = 0; +#ifdef WOLFMQTT_SEM_SIGNAL + if (client->decodeWaiters > 0) { + wm_SemUnlock(&client->decodeDone); + } +#else + (void)client; +#endif +} + +/* Unlink an owner's pending response once no reader is decoding into it, so + * the owner may free the object on return. A reader marks the entry under + * lockRecv and clears the mark with MqttClient_RespList_EndProcessing, which + * posts decodeDone. Without WOLFMQTT_SEM_SIGNAL, taking lockRecv waits the + * reader out instead. Returns MQTT_CODE_ERROR_STAT, leaving the entry linked, + * when the caller holds lockRecv or lockSend, since the reader may need either + * to finish. Caller must not hold lockClient. */ +WOLFMQTT_LOCAL int MqttClient_RespList_RemoveIdle(MqttClient *client, + MqttPendResp *rmResp) +{ + int rc; + int busy = 0; +#ifdef WOLFMQTT_SEM_SIGNAL + int waited = 0; +#endif + + do { + rc = wm_SemLock(&client->lockClient); + if (rc == 0) { + busy = rmResp->packetProcessing; + if (busy && MqttClient_OwnsIoLock(client, 1)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + else if (!busy) { + MqttClient_RespList_Remove(client, rmResp); + } + #ifdef WOLFMQTT_SEM_SIGNAL + if (waited) { + client->decodeWaiters--; + waited = 0; + /* decodeDone is binary, so one post wakes one waiter. Pass it + * on when leaving so the others recheck too. */ + if ((!busy || rc != 0) && client->decodeWaiters > 0) { + wm_SemUnlock(&client->decodeDone); + } + } + if (busy && rc == 0) { + client->decodeWaiters++; + waited = 1; + } + #endif + wm_SemUnlock(&client->lockClient); + } + if (rc == 0 && busy) { + #ifdef WOLFMQTT_SEM_SIGNAL + rc = wm_SemLock(&client->decodeDone); + if (rc != 0 && wm_SemLock(&client->lockClient) == 0) { + client->decodeWaiters--; + wm_SemUnlock(&client->lockClient); + } + #else + rc = wm_SemLock(&client->lockRecv); + if (rc == 0) { + wm_SemUnlock(&client->lockRecv); + } + #endif + } + } while (rc == 0 && busy); + + return rc; +} + +/* Find the pending response belonging to msg, by its packet object or its + * embedded pendResp. Caller must hold lockClient. */ +static MqttPendResp* MqttClient_RespList_FindObj(MqttClient *client, + MqttObject* msg) +{ + MqttPendResp *tmpResp; + + for (tmpResp = client->firstPendResp; + tmpResp != NULL; + tmpResp = tmpResp->next) + { + #ifdef WOLFMQTT_DEBUG_CLIENT + PRINTF("\tMsg: %p (obj %p), Type %s (%d), ID %d, InProc %d, Done %d", + tmpResp, tmpResp->packet_obj, + MqttPacket_TypeDesc(tmpResp->packet_type), + tmpResp->packet_type, tmpResp->packet_id, + tmpResp->packetProcessing, tmpResp->packetDone); + #endif + if ((size_t)tmpResp->packet_obj == (size_t)msg || + (size_t)tmpResp - OFFSETOF(MqttMessage, pendResp) == (size_t)msg) { + break; + } + } + + return tmpResp; +} + /* return codes: 0=not found, 1=found */ int MqttClient_RespList_Find(MqttClient *client, MqttPacketType packet_type, word16 packet_id, MqttPendResp **retResp) @@ -1750,6 +1889,27 @@ static int MqttClient_DecodePacket(MqttClient* client, byte* rx_buf, return rc; } +#ifdef WOLFMQTT_V5 +/* Clear mask then set flags in client->flags, reporting a lockClient failure + * instead of skipping the update the way MqttClient_Flags does. */ +static int MqttClient_UpdateFlags(MqttClient *client, word32 mask, + word32 flags) +{ +#ifdef WOLFMQTT_MULTITHREAD + int rc = wm_SemLock(&client->lockClient); + if (rc != 0) { + return rc; + } +#endif + client->flags &= ~mask; + client->flags |= flags; +#ifdef WOLFMQTT_MULTITHREAD + wm_SemUnlock(&client->lockClient); +#endif + return MQTT_CODE_SUCCESS; +} +#endif + static int MqttClient_HandlePacket(MqttClient* client, MqttPacketType packet_type, void *packet_obj, MqttPublishResp* resp, int timeout_ms) @@ -1757,6 +1917,9 @@ static int MqttClient_HandlePacket(MqttClient* client, int rc = MQTT_CODE_SUCCESS; MqttQoS packet_qos = MQTT_QOS_0; word16 packet_id = 0; +#ifdef WOLFMQTT_V5 + int flag_rc; +#endif if (client == NULL || packet_obj == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); @@ -1773,8 +1936,13 @@ static int MqttClient_HandlePacket(MqttClient* client, client->packet.buf_len, packet_obj, &packet_type, &packet_qos, &packet_id, 1); #ifdef WOLFMQTT_V5 - /* The CONNACK ends the [MQTT-3.1.2-30] authentication window. */ - (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, 0); + /* The CONNACK ends the [MQTT-3.1.2-30] authentication window. A + * failed clear fails the CONNACK; the next CONNECT resets it. */ + flag_rc = MqttClient_UpdateFlags(client, + MQTT_CLIENT_FLAG_AUTH_PENDING, 0); + if (flag_rc != MQTT_CODE_SUCCESS && rc >= 0) { + rc = flag_rc; + } #endif break; } @@ -2073,7 +2241,10 @@ WOLFMQTT_LOCAL int MqttClient_CheckPendResp(MqttClient *client, byte wait_type, if (MqttClient_RespList_Find(client, (MqttPacketType)wait_type, wait_packet_id, &pendResp)) { - if ((pendResp != NULL) && (pendResp->packetDone)) { + /* A duplicate ack can be decoding into a completed entry, so + * it is only done once no reader is processing it. */ + if ((pendResp != NULL) && (pendResp->packetDone) && + !pendResp->packetProcessing) { /* pending response is already done, so return */ rc = pendResp->packet_ret; #ifdef WOLFMQTT_DEBUG_CLIENT @@ -2482,6 +2653,14 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, rc = MQTT_CODE_SUCCESS; } else { + #ifdef WOLFMQTT_MULTITHREAD + /* Release the entry so its owner does not wait on it */ + if (pendResp != NULL && + wm_SemLock(&client->lockClient) == 0) { + MqttClient_RespList_EndProcessing(client, pendResp); + wm_SemUnlock(&client->lockClient); + } + #endif /* error, break */ break; } @@ -2492,6 +2671,7 @@ static int MqttClient_WaitType(MqttClient *client, void *packet_obj, if (wm_SemLock(&client->lockClient) == 0) { pendResp->packetDone = 1; pendResp->packet_ret = rc; + MqttClient_RespList_EndProcessing(client, pendResp); #ifdef WOLFMQTT_V5 /* A write-only QoS>0 publish reserves a Receive Maximum unit * but does not wait for its own ack; release it here as this @@ -2879,6 +3059,16 @@ int MqttClient_Init(MqttClient *client, MqttNet* net, } } #endif + #ifdef WOLFMQTT_SEM_SIGNAL + if (rc == 0) { + rc = wm_SemInit(&client->decodeDone); + if (rc == 0) { + client->init_flags |= MQTT_CLIENT_INIT_DECODE_DONE; + /* Start taken, so a waiter blocks until a reader posts. */ + rc = wm_SemLock(&client->decodeDone); + } + } + #endif #endif if (rc == 0) { @@ -2902,6 +3092,13 @@ void MqttClient_DeInit(MqttClient *client) MqttClient_Replay_Reset(client); #endif #ifdef WOLFMQTT_MULTITHREAD + #ifdef WOLFMQTT_SEM_SIGNAL + if ((client->init_flags & MQTT_CLIENT_INIT_DECODE_DONE) != 0U) { + if (wm_SemFree(&client->decodeDone) == MQTT_CODE_SUCCESS) { + client->init_flags &= (byte)~MQTT_CLIENT_INIT_DECODE_DONE; + } + } + #endif #ifdef ENABLE_MQTT_CURL if ((client->init_flags & MQTT_CLIENT_INIT_LOCK_CURL) != 0U) { if (wm_SemFree(&client->lockCURL) == MQTT_CODE_SUCCESS) { @@ -3513,8 +3710,16 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) * DISCONNECT may follow until the CONNACK. Raised before any CONNECT * byte can reach the peer, so neither another sender nor a reader * that processes the CONNACK can see the window open late. */ - if (client->auth_method_set) { - (void)MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_AUTH_PENDING); + rc = MqttClient_UpdateFlags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, + client->auth_method_set ? MQTT_CLIENT_FLAG_AUTH_PENDING : 0); + if (rc != MQTT_CODE_SUCCESS) { + /* Without the window raised nothing would hold back other + * packets, so do not send the CONNECT. */ + int xfer = client->write.len; + CLIENT_FORCE_ZERO(client->tx_buf, xfer); + MqttWriteStop(client, &mc_connect->stat); + (void)MqttClient_CancelMessage(client, (MqttObject*)mc_connect); + return rc; } #endif @@ -3577,9 +3782,11 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect) * CONNECT is already out there. */ #ifdef WOLFMQTT_V5 if (wrote == 0) { - /* No CONNECT reached the peer, so no CONNACK is coming. */ - (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_AUTH_PENDING, - 0); + /* No CONNECT reached the peer, so no CONNACK is coming. The + * write error is returned either way, and a flag left set by + * a lock failure is reset by the next CONNECT. */ + (void)MqttClient_UpdateFlags(client, + MQTT_CLIENT_FLAG_AUTH_PENDING, 0); } #endif MqttClient_CancelMessage(client, (MqttObject*)mc_connect); @@ -4169,6 +4376,9 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, { int rc = MQTT_CODE_SUCCESS; MqttPacketType resp_type; +#ifdef WOLFMQTT_MULTITHREAD + int rmRc; +#endif /* Validate required arguments */ if (client == NULL || publish == NULL) { @@ -4553,9 +4763,10 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish, * WOLFMQTT_NONBLOCK returned CONTINUE above and broke out before * here, leaving its entry for the reading thread to complete and * remove. */ - if (wm_SemLock(&client->lockClient) == 0) { - MqttClient_RespList_Remove(client, &publish->pendResp); - wm_SemUnlock(&client->lockClient); + rmRc = MqttClient_RespList_RemoveIdle(client, + &publish->pendResp); + if (rmRc != MQTT_CODE_SUCCESS && rc >= 0) { + rc = rmRc; } #endif } @@ -4943,6 +5154,19 @@ int MqttClient_NetDisconnect_Wait(MqttClient *client) if (client == NULL) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG); } + /* From a callback this thread may already hold an I/O lock, and taking it + * again would self-deadlock. */ + rc = wm_SemLock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + if (MqttClient_OwnsIoLock(client, 1)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + wm_SemUnlock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } /* A message callback can hold lockRecv while starting a publish, so take * the I/O locks in that same direction. Once both are held, no read or * write callback can still reference socket, TLS, or curl state while the @@ -5591,6 +5815,27 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) PRINTF("Cancel Msg: %p", msg); #endif +#ifdef WOLFMQTT_MULTITHREAD + /* Refuse, before changing any state, when a reader is decoding into the + * message and this thread holds a lock that reader may need, e.g. + * cancelling it from its own ack's property callback. This message's own + * write is released below before waiting, so it does not count. */ + rc = wm_SemLock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + tmpResp = MqttClient_RespList_FindObj(client, msg); + if (tmpResp != NULL && tmpResp->packetProcessing && + (MqttClient_OwnsIoLock(client, 0) || + (!mms_stat->isWriteActive && MqttClient_OwnsIoLock(client, 1)))) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_STAT); + } + wm_SemUnlock(&client->lockClient); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } +#endif + /* Whether this message's packet finished going out. MQTT_MSG_WAIT is only * reached once the whole Control Packet has been written. */ onWire = (mms_stat->write == MQTT_MSG_WAIT) ? 1 : 0; @@ -5633,6 +5878,19 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) mms_stat->recvQuotaHeld = 0; #endif + /* Release this message's write before waiting for a reader below, which + * may need lockSend to answer the packet it is decoding. */ + if (mms_stat->isWriteActive) { + #ifdef WOLFMQTT_DEBUG_CLIENT + PRINTF("Cancel Write Lock"); + #endif + /* An abandoned write (e.g. a partial nonblocking CONNECT) leaves the + * encoded packet - possibly plaintext credentials - in tx_buf. Scrub + * it before MqttWriteStop releases lockSend. */ + CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); + MqttWriteStop(client, mms_stat); + } + #ifdef WOLFMQTT_MULTITHREAD /* Remove any pending responses expected */ rc = wm_SemLock(&client->lockClient); @@ -5640,37 +5898,38 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) return rc; } - for (tmpResp = client->firstPendResp; - tmpResp != NULL; - tmpResp = tmpResp->next) - { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("\tMsg: %p (obj %p), Type %s (%d), ID %d, InProc %d, Done %d", + /* Do not credit any reserved Receive Maximum unit here: the PUBLISH may + * already be on the wire, where the server keeps counting it [MQTT-4.9], + * so crediting it on a local cancel could exceed the negotiated quota. The + * unit is released on the acknowledgement, or recovered when the + * connection resets server_recv_max. */ + tmpResp = MqttClient_RespList_FindObj(client, msg); +#ifdef WOLFMQTT_DEBUG_CLIENT + if (tmpResp != NULL) { + PRINTF("Found Cancel Msg: %p (obj %p), Type %s (%d), ID %d, " + "InProc %d, Done %d", tmpResp, tmpResp->packet_obj, MqttPacket_TypeDesc(tmpResp->packet_type), tmpResp->packet_type, tmpResp->packet_id, tmpResp->packetProcessing, tmpResp->packetDone); - #endif - if ((size_t)tmpResp->packet_obj == (size_t)msg || - (size_t)tmpResp - OFFSETOF(MqttMessage, pendResp) == (size_t)msg) { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("Found Cancel Msg: %p (obj %p), Type %s (%d), ID %d, " - "InProc %d, Done %d", - tmpResp, tmpResp->packet_obj, - MqttPacket_TypeDesc(tmpResp->packet_type), - tmpResp->packet_type, tmpResp->packet_id, - tmpResp->packetProcessing, tmpResp->packetDone); - #endif - /* Do not credit any reserved Receive Maximum unit here: the PUBLISH - * may already be on the wire, where the server keeps counting it - * [MQTT-4.9], so crediting it on a local cancel could exceed the - * negotiated quota. The unit is released on the acknowledgement, or - * recovered when the connection resets server_recv_max. */ - MqttClient_RespList_Remove(client, tmpResp); - break; - } } +#endif wm_SemUnlock(&client->lockClient); + + if (tmpResp != NULL) { + if (mms_stat->isReadActive) { + /* This thread holds lockRecv, so no other reader can be using + * the entry and waiting for one would self-deadlock. */ + rc = wm_SemLock(&client->lockClient); + if (rc == MQTT_CODE_SUCCESS) { + MqttClient_RespList_Remove(client, tmpResp); + wm_SemUnlock(&client->lockClient); + } + } + else { + rc = MqttClient_RespList_RemoveIdle(client, tmpResp); + } + } #endif /* WOLFMQTT_MULTITHREAD */ /* cancel any active flags / locks */ @@ -5684,16 +5943,6 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg) CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); MqttReadStop(client, mms_stat); } - if (mms_stat->isWriteActive) { - #ifdef WOLFMQTT_DEBUG_CLIENT - PRINTF("Cancel Write Lock"); - #endif - /* An abandoned write (e.g. a partial nonblocking CONNECT) leaves the - * encoded packet - possibly plaintext credentials - in tx_buf. Scrub - * it before MqttWriteStop releases lockSend. */ - CLIENT_FORCE_ZERO(client->tx_buf, client->tx_buf_len); - MqttWriteStop(client, mms_stat); - } return rc; } diff --git a/src/mqtt_sn_client.c b/src/mqtt_sn_client.c index 61d2255da..5cb8465e2 100644 --- a/src/mqtt_sn_client.c +++ b/src/mqtt_sn_client.c @@ -846,6 +846,15 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, mms_stat, &response_pending, timeout_ms); if (rc == MQTT_CODE_CONTINUE) { + #ifdef WOLFMQTT_MULTITHREAD + /* A resumed wait finds the entry again, so release it now + * and let its owner unlink it meanwhile. */ + if (pendResp != NULL && + wm_SemLock(&client->lockClient) == 0) { + MqttClient_RespList_EndProcessing(client, pendResp); + wm_SemUnlock(&client->lockClient); + } + #endif break; } @@ -860,6 +869,7 @@ static int SN_Client_WaitType(MqttClient *client, void* packet_obj, if (wm_SemLock(&client->lockClient) == 0) { pendResp->packetDone = 1; pendResp->packet_ret = rc; + MqttClient_RespList_EndProcessing(client, pendResp); #ifdef WOLFMQTT_DEBUG_CLIENT PRINTF("PendResp Marked Done %p", pendResp); #endif @@ -1430,24 +1440,32 @@ enum { /* The Will exchange after CONNECT failed. Release the CONNACK pending * response and rewind the state so a retry with the same SN_Connect sends a - * new CONNECT; the gateway only requests the Will in reply to one. */ -static void SN_Client_ConnectReset(MqttClient *client, SN_Connect *mc_connect) + * new CONNECT; the gateway only requests the Will in reply to one. A reader + * may already be decoding an early CONNACK into mc_connect, so the entry is + * unlinked only once it is idle, and the state is left alone if that fails. */ +static int SN_Client_ConnectReset(MqttClient *client, SN_Connect *mc_connect) { + int rc = MQTT_CODE_SUCCESS; + #ifdef WOLFMQTT_MULTITHREAD - if (wm_SemLock(&client->lockClient) == 0) { - MqttClient_RespList_Remove(client, &mc_connect->pendResp); - wm_SemUnlock(&client->lockClient); - } + rc = MqttClient_RespList_RemoveIdle(client, &mc_connect->pendResp); #else (void)client; #endif - mc_connect->stat.write = MQTT_MSG_BEGIN; - mc_connect->will_done = SN_WILL_DONE_NONE; + if (rc == MQTT_CODE_SUCCESS) { + mc_connect->stat.write = MQTT_MSG_BEGIN; + mc_connect->will_done = SN_WILL_DONE_NONE; + } + return rc; } int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) { int rc = 0; + int reset_rc; +#if WOLFMQTT_MAX_QOS >= 2 + int session_id_matched; +#endif /* Validate required arguments */ if ((client == NULL) || (mc_connect == NULL)) { @@ -1520,7 +1538,10 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) rc = SN_WillTopic(client, &mc_connect->will); if (rc != 0) { if (rc != MQTT_CODE_CONTINUE) { - SN_Client_ConnectReset(client, mc_connect); + reset_rc = SN_Client_ConnectReset(client, mc_connect); + if (reset_rc != MQTT_CODE_SUCCESS) { + rc = reset_rc; + } } return rc; } @@ -1530,7 +1551,10 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) rc = SN_WillMessage(client, &mc_connect->will); if (rc != 0) { if (rc != MQTT_CODE_CONTINUE) { - SN_Client_ConnectReset(client, mc_connect); + reset_rc = SN_Client_ConnectReset(client, mc_connect); + if (reset_rc != MQTT_CODE_SUCCESS) { + rc = reset_rc; + } } return rc; } @@ -1563,10 +1587,16 @@ int SN_Client_Connect(MqttClient *client, SN_Connect *mc_connect) rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_CONNECT_REFUSED); } #if WOLFMQTT_MAX_QOS >= 2 - /* A clean session starts with no inbound QoS 2 flows in progress. */ - if (rc == MQTT_CODE_SUCCESS && mc_connect->clean_session) { - XMEMSET(client->recv_qos2_pending, 0, - sizeof(client->recv_qos2_pending)); + /* A clean session, or a session for a different ClientId, starts with + * no inbound QoS 2 flows in progress. */ + if (rc == MQTT_CODE_SUCCESS) { + session_id_matched = + MqttClient_SessionIdMatches(client, mc_connect->client_id); + MqttClient_SessionIdRecord(client, mc_connect->client_id); + if (mc_connect->clean_session || !session_id_matched) { + XMEMSET(client->recv_qos2_pending, 0, + sizeof(client->recv_qos2_pending)); + } } #endif diff --git a/tests/include.am b/tests/include.am index 6f27a93a6..f2cf26a93 100644 --- a/tests/include.am +++ b/tests/include.am @@ -196,6 +196,7 @@ tests_test_broker_connect_static_SOURCES = \ tests_test_broker_connect_static_CFLAGS = -DWOLFMQTT_BROKER \ -DWOLFMQTT_BROKER_CUSTOM_NET -DWOLFMQTT_BROKER_NO_LOG \ -DWOLFMQTT_STATIC_MEMORY -DNO_MAIN_DRIVER \ + -DBROKER_MAX_WILL_PAYLOAD_LEN=320 \ '-DWOLFMQTT_BROKER_GET_TIME_S()=wolfmqtt_test_broker_time_s()' \ '-DBROKER_SLEEP_MS(ms)=do{}while(0)' $(AM_CFLAGS) tests_test_broker_connect_static_CPPFLAGS = -I$(top_srcdir) \ diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 9ada370ae..8b2639c86 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -202,6 +202,8 @@ typedef struct MockClient { /* When set, the next PUBLISH frame (and only that) fails. Lets a test aim * at a delivery without also failing the SUBACK that precedes it. */ int write_err_on_publish; + /* positive: short-write the next PUBLISH frame to this many bytes */ + int write_limit_on_publish; /* Next mock_write call reports a transient timeout: the socket is alive, * select() just did not report it writable in time. */ int write_timeout_pending; @@ -329,6 +331,13 @@ static int mock_write(void* ctx, BROKER_SOCKET_T sock, if (mc->write_continue) { return MQTT_CODE_CONTINUE; } + if (mc->write_limit_on_publish > 0 && buf != NULL && buf_len > 0 && + ((buf[0] & 0xF0) >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (write_len > mc->write_limit_on_publish) { + write_len = mc->write_limit_on_publish; + } + mc->write_limit_on_publish = 0; + } if (mc->write_limit_then_err > 0) { if (write_len > mc->write_limit_then_err) { write_len = mc->write_limit_then_err; @@ -986,6 +995,248 @@ TEST(connect_v5_oversize_will_payload_emits_connack) } #ifdef WOLFMQTT_STATIC_MEMORY +/* A static Session cannot store Will Properties larger than + * BROKER_MAX_WILL_PROPS_LEN. Dropping some would publish the Will altered, so + * the CONNECT is refused instead. Wire: v5 CONNECT with Will Flag and Clean + * Start, ClientId "P", Will Properties holding one User Property (0x26) + * "k" = BROKER_MAX_WILL_PROPS_LEN bytes of 'v', Will Topic "lwt", Will + * Payload "bye". */ +TEST(connect_v5_oversize_will_props_emits_connack) +{ + MqttBroker broker; + MqttBrokerNet net; + byte connect[MOCK_BUF_SZ]; + size_t pos = 0; + word32 value_len = BROKER_MAX_WILL_PROPS_LEN; + word32 props_len = 1 + 3 + 2 + value_len; + word32 remain; + word32 v; + + remain = 11 + 3 + ((props_len > 127) ? 2 : 1) + props_len + 5 + 5; + ASSERT_TRUE(remain + 3 <= sizeof(connect)); + connect[pos++] = 0x10; + for (v = remain; ; ) { + connect[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + connect[pos++] |= 0x80; + } + XMEMCPY(&connect[pos], "\x00\x04MQTT\x05\x06\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&connect[pos], "\x00\x01P", 3); + pos += 3; + for (v = props_len; ; ) { + connect[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + connect[pos++] |= 0x80; + } + connect[pos++] = MQTT_PROP_USER_PROP; + XMEMCPY(&connect[pos], "\x00\x01k", 3); + pos += 3; + connect[pos++] = (byte)(value_len >> 8); + connect[pos++] = (byte)value_len; + XMEMSET(&connect[pos], 'v', value_len); + pos += value_len; + XMEMCPY(&connect[pos], "\x00\x03lwt\x00\x03" "bye", 10); + pos += 10; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, pos); + run_broker_one_connect(&broker); + + ASSERT_TRUE(g_out_len >= 5); + ASSERT_EQ(0x20, g_out_buf[0]); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, g_out_buf[3]); + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + +#if BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5 < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 +/* Builds a v5 CONNECT, MQTT 5.0 section 3.1: Clean Start, Will Flag, Will QoS + * 1, Keep Alive 60, ClientId "P", Will Properties holding a Message Expiry + * Interval (0x02) of 60 when with_expiry is set (otherwise an empty Will + * Properties length of 0), Will Topic "lwt" and payload_len bytes of 'x'. */ +static size_t build_v5_connect_will_expiry(byte* buf, size_t buf_sz, + word32 payload_len, int with_expiry) +{ + size_t pos = 0; + word32 remain = 11 + 3 + 1 + (with_expiry ? 5 : 0) + 5 + 2 + payload_len; + word32 v; + + if (remain + 4 > buf_sz) { + return 0; + } + buf[pos++] = 0x10; + for (v = remain; ; ) { + buf[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + buf[pos++] |= 0x80; + } + XMEMCPY(&buf[pos], "\x00\x04MQTT\x05\x0E\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&buf[pos], "\x00\x01P", 3); + pos += 3; + if (with_expiry) { + XMEMCPY(&buf[pos], "\x05\x02\x00\x00\x00\x3C", 6); + pos += 6; + } + else { + buf[pos++] = 0x00; + } + XMEMCPY(&buf[pos], "\x00\x03lwt", 5); + pos += 5; + buf[pos++] = (byte)(payload_len >> 8); + buf[pos++] = (byte)payload_len; + XMEMSET(&buf[pos], 'x', payload_len); + pos += payload_len; + return pos; +} + +/* Builds a v3.1.1 CONNECT, MQTT 3.1.1 section 3.1: Clean Session, Will Flag, + * Will QoS will_qos, Keep Alive 60, ClientId "P", Will Topic "lwt" and + * payload_len bytes of 'x'. */ +static size_t build_v311_connect_will_qos(byte* buf, size_t buf_sz, + byte will_qos, word32 payload_len) +{ + size_t pos = 0; + word32 remain = 10 + 3 + 5 + 2 + payload_len; + word32 v; + + if (remain + 4 > buf_sz) { + return 0; + } + buf[pos++] = 0x10; + for (v = remain; ; ) { + buf[pos] = (byte)(v & 0x7F); + v >>= 7; + if (v == 0) { + pos++; + break; + } + buf[pos++] |= 0x80; + } + XMEMCPY(&buf[pos], "\x00\x04MQTT\x04", 7); + pos += 7; + buf[pos++] = (byte)(0x06 | (will_qos << 3)); + buf[pos++] = 0x00; + buf[pos++] = 0x3C; + XMEMCPY(&buf[pos], "\x00\x01P", 3); + pos += 3; + XMEMCPY(&buf[pos], "\x00\x03lwt", 5); + pos += 5; + buf[pos++] = (byte)(payload_len >> 8); + buf[pos++] = (byte)payload_len; + XMEMSET(&buf[pos], 'x', payload_len); + pos += payload_len; + return pos; +} + +/* Runs one CONNECT and returns the CONNACK return or Reason Code, or -1. */ +static int connect_connack_code(const byte* connect, size_t connect_len) +{ + MqttBroker broker; + MqttBrokerNet net; + int reason = -1; + + if (connect_len == 0) { + return -1; + } + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS) { + return -1; + } + if (MqttBroker_Start(&broker) == MQTT_CODE_SUCCESS) { + reset_mock_state(connect, connect_len); + run_broker_one_connect(&broker); + if (g_out_len >= 4 && g_out_buf[0] == 0x20) { + reason = g_out_buf[3]; + } + MqttBroker_Stop(&broker); + } + MqttBroker_Free(&broker); + return reason; +} + +/* Runs one v5 Will CONNECT and returns the CONNACK Reason Code, or -1. */ +static int will_expiry_connack(word32 payload_len, int with_expiry) +{ + byte connect[MOCK_BUF_SZ]; + + return connect_connack_code(connect, + build_v5_connect_will_expiry(connect, sizeof(connect), payload_len, + with_expiry)); +} + +/* Runs one v3.1.1 Will CONNECT and returns the CONNACK return code, or -1. */ +static int v311_will_connack(byte will_qos, word32 payload_len) +{ + byte connect[MOCK_BUF_SZ]; + + return connect_connack_code(connect, + build_v311_connect_will_qos(connect, sizeof(connect), will_qos, + payload_len)); +} + +/* A static QoS 1/2 Will may have to be queued for an offline Session, where + * its payload and properties share one BROKER_MAX_STATIC_OFFLINE_DATA_LEN + * slot. One that cannot fit is refused at CONNECT rather than accepted and + * then dropped when it is published. */ +TEST(connect_v5_static_will_exceeding_offline_slot_refused) +{ + /* Payload plus the 5-byte Message Expiry Interval fills the slot. */ + ASSERT_EQ(MQTT_REASON_SUCCESS, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5, 1)); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 4, 1)); + ASSERT_TRUE(g_client_closed); +} + +#if BROKER_MAX_STATIC_OFFLINE_DATA_LEN < BROKER_MAX_WILL_PAYLOAD_LEN +/* A Will with no publishable properties must also fit the offline slot. */ +TEST(connect_v5_static_payload_only_will_exceeding_offline_slot_refused) +{ + ASSERT_EQ(MQTT_REASON_SUCCESS, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN, 0)); + ASSERT_EQ(MQTT_REASON_PACKET_TOO_LARGE, + will_expiry_connack(BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1, 0)); + ASSERT_TRUE(g_client_closed); +} + +/* The same limit applies to a v3.1.1 QoS 1 Will, refused with return code + * 0x03 Server unavailable (MQTT 3.1.1 section 3.2.2.3). A QoS 0 Will is never + * queued offline, so it only has to fit BROKER_MAX_WILL_PAYLOAD_LEN. */ +TEST(connect_v311_static_will_exceeding_offline_slot_refused) +{ + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, + v311_will_connack(1, BROKER_MAX_STATIC_OFFLINE_DATA_LEN)); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL, + v311_will_connack(1, BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1)); + ASSERT_TRUE(g_client_closed); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, + v311_will_connack(0, BROKER_MAX_STATIC_OFFLINE_DATA_LEN + 1)); +} +#endif +#endif + TEST(connect_v5_oversize_will_topic_emits_connack) { MqttBroker broker; @@ -6036,6 +6287,83 @@ TEST(takeover_v5_own_will_not_sent_after_disconnect) MqttBroker_Free(&broker); } +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) +/* A short write of the old client's own Will copy must not leave the + * Session Taken Over DISCONNECT unsent behind it [MQTT-3.14.4-1]. The QoS 0 + * self-copy is not started on a connection that is closing, so the old client + * receives exactly one complete DISCONNECT 0x8E after its SUBACK. */ +TEST(takeover_v5_own_will_partial_write_still_disconnects) +{ + MqttBroker broker; + MqttBrokerNet net; + size_t before; + int publishes_before; + int i; + /* v5 CONNECT for "P" with Will "lwt"/"bye", Session Expiry 0, Will + * Delay 0 (layout as in takeover_v5_wills). */ + static const byte old_connect[] = { + 0x10, 0x23, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x04, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 'P', + 0x05, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE (MQTT 5.0 section 3.8): packet id 1, Properties length + * 0, filter "lwt", options 0 (QoS 0). Remaining Length 9. */ + static const byte old_subscribe[] = { + 0x82, 0x09, + 0x00, 0x01, + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00 + }; + /* v5 CONNECT for "P", Clean Start 1, no Will, no properties. */ + static const byte new_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + before = g_clients[0].out_len; + publishes_before = g_clients[0].publish_writes; + g_clients[0].write_limit_on_publish = 1; + mock_client_input_append(1, new_connect, sizeof(new_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_clients[0].closed); + ASSERT_EQ(publishes_before, g_clients[0].publish_writes); + /* MQTT 5.0 section 3.14: fixed header 0xE0, Remaining Length, Reason + * Code 0x8E Session taken over, and nothing after the frame. */ + ASSERT_TRUE(g_clients[0].out_len >= before + 3); + ASSERT_EQ(0xE0, g_clients[0].out_buf[before]); + ASSERT_EQ(MQTT_REASON_SESSION_TAKEN_OVER, + g_clients[0].out_buf[before + 2]); + ASSERT_EQ(before + 2 + g_clients[0].out_buf[before + 1], + g_clients[0].out_len); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + /* MQTT 5.0 section 3.1.4: with a zero Will Delay the old Will is published * because its Network Connection is closed, even though the Session * survives the takeover. */ @@ -6212,6 +6540,14 @@ TEST(disconnect_v5_props_pool_exhausted_reason_error_publishes_will) #endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ #ifdef WOLFMQTT_V5 +#if defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_NONBLOCK) + /* Static clients cannot resume a deferred write, so these builds close + * without the error DISCONNECT rather than risk truncating it. */ + #define ERROR_DISCONNECT_REASON(reason) (-1) +#else + #define ERROR_DISCONNECT_REASON(reason) (reason) +#endif + /* MQTT 5.0 section 4.13.1: a Server that detects a Malformed Packet or * Protocol Error in a packet other than CONNECT SHOULD send DISCONNECT with * the Reason Code before closing. Connect a v5 client, send one bad packet, @@ -6258,7 +6594,7 @@ TEST(v5_invalid_fixed_header_flags_sends_malformed_disconnect) static const byte puback[] = { 0x41, 0x02, 0x00, 0x01 }; int closed = 0; - ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_MALFORMED_PACKET), v5_bad_packet_disconnect_reason(puback, sizeof(puback), &closed)); ASSERT_TRUE(closed); } @@ -6275,7 +6611,7 @@ TEST(v5_second_connect_sends_protocol_error_disconnect) }; int closed = 0; - ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_PROTOCOL_ERR), v5_bad_packet_disconnect_reason(connect, sizeof(connect), &closed)); ASSERT_TRUE(closed); } @@ -6287,7 +6623,7 @@ TEST(v5_pingreq_with_body_sends_malformed_disconnect) static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; int closed = 0; - ASSERT_EQ(MQTT_REASON_MALFORMED_PACKET, + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_MALFORMED_PACKET), v5_bad_packet_disconnect_reason(pingreq, sizeof(pingreq), &closed)); ASSERT_TRUE(closed); } @@ -6300,7 +6636,7 @@ TEST(v5_unexpected_auth_sends_protocol_error_disconnect) static const byte auth[] = { 0xF0, 0x00 }; int closed = 0; - ASSERT_EQ(MQTT_REASON_PROTOCOL_ERR, + ASSERT_EQ(ERROR_DISCONNECT_REASON(MQTT_REASON_PROTOCOL_ERR), v5_bad_packet_disconnect_reason(auth, sizeof(auth), &closed)); ASSERT_TRUE(closed); } @@ -6354,6 +6690,50 @@ TEST(v5_error_close_waits_for_deferred_disconnect) MqttBroker_Free(&broker); } #endif /* WOLFMQTT_NONBLOCK && !WOLFMQTT_STATIC_MEMORY */ + +#if defined(WOLFMQTT_NONBLOCK) && defined(WOLFMQTT_STATIC_MEMORY) +/* A static client cannot resume a deferred write, so an error close must not + * start a DISCONNECT it may be unable to finish: a short write would leave a + * truncated packet on the wire before the close. The connection is closed + * with nothing written after the CONNACK. */ +TEST(v5_static_error_close_leaves_no_partial_disconnect) +{ + MqttBroker broker; + MqttBrokerNet net; + size_t connack_len; + int i; + static const byte connect[] = { + 0x10, 14, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'A' + }; + /* PINGREQ with a body: a Malformed Packet (MQTT 5.0 section 3.12). */ + static const byte pingreq[] = { 0xC0, 0x01, 0x00 }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_state(connect, sizeof(connect)); + run_broker_one_connect(&broker); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_out_buf[3]); + connack_len = g_out_len; + + g_clients[0].write_limit_once = 1; + mock_client_input_append(0, pingreq, sizeof(pingreq)); + for (i = 0; i < 4; i++) { + (void)MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_client_closed); + ASSERT_EQ(connack_len, g_out_len); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_NONBLOCK && WOLFMQTT_STATIC_MEMORY */ #endif /* WOLFMQTT_V5 */ #if defined(WOLFMQTT_V5) && !defined(WOLFMQTT_STATIC_MEMORY) @@ -6689,54 +7069,86 @@ TEST(will_qos1_queued_for_offline_persistent_subscriber) } #endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_BROKER_WILL */ -#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) -/* Queue a v5 QoS 1 PUBLISH with Message Expiry Interval 10 for an offline - * persistent subscriber, let wait_sec pass, then resume the Session. - * Returns the PUBLISH packets delivered and sets *wire_expiry to the Message - * Expiry Interval of the first one (-1 if not found). */ -static int queued_publish_after_wait(word32 wait_sec, long* wire_expiry) +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) +/* Returns 1 if needle occurs in hay. */ +static int will_bytes_found(const byte* hay, size_t hay_len, + const byte* needle, size_t needle_len) +{ + size_t i; + + for (i = 0; i + needle_len <= hay_len; i++) { + if (XMEMCMP(hay + i, needle, needle_len) == 0) { + return 1; + } + } + return 0; +} + +/* A v5 Will carries its Will Properties, other than the Will Delay Interval, + * onto the Will PUBLISH in their original order (MQTT 5.0 section 3.1.3.2, + * [MQTT-3.1.3-10]). With_delay adds a Will Delay Interval; Session Expiry 0 + * then publishes through the pending-Will path at once. Returns 1 when a + * live v5 subscriber receives the expected PUBLISH. */ +static int will_props_reach_live_subscriber(int with_delay) { MqttBroker broker; MqttBrokerNet net; int i; - int publishes; - size_t pos; - const byte* out; - size_t out_len; - /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry - * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + int found; + byte connect_pub[48]; + int pos = 0; + /* v5 CONNECT, Clean Start, no properties, ClientId "S" */ static const byte connect_sub[] = { - 0x10, 0x13, - 0x00, 0x04, 'M', 'Q', 'T', 'T', - 0x05, 0x00, 0x00, 0x3C, - 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, - 0x00, 0x01, 'S' - }; - /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 1 */ - static const byte subscribe_x[] = { - 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 - }; - static const byte disconnect[] = { 0xE0, 0x00 }; - /* v5 CONNECT, Clean Start 1, Properties length 0, ClientId "P" */ - static const byte connect_pub[] = { 0x10, 0x0E, 0x00, 0x04, 'M', 'Q', 'T', 'T', 0x05, 0x02, 0x00, 0x3C, 0x00, - 0x00, 0x01, 'P' + 0x00, 0x01, 'S' }; - /* v5 PUBLISH QoS 1 (MQTT 5.0 section 3.3): topic "x", packet id 1, - * Properties length 5 holding Message Expiry Interval 10 (0x02), - * payload "hi". Remaining Length 13. */ - static const byte publish_x[] = { - 0x32, 0x0D, - 0x00, 0x01, 'x', - 0x00, 0x01, - 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, - 'h', 'i' + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 0 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x00 + }; + /* Will Delay Interval (0x18) 5 seconds */ + static const byte will_delay[] = { 0x18, 0x00, 0x00, 0x00, 0x05 }; + /* Content Type (0x03) "t", then User Property (0x26) "k" = "v" */ + static const byte will_props[] = { + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v' + }; + /* Expected v5 QoS 0 PUBLISH (MQTT 5.0 section 3.3): topic "lwt", + * Properties length 11 holding the two properties above, payload "bye". + * Remaining Length 20. */ + static const byte expected[] = { + 0x30, 0x14, + 0x00, 0x03, 'l', 'w', 't', + 0x0B, + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 'b', 'y', 'e' }; + int props_len = (int)sizeof(will_props) + + (with_delay ? (int)sizeof(will_delay) : 0); + + /* v5 CONNECT (MQTT 5.0 section 3.1): Will Flag and Clean Start, keep + * alive 60, no CONNECT properties, ClientId "P", Will Properties, Will + * Topic "lwt", Will Payload "bye". */ + connect_pub[pos++] = 0x10; + connect_pub[pos++] = (byte)(11 + 3 + 1 + props_len + 5 + 5); + XMEMCPY(&connect_pub[pos], "\x00\x04MQTT\x05\x06\x00\x3C\x00", 11); + pos += 11; + XMEMCPY(&connect_pub[pos], "\x00\x01P", 3); + pos += 3; + connect_pub[pos++] = (byte)props_len; + if (with_delay) { + XMEMCPY(&connect_pub[pos], will_delay, sizeof(will_delay)); + pos += (int)sizeof(will_delay); + } + XMEMCPY(&connect_pub[pos], will_props, sizeof(will_props)); + pos += (int)sizeof(will_props); + XMEMCPY(&connect_pub[pos], "\x00\x03lwt\x00\x03" "bye", 10); + pos += 10; - *wire_expiry = -1; install_mock_net(&net); XMEMSET(&broker, 0, sizeof(broker)); if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || @@ -6744,16 +7156,419 @@ static int queued_publish_after_wait(word32 wait_sec, long* wire_expiry) return -1; } - g_broker_time_s = 100; - reset_mock_clients(3); + reset_mock_clients(2); mock_client_input_append(0, connect_sub, sizeof(connect_sub)); - mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); - mock_client_input_append(0, disconnect, sizeof(disconnect)); - mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(1, connect_pub, (size_t)pos); for (i = 0; i < 16; i++) { MqttBroker_Step(&broker); } - mock_client_input_append(1, publish_x, sizeof(publish_x)); + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + found = will_bytes_found(g_clients[0].out_buf, g_clients[0].out_len, + expected, sizeof(expected)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + return found; +} + +TEST(will_props_forwarded_to_live_subscriber) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0)); +} + +TEST(delayed_will_props_forwarded_to_live_subscriber) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(1)); +} + +#if WOLFMQTT_MAX_QOS >= 1 +/* A delayed Will can fire after every client has been freed, which releases + * the last MqttClient_Init reference to the shared property pool. The Will + * Properties must still be decoded: the copy queued for the offline Session + * keeps them (MQTT 5.0 section 3.1.3.2) and delivers them on resume. */ +TEST(delayed_will_props_decoded_with_no_clients) +{ + MqttBroker broker; + MqttBrokerNet net; + const byte* out; + size_t out_len; + size_t pos; + int found = 0; + int i; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT for "P": Will Flag, Will QoS 1, Clean Start 0, Properties + * length 5 holding Session Expiry Interval 60 (0x11), Will Properties + * length 14 holding Will Delay Interval (0x18) 2, Message Expiry + * Interval (0x02) 100 and Content Type (0x03) "t", Will Topic "lwt", + * Will Payload "bye". Remaining Length 44. */ + static const byte connect_pub[] = { + 0x10, 0x2C, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0C, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x0E, + 0x18, 0x00, 0x00, 0x00, 0x02, + 0x02, 0x00, 0x00, 0x00, 0x64, + 0x03, 0x00, 0x01, 't', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* Forwarded QoS 1 PUBLISH (MQTT 5.0 section 3.3), Remaining Length 20: + * topic "lwt", a broker-assigned packet id (skipped), then this. */ + static const byte expected_props[] = { + 0x09, + 0x02, 0x00, 0x00, 0x00, 0x64, + 0x03, 0x00, 0x01, 't', + 'b', 'y', 'e' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + g_broker_time_s = 100; + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + /* Both connections are gone before the Will Delay ends. */ + ASSERT_TRUE(g_clients[0].closed); + ASSERT_TRUE(g_clients[1].closed); + g_broker_time_s += 3; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_clients_active = 3; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + for (pos = 0; pos + 9 + sizeof(expected_props) <= out_len; pos++) { + if ((out[pos] & 0xF7) == 0x32 && out[pos + 1] == 0x14 && + XMEMCMP(&out[pos + 2], "\x00\x03lwt", 5) == 0 && + XMEMCMP(&out[pos + 9], expected_props, + sizeof(expected_props)) == 0) { + found = 1; + break; + } + } + g_broker_time_s = 0; + ASSERT_EQ(1, found); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + +#if WOLFMQTT_MAX_QOS >= 1 +/* A QoS 1 Will with Message Expiry Interval `expiry` is queued for an offline + * v5 subscriber, which resumes after wait_sec. Returns the PUBLISH packets it + * receives and sets *wire_expiry to the forwarded Message Expiry Interval + * (-1 if none). */ +static int queued_will_after_wait(word32 wait_sec, byte expiry, + long* wire_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + size_t pos; + const byte* out; + size_t out_len; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT: Will Flag, Will QoS 1 and Clean Start, no CONNECT + * properties, ClientId "P", Will Properties length 5 holding Message + * Expiry Interval (0x02) 10 (low byte at offset 21, set from expiry), + * Will Topic "lwt", Will Payload "bye". Remaining Length 30. */ + byte connect_pub[] = { + 0x10, 0x1E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0E, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + *wire_expiry = -1; + connect_pub[21] = expiry; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + g_broker_time_s += wait_sec; + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + out = g_clients[2].out_buf; + out_len = g_clients[2].out_len; + publishes = count_packets_of_type(out, out_len, + MQTT_PACKET_TYPE_PUBLISH); + /* Forwarded layout: 0x32 0x10, topic "lwt", 2 byte packet id, Properties + * length 5, then 0x02 and the four byte Message Expiry Interval. */ + for (pos = 0; pos + 16 <= out_len; pos++) { + if ((out[pos] & 0xF6) == 0x32 && out[pos + 1] == 0x10 && + out[pos + 2] == 0x00 && out[pos + 3] == 0x03 && + out[pos + 4] == 'l' && out[pos + 9] == 0x05 && + out[pos + 10] == MQTT_PROP_MSG_EXPIRY_INTERVAL) { + *wire_expiry = ((long)out[pos + 11] << 24) | + ((long)out[pos + 12] << 16) | ((long)out[pos + 13] << 8) | + (long)out[pos + 14]; + break; + } + } + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + g_broker_time_s = 0; + return publishes; +} + +/* [MQTT-3.3.2-5] A queued Will whose Message Expiry Interval passed before + * the subscriber resumed is deleted, not delivered. */ +TEST(queued_will_expired_before_delivery_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_will_after_wait(11, 10, &wire_expiry)); +} + +/* [MQTT-3.3.2-6] A queued Will is delivered with its Message Expiry Interval + * minus the time it waited. */ +TEST(queued_will_expiry_reduced_by_wait) +{ + long wire_expiry; + + ASSERT_EQ(1, queued_will_after_wait(4, 10, &wire_expiry)); + ASSERT_EQ(6, wire_expiry); +} + +/* MQTT 5.0 section 3.3.2.3.3: an explicit Message Expiry Interval of 0 has + * passed by the time an offline subscriber resumes, so the queued Will is + * deleted rather than treated as never expiring. */ +TEST(queued_will_zero_expiry_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_will_after_wait(1, 0, &wire_expiry)); +} +#endif /* WOLFMQTT_MAX_QOS >= 1 */ +#endif /* WOLFMQTT_V5 && WOLFMQTT_BROKER_WILL */ + +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_BROKER_WILDCARDS) && \ + WOLFMQTT_MAX_QOS >= 2 +/* [MQTT-3.3.4-2] An offline Session whose QoS 1 and QoS 2 subscriptions both + * match a QoS 2 Will gets it at QoS 2. One copy is queued, at the highest + * matching QoS, so a full queue cannot evict that copy for the other. */ +TEST(will_offline_overlapping_subs_queue_max_qos_once) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + size_t pos; + int qos2 = 0; + /* v3.1.1 CONNECT, CleanSession=0, ClientId "S" */ + static const byte connect_sub[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x00, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + /* SUBSCRIBE packet id 1, filter "lwt" QoS 1, then packet id 2, filter + * "+" QoS 2 */ + static const byte subscribe_lwt[] = { + 0x82, 0x08, 0x00, 0x01, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte subscribe_any[] = { + 0x82, 0x06, 0x00, 0x02, 0x00, 0x01, '+', 0x02 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v3.1.1 CONNECT, Will Flag, Will QoS 2 and CleanSession, ClientId "P", + * Will Topic "lwt", Will Message "bye". Remaining Length 23. */ + static const byte connect_pub[] = { + 0x10, 0x17, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x16, 0x00, 0x3C, + 0x00, 0x01, 'P', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, subscribe_any, sizeof(subscribe_any)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[0].closed); + + g_clients[1].read_err = 1; + for (i = 0; i < 8; i++) { + MqttBroker_Step(&broker); + } + ASSERT_TRUE(g_clients[1].closed); + + mock_client_input_append(2, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(1, count_packets_of_type(g_clients[2].out_buf, + g_clients[2].out_len, MQTT_PACKET_TYPE_PUBLISH)); + for (pos = 0; pos + 4 < g_clients[2].out_len; pos++) { + if ((g_clients[2].out_buf[pos] & 0xF0) == 0x30 && + g_clients[2].out_buf[pos + 2] == 0x00 && + g_clients[2].out_buf[pos + 3] == 0x03 && + g_clients[2].out_buf[pos + 4] == 'l') { + qos2 = ((g_clients[2].out_buf[pos] & 0x06) == 0x04); + break; + } + } + ASSERT_TRUE(qos2); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif + +#if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) +/* Queue a v5 QoS 1 PUBLISH with Message Expiry Interval `expiry` for an + * offline persistent subscriber, let wait_sec pass, then resume the Session. + * Returns the PUBLISH packets delivered and sets *wire_expiry to the Message + * Expiry Interval of the first one (-1 if not found). */ +static int queued_publish_after_wait(word32 wait_sec, byte expiry, + long* wire_expiry) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + int publishes; + size_t pos; + const byte* out; + size_t out_len; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "x", QoS 1 */ + static const byte subscribe_x[] = { + 0x82, 0x07, 0x00, 0x01, 0x00, 0x00, 0x01, 'x', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT, Clean Start 1, Properties length 0, ClientId "P" */ + static const byte connect_pub[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + /* v5 PUBLISH QoS 1 (MQTT 5.0 section 3.3): topic "x", packet id 1, + * Properties length 5 holding Message Expiry Interval 10 (0x02, low byte + * at offset 12, set from expiry), payload "hi". Remaining Length 13. */ + byte publish_x[] = { + 0x32, 0x0D, + 0x00, 0x01, 'x', + 0x00, 0x01, + 0x05, 0x02, 0x00, 0x00, 0x00, 0x0A, + 'h', 'i' + }; + + *wire_expiry = -1; + publish_x[12] = expiry; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + g_broker_time_s = 100; + reset_mock_clients(3); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_x, sizeof(subscribe_x)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + mock_client_input_append(1, publish_x, sizeof(publish_x)); for (i = 0; i < 16; i++) { MqttBroker_Step(&broker); } @@ -6794,7 +7609,7 @@ TEST(queued_publish_expired_before_delivery_dropped) { long wire_expiry; - ASSERT_EQ(0, queued_publish_after_wait(11, &wire_expiry)); + ASSERT_EQ(0, queued_publish_after_wait(11, 10, &wire_expiry)); } /* [MQTT-3.3.2-6] A queued copy is delivered with the received Message Expiry @@ -6803,9 +7618,21 @@ TEST(queued_publish_expiry_reduced_by_wait) { long wire_expiry; - ASSERT_EQ(1, queued_publish_after_wait(4, &wire_expiry)); + ASSERT_EQ(1, queued_publish_after_wait(4, 10, &wire_expiry)); ASSERT_EQ(6, wire_expiry); } + +/* MQTT 5.0 section 3.3.2.3.3: an explicit Message Expiry Interval of 0 is + * not "no expiry". A copy still queued once a second has passed is deleted; + * one whose delivery starts within the same second goes out with 0. */ +TEST(queued_publish_zero_expiry_dropped) +{ + long wire_expiry; + + ASSERT_EQ(0, queued_publish_after_wait(1, 0, &wire_expiry)); + ASSERT_EQ(1, queued_publish_after_wait(0, 0, &wire_expiry)); + ASSERT_EQ(0, wire_expiry); +} #endif /* !WOLFMQTT_STATIC_MEMORY && WOLFMQTT_V5 */ /* Parsed details of a single PUBLISH packet on the wire. found = 0 means @@ -6864,15 +7691,144 @@ static PublishInfo first_publish_info(const byte* buf, size_t len) } } } - return info; - } - if (remain > len - pos - hdr_len) { - break; + return info; + } + if (remain > len - pos - hdr_len) { + break; + } + pos += hdr_len + remain; + } + return info; +} + +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + !defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 +/* The old client subscribes at QoS 1 to its own QoS 1 Will topic and is + * taken over with Will Delay 0. The copy for the old connection is queued + * rather than written, so the old connection ends with its DISCONNECT 0x8E + * [MQTT-3.14.4-1]. A resumed Session delivers it once on the new connection + * [MQTT-4.4.0-1]; a Clean Start ends the Session and frees it. Returns the + * PUBLISH count on the new connection, or -1 on a setup or leak failure, and + * sets *first_byte to the first PUBLISH's fixed header byte. */ +static int takeover_qos1_self_will(int new_clean_start, byte* first_byte) +{ + MqttBroker broker; + MqttBrokerNet net; + PublishInfo info; + size_t before; + int publishes_before; + int live_before; + int publishes; + int i; + /* v5 CONNECT for "P": Will Flag, Will QoS 1, Clean Start 0, Properties + * length 5 holding Session Expiry Interval 300 (0x11), Will Properties + * length 0, Will Topic "lwt", Will Payload "bye". Remaining Length 30. */ + static const byte old_connect[] = { + 0x10, 0x1E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0C, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'P', + 0x00, + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte old_subscribe[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + /* v5 CONNECT for "P", Clean Start 0, Session Expiry Interval 300 */ + static const byte resume_connect[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'P' + }; + /* v5 CONNECT for "P", Clean Start 1, no properties */ + static const byte clean_connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P' + }; + + *first_byte = 0; + live_before = g_alloc_success_count - g_alloc_free_count; + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || + MqttBroker_Start(&broker) != MQTT_CODE_SUCCESS) { + return -1; + } + + reset_mock_clients(2); + mock_client_input_append(0, old_connect, sizeof(old_connect)); + mock_client_input_append(0, old_subscribe, sizeof(old_subscribe)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + before = g_clients[0].out_len; + publishes_before = g_clients[0].publish_writes; + if (new_clean_start) { + mock_client_input_append(1, clean_connect, sizeof(clean_connect)); + } + else { + mock_client_input_append(1, resume_connect, sizeof(resume_connect)); + } + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* MQTT 5.0 section 3.14: the old connection's output after its SUBACK + * is one DISCONNECT (0xE0) carrying Reason Code 0x8E and nothing else. */ + if (!g_clients[0].closed || + g_clients[0].publish_writes != publishes_before || + g_clients[0].out_len < before + 3 || + g_clients[0].out_buf[before] != 0xE0 || + g_clients[0].out_buf[before + 2] != + MQTT_REASON_SESSION_TAKEN_OVER || + g_clients[0].out_len != + before + 2 + g_clients[0].out_buf[before + 1]) { + publishes = -1; + } + else { + publishes = count_packets_of_type(g_clients[1].out_buf, + g_clients[1].out_len, MQTT_PACKET_TYPE_PUBLISH); + info = first_publish_info(g_clients[1].out_buf, g_clients[1].out_len); + if (info.found) { + *first_byte = info.first_byte; + if (info.packet_id == 0) { + publishes = -1; + } } - pos += hdr_len + remain; } - return info; + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); + if (g_alloc_success_count - g_alloc_free_count != live_before) { + publishes = -1; + } + return publishes; +} + +TEST(takeover_v5_resumed_session_gets_queued_self_will) +{ + byte first_byte; + + /* MQTT 5.0 section 3.3.1: QoS 1, DUP 0, RETAIN 0 */ + ASSERT_EQ(1, takeover_qos1_self_will(0, &first_byte)); + ASSERT_EQ(0x32, first_byte); +} + +TEST(takeover_v5_clean_start_frees_queued_self_will) +{ + byte first_byte; + + ASSERT_EQ(0, takeover_qos1_self_will(1, &first_byte)); } +#endif #if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) /* v5 PUBLISH QoS 1 on "x" with Message Expiry Interval 10 (MQTT 5.0 @@ -12236,6 +13192,131 @@ TEST(persist_partial_publish_restart_keeps_dup) } #endif +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + WOLFMQTT_MAX_QOS >= 1 +/* A Will queued for an offline v5 Session keeps its Properties across a + * broker restart (MQTT 5.0 section 3.1.3.2), and the forwarded Message + * Expiry Interval is reduced by the time waited [MQTT-3.3.2-6]. */ +TEST(persist_restart_keeps_queued_will_props) +{ + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + PersistOrderStore store; + const byte* out; + size_t out_len; + size_t pos; + int found = 0; + int i; + /* v5 CONNECT, Clean Start 0, Properties length 5 holding Session Expiry + * Interval 300 (0x11), ClientId "S". Remaining Length 19. */ + static const byte connect_sub[] = { + 0x10, 0x13, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x00, 0x00, 0x3C, + 0x05, 0x11, 0x00, 0x00, 0x01, 0x2C, + 0x00, 0x01, 'S' + }; + /* v5 SUBSCRIBE packet id 1, Properties length 0, filter "lwt", QoS 1 */ + static const byte subscribe_lwt[] = { + 0x82, 0x09, 0x00, 0x01, 0x00, 0x00, 0x03, 'l', 'w', 't', 0x01 + }; + static const byte disconnect[] = { 0xE0, 0x00 }; + /* v5 CONNECT: Will Flag, Will QoS 1 and Clean Start, no CONNECT + * properties, ClientId "P", Will Properties length 16 holding Message + * Expiry Interval (0x02) 60, Content Type (0x03) "t" and User Property + * (0x26) "k"="v", Will Topic "lwt", Will Payload "bye". + * Remaining Length 41. */ + static const byte connect_pub[] = { + 0x10, 0x29, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x0E, 0x00, 0x3C, + 0x00, + 0x00, 0x01, 'P', + 0x10, + 0x02, 0x00, 0x00, 0x00, 0x3C, + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 0x00, 0x03, 'l', 'w', 't', + 0x00, 0x03, 'b', 'y', 'e' + }; + /* Forwarded PUBLISH QoS 1 (MQTT 5.0 section 3.3), Remaining Length 27: + * topic "lwt", a broker-assigned packet id (skipped), then this. The + * Message Expiry Interval is 60 less the 4 seconds waited. */ + static const byte expected_props[] = { + 0x10, + 0x02, 0x00, 0x00, 0x00, 0x38, + 0x03, 0x00, 0x01, 't', + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', + 'b', 'y', 'e' + }; + + install_mock_net(&net); + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&restored, 0, sizeof(restored)); + XMEMSET(&hooks, 0, sizeof(hooks)); + XMEMSET(&store, 0, sizeof(store)); + hooks.kv_put = persist_order_put; + hooks.kv_get = persist_order_get; + hooks.kv_iter = persist_order_iter; + hooks.ctx = &store; +#ifdef WOLFMQTT_BROKER_PERSIST_ENCRYPT + hooks.derive_key = persist_order_derive_key; +#endif + + g_broker_time_s = 100; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&source, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_SetPersistHooks(&source, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&source)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&source)); + reset_mock_clients(2); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + mock_client_input_append(0, subscribe_lwt, sizeof(subscribe_lwt)); + mock_client_input_append(0, disconnect, sizeof(disconnect)); + mock_client_input_append(1, connect_pub, sizeof(connect_pub)); + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&source); + } + g_clients[1].read_err = 1; + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&source); + } + ASSERT_EQ(1, store.outq_count); + MqttBroker_Stop(&source); + MqttBroker_Free(&source); + + g_broker_time_s += 4; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&restored, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, + MqttBroker_SetPersistHooks(&restored, &hooks)); + ASSERT_EQ(MQTT_CODE_SUCCESS, BrokerPersist_Restore(&restored)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&restored)); + reset_mock_clients(1); + mock_client_input_append(0, connect_sub, sizeof(connect_sub)); + for (i = 0; i < 16; i++) { + (void)MqttBroker_Step(&restored); + } + + out = g_clients[0].out_buf; + out_len = g_clients[0].out_len; + for (pos = 0; pos + 9 + sizeof(expected_props) <= out_len; pos++) { + if ((out[pos] & 0xF7) == 0x32 && out[pos + 1] == 0x1B && + XMEMCMP(&out[pos + 2], "\x00\x03lwt", 5) == 0 && + XMEMCMP(&out[pos + 9], expected_props, + sizeof(expected_props)) == 0) { + found = 1; + break; + } + } + g_broker_time_s = 0; + ASSERT_EQ(1, found); + + MqttBroker_Stop(&restored); + MqttBroker_Free(&restored); +} +#endif + /* Non-persisted QoS 0 nodes still occupy FIFO positions. Assigning sequence * numbers only to durable nodes can make a later offline QoS 1 enqueue reuse * an earlier sequence and restore ahead of older messages. */ @@ -13735,6 +14816,232 @@ TEST(persist_restore_malformed_outq_scrubs_topic) ASSERT_TRUE(seen); ASSERT_TRUE(zeroed); } + +#ifdef WOLFMQTT_V5 +/* Ways to corrupt a schema 6 NS_OUTQ record body (layout in + * BrokerPersist_PutOutPub): flags(1) at offset 5, topic_len(2) at 28, then + * topic, payload_len(4), payload, props_len(4) and props. */ +enum { + OUTQ_MUT_RESERVED_FLAG, /* set reserved flags bit 1 */ + OUTQ_MUT_EXPIRY_FLAG, /* set has_expiry with no expiry property */ + OUTQ_MUT_PROPS_LEN_LONG, /* props_len one more than the bytes left */ + OUTQ_MUT_PROPS_LEN_SHORT,/* props_len one less than the bytes left */ + OUTQ_MUT_BAD_PROP_ID, /* property identifier 0x7F is not defined */ + OUTQ_MUT_PROP_TRAILING /* a stray byte after the last property */ +}; + +static word32 persist_rd32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; +} + +static void persist_wr32(byte* p, word32 v) +{ + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; +} + +/* Rewrite the only record in root/ with one corruption. The record + * header is magic(4) version(2) kind(1) wrap(1) body_len(4). Returns 0. */ +static int persist_mutate_only_outq(const char* root, int kind) +{ + char dir_path[640]; + char rec_path[1024]; + byte rec[512]; + struct dirent* ent; + DIR* d; + FILE* f; + size_t len = 0; + size_t body = 12; + size_t props_len_off; + word32 topic_len; + word32 payload_len; + word32 props_len; + int rc = -1; + + if (snprintf(dir_path, sizeof(dir_path), "%s/%u", root, + (unsigned)BROKER_PERSIST_NS_OUTQ) <= 0) { + return -1; + } + d = opendir(dir_path); + if (d == NULL) { + return -1; + } + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] != '.') { + break; + } + } + if (ent != NULL && snprintf(rec_path, sizeof(rec_path), "%s/%s", + dir_path, ent->d_name) > 0) { + f = fopen(rec_path, "rb"); + if (f != NULL) { + len = fread(rec, 1, sizeof(rec) - 1, f); + if (fclose(f) == 0 && len > body + 30) { + rc = 0; + } + } + } + (void)closedir(d); + if (rc != 0) { + return -1; + } + + topic_len = ((word32)rec[body + 28] << 8) | rec[body + 29]; + payload_len = persist_rd32(&rec[body + 30 + topic_len]); + props_len_off = body + 34 + topic_len + payload_len; + if (props_len_off + 4 > len) { + return -1; + } + props_len = persist_rd32(&rec[props_len_off]); + switch (kind) { + case OUTQ_MUT_RESERVED_FLAG: + rec[body + 5] |= 0x02; + break; + case OUTQ_MUT_EXPIRY_FLAG: + rec[body + 5] |= 0x01; + break; + case OUTQ_MUT_PROPS_LEN_LONG: + persist_wr32(&rec[props_len_off], props_len + 1); + break; + case OUTQ_MUT_PROPS_LEN_SHORT: + persist_wr32(&rec[props_len_off], props_len - 1); + break; + case OUTQ_MUT_BAD_PROP_ID: + rec[props_len_off + 4] = 0x7F; + break; + case OUTQ_MUT_PROP_TRAILING: + /* 0x01 starts a Payload Format Indicator with no value byte. */ + rec[len++] = 0x01; + persist_wr32(&rec[props_len_off], props_len + 1); + persist_wr32(&rec[8], persist_rd32(&rec[8]) + 1); + break; + default: + return -1; + } + + rc = -1; + f = fopen(rec_path, "wb"); + if (f != NULL) { + if (fwrite(rec, 1, len, f) == len) { + rc = 0; + } + if (fclose(f) != 0) { + rc = -1; + } + } + return rc; +} + +/* A 53-byte payload, a size nothing else allocates during these restores. */ +#define PERSIST_SCRUB_PAYLOAD \ + "payload-secret-0123456789-0123456789-0123456789-01234" + +/* Persist a v5 QoS 1 OUTQ record carrying a Payload Format Indicator, + * corrupt it with `kind`, and restore it. The record must be skipped without + * failing the restore. capture_size picks the heap copy whose free must be + * scrubbed (0 when the record is rejected before any copy is made). Returns + * 1 when all of that holds. */ +static int persist_restore_mutated_outq(int kind, size_t capture_size) +{ + char root_template[] = "./wolfmqtt-persist-XXXXXX"; + MqttBroker source; + MqttBroker restored; + MqttBrokerNet net; + MqttBrokerPersistHooks hooks; + BrokerOutPub out; + MqttProp prop; + char topic[] = PERSIST_SCRUB_TOPIC; + byte payload[] = PERSIST_SCRUB_PAYLOAD; + char* root; + int ok = 0; + int put_rc = MQTT_CODE_ERROR_SYSTEM; + int restore_rc = MQTT_CODE_ERROR_SYSTEM; + int queue_len = -1; + int seen = 0; + int zeroed = 0; + + install_mock_net(&net); + XMEMSET(&hooks, 0, sizeof(hooks)); + root = mkdtemp(root_template); + if (root == NULL) { + return 0; + } + if (MqttBrokerNet_PersistPosix_Init(&hooks, root) == MQTT_CODE_SUCCESS) { + XMEMSET(&source, 0, sizeof(source)); + XMEMSET(&out, 0, sizeof(out)); + XMEMSET(&prop, 0, sizeof(prop)); + prop.type = MQTT_PROP_PAYLOAD_FORMAT_IND; + prop.data_byte = 1; + out.topic = topic; + out.payload = payload; + out.payload_len = (word32)sizeof(payload); + out.qos = MQTT_QOS_1; + out.packet_id = 1; + out.state = BROKER_OUTQ_QUEUED; + out.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + out.enq_time = (WOLFMQTT_BROKER_TIME_T)1; + out.enqueue_seq = 1; + out.props = ∝ + if (MqttBroker_Init(&source, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&source, &hooks) == + MQTT_CODE_SUCCESS && + BrokerPersist_Restore(&source) == MQTT_CODE_SUCCESS && + BrokerPersist_PutOrphanSession(&source, "cid", + MQTT_CONNECT_PROTOCOL_LEVEL_5, 0xFFFFFFFFu, + (WOLFMQTT_BROKER_TIME_T)1) == MQTT_CODE_SUCCESS) { + put_rc = BrokerPersist_PutOutPub(&source, "cid", &out); + } + MqttBroker_Free(&source); + + if (put_rc == MQTT_CODE_SUCCESS && + persist_mutate_only_outq(root, kind) == 0) { + XMEMSET(&restored, 0, sizeof(restored)); + if (capture_size != 0) { + broker_test_capture_free(capture_size); + } + if (MqttBroker_Init(&restored, &net) == MQTT_CODE_SUCCESS && + MqttBroker_SetPersistHooks(&restored, &hooks) == + MQTT_CODE_SUCCESS) { + restore_rc = BrokerPersist_Restore(&restored); + } + seen = g_capture_free_seen; + zeroed = persist_captured_free_zeroed(); + broker_test_disable_free_capture(); + if (restored.orphan_sessions != NULL) { + queue_len = restored.orphan_sessions->out_q_count; + } + MqttBroker_Free(&restored); + ok = (restore_rc == MQTT_CODE_SUCCESS && queue_len == 0 && + (capture_size == 0 || (seen && zeroed))); + } + MqttBrokerNet_PersistPosix_Free(&hooks); + } + persist_remove_tree(root); + return ok; +} + +/* Schema 6 restore rejects each malformed OUTQ record on its own, skipping + * it instead of failing the whole restore, and wipes any topic or payload + * copy it already made. */ +TEST(persist_restore_rejects_malformed_outq_props) +{ + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_RESERVED_FLAG, 0)); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROPS_LEN_LONG, + sizeof(PERSIST_SCRUB_TOPIC))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROPS_LEN_SHORT, + sizeof(PERSIST_SCRUB_TOPIC))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_BAD_PROP_ID, + sizeof(PERSIST_SCRUB_PAYLOAD))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_PROP_TRAILING, + sizeof(PERSIST_SCRUB_PAYLOAD))); + ASSERT_EQ(1, persist_restore_mutated_outq(OUTQ_MUT_EXPIRY_FLAG, + sizeof(PERSIST_SCRUB_PAYLOAD))); +} +#endif /* WOLFMQTT_V5 */ #endif /* !WOLFMQTT_BROKER_PERSIST_ENCRYPT */ #endif /* !WOLFMQTT_STATIC_MEMORY */ @@ -15118,6 +16425,16 @@ int main(int argc, char** argv) #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) RUN_TEST(connect_v5_oversize_will_payload_emits_connack); #ifdef WOLFMQTT_STATIC_MEMORY + RUN_TEST(connect_v5_oversize_will_props_emits_connack); + #if BROKER_MAX_STATIC_OFFLINE_DATA_LEN - 5 < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(connect_v5_static_will_exceeding_offline_slot_refused); + #endif + #if BROKER_MAX_STATIC_OFFLINE_DATA_LEN < BROKER_MAX_WILL_PAYLOAD_LEN && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(connect_v5_static_payload_only_will_exceeding_offline_slot_refused); + RUN_TEST(connect_v311_static_will_exceeding_offline_slot_refused); + #endif RUN_TEST(connect_v5_oversize_will_topic_emits_connack); #endif #endif @@ -15262,6 +16579,13 @@ int main(int argc, char** argv) RUN_TEST(disconnect_v5_reason_error_publishes_will); RUN_TEST(takeover_v5_zero_will_delay_publishes_will); RUN_TEST(takeover_v5_own_will_not_sent_after_disconnect); +#if !defined(WOLFMQTT_STATIC_MEMORY) && WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(takeover_v5_resumed_session_gets_queued_self_will); + RUN_TEST(takeover_v5_clean_start_frees_queued_self_will); +#endif +#if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(takeover_v5_own_will_partial_write_still_disconnects); +#endif RUN_TEST(takeover_v5_will_delay_resumed_session_discards_will); RUN_TEST(takeover_v5_clean_start_publishes_will); RUN_TEST(takeover_v5_zero_session_expiry_publishes_will); @@ -15283,9 +16607,26 @@ int main(int argc, char** argv) RUN_TEST(will_qos0_exceeding_tx_buf_not_sent_truncated); RUN_TEST(will_qos1_queued_for_offline_persistent_subscriber); #endif +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) + RUN_TEST(will_props_forwarded_to_live_subscriber); + RUN_TEST(delayed_will_props_forwarded_to_live_subscriber); +#if WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(delayed_will_props_decoded_with_no_clients); +#endif +#if WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(queued_will_expired_before_delivery_dropped); + RUN_TEST(queued_will_expiry_reduced_by_wait); + RUN_TEST(queued_will_zero_expiry_dropped); +#endif +#endif +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_BROKER_WILDCARDS) && \ + WOLFMQTT_MAX_QOS >= 2 + RUN_TEST(will_offline_overlapping_subs_queue_max_qos_once); +#endif #if !defined(WOLFMQTT_STATIC_MEMORY) && defined(WOLFMQTT_V5) RUN_TEST(queued_publish_expired_before_delivery_dropped); RUN_TEST(queued_publish_expiry_reduced_by_wait); + RUN_TEST(queued_publish_zero_expiry_dropped); RUN_TEST(queued_publish_expired_behind_inflight_cap_dropped); RUN_TEST(inflight_publish_resent_after_expiry); #ifdef WOLFMQTT_BROKER_PERSIST @@ -15400,6 +16741,9 @@ int main(int argc, char** argv) #if defined(WOLFMQTT_NONBLOCK) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(v5_error_close_waits_for_deferred_disconnect); #endif +#if defined(WOLFMQTT_NONBLOCK) && defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(v5_static_error_close_leaves_no_partial_disconnect); +#endif #endif #ifdef WOLFMQTT_V5 #ifndef WOLFMQTT_STATIC_MEMORY @@ -15513,6 +16857,10 @@ int main(int argc, char** argv) RUN_TEST(persist_partial_publish_restart_keeps_dup); #endif RUN_TEST(persist_mixed_qos_queue_preserves_fifo); + #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) && \ + WOLFMQTT_MAX_QOS >= 1 + RUN_TEST(persist_restart_keeps_queued_will_props); + #endif RUN_TEST(orphan_reclaim_keeps_persisted_outq); #endif RUN_TEST(persist_stream_only_hooks_rejected); @@ -15536,6 +16884,9 @@ int main(int argc, char** argv) RUN_TEST(persist_restore_malformed_retained_scrubs_topic); #endif RUN_TEST(persist_restore_malformed_outq_scrubs_topic); +#ifdef WOLFMQTT_V5 + RUN_TEST(persist_restore_rejects_malformed_outq_props); +#endif #endif #endif RUN_TEST(persist_iter_reports_unreadable_record); diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index cd8003cc7..1aa8af144 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -5727,6 +5727,33 @@ TEST(publish_header_fills_tx_buf_sends_payload) ASSERT_EQ(0, XMEMCMP(expected, g_connect_wire, sizeof(expected))); } +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) +/* WaitType stops a read twice. The repeat, after the first released lockRecv, + * must not reset the read state of a reader that has since taken it, or that + * reader would no longer be known to own lockRecv. */ +TEST(read_stop_repeat_keeps_new_reader_state) +{ + MqttMsgStat first; + MqttMsgStat second; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + XMEMSET(&first, 0, sizeof(first)); + XMEMSET(&second, 0, sizeof(second)); + + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttReadStart(&test_client, &first)); + MqttReadStop(&test_client, &first); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttReadStart(&test_client, &second)); + MqttReadStop(&test_client, &first); + + ASSERT_EQ(1, test_client.read.isActive); + ASSERT_EQ(1, test_client.read_owner_valid); + ASSERT_EQ(1, second.isReadActive); + MqttReadStop(&test_client, &second); + ASSERT_EQ(0, test_client.read.isActive); + ASSERT_EQ(0, test_client.read_owner_valid); +} +#endif + #if defined(WOLFMQTT_V5) && WOLFMQTT_MAX_QOS >= 1 /* Drives one QoS>0 publish to completion against the canned-response mock and * returns the final MqttClient_Publish result. The caller stages the broker's @@ -6805,6 +6832,746 @@ TEST(publish_writeonly_v5_no_dangling_pendresp_on_success) /* No unit was reserved, so the quota is untouched. */ ASSERT_EQ(1, test_client.server_recv_max); } + +#if defined(WOLFMQTT_PROPERTY_CB) && \ + (defined(__MACH__) || defined(WOLFMQTT_POSIX_SEMAPHORES)) +/* A reader thread that matches a PUBACK to a write-only publish decodes into + * that publish object outside lockClient. The publisher must not return, and so + * let its caller free the object, until that reader is done with it. The v5 + * property callback runs inside the decode, so it parks the reader there. */ +typedef struct WriteOnlyAckRaceCtx { + pthread_mutex_t mutex; + pthread_cond_t cond; + MqttPublish publish; + int ack_pos; + int published; + int in_decode; + int publish_returned; + int returned_during_decode; + int sync_error; + int publish_rc; + int acks; /* copies of g_race_puback to serve */ + int prop_calls; + int next_read; /* reader entered the read after the acks */ + int returned_during_next_read; + int reader_done; +} WriteOnlyAckRaceCtx; + +/* PUBACK, MQTT 5.0 section 3.4: Packet Identifier 23, Reason Code 0x00 and a + * Reason String property (0x1F) "ok" so the property callback runs. */ +static const byte g_race_puback[] = { + 0x40, 0x09, /* PUBACK, Remaining Length 9 */ + 0x00, 0x17, /* Packet Identifier 23 */ + 0x00, /* Reason Code: Success */ + 0x05, /* Property Length 5 */ + 0x1F, 0x00, 0x02, /* Reason String, length 2 */ + 'o', 'k' +}; + +/* Wait on the race condition variable until *flag is set or one second + * passes. Called with race->mutex held. */ +static void writeonly_ack_race_wait(WriteOnlyAckRaceCtx* race, + const int* flag) +{ + struct timespec deadline; + int wait_rc = 0; + + if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + race->sync_error = 1; + return; + } + deadline.tv_sec++; + while (*flag == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&race->cond, &race->mutex, + &deadline); + } + if (wait_rc != 0 && wait_rc != ETIMEDOUT) { + race->sync_error = 1; + } +} + +/* Holds the PUBLISH write until the reader is inside the PUBACK decode, so the + * publisher checks its pending response at the worst moment. */ +static int mock_net_write_writeonly_ack_race(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + + (void)timeout_ms; + if (buf_len > 0 && (buf[0] >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->published = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->in_decode); + if (race->in_decode == 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + } + return buf_len; +} + +/* Serves the PUBACK once the PUBLISH is out, then reports an idle timeout. */ +static int mock_net_read_writeonly_ack_race(void *context, byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + int n; + + (void)timeout_ms; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + writeonly_ack_race_wait(race, &race->published); + n = (int)sizeof(g_race_puback) - race->ack_pos; + if (race->published == 0 || n <= 0) { + n = MQTT_CODE_ERROR_TIMEOUT; + } + else { + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_race_puback + race->ack_pos, n); + race->ack_pos += n; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return n; +} + +/* Runs on the reader thread while it decodes into the publisher's object. */ +static int writeonly_ack_race_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_decode = race->publish_returned; + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return MQTT_CODE_SUCCESS; +} + +static void* writeonly_ack_race_reader_thread(void* arg) +{ + (void)arg; + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + return NULL; +} + +static void* writeonly_ack_race_publish_thread(void* arg) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)arg; + int rc; + + rc = MqttClient_Publish_WriteOnly(&test_client, &race->publish, NULL); + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return NULL; + } + race->publish_rc = rc; + race->publish_returned = 1; + if (pthread_cond_broadcast(&race->cond) != 0 || + pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + } + return NULL; +} + +TEST(publish_writeonly_waits_for_reader_decoding_ack) +{ + static WriteOnlyAckRaceCtx race; + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + int rc; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(&race, 0, sizeof(race)); + ASSERT_EQ(0, pthread_mutex_init(&race.mutex, NULL)); + ASSERT_EQ(0, pthread_cond_init(&race.cond, NULL)); + + race.publish.qos = MQTT_QOS_1; + race.publish.packet_id = 23; + race.publish.topic_name = "test/topic"; + race.publish.buffer = payload; + race.publish.total_len = (word32)(sizeof(payload) - 1); + race.publish.buffer_len = race.publish.total_len; + race.publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = ∽̱ + test_net.write = mock_net_write_writeonly_ack_race; + test_net.read = mock_net_read_writeonly_ack_race; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_SetPropertyCallback(&test_client, + writeonly_ack_race_prop_cb, &race)); + + ASSERT_EQ(0, pthread_create(&reader_thread, NULL, + writeonly_ack_race_reader_thread, &race)); + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, &race)); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(0, pthread_join(reader_thread, NULL)); + + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.in_decode); + /* The publisher did not return while the reader was inside its object. */ + ASSERT_EQ(0, race.returned_during_decode); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); + + ASSERT_EQ(0, pthread_cond_destroy(&race.cond)); + ASSERT_EQ(0, pthread_mutex_destroy(&race.mutex)); +} + +/* Serves race->acks copies of g_race_puback once the PUBLISH is out. The read + * after them waits for the publisher to return, as a dedicated reader blocks + * in its next read, then reports an idle timeout. */ +static int mock_net_read_writeonly_acks(void *context, byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + int total; + int n; + int i; + + (void)timeout_ms; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + writeonly_ack_race_wait(race, &race->published); + total = (int)sizeof(g_race_puback) * race->acks; + n = total - race->ack_pos; + if (race->published == 0) { + n = MQTT_CODE_ERROR_TIMEOUT; + } + else if (n <= 0) { + race->next_read = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_next_read = race->publish_returned; + n = MQTT_CODE_ERROR_TIMEOUT; + } + else { + if (n > buf_len) { + n = buf_len; + } + for (i = 0; i < n; i++) { + buf[i] = g_race_puback[(race->ack_pos + i) % + (int)sizeof(g_race_puback)]; + } + race->ack_pos += n; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return n; +} + +/* Parks the reader in the decode of the last ack until the publisher returns + * or one second passes. Earlier acks pass straight through. */ +static int writeonly_last_ack_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->prop_calls++; + if (race->prop_calls == race->acks) { + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->publish_returned); + race->returned_during_decode = race->publish_returned; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + return MQTT_CODE_SUCCESS; +} + +/* Sets up a write-only QoS 1 publish raced against a WaitMessage reader that + * receives race->acks PUBACKs for it, runs both threads and joins them. */ +static int writeonly_acks_run(WriteOnlyAckRaceCtx* race, int acks, + MqttPropertyCb prop_cb) +{ + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + + if (test_init_client() != MQTT_CODE_SUCCESS) { + return -1; + } + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(race, 0, sizeof(*race)); + if (pthread_mutex_init(&race->mutex, NULL) != 0 || + pthread_cond_init(&race->cond, NULL) != 0) { + return -1; + } + race->acks = acks; + race->publish.qos = MQTT_QOS_1; + race->publish.packet_id = 23; + race->publish.topic_name = "test/topic"; + race->publish.buffer = payload; + race->publish.total_len = (word32)(sizeof(payload) - 1); + race->publish.buffer_len = race->publish.total_len; + race->publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = race; + test_net.write = mock_net_write_writeonly_ack_race; + test_net.read = mock_net_read_writeonly_acks; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + if (MqttClient_SetPropertyCallback(&test_client, prop_cb, race) != + MQTT_CODE_SUCCESS) { + return -1; + } + if (pthread_create(&reader_thread, NULL, + writeonly_ack_race_reader_thread, race) != 0) { + return -1; + } + if (pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, race) != 0) { + (void)pthread_join(reader_thread, NULL); + return -1; + } + if (pthread_join(publish_thread, NULL) != 0 || + pthread_join(reader_thread, NULL) != 0) { + return -1; + } + if (pthread_cond_destroy(&race->cond) != 0 || + pthread_mutex_destroy(&race->mutex) != 0) { + return -1; + } + return 0; +} + +/* Lets the publisher reach its wait before the reader leaves the decode. */ +static int writeonly_brief_ack_prop_cb(MqttClient* client, MqttProp* head, + void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + struct timespec pause; + + (void)client; + (void)head; + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->prop_calls++; + race->in_decode = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + pause.tv_sec = 0; + pause.tv_nsec = 100 * 1000 * 1000; + (void)nanosleep(&pause, NULL); + return MQTT_CODE_SUCCESS; +} + +#ifdef WOLFMQTT_SEM_SIGNAL +/* A dedicated reader retakes lockRecv for its next blocking read as soon as it + * finishes the ack. A publisher waiting for that decode to end must be woken + * by it, not left waiting until the reader's next read returns. */ +TEST(publish_writeonly_not_held_by_reader_next_read) +{ + static WriteOnlyAckRaceCtx race; + + ASSERT_EQ(0, writeonly_acks_run(&race, 1, writeonly_brief_ack_prop_cb)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.in_decode); + ASSERT_EQ(1, race.next_read); + ASSERT_EQ(1, race.returned_during_next_read); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); +} +#endif + +/* A duplicate PUBACK matches the entry the first one already completed, and + * the reader decodes it into the same publish object. The publisher must not + * take the completed entry and return while that decode runs. */ +TEST(publish_writeonly_waits_for_duplicate_ack_decode) +{ + static WriteOnlyAckRaceCtx race; + + ASSERT_EQ(0, writeonly_acks_run(&race, 2, writeonly_last_ack_prop_cb)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(2, race.prop_calls); + ASSERT_EQ(1, race.in_decode); + ASSERT_EQ(0, race.returned_during_decode); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); +} + +/* Holds the PUBLISH write until the reader has failed on the PUBACK. */ +static int mock_net_write_after_reader(void *context, const byte* buf, + int buf_len, int timeout_ms) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)context; + + (void)timeout_ms; + if (buf_len > 0 && (buf[0] >> 4) == MQTT_PACKET_TYPE_PUBLISH) { + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + race->published = 1; + if (pthread_cond_broadcast(&race->cond) != 0) { + race->sync_error = 1; + } + writeonly_ack_race_wait(race, &race->reader_done); + if (race->reader_done == 0) { + race->sync_error = 1; + } + if (pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + return MQTT_CODE_ERROR_SYSTEM; + } + } + return buf_len; +} + +/* Fails the decode of the PUBACK after the reader has claimed the entry. */ +static int failing_ack_prop_cb(MqttClient* client, MqttProp* head, void* ctx) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)ctx; + + (void)client; + (void)head; + race->prop_calls++; + return MQTT_CODE_ERROR_SYSTEM; +} + +static void* failing_ack_reader_thread(void* arg) +{ + WriteOnlyAckRaceCtx* race = (WriteOnlyAckRaceCtx*)arg; + + (void)MqttClient_WaitMessage(&test_client, TEST_CMD_TIMEOUT_MS); + if (pthread_mutex_lock(&race->mutex) != 0) { + race->sync_error = 1; + return NULL; + } + race->reader_done = 1; + if (pthread_cond_broadcast(&race->cond) != 0 || + pthread_mutex_unlock(&race->mutex) != 0) { + race->sync_error = 1; + } + return NULL; +} + +/* A reader that fails while decoding into another thread's entry must release + * its claim, or the owner would wait forever for the decode to end. */ +TEST(publish_writeonly_after_reader_decode_error) +{ + static WriteOnlyAckRaceCtx race; + static byte payload[] = "hello"; + pthread_t reader_thread; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int returned; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(&race, 0, sizeof(race)); + ASSERT_EQ(0, pthread_mutex_init(&race.mutex, NULL)); + ASSERT_EQ(0, pthread_cond_init(&race.cond, NULL)); + race.publish.qos = MQTT_QOS_1; + race.publish.packet_id = 23; + race.publish.topic_name = "test/topic"; + race.publish.buffer = payload; + race.publish.total_len = (word32)(sizeof(payload) - 1); + race.publish.buffer_len = race.publish.total_len; + race.publish_rc = MQTT_CODE_ERROR_SYSTEM; + test_net.context = ∽̱ + race.acks = 1; + test_net.write = mock_net_write_after_reader; + test_net.read = mock_net_read_writeonly_acks; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttClient_SetPropertyCallback(&test_client, + failing_ack_prop_cb, &race)); + + ASSERT_EQ(0, pthread_create(&reader_thread, NULL, + failing_ack_reader_thread, &race)); + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + writeonly_ack_race_publish_thread, &race)); + ASSERT_EQ(0, pthread_join(reader_thread, NULL)); + + ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &deadline)); + deadline.tv_sec += 2; + ASSERT_EQ(0, pthread_mutex_lock(&race.mutex)); + while (race.publish_returned == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&race.cond, &race.mutex, &deadline); + } + returned = race.publish_returned; + ASSERT_EQ(0, pthread_mutex_unlock(&race.mutex)); + if (!returned) { + /* Stuck waiting: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + } + ASSERT_EQ(1, returned); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(0, race.sync_error); + ASSERT_EQ(1, race.prop_calls); + ASSERT_EQ(MQTT_CODE_SUCCESS, race.publish_rc); + ASSERT_NULL(test_client.firstPendResp); + ASSERT_EQ(0, pthread_cond_destroy(&race.cond)); + ASSERT_EQ(0, pthread_mutex_destroy(&race.mutex)); +} + +#ifdef WOLFMQTT_THREAD_ID_T +/* A property callback runs on the thread that holds lockRecv while it decodes + * an ack. Client calls from it that would wait on that lock must refuse with + * MQTT_CODE_ERROR_STAT rather than self-deadlock. The publish runs on its own + * thread so a deadlock fails the test instead of hanging it. */ +#define CB_REENTRY_CANCEL 0 +#define CB_REENTRY_NET_DISCONNECT 1 + +typedef struct CbReentryCtx { + pthread_mutex_t mutex; + pthread_cond_t cond; + MqttPublish publish; + int action; + int cb_called; + int cb_rc; + int publish_rc; + int done; +} CbReentryCtx; + +static int cb_reentry_prop_cb(MqttClient* client, MqttProp* head, void* ctx) +{ + CbReentryCtx* reentry = (CbReentryCtx*)ctx; + + (void)head; + reentry->cb_called = 1; + if (reentry->action == CB_REENTRY_CANCEL) { + reentry->cb_rc = MqttClient_CancelMessage(client, + (MqttObject*)&reentry->publish); + } + else { + reentry->cb_rc = MqttClient_NetDisconnect_Wait(client); + } + return MQTT_CODE_SUCCESS; +} + +static void* cb_reentry_publish_thread(void* arg) +{ + CbReentryCtx* reentry = (CbReentryCtx*)arg; + int rc; + + rc = MqttClient_Publish(&test_client, &reentry->publish); + (void)pthread_mutex_lock(&reentry->mutex); + reentry->publish_rc = rc; + reentry->done = 1; + (void)pthread_cond_broadcast(&reentry->cond); + (void)pthread_mutex_unlock(&reentry->mutex); + return NULL; +} + +/* Runs a blocking QoS 1 publish whose PUBACK carries a property, with the + * property callback doing reentry->action. Returns 0 once the publish + * finished, or -1 if it is still stuck after two seconds. */ +static int cb_reentry_run(CbReentryCtx* reentry, int action) +{ + static byte payload[] = "hello"; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int finished; + + if (test_init_client() != MQTT_CODE_SUCCESS) { + return -1; + } + test_client_connect_sent(); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + XMEMSET(reentry, 0, sizeof(*reentry)); + if (pthread_mutex_init(&reentry->mutex, NULL) != 0 || + pthread_cond_init(&reentry->cond, NULL) != 0) { + return -1; + } + reentry->action = action; + reentry->cb_rc = MQTT_CODE_SUCCESS; + reentry->publish.qos = MQTT_QOS_1; + reentry->publish.packet_id = 23; + reentry->publish.topic_name = "test/topic"; + reentry->publish.buffer = payload; + reentry->publish.total_len = (word32)(sizeof(payload) - 1); + reentry->publish.buffer_len = reentry->publish.total_len; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, g_race_puback, sizeof(g_race_puback)); + g_canned_len = (int)sizeof(g_race_puback); + g_canned_pos = 0; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + if (MqttClient_SetPropertyCallback(&test_client, cb_reentry_prop_cb, + reentry) != MQTT_CODE_SUCCESS) { + return -1; + } + + if (pthread_create(&publish_thread, NULL, cb_reentry_publish_thread, + reentry) != 0 || + clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + return -1; + } + deadline.tv_sec += 2; + (void)pthread_mutex_lock(&reentry->mutex); + while (reentry->done == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&reentry->cond, &reentry->mutex, + &deadline); + } + finished = reentry->done; + (void)pthread_mutex_unlock(&reentry->mutex); + if (!finished) { + /* Deadlocked: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + return -1; + } + (void)pthread_join(publish_thread, NULL); + (void)pthread_cond_destroy(&reentry->cond); + (void)pthread_mutex_destroy(&reentry->mutex); + return 0; +} + +TEST(cancel_from_own_ack_property_cb_refuses) +{ + static CbReentryCtx reentry; + + ASSERT_EQ(0, cb_reentry_run(&reentry, CB_REENTRY_CANCEL)); + ASSERT_EQ(1, reentry.cb_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, reentry.cb_rc); + /* The refused cancel left the exchange intact, so it completes. */ + ASSERT_EQ(MQTT_CODE_SUCCESS, reentry.publish_rc); + ASSERT_NULL(test_client.firstPendResp); +} + +TEST(net_disconnect_wait_from_property_cb_refuses) +{ + static CbReentryCtx reentry; + + ASSERT_EQ(0, cb_reentry_run(&reentry, CB_REENTRY_NET_DISCONNECT)); + ASSERT_EQ(1, reentry.cb_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, reentry.cb_rc); + ASSERT_EQ(MQTT_CODE_SUCCESS, reentry.publish_rc); +} + +static pthread_mutex_t g_pub_cb_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_pub_cb_cond = PTHREAD_COND_INITIALIZER; +static int g_pub_cb_disconnect_rc; +static int g_pub_cb_disconnect_called; +static int g_pub_cb_disconnect_done; + +/* Runs while the publish holds lockSend. */ +static int pub_cb_net_disconnect_wait(MqttPublish* publish) +{ + g_pub_cb_disconnect_called = 1; + g_pub_cb_disconnect_rc = MqttClient_NetDisconnect_Wait(&test_client); + XMEMSET(publish->buffer, 'x', publish->buffer_len); + return (int)publish->buffer_len; +} + +static void* pub_cb_disconnect_thread(void* arg) +{ + MqttPublish* publish = (MqttPublish*)arg; + + (void)MqttClient_Publish_ex(&test_client, publish, + pub_cb_net_disconnect_wait); + (void)pthread_mutex_lock(&g_pub_cb_mutex); + g_pub_cb_disconnect_done = 1; + (void)pthread_cond_broadcast(&g_pub_cb_cond); + (void)pthread_mutex_unlock(&g_pub_cb_mutex); + return NULL; +} + +TEST(net_disconnect_wait_from_publish_payload_cb_refuses) +{ + static byte payload[8]; + static MqttPublish publish; + pthread_t publish_thread; + struct timespec deadline; + int wait_rc = 0; + int finished; + + ASSERT_EQ(MQTT_CODE_SUCCESS, test_init_client()); + test_client_connect_sent(); + test_net.write = mock_net_write_accept; + (void)MqttClient_Flags(&test_client, 0, MQTT_CLIENT_FLAG_IS_CONNECTED); + g_pub_cb_disconnect_rc = MQTT_CODE_SUCCESS; + g_pub_cb_disconnect_called = 0; + g_pub_cb_disconnect_done = 0; + XMEMSET(&publish, 0, sizeof(publish)); + publish.qos = MQTT_QOS_0; + publish.topic_name = "t"; + publish.buffer = payload; + publish.buffer_len = (word32)sizeof(payload); + publish.total_len = (word32)sizeof(payload); + + ASSERT_EQ(0, pthread_create(&publish_thread, NULL, + pub_cb_disconnect_thread, &publish)); + ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &deadline)); + deadline.tv_sec += 2; + ASSERT_EQ(0, pthread_mutex_lock(&g_pub_cb_mutex)); + while (g_pub_cb_disconnect_done == 0 && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&g_pub_cb_cond, &g_pub_cb_mutex, + &deadline); + } + finished = g_pub_cb_disconnect_done; + ASSERT_EQ(0, pthread_mutex_unlock(&g_pub_cb_mutex)); + if (!finished) { + /* Deadlocked: leave the thread behind rather than hang the run. */ + (void)pthread_detach(publish_thread); + test_client_inited = 0; + } + ASSERT_EQ(1, finished); + ASSERT_EQ(0, pthread_join(publish_thread, NULL)); + ASSERT_EQ(1, g_pub_cb_disconnect_called); + ASSERT_EQ(MQTT_CODE_ERROR_STAT, g_pub_cb_disconnect_rc); +} +#endif /* WOLFMQTT_THREAD_ID_T */ +#endif /* WOLFMQTT_PROPERTY_CB && (__MACH__ || WOLFMQTT_POSIX_SEMAPHORES) */ #endif /* !WOLFMQTT_NONBLOCK */ #ifdef WOLFMQTT_NONBLOCK @@ -9229,8 +9996,25 @@ void run_mqtt_client_tests(void) RUN_TEST(disconnect_clears_recv_quota_ownership); #else RUN_TEST(publish_writeonly_v5_no_dangling_pendresp_on_success); +#if defined(WOLFMQTT_PROPERTY_CB) && \ + (defined(__MACH__) || defined(WOLFMQTT_POSIX_SEMAPHORES)) + RUN_TEST(publish_writeonly_waits_for_reader_decoding_ack); +#ifdef WOLFMQTT_SEM_SIGNAL + RUN_TEST(publish_writeonly_not_held_by_reader_next_read); +#endif + RUN_TEST(publish_writeonly_waits_for_duplicate_ack_decode); + RUN_TEST(publish_writeonly_after_reader_decode_error); +#ifdef WOLFMQTT_THREAD_ID_T + RUN_TEST(cancel_from_own_ack_property_cb_refuses); + RUN_TEST(net_disconnect_wait_from_property_cb_refuses); + RUN_TEST(net_disconnect_wait_from_publish_payload_cb_refuses); +#endif +#endif +#endif #endif #endif +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_THREAD_ID_T) + RUN_TEST(read_stop_repeat_keeps_new_reader_state); #endif #if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_NONBLOCK) #if WOLFMQTT_MAX_QOS >= 1 diff --git a/tests/test_mqtt_props_init.c b/tests/test_mqtt_props_init.c index 8b3c0ac34..f7705952e 100644 --- a/tests/test_mqtt_props_init.c +++ b/tests/test_mqtt_props_init.c @@ -33,6 +33,16 @@ static MqttClient* sem_flag_client; static word32 sem_flag_bits; static MqttClient* io_lock_client; static int io_locks_held_at_close; +/* Counts lockClient acquisitions on sem_track_client made while + * sem_track_while_held is owned, and records which one changed + * sem_track_bit in client->flags, so a later run can fail exactly that one. */ +static MqttClient* sem_track_client; +static wm_Sem* sem_track_while_held; +static word32 sem_track_bit; +static word32 sem_track_before; +static int sem_track_count; +static int sem_track_counted; +static int sem_track_toggle_at; #ifdef ENABLE_MQTT_CURL #define TEST_CLIENT_LOCK_COUNT 4 @@ -66,6 +76,13 @@ static void reset_sem_state(void) quota_write_calls = 0; sem_flag_client = NULL; sem_flag_bits = 0; + sem_track_client = NULL; + sem_track_while_held = NULL; + sem_track_bit = 0; + sem_track_before = 0; + sem_track_count = 0; + sem_track_counted = 0; + sem_track_toggle_at = 0; } int wm_SemInit(wm_Sem* sem) @@ -112,6 +129,13 @@ int wm_SemLock(wm_Sem* sem) } } sem->lock_count++; + if (sem_track_client != NULL && sem == &sem_track_client->lockClient && + (sem_track_while_held == NULL || + sem_track_while_held->lock_count > 0)) { + sem_track_count++; + sem_track_counted = 1; + sem_track_before = sem_track_client->flags & sem_track_bit; + } /* Model another thread that updated client->flags under lockClient and * released it just as this acquisition succeeded. */ if (sem_flag_client != NULL && sem == &sem_flag_client->lockClient) { @@ -138,6 +162,15 @@ int wm_SemLock(wm_Sem* sem) int wm_SemUnlock(wm_Sem* sem) { + if (sem_track_client != NULL && sem == &sem_track_client->lockClient && + sem_track_counted) { + sem_track_counted = 0; + if (sem_track_toggle_at == 0 && + (sem_track_client->flags & sem_track_bit) != + sem_track_before) { + sem_track_toggle_at = sem_track_count; + } + } if (sem->lock_count == 0) { sem_invalid_unlock_calls++; } @@ -756,6 +789,235 @@ static int run_net_disconnect_wait_lock_failure(int fail_send) return fail; } +typedef struct AuthWindowNet { + int connack_pos; + int writes; +} AuthWindowNet; + +/* v5 CONNACK (MQTT 5.0 section 3.2): accepted, Session Present 0, Property + * Length 0. */ +static const byte auth_window_connack[] = { 0x20, 0x03, 0x00, 0x00, 0x00 }; + +static int auth_window_net_read(void* context, byte* buf, int buf_len, + int timeout_ms) +{ + AuthWindowNet* an = (AuthWindowNet*)context; + int n = (int)sizeof(auth_window_connack) - an->connack_pos; + + (void)timeout_ms; + if (n <= 0) { + return MQTT_CODE_ERROR_TIMEOUT; + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, auth_window_connack + an->connack_pos, (size_t)n); + an->connack_pos += n; + return n; +} + +static int auth_window_net_write(void* context, const byte* buf, int buf_len, + int timeout_ms) +{ + AuthWindowNet* an = (AuthWindowNet*)context; + + (void)buf; + (void)timeout_ms; + an->writes++; + return buf_len; +} + +/* Runs one v5 CONNECT, carrying an Authentication Method when with_method is + * set, against a CONNACK. When fail_at is non-zero, the fail_at'th lockClient + * acquisition made while while_held is owned fails. Otherwise the run records + * in sem_track_toggle_at which acquisition changed AUTH_PENDING. */ +static int run_auth_window_connect(MqttClient* client, AuthWindowNet* an, + int with_method, wm_Sem* (*while_held)(MqttClient*), int fail_at) +{ + MqttConnect connect; + MqttProp method_prop; + int rc; + + XMEMSET(an, 0, sizeof(*an)); + XMEMSET(&connect, 0, sizeof(connect)); + connect.keep_alive_sec = 60; + connect.clean_session = 1; + connect.client_id = "auth"; + if (with_method) { + XMEMSET(&method_prop, 0, sizeof(method_prop)); + method_prop.type = MQTT_PROP_AUTH_METHOD; + method_prop.data_str.str = (char*)"m"; + method_prop.data_str.len = 1; + connect.props = &method_prop; + } + (void)MqttClient_Flags(client, MQTT_CLIENT_FLAG_CONNECT_SENT, + MQTT_CLIENT_FLAG_IS_CONNECTED); + + sem_track_client = client; + sem_track_while_held = while_held(client); + sem_track_bit = MQTT_CLIENT_FLAG_AUTH_PENDING; + sem_track_count = 0; + sem_track_counted = 0; + sem_track_toggle_at = 0; + if (fail_at > 0) { + sem_fail_lock = &client->lockClient; + sem_fail_lock_while_held = while_held(client); + sem_fail_lock_skip = fail_at - 1; + } + rc = MqttClient_Connect(client, &connect); + sem_fail_lock = NULL; + sem_fail_lock_while_held = NULL; + sem_fail_lock_skip = 0; + sem_track_client = NULL; + return rc; +} + +static wm_Sem* auth_window_send_lock(MqttClient* client) +{ + return &client->lockSend; +} + +static wm_Sem* auth_window_recv_lock(MqttClient* client) +{ + return &client->lockRecv; +} + +static int auth_window_client_init(MqttClient* client, MqttNet* net, + AuthWindowNet* an, byte* tx_buf, byte* rx_buf, int buf_len) +{ + XMEMSET(net, 0, sizeof(*net)); + net->context = an; + net->connect = ping_net_connect; + net->read = auth_window_net_read; + net->write = auth_window_net_write; + net->disconnect = ping_net_disconnect; + return MqttClient_Init(client, net, NULL, tx_buf, buf_len, rx_buf, + buf_len, 1000); +} + +/* [MQTT-3.1.2-30] With an Authentication Method only AUTH or DISCONNECT may + * follow CONNECT until the CONNACK. If lockClient fails while raising that + * window, the CONNECT must not be sent, since nothing would hold back other + * packets, and no lock or pending response may be left behind. */ +static int test_auth_window_set_lock_failure(void) +{ + int rc; + int set_at; + MqttClient client; + MqttNet net; + AuthWindowNet an; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "auth-window client initialization returned %d\n", + rc); + return 1; + } + + /* Learn which acquisition under lockSend raises the window. */ + rc = run_auth_window_connect(&client, &an, 1, auth_window_send_lock, 0); + set_at = sem_track_toggle_at; + MqttClient_DeInit(&client); + if (rc != MQTT_CODE_SUCCESS || set_at == 0) { + fprintf(stderr, "auth-window dry run rc=%d set_at=%d\n", rc, set_at); + return 1; + } + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + return 1; + } + rc = run_auth_window_connect(&client, &an, 1, auth_window_send_lock, + set_at); + if (rc != MQTT_CODE_ERROR_SYSTEM || an.writes != 0 || + client.lockSend.lock_count != 0 || + client.lockClient.lock_count != 0 || + client.firstPendResp != NULL || + (client.flags & MQTT_CLIENT_FLAG_CONNECT_SENT) != 0) { + fprintf(stderr, + "auth-window set failure rc=%d writes=%d send=%d client=%d " + "pend=%p flags=0x%x\n", rc, an.writes, + client.lockSend.lock_count, client.lockClient.lock_count, + (void*)client.firstPendResp, (unsigned)client.flags); + fail = 1; + } + + MqttClient_DeInit(&client); + return fail; +} + +/* If lockClient fails while the CONNACK closes the authentication window, the + * CONNACK must fail rather than leave the window silently open-ended, and the + * next CONNECT must reset the stale flag. */ +static int test_auth_window_clear_lock_failure(void) +{ + int rc; + int clear_at; + MqttClient client; + MqttNet net; + AuthWindowNet an; + byte tx_buf[64]; + byte rx_buf[64]; + int fail = 0; + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + fprintf(stderr, "auth-window client initialization returned %d\n", + rc); + return 1; + } + + /* Learn which acquisition under lockRecv closes the window. */ + rc = run_auth_window_connect(&client, &an, 1, auth_window_recv_lock, 0); + clear_at = sem_track_toggle_at; + MqttClient_DeInit(&client); + if (rc != MQTT_CODE_SUCCESS || clear_at == 0) { + fprintf(stderr, "auth-window dry run rc=%d clear_at=%d\n", rc, + clear_at); + return 1; + } + + reset_sem_state(); + rc = auth_window_client_init(&client, &net, &an, tx_buf, rx_buf, + (int)sizeof(tx_buf)); + if (rc != MQTT_CODE_SUCCESS) { + return 1; + } + rc = run_auth_window_connect(&client, &an, 1, auth_window_recv_lock, + clear_at); + if (rc != MQTT_CODE_ERROR_SYSTEM || + client.lockRecv.lock_count != 0 || + client.lockClient.lock_count != 0) { + fprintf(stderr, "auth-window clear failure rc=%d recv=%d client=%d\n", + rc, client.lockRecv.lock_count, client.lockClient.lock_count); + fail = 1; + } + + /* A later CONNECT without a method resets the window it left set. */ + if (!fail) { + rc = run_auth_window_connect(&client, &an, 0, auth_window_send_lock, + 0); + if (rc != MQTT_CODE_SUCCESS || + (client.flags & MQTT_CLIENT_FLAG_AUTH_PENDING) != 0) { + fprintf(stderr, "auth-window reconnect rc=%d flags=0x%x\n", rc, + (unsigned)client.flags); + fail = 1; + } + } + + MqttClient_DeInit(&client); + return fail; +} + int main(void) { if (test_sn_null_ping_resets_terminal_read_state() != 0) { @@ -794,5 +1056,11 @@ int main(void) if (run_net_disconnect_wait_lock_failure(0) != 0) { return 1; } + if (test_auth_window_set_lock_failure() != 0) { + return 1; + } + if (test_auth_window_clear_lock_failure() != 0) { + return 1; + } return 0; } diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index 7dcc0964e..5bea2b320 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -147,6 +147,19 @@ typedef struct SnPingRaceCtx { } SnPingRaceCtx; static SnMtOnlyRaceCtx* g_mt_only_race; + +/* Stands in for a reader thread that claimed an early CONNACK and is still + * decoding into the SN_Connect when the Will exchange fails. */ +typedef struct SnConnackClaimCtx { + SN_Connect* connect; + pthread_t thread; + int started; + int released; + int sync_error; +} SnConnackClaimCtx; + +static SnConnackClaimCtx* g_connack_claim; +static void sn_connack_claim_start(void); static SnPingRaceCtx* g_ping_race; static int g_mt_only_reentrant_send_rc; static int g_mt_only_reentrant_wait_rc; @@ -237,6 +250,11 @@ static int mock_write(void *ctx, const byte* buf, int buf_len, int timeout_ms) net->write_calls++; if (net->write_fail_rc != 0 && (net->write_fail_type == 0 || (buf_len > 1 && buf[1] == net->write_fail_type))) { +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS + if (g_connack_claim != NULL) { + sn_connack_claim_start(); + } +#endif /* Simulate a short/failed write so the caller's rc != xfer error path is * exercised. The buffer is left as the caller encoded it (not captured), * matching a real transport that errored mid-send. */ @@ -483,6 +501,52 @@ static const byte SN_PUBACK_FRAME[] = { * ============================================================================ */ static MqttClient g_client; + +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS +static void* sn_connack_claim_thread(void* arg) +{ + SnConnackClaimCtx* claim = (SnConnackClaimCtx*)arg; + struct timespec delay; + + /* Long enough that a reset not waiting for the claim returns first. */ + delay.tv_sec = 0; + delay.tv_nsec = 100 * 1000 * 1000; + (void)nanosleep(&delay, NULL); + if (wm_SemLock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + return NULL; + } + claim->released = 1; + MqttClient_RespList_EndProcessing(&g_client, &claim->connect->pendResp); + if (wm_SemUnlock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + } + return NULL; +} + +/* Called once from the failing Will write, on the connecting thread. */ +static void sn_connack_claim_start(void) +{ + SnConnackClaimCtx* claim = g_connack_claim; + + g_connack_claim = NULL; + if (wm_SemLock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + return; + } + claim->connect->pendResp.packetProcessing = 1; + if (wm_SemUnlock(&g_client.lockClient) != 0) { + claim->sync_error = 1; + } + if (pthread_create(&claim->thread, NULL, sn_connack_claim_thread, + claim) != 0) { + claim->sync_error = 1; + return; + } + claim->started = 1; +} +#endif + static MqttNet g_net; static MockNet g_mock; static byte g_tx[512]; @@ -906,6 +970,45 @@ TEST(sn_connect_will_error_allows_retry) ASSERT_NO_PENDRESP(); } +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS +/* A reader can claim an early CONNACK and be decoding into the SN_Connect + * when the Will exchange fails. The reset must wait for that claim to end + * before unlinking the entry and returning, since the caller may then free or + * reuse the object. */ +TEST(sn_connect_will_error_waits_for_connack_reader) +{ + SN_Connect mc; + SnConnackClaimCtx claim; + int released_at_return; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0 /* no CONTINUE */)); + mock_net_push(&g_mock, WILLTOPICREQ_FRAME, (int)sizeof(WILLTOPICREQ_FRAME)); + g_mock.write_fail_rc = MQTT_CODE_ERROR_NETWORK; + g_mock.write_fail_type = SN_MSG_TYPE_WILLTOPIC; + XMEMSET(&claim, 0, sizeof(claim)); + claim.connect = &mc; + g_connack_claim = &claim; + + sn_will_setup_connect(&mc); + rc = sn_connect_pump(&mc, NULL); + ASSERT_EQ(0, wm_SemLock(&g_client.lockClient)); + released_at_return = claim.released; + ASSERT_EQ(0, wm_SemUnlock(&g_client.lockClient)); + g_connack_claim = NULL; + if (claim.started) { + ASSERT_EQ(0, pthread_join(claim.thread, NULL)); + } + + ASSERT_EQ(0, claim.sync_error); + ASSERT_EQ(1, claim.started); + ASSERT_EQ(1, released_at_return); + ASSERT_TRUE(rc < 0); + ASSERT_EQ(MQTT_MSG_BEGIN, mc.stat.write); + ASSERT_NO_PENDRESP(); +} +#endif + TEST(sn_connect_refused) { SN_Connect mc; @@ -1171,6 +1274,38 @@ TEST(sn_will_response_reset_clears_return_code) } } } + +/* A reader that completes another thread's SN response must release its claim + * on the entry. MqttClient_CancelMessage waits while an entry is claimed, so a + * claim left set after the response completed would read as a reader still + * decoding into the caller's object. */ +TEST(sn_reader_releases_completed_pendresp) +{ + SN_Subscribe sub; + int rc; + + ASSERT_EQ(MQTT_CODE_SUCCESS, sn_client_init(0)); + XMEMSET(&sub, 0, sizeof(sub)); + + ASSERT_EQ(0, wm_SemLock(&g_client.lockClient)); + rc = MqttClient_RespList_Add(&g_client, + (MqttPacketType)SN_MSG_TYPE_SUBACK, SN_TEST_SUB_PACKET_ID, + &sub.pendResp, &sub.subAck); + ASSERT_EQ(0, wm_SemUnlock(&g_client.lockClient)); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + + /* A generic reader routes the SUBACK into the subscriber's object. */ + mock_net_push(&g_mock, SUBACK_FRAME, (int)sizeof(SUBACK_FRAME)); + (void)SN_Client_WaitMessage(&g_client, 1000); + + ASSERT_EQ(1, sub.pendResp.packetDone); + ASSERT_EQ(0, sub.pendResp.packetProcessing); + ASSERT_EQ(SN_TEST_SUB_TOPIC_ID, sub.subAck.topicId); + + rc = MqttClient_CancelMessage(&g_client, (MqttObject*)&sub); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + ASSERT_NO_PENDRESP(); +} #endif /* WOLFMQTT_MULTITHREAD */ /* ============================================================================ @@ -1561,8 +1696,10 @@ TEST(sn_qos2_full_table_refuses_new_id) /* A clean-session CONNECT starts with no inbound QoS 2 flows, so the * de-duplication table is cleared once the gateway accepts it; a - * non-clean-session CONNECT resumes and keeps it. */ -static int sn_connect_keeps_qos2_id(int clean_session) + * non-clean-session CONNECT resumes and keeps it, but only for the ClientId + * whose Session established it. The first CONNECT always uses the default + * ClientId; the reconnect uses client_id. */ +static int sn_connect_keeps_qos2_id(int clean_session, const char* client_id) { SN_Connect mc; int rc; @@ -1570,11 +1707,19 @@ static int sn_connect_keeps_qos2_id(int clean_session) if (sn_client_init(0) != MQTT_CODE_SUCCESS) { return -1; } + mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); + sn_will_setup_connect(&mc); + mc.enable_lwt = 0; + rc = sn_connect_pump(&mc, NULL); + if (rc != MQTT_CODE_SUCCESS) { + return -1; + } g_client.recv_qos2_pending[0] = 0x0042; mock_net_push(&g_mock, CONNACK_FRAME, (int)sizeof(CONNACK_FRAME)); sn_will_setup_connect(&mc); mc.enable_lwt = 0; mc.clean_session = (byte)clean_session; + mc.client_id = client_id; rc = sn_connect_pump(&mc, NULL); if (rc != MQTT_CODE_SUCCESS) { return -1; @@ -1584,8 +1729,16 @@ static int sn_connect_keeps_qos2_id(int clean_session) TEST(sn_qos2_table_cleared_by_clean_session_connect) { - ASSERT_EQ(0, sn_connect_keeps_qos2_id(1)); - ASSERT_EQ(1, sn_connect_keeps_qos2_id(0)); + ASSERT_EQ(0, sn_connect_keeps_qos2_id(1, "wolfMQTT-sn-test")); + ASSERT_EQ(1, sn_connect_keeps_qos2_id(0, "wolfMQTT-sn-test")); +} + +/* A non-clean CONNECT under a different ClientId is a different Session, so + * a pending id from the previous one must not suppress a new QoS 2 PUBLISH + * that reuses it. */ +TEST(sn_qos2_table_cleared_by_client_id_change) +{ + ASSERT_EQ(0, sn_connect_keeps_qos2_id(0, "wolfMQTT-sn-other")); } #endif /* WOLFMQTT_MAX_QOS >= 2 */ @@ -3609,6 +3762,9 @@ int main(int argc, char** argv) RUN_TEST(sn_connect_lwt_no_continue); RUN_TEST(sn_connect_refused); RUN_TEST(sn_connect_will_error_allows_retry); +#ifdef WOLFMQTT_TEST_SN_MT_ONLY_THREADS + RUN_TEST(sn_connect_will_error_waits_for_connack_reader); +#endif RUN_TEST(sn_will_payload_scrubbed_after_send); RUN_TEST(sn_willmsg_encode_failure_scrubs_tx_buf); RUN_TEST(sn_willmsgupd_payload_scrubbed_after_send); @@ -3617,6 +3773,7 @@ int main(int argc, char** argv) #ifdef WOLFMQTT_MULTITHREAD RUN_TEST(sn_willmsgupd_scrubbed_on_response_registration_error); RUN_TEST(sn_will_response_reset_clears_return_code); + RUN_TEST(sn_reader_releases_completed_pendresp); #endif RUN_TEST(sn_subscribe_no_continue); RUN_TEST(sn_subscribe_rejected); @@ -3639,6 +3796,7 @@ int main(int argc, char** argv) RUN_TEST(sn_qos2_retransmit_not_delivered_twice); RUN_TEST(sn_qos2_full_table_refuses_new_id); RUN_TEST(sn_qos2_table_cleared_by_clean_session_connect); + RUN_TEST(sn_qos2_table_cleared_by_client_id_change); #endif RUN_TEST(sn_ping_no_continue); RUN_TEST(sn_ping_null_no_continue); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 21f30ff7d..1394ce195 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -132,6 +132,10 @@ #ifndef BROKER_MAX_WILL_PAYLOAD_LEN #define BROKER_MAX_WILL_PAYLOAD_LEN 256 #endif +/* Static-memory room for a v5 Will's encoded publishable properties. */ +#ifndef BROKER_MAX_WILL_PROPS_LEN + #define BROKER_MAX_WILL_PROPS_LEN 128 +#endif #ifndef BROKER_MAX_PENDING_WILLS #define BROKER_MAX_PENDING_WILLS 4 #endif @@ -262,8 +266,9 @@ * wrap_mode byte out of the schema-version field. Bumped 2 -> 3 when * the NS_SESSION record gained an orphan_since timestamp. Bumped 3 -> 4 * when NS_OUTQ gained an explicit enqueue sequence, then 4 -> 5 when it - * gained retransmission state. */ - #define WOLFMQTT_BROKER_PERSIST_SCHEMA_VER 5 + * gained retransmission state, then 5 -> 6 when it gained v5 Properties + * and an explicit Message Expiry presence flag. */ + #define WOLFMQTT_BROKER_PERSIST_SCHEMA_VER 6 #endif /* Header wrap_mode byte values (record body framing on disk). */ @@ -536,18 +541,17 @@ typedef struct BrokerOutPub { * MQTT-4.4.0-1, then clears the flag. */ byte retransmit_dup; /* 0 or 1 */ byte partial_send; /* QoS 0 PUBLISH started on the old socket */ + byte has_expiry; /* v5 Message Expiry Interval present */ WOLFMQTT_BROKER_TIME_T enq_time; word64 enqueue_seq; /* persistent FIFO order within this session */ - word32 expiry_sec; /* v5 Message Expiry Interval, 0 = no expiry */ + word32 expiry_sec; /* v5 Message Expiry Interval when has_expiry */ byte protocol_level; /* echoed back to subscriber on send */ #ifdef WOLFMQTT_V5 /* Deep copy (BrokerProps_Clone) of the originating PUBLISH's v5 * Application Message properties (Payload Format Indicator, Content * Type, Response Topic, Correlation Data, User Property, etc.), or * NULL if none. Owned by this entry; freed once by BrokerOutPub_Free - * via BrokerProps_FreeClone - never via MqttProps_Free(). - * Not serialized by the persist layer: a queued message replayed after a - * broker restart is delivered without these properties. */ + * via BrokerProps_FreeClone - never via MqttProps_Free(). */ MqttProp* props; #endif struct BrokerOutPub* next; @@ -721,6 +725,19 @@ typedef struct BrokerClient { * direct_pending_len. Once that write finishes or fails, the client is * closed instead of reading another packet. */ unsigned int close_after_direct : 1; + /* The connection is being taken over and closes once its own Will has + * been published, so nothing new may start writing to it. */ + unsigned int takeover_closing : 1; +#endif +#if defined(WOLFMQTT_BROKER_WILL) && defined(WOLFMQTT_V5) + /* Encoded Will Properties carried onto the Will PUBLISH, i.e. all but + * the Will Delay Interval (MQTT 5.0 section 3.1.3.2). */ +#ifdef WOLFMQTT_STATIC_MEMORY + byte will_props[BROKER_MAX_WILL_PROPS_LEN]; +#else + byte* will_props; +#endif + word16 will_props_len; #endif } BrokerClient; @@ -783,6 +800,15 @@ typedef struct BrokerPendingWill { MqttQoS qos; byte retain; WOLFMQTT_BROKER_TIME_T publish_time; /* absolute time to publish */ +#ifdef WOLFMQTT_V5 + /* Encoded Will Properties, as in BrokerClient.will_props. */ +#ifdef WOLFMQTT_STATIC_MEMORY + byte props[BROKER_MAX_WILL_PROPS_LEN]; +#else + byte* props; +#endif + word16 props_len; +#endif } BrokerPendingWill; #endif /* WOLFMQTT_BROKER_WILL */ @@ -1026,6 +1052,12 @@ WOLFMQTT_LOCAL void BrokerStaticOrphan_DropFull(MqttBroker* broker, * and restore-time expiry sweep so the two paths can't drift. */ WOLFMQTT_LOCAL void BrokerOrphan_DropFull(MqttBroker* broker, BrokerOrphanSession* o); +#ifdef WOLFMQTT_V5 +/* v5 Properties of a queued PUBLISH, for the persistence layer. */ +WOLFMQTT_LOCAL int BrokerOutPub_EncodeProps(const BrokerOutPub* e, byte* buf); +WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, + word32 len); +#endif #endif #ifndef WOLFMQTT_BROKER_NO_LOG diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 947706b7d..c6be1d77c 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -467,7 +467,9 @@ typedef struct _MqttClient { byte init_flags; /* Internal resource-initialization flags. */ #ifdef WOLFMQTT_THREAD_ID_T byte write_owner_valid; /* lockSend owner is known. */ + byte read_owner_valid; /* lockRecv owner is known. */ WOLFMQTT_THREAD_ID_T write_owner; + WOLFMQTT_THREAD_ID_T read_owner; #endif wm_Sem lockSend; wm_Sem lockRecv; @@ -543,6 +545,13 @@ typedef struct _MqttClient { word16 auth_method_len; byte auth_method[MQTT_AUTH_METHOD_MAX]; #endif +#if defined(WOLFMQTT_MULTITHREAD) && defined(WOLFMQTT_SEM_SIGNAL) + /* Posted when a reader stops decoding into a pending response, so a + * thread unlinking that entry need not wait on lockRecv, which a + * dedicated reader retakes for its next blocking read. */ + wm_Sem decodeDone; + word16 decodeWaiters; /* threads waiting on decodeDone, under lockClient */ +#endif } MqttClient; #ifdef WOLFMQTT_SN @@ -937,7 +946,15 @@ WOLFMQTT_API int MqttClient_WaitMessage_ex( /*! \brief In a multi-threaded and non-blocking mode this allows you to cancel an MQTT object that was previously submitted. * \note This function performs no network I/O. In multi-threaded - builds it may wait to take the client locks. + builds it may wait to take the client locks, and waits for + another thread that is decoding a response into msg to + finish, so msg may be freed on return. Do not call it from + a callback (message, property or publish payload) while a + response is being decoded into msg, such as a property + callback for msg's own ack. Where the platform provides + thread identities (WOLFMQTT_THREAD_ID_T) it detects this and + returns MQTT_CODE_ERROR_STAT without cancelling; otherwise it + deadlocks. * \param client Pointer to MqttClient structure * \param msg Pointer to MqttObject structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* @@ -1015,10 +1032,13 @@ WOLFMQTT_API int MqttClient_NetDisconnect( then the send lock before releasing the transport, so a read or write in progress on another thread completes (or times out) first and nothing can use transport or TLS state while it - is freed. It must not be called from a message callback or - other code already holding those locks, which would deadlock; - use MqttClient_NetDisconnect there. In other builds it is the - same as MqttClient_NetDisconnect. + is freed. It must not be called from a callback (message, + property or publish payload) or other code already holding + those locks; use MqttClient_NetDisconnect there. Where the + platform provides thread identities (WOLFMQTT_THREAD_ID_T) it + detects this and returns MQTT_CODE_ERROR_STAT instead of + deadlocking. In other builds it is the same as + MqttClient_NetDisconnect. * \param client Pointer to MqttClient structure * \return MQTT_CODE_SUCCESS or MQTT_CODE_ERROR_* (see enum MqttPacketResponseCodes) @@ -1065,6 +1085,10 @@ WOLFMQTT_LOCAL int MqttClient_RespList_Add(MqttClient *client, void *packet_obj); WOLFMQTT_LOCAL int MqttClient_CheckPendResp(MqttClient *client, byte wait_type, word16 wait_packet_id); +WOLFMQTT_LOCAL void MqttClient_RespList_EndProcessing(MqttClient *client, + MqttPendResp *resp); +WOLFMQTT_LOCAL int MqttClient_RespList_RemoveIdle(MqttClient *client, + MqttPendResp *rmResp); #endif WOLFMQTT_LOCAL int MqttPacket_HandleNetError(MqttClient *client, int rc); #if WOLFMQTT_MAX_QOS >= 2 @@ -1078,6 +1102,14 @@ WOLFMQTT_LOCAL void MqttClient_RecvQos2_Add(MqttClient* client, WOLFMQTT_LOCAL void MqttClient_RecvQos2_Remove(MqttClient* client, word16 packet_id); #endif +#ifdef WOLFMQTT_SESSION_ID_TRACK +/* ClientId of the Session whose state the client holds, shared by the MQTT + * and MQTT-SN clients. */ +WOLFMQTT_LOCAL int MqttClient_SessionIdMatches(const MqttClient* client, + const char* client_id); +WOLFMQTT_LOCAL void MqttClient_SessionIdRecord(MqttClient* client, + const char* client_id); +#endif /* Securely zero `len` bytes of `mem` (e.g. client->tx_buf) to scrub plaintext * such as credentials or will payloads before releasing the send lock. Uses a diff --git a/wolfmqtt/mqtt_types.h b/wolfmqtt/mqtt_types.h index f46eb78b5..862155e40 100644 --- a/wolfmqtt/mqtt_types.h +++ b/wolfmqtt/mqtt_types.h @@ -92,7 +92,9 @@ /* Multi-threading uses binary semaphores */ #if defined(WOLFMQTT_USER_THREADING) /* User provides API's and wm_Sem type. - * Add your wc_Sem into user_settings.h */ + * Add your wc_Sem into user_settings.h. Define WOLFMQTT_SEM_SIGNAL + * if wm_SemUnlock may be called by a thread other than the one that + * locked the semaphore. */ #elif defined(__MACH__) /* Apple Style Dispatch Semaphore */ @@ -101,6 +103,7 @@ typedef struct { dispatch_semaphore_t sem; } wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T pthread_t #define WOLFMQTT_THREAD_SELF() pthread_self() #define WOLFMQTT_THREAD_EQUAL(a, b) pthread_equal((a), (b)) @@ -116,6 +119,9 @@ #endif pthread_mutex_t mutex; } wm_Sem; + #ifndef WOLFMQTT_NO_COND_SIGNAL + #define WOLFMQTT_SEM_SIGNAL + #endif #define WOLFMQTT_THREAD_ID_T pthread_t #define WOLFMQTT_THREAD_SELF() pthread_self() #define WOLFMQTT_THREAD_EQUAL(a, b) pthread_equal((a), (b)) @@ -125,6 +131,7 @@ #include #include typedef SemaphoreHandle_t wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #if defined(INCLUDE_xTaskGetCurrentTaskHandle) && \ (INCLUDE_xTaskGetCurrentTaskHandle == 1) #define WOLFMQTT_THREAD_ID_T TaskHandle_t @@ -138,6 +145,7 @@ #include #include typedef HANDLE wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T DWORD #define WOLFMQTT_THREAD_SELF() GetCurrentThreadId() #define WOLFMQTT_THREAD_EQUAL(a, b) ((a) == (b)) @@ -145,6 +153,7 @@ #elif defined(THREADX) #include typedef TX_SEMAPHORE wm_Sem; + #define WOLFMQTT_SEM_SIGNAL #define WOLFMQTT_THREAD_ID_T TX_THREAD* #define WOLFMQTT_THREAD_SELF() tx_thread_identify() #define WOLFMQTT_THREAD_EQUAL(a, b) ((a) == (b)) From 53b4775a681305530e22fe751332da926bf4916a Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 23 Oct 2026 15:14:01 -0500 Subject: [PATCH 52/52] Fixes from review --- src/mqtt_broker.c | 239 +++++++++++++++++++++++++++++------- tests/test_broker_connect.c | 166 +++++++++++++++++++++---- wolfmqtt/mqtt_broker.h | 26 +++- 3 files changed, 360 insertions(+), 71 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index b7d36137d..c86adc03d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1974,7 +1974,6 @@ WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, MqttProp* decoded = NULL; const MqttProp* prop; int rc; - int shut_rc; int expiry_found = 0; if (e == NULL || e->props != NULL || (buf == NULL && len > 0)) { @@ -1984,12 +1983,8 @@ WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, return e->has_expiry ? MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA) : 0; } - /* Restore runs before any client exists, so nothing else holds the - * shared property pool lock yet. */ - rc = MqttProps_Init(); - if (rc != MQTT_CODE_SUCCESS) { - return rc; - } + /* The broker's own property pool reference (MqttBroker_Init) covers + * restore, which runs before any client exists. */ rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &decoded, (byte*)buf, len, len); if (rc >= 0 && (word32)rc != len) { @@ -2016,12 +2011,6 @@ WOLFMQTT_LOCAL int BrokerOutPub_DecodeProps(BrokerOutPub* e, const byte* buf, if (decoded != NULL) { (void)MqttProps_Free(decoded); } - shut_rc = MqttProps_ShutDown(); - if (rc >= 0 && shut_rc != MQTT_CODE_SUCCESS) { - BrokerProps_FreeClone(e->props); - e->props = NULL; - rc = shut_rc; - } return (rc < 0) ? rc : 0; } #endif /* WOLFMQTT_V5 */ @@ -5514,6 +5503,135 @@ static int BrokerWill_EncodeProps(const MqttProp* props, byte* buf) return (rc < 0) ? rc : len; } +#ifdef WOLFMQTT_STATIC_MEMORY +/* Number of publishable Will Properties in props. */ +static int BrokerWill_CountProps(const MqttProp* props) +{ + int count = 0; + + for (; props != NULL; props = props->next) { + if (BrokerWill_IsPublishProp(props)) { + count++; + } + } + return count; +} +#endif + +/* Take one 2-byte-length-prefixed field (MQTT 5.0 section 1.5.4/1.5.6) from + * buf at *pos. */ +static int BrokerWill_TakeField(const byte* buf, word16 len, word16* pos, + const byte** data, word16* data_len) +{ + if (len - *pos < 2) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + *data_len = (word16)(((word16)buf[*pos] << 8) | buf[*pos + 1]); + *pos = (word16)(*pos + 2); + if (len - *pos < *data_len) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + *data = &buf[*pos]; + *pos = (word16)(*pos + *data_len); + return 0; +} + +/* Rebuild Will Properties written by BrokerWill_EncodeProps as a list in + * nodes, without the shared property pool, so a Will can always be published + * with them. String and binary values point into buf, which must outlive the + * list. With nodes NULL the properties are only counted. Returns the count + * or a negative error. */ +static int BrokerWill_DecodeProps(const byte* buf, word16 len, + MqttProp* nodes, int max_nodes) +{ + MqttProp* prop = NULL; + const byte* data; + word16 data_len; + word16 pos = 0; + byte id; + int count = 0; + int rc = 0; + + while (rc == 0 && pos < len) { + id = buf[pos++]; + if (nodes != NULL) { + if (count >= max_nodes) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; + } + prop = &nodes[count]; + XMEMSET(prop, 0, sizeof(*prop)); + prop->type = (MqttPropertyType)id; + if (count > 0) { + nodes[count - 1].next = prop; + } + } + switch (id) { + case MQTT_PROP_PAYLOAD_FORMAT_IND: + if (len - pos < 1) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (prop != NULL) { + prop->data_byte = buf[pos]; + } + pos++; + break; + case MQTT_PROP_MSG_EXPIRY_INTERVAL: + if (len - pos < 4) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (prop != NULL) { + prop->data_int = ((word32)buf[pos] << 24) | + ((word32)buf[pos + 1] << 16) | + ((word32)buf[pos + 2] << 8) | (word32)buf[pos + 3]; + } + pos = (word16)(pos + 4); + break; + case MQTT_PROP_CONTENT_TYPE: + case MQTT_PROP_RESP_TOPIC: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + /* The encoder only reads through these pointers. */ + prop->data_str.str = (char*)data; + prop->data_str.len = data_len; + } + break; + case MQTT_PROP_CORRELATION_DATA: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + prop->data_bin.data = (byte*)data; + prop->data_bin.len = data_len; + } + break; + case MQTT_PROP_USER_PROP: + rc = BrokerWill_TakeField(buf, len, &pos, &data, &data_len); + if (rc == 0 && prop != NULL) { + prop->data_str.str = (char*)data; + prop->data_str.len = data_len; + } + if (rc == 0) { + rc = BrokerWill_TakeField(buf, len, &pos, &data, + &data_len); + } + if (rc == 0 && prop != NULL) { + prop->data_str2.str = (char*)data; + prop->data_str2.len = data_len; + } + break; + default: + /* Only publishable Will Properties are ever stored. */ + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; + } + if (rc == 0) { + count++; + } + } + return (rc < 0) ? rc : count; +} + /* Keep the publishable Will Properties until the Will is published. */ static int BrokerClient_StoreWillProps(BrokerClient* bc, const MqttProp* props) @@ -5877,7 +5995,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, MqttQoS qos, byte retain #ifdef WOLFMQTT_V5 - , byte* props_buf, word16 props_len + , const byte* props_buf, word16 props_len #endif ); @@ -6423,7 +6541,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, const char* topic, const byte* payload, word16 payload_len, MqttQoS qos, byte retain #ifdef WOLFMQTT_V5 - , byte* props_buf, word16 props_len + , const byte* props_buf, word16 props_len #endif ) { @@ -6437,11 +6555,13 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, int t; #endif #ifdef WOLFMQTT_V5 + MqttProp stack_props[BROKER_WILL_PROPS_STACK_ENTRIES]; + MqttProp* nodes = stack_props; MqttProp* props = NULL; MqttProp* expiry_prop; word32 expiry = 0; + int node_count = BROKER_WILL_PROPS_STACK_ENTRIES; int prop_rc; - int props_ref = 0; #endif if (broker == NULL || topic == NULL) { @@ -6452,26 +6572,33 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, /* The Will Properties travel with the Will on every delivery path * (MQTT 5.0 section 3.1.3.2). */ if (props_buf != NULL && props_len > 0) { - /* A delayed Will can fire after the last client was freed, so hold - * the shared property pool lock for the decoded list's lifetime. */ - prop_rc = MqttProps_Init(); - if (prop_rc != MQTT_CODE_SUCCESS) { - WBLOG_ERR(broker, - "broker: will property pool init failed rc=%d topic=%s", - prop_rc, BrokerLog_Sanitize(topic)); - return; + prop_rc = BrokerWill_DecodeProps(props_buf, props_len, NULL, 0); + #ifndef WOLFMQTT_STATIC_MEMORY + if (prop_rc > BROKER_WILL_PROPS_STACK_ENTRIES) { + nodes = (MqttProp*)WOLFMQTT_MALLOC(sizeof(MqttProp) * + (size_t)prop_rc); + if (nodes == NULL) { + nodes = stack_props; + prop_rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MEMORY); + } + else { + node_count = prop_rc; + } } - props_ref = 1; - prop_rc = MqttDecode_Props(MQTT_PACKET_TYPE_PUBLISH, &props, - props_buf, props_len, props_len); - if (prop_rc < 0) { + #endif + if (prop_rc > 0) { + prop_rc = BrokerWill_DecodeProps(props_buf, props_len, nodes, + node_count); + } + if (prop_rc > 0) { + props = nodes; + } + else if (prop_rc < 0) { + /* Not reachable for properties this broker encoded, except on + * heap exhaustion in a dynamic build. */ WBLOG_ERR(broker, - "broker: will properties decode failed rc=%d topic=%s", - prop_rc, BrokerLog_Sanitize(topic)); - if (props != NULL) { - (void)MqttProps_Free(props); - props = NULL; - } + "broker: will properties rebuild failed rc=%d, publishing " + "without them topic=%s", prop_rc, BrokerLog_Sanitize(topic)); } expiry_prop = BrokerProps_Find(props, MQTT_PROP_MSG_EXPIRY_INTERVAL); if (expiry_prop != NULL) { @@ -6806,15 +6933,9 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, BrokerFanout_Free(&targets); #endif #ifdef WOLFMQTT_V5 - if (props != NULL) { - (void)MqttProps_Free(props); - } - if (props_ref) { - prop_rc = MqttProps_ShutDown(); - if (prop_rc != MQTT_CODE_SUCCESS) { - WBLOG_ERR(broker, "broker: will property pool release failed " - "rc=%d", prop_rc); - } + if (nodes != stack_props) { + BROKER_FORCE_ZERO(nodes, sizeof(MqttProp) * (size_t)node_count); + WOLFMQTT_FREE(nodes); } #endif } @@ -7535,7 +7656,9 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_STATIC_MEMORY) /* Will Properties are v5 only. Refuse rather than drop any. */ will_props_len = BrokerWill_EncodeProps(mc.lwt_msg->props, NULL); - if (will_props_len > BROKER_MAX_WILL_PROPS_LEN) { + if (will_props_len > BROKER_MAX_WILL_PROPS_LEN || + BrokerWill_CountProps(mc.lwt_msg->props) > + BROKER_WILL_PROPS_STACK_ENTRIES) { WBLOG_ERR(broker, "broker: LWT properties too large (max %d) sock=%d", BROKER_MAX_WILL_PROPS_LEN, (int)bc->sock); @@ -9999,10 +10122,23 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) /* -------------------------------------------------------------------------- */ int MqttBroker_Init(MqttBroker* broker, MqttBrokerNet* net) { +#ifdef WOLFMQTT_V5 + int rc; +#endif + if (broker == NULL || net == NULL) { return MQTT_CODE_ERROR_BAD_ARG; } XMEMSET(broker, 0, sizeof(*broker)); +#ifdef WOLFMQTT_V5 + /* Restored queue properties are decoded with the shared property pool, + * also before the first and after the last client holds a reference. */ + rc = MqttProps_Init(); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + broker->props_ref = 1; +#endif XMEMCPY(&broker->net, net, sizeof(MqttBrokerNet)); broker->listen_sock = BROKER_SOCKET_INVALID; broker->port = MQTT_DEFAULT_PORT; @@ -10627,6 +10763,14 @@ int MqttBroker_Free(MqttBroker* broker) sizeof(broker->persist_key_cache)); broker->persist_key_loaded = 0; #endif +#ifdef WOLFMQTT_V5 + if (broker->props_ref) { + broker->props_ref = 0; + if (MqttProps_ShutDown() != MQTT_CODE_SUCCESS) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SYSTEM); + } + } +#endif return MQTT_CODE_SUCCESS; } @@ -10846,6 +10990,7 @@ int wolfmqtt_broker(int argc, char** argv) rc = wolfmqtt_broker_set_auth_pass(&broker, argv[++i], auth_pass_buf, (word32)sizeof(auth_pass_buf)); if (rc != MQTT_CODE_SUCCESS) { + (void)MqttBroker_Free(&broker); return rc; } } @@ -10896,11 +11041,13 @@ int wolfmqtt_broker(int argc, char** argv) #endif else if (XSTRCMP(argv[i], "-h") == 0) { BrokerUsage(argv[0]); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return 0; } else { BrokerUsage(argv[0]); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } @@ -10923,12 +11070,14 @@ int wolfmqtt_broker(int argc, char** argv) PRINTF("broker: ERROR persist+encrypt build needs -E " "(only \"dev\" is recognized; production deployments " "must install MqttBrokerPersistHooks.derive_key)"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } if (XSTRCMP(encrypt_key_source, "dev") != 0) { PRINTF("broker: ERROR unknown -E source \"%s\" " "(only \"dev\" is recognized)", encrypt_key_source); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; } @@ -10939,6 +11088,7 @@ int wolfmqtt_broker(int argc, char** argv) PRINTF("broker: ERROR persist+encrypt build has no built-in key " "source (rebuild with --enable-broker-persist-encrypt-dev-key " "for testing, or install MqttBrokerPersistHooks.derive_key)"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return MQTT_CODE_ERROR_BAD_ARG; #endif @@ -10953,6 +11103,7 @@ int wolfmqtt_broker(int argc, char** argv) "subdirectories must be owned by this user, must not be " "group- or other-writable (mode 0700), and no path " "component may be a symlink"); + (void)MqttBroker_Free(&broker); BROKER_WIPE_AUTH_PASS(); return rc; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 8b2639c86..384d99bce 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -7084,19 +7084,45 @@ static int will_bytes_found(const byte* hay, size_t hay_len, return 0; } +#ifndef WOLFMQTT_DYN_PROP +/* Take every free entry of the shared property pool, so a decode through it + * fails as it would under load. Release with MqttProps_Free. */ +static MqttProp* will_test_exhaust_prop_pool(void) +{ + MqttProp* head = NULL; + + while (MqttProps_Add(&head) != NULL) { + } + return head; +} +#endif + +/* Most User Properties will_props_reach_live_subscriber can carry while the + * CONNECT and PUBLISH Remaining Lengths stay one byte long. */ +#define WILL_TEST_MAX_USER_PROPS 12 + /* A v5 Will carries its Will Properties, other than the Will Delay Interval, * onto the Will PUBLISH in their original order (MQTT 5.0 section 3.1.3.2, - * [MQTT-3.1.3-10]). With_delay adds a Will Delay Interval; Session Expiry 0 - * then publishes through the pending-Will path at once. Returns 1 when a - * live v5 subscriber receives the expected PUBLISH. */ -static int will_props_reach_live_subscriber(int with_delay) + * [MQTT-3.1.3-10]). The Will Properties are a Content Type "t" followed by + * user_props User Properties "k" = "v". With_delay adds a Will Delay + * Interval; Session Expiry 0 then publishes through the pending-Will path at + * once. With exhaust_pool the shared property pool is full while the Will is + * published, which must not drop or strip it. Returns 1 when a live v5 + * subscriber receives exactly the expected PUBLISH. */ +static int will_props_reach_live_subscriber(int with_delay, int exhaust_pool, + int user_props) { MqttBroker broker; MqttBrokerNet net; +#ifndef WOLFMQTT_DYN_PROP + MqttProp* held = NULL; +#endif int i; int found; - byte connect_pub[48]; + byte connect_pub[128]; + byte expected[128]; int pos = 0; + int exp_pos = 0; /* v5 CONNECT, Clean Start, no properties, ClientId "S" */ static const byte connect_sub[] = { 0x10, 0x0E, @@ -7111,24 +7137,21 @@ static int will_props_reach_live_subscriber(int with_delay) }; /* Will Delay Interval (0x18) 5 seconds */ static const byte will_delay[] = { 0x18, 0x00, 0x00, 0x00, 0x05 }; - /* Content Type (0x03) "t", then User Property (0x26) "k" = "v" */ - static const byte will_props[] = { - 0x03, 0x00, 0x01, 't', + /* Content Type (0x03) "t" */ + static const byte content_type[] = { 0x03, 0x00, 0x01, 't' }; + /* User Property (0x26) "k" = "v" */ + static const byte user_prop[] = { 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v' }; - /* Expected v5 QoS 0 PUBLISH (MQTT 5.0 section 3.3): topic "lwt", - * Properties length 11 holding the two properties above, payload "bye". - * Remaining Length 20. */ - static const byte expected[] = { - 0x30, 0x14, - 0x00, 0x03, 'l', 'w', 't', - 0x0B, - 0x03, 0x00, 0x01, 't', - 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', - 'b', 'y', 'e' - }; - int props_len = (int)sizeof(will_props) + - (with_delay ? (int)sizeof(will_delay) : 0); + int pub_props_len; + int props_len; + + if (user_props < 0 || user_props > WILL_TEST_MAX_USER_PROPS) { + return -1; + } + pub_props_len = (int)sizeof(content_type) + + user_props * (int)sizeof(user_prop); + props_len = pub_props_len + (with_delay ? (int)sizeof(will_delay) : 0); /* v5 CONNECT (MQTT 5.0 section 3.1): Will Flag and Clean Start, keep * alive 60, no CONNECT properties, ClientId "P", Will Properties, Will @@ -7144,11 +7167,31 @@ static int will_props_reach_live_subscriber(int with_delay) XMEMCPY(&connect_pub[pos], will_delay, sizeof(will_delay)); pos += (int)sizeof(will_delay); } - XMEMCPY(&connect_pub[pos], will_props, sizeof(will_props)); - pos += (int)sizeof(will_props); + XMEMCPY(&connect_pub[pos], content_type, sizeof(content_type)); + pos += (int)sizeof(content_type); + for (i = 0; i < user_props; i++) { + XMEMCPY(&connect_pub[pos], user_prop, sizeof(user_prop)); + pos += (int)sizeof(user_prop); + } XMEMCPY(&connect_pub[pos], "\x00\x03lwt\x00\x03" "bye", 10); pos += 10; + /* Expected v5 QoS 0 PUBLISH (MQTT 5.0 section 3.3): topic "lwt", the + * Will Properties without the Will Delay Interval, payload "bye". */ + expected[exp_pos++] = 0x30; + expected[exp_pos++] = (byte)(5 + 1 + pub_props_len + 3); + XMEMCPY(&expected[exp_pos], "\x00\x03lwt", 5); + exp_pos += 5; + expected[exp_pos++] = (byte)pub_props_len; + XMEMCPY(&expected[exp_pos], content_type, sizeof(content_type)); + exp_pos += (int)sizeof(content_type); + for (i = 0; i < user_props; i++) { + XMEMCPY(&expected[exp_pos], user_prop, sizeof(user_prop)); + exp_pos += (int)sizeof(user_prop); + } + XMEMCPY(&expected[exp_pos], "bye", 3); + exp_pos += 3; + install_mock_net(&net); XMEMSET(&broker, 0, sizeof(broker)); if (MqttBroker_Init(&broker, &net) != MQTT_CODE_SUCCESS || @@ -7163,13 +7206,34 @@ static int will_props_reach_live_subscriber(int with_delay) for (i = 0; i < 16; i++) { MqttBroker_Step(&broker); } +#ifndef WOLFMQTT_DYN_PROP + if (exhaust_pool) { + held = will_test_exhaust_prop_pool(); + if (held == NULL) { + exhaust_pool = -1; + } + } +#else + if (exhaust_pool) { + exhaust_pool = -1; /* no shared pool to exhaust */ + } +#endif g_clients[1].read_err = 1; for (i = 0; i < 16; i++) { MqttBroker_Step(&broker); } +#ifndef WOLFMQTT_DYN_PROP + if (held != NULL) { + (void)MqttProps_Free(held); + } +#endif found = will_bytes_found(g_clients[0].out_buf, g_clients[0].out_len, - expected, sizeof(expected)); + expected, (size_t)exp_pos); + if (count_packets_of_type(g_clients[0].out_buf, g_clients[0].out_len, + MQTT_PACKET_TYPE_PUBLISH) != 1 || exhaust_pool < 0) { + found = 0; + } MqttBroker_Stop(&broker); MqttBroker_Free(&broker); @@ -7178,14 +7242,57 @@ static int will_props_reach_live_subscriber(int with_delay) TEST(will_props_forwarded_to_live_subscriber) { - ASSERT_EQ(1, will_props_reach_live_subscriber(0)); + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 0, 1)); } TEST(delayed_will_props_forwarded_to_live_subscriber) { - ASSERT_EQ(1, will_props_reach_live_subscriber(1)); + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 0, 1)); } +#ifndef WOLFMQTT_DYN_PROP +#ifdef WOLFMQTT_STATIC_MEMORY +/* User Properties, after the Content Type, in the largest Will this static + * build accepts that will_props_reach_live_subscriber can carry. */ +#define WILL_TEST_STATIC_USER_PROPS_BY_LEN ((BROKER_MAX_WILL_PROPS_LEN - 4) / 7) +#define WILL_TEST_STATIC_USER_PROPS_BY_ENTRIES \ + (BROKER_WILL_PROPS_STACK_ENTRIES - 1) +#define WILL_TEST_STATIC_USER_PROPS_MIN2(a, b) (((a) < (b)) ? (a) : (b)) +#define WILL_TEST_STATIC_USER_PROPS WILL_TEST_STATIC_USER_PROPS_MIN2( \ + WILL_TEST_STATIC_USER_PROPS_MIN2(WILL_TEST_STATIC_USER_PROPS_BY_LEN, \ + WILL_TEST_STATIC_USER_PROPS_BY_ENTRIES), WILL_TEST_MAX_USER_PROPS) +#endif + +/* Will Properties are rebuilt without the shared property pool, so a full + * pool can neither drop the Will nor strip its properties, on the immediate + * or the deferred path. In a static build the largest accepted Will still + * fits the stack entries. */ +TEST(will_props_published_with_prop_pool_exhausted) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, 0)); + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 1, 0)); +#if !defined(WOLFMQTT_STATIC_MEMORY) || BROKER_WILL_PROPS_STACK_ENTRIES >= 2 + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, 1)); + ASSERT_EQ(1, will_props_reach_live_subscriber(1, 1, 1)); +#endif +#ifdef WOLFMQTT_STATIC_MEMORY + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 1, + WILL_TEST_STATIC_USER_PROPS)); +#endif +} +#endif + +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + BROKER_WILL_PROPS_STACK_ENTRIES < WILL_TEST_MAX_USER_PROPS +/* A dynamic build rebuilds a Will with more properties than the stack + * entries on the heap; all of them still reach the subscriber in order. */ +TEST(will_props_beyond_stack_entries_forwarded) +{ + ASSERT_EQ(1, will_props_reach_live_subscriber(0, 0, + WILL_TEST_MAX_USER_PROPS)); +} +#endif + #if WOLFMQTT_MAX_QOS >= 1 /* A delayed Will can fire after every client has been freed, which releases * the last MqttClient_Init reference to the shared property pool. The Will @@ -16613,6 +16720,13 @@ int main(int argc, char** argv) #if WOLFMQTT_MAX_QOS >= 1 RUN_TEST(delayed_will_props_decoded_with_no_clients); #endif +#ifndef WOLFMQTT_DYN_PROP + RUN_TEST(will_props_published_with_prop_pool_exhausted); +#endif +#if !defined(WOLFMQTT_STATIC_MEMORY) && \ + BROKER_WILL_PROPS_STACK_ENTRIES < WILL_TEST_MAX_USER_PROPS + RUN_TEST(will_props_beyond_stack_entries_forwarded); +#endif #if WOLFMQTT_MAX_QOS >= 1 RUN_TEST(queued_will_expired_before_delivery_dropped); RUN_TEST(queued_will_expiry_reduced_by_wait); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 1394ce195..4c9742519 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -136,6 +136,23 @@ #ifndef BROKER_MAX_WILL_PROPS_LEN #define BROKER_MAX_WILL_PROPS_LEN 128 #endif +/* Will Properties rebuilt on the stack when the Will is published, so that + * cannot fail on the shared property pool. In a static build this bounds + * them and a Will with more is refused at CONNECT; the default fits any + * Will within BROKER_MAX_WILL_PROPS_LEN, since every publishable property + * other than User Property appears at most once and a User Property takes at + * least 5 bytes. A dynamic build puts a larger Will on the heap. */ +#ifndef BROKER_WILL_PROPS_STACK_ENTRIES + #ifdef WOLFMQTT_STATIC_MEMORY + #define BROKER_WILL_PROPS_STACK_ENTRIES \ + (5 + BROKER_MAX_WILL_PROPS_LEN / 5) + #else + #define BROKER_WILL_PROPS_STACK_ENTRIES 8 + #endif +#endif +#if BROKER_WILL_PROPS_STACK_ENTRIES < 1 + #error BROKER_WILL_PROPS_STACK_ENTRIES must be at least 1 +#endif #ifndef BROKER_MAX_PENDING_WILLS #define BROKER_MAX_PENDING_WILLS 4 #endif @@ -922,13 +939,20 @@ typedef struct MqttBroker { * failure is reported once rather than on every step. */ int accept_log_err; int accept_log_err_tls; +#ifdef WOLFMQTT_V5 + /* Holds one MqttProps_Init reference from MqttBroker_Init to + * MqttBroker_Free, so restored v5 queue properties can be decoded when + * no client is connected. */ + byte props_ref; +#endif } MqttBroker; /* -------------------------------------------------------------------------- */ /* Public API */ /* -------------------------------------------------------------------------- */ -/* Initialize the broker context with network callbacks */ +/* Initialize the broker context with network callbacks. Every successful + * call must be paired with MqttBroker_Free, which releases what it holds. */ WOLFMQTT_API int MqttBroker_Init(MqttBroker* broker, MqttBrokerNet* net); /* Run the broker main loop (blocking) */