Skip to content

Commit 4b8dac9

Browse files
committed
Fix from review
1 parent e0eb42b commit 4b8dac9

5 files changed

Lines changed: 251 additions & 21 deletions

File tree

src/mqtt_broker.c

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3417,6 +3417,7 @@ WOLFMQTT_LOCAL void BrokerOrphan_DropFull(MqttBroker* broker,
34173417
WOLFMQTT_FREE(sp->client_id);
34183418
}
34193419
WOLFMQTT_FREE(sp);
3420+
broker->subs_gen++;
34203421
}
34213422
else {
34223423
prev = sp;
@@ -3902,6 +3903,7 @@ static void BrokerSubs_RemoveClient(MqttBroker* broker, BrokerClient* bc)
39023903
WOLFMQTT_FREE(cur->client_id);
39033904
}
39043905
WOLFMQTT_FREE(cur);
3906+
broker->subs_gen++;
39053907
}
39063908
else {
39073909
prev = cur;
@@ -4053,6 +4055,11 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc,
40534055
sub->filter[filter_len] = '\0';
40544056
sub->next = broker->subs;
40554057
broker->subs = sub;
4058+
/* Bump on add too so subs_gen reflects every structural change to the
4059+
* list, not only removals. Also narrows the ABA window where a free and
4060+
* a same-address add in one fan-out iteration could otherwise pass the
4061+
* successor-still-linked check. */
4062+
broker->subs_gen++;
40564063
}
40574064
else if (sub != NULL) {
40584065
WOLFMQTT_FREE(sub);
@@ -4138,6 +4145,7 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc,
41384145
WOLFMQTT_FREE(cur->client_id);
41394146
}
41404147
WOLFMQTT_FREE(cur);
4148+
broker->subs_gen++;
41414149
if (bc->sub_count > 0) {
41424150
bc->sub_count--;
41434151
}
@@ -4263,6 +4271,7 @@ static void BrokerSubs_RemoveByClientId(MqttBroker* broker,
42634271
WOLFMQTT_FREE(cur->client_id);
42644272
}
42654273
WOLFMQTT_FREE(cur);
4274+
broker->subs_gen++;
42664275
}
42674276
else {
42684277
prev = cur;
@@ -5394,6 +5403,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker,
53945403
#else
53955404
BrokerSub* sub;
53965405
BrokerSub* next_sub = NULL;
5406+
word32 subs_gen_snapshot = 0;
53975407
#endif
53985408

53995409
if (broker == NULL || topic == NULL) {
@@ -5426,8 +5436,10 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker,
54265436
/* Snapshot the successor before any MqttPacket_Write: a WS fan-out
54275437
* write can drive an lws_service spin whose re-entrant CLOSED frees
54285438
* this client's BrokerSub nodes, so reading sub->next afterwards
5429-
* would dereference a freed node. */
5439+
* would dereference a freed node. Also snapshot the subs generation so
5440+
* next_sub is only re-validated when a free actually happened. */
54305441
next_sub = sub->next;
5442+
subs_gen_snapshot = broker->subs_gen;
54315443
#endif
54325444
if (sub->client != NULL && sub->client->protocol_level != 0 &&
54335445
#ifdef WOLFMQTT_STATIC_MEMORY
@@ -5534,6 +5546,13 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker,
55345546
{
55355547
MqttPublish out_pub;
55365548
int enc_rc, wr_rc;
5549+
/* Cache the client before the write: MqttPacket_Write can drive
5550+
* a re-entrant WS close that frees this subscriber's BrokerSub
5551+
* nodes (this `sub`), while the BrokerClient itself survives via
5552+
* deferred removal. Reaching it through the freed `sub` after
5553+
* the write would be a use-after-free; go through wc instead,
5554+
* mirroring BrokerHandle_Publish. */
5555+
BrokerClient* wc = sub->client;
55375556
XMEMSET(&out_pub, 0, sizeof(out_pub));
55385557
out_pub.topic_name = (char*)topic;
55395558
out_pub.qos = eff_qos;
@@ -5545,16 +5564,16 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker,
55455564
out_pub.packet_id = BrokerNextPacketId(broker);
55465565
}
55475566
#ifdef WOLFMQTT_V5
5548-
out_pub.protocol_level = sub->client->protocol_level;
5567+
out_pub.protocol_level = wc->protocol_level;
55495568
#endif
5550-
enc_rc = MqttEncode_Publish(sub->client->tx_buf,
5551-
BROKER_CLIENT_TX_SZ(sub->client), &out_pub, 0);
5569+
enc_rc = MqttEncode_Publish(wc->tx_buf,
5570+
BROKER_CLIENT_TX_SZ(wc), &out_pub, 0);
55525571
if (enc_rc > 0) {
5553-
wr_rc = MqttPacket_Write(&sub->client->client,
5554-
sub->client->tx_buf, enc_rc);
5572+
wr_rc = MqttPacket_Write(&wc->client,
5573+
wc->tx_buf, enc_rc);
55555574
/* Scrub tx_buf unless still in-progress (CONTINUE). */
55565575
if (wr_rc != MQTT_CODE_CONTINUE) {
5557-
BROKER_FORCE_ZERO(sub->client->tx_buf, enc_rc);
5576+
BROKER_FORCE_ZERO(wc->tx_buf, enc_rc);
55585577
}
55595578
}
55605579
}
@@ -5586,9 +5605,11 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker,
55865605
}
55875606
#endif
55885607
#ifndef WOLFMQTT_STATIC_MEMORY
5589-
/* The write above can drive a re-entrant WS close that frees next_sub;
5590-
* stop the walk if that snapshot is no longer linked. */
5591-
if (next_sub != NULL && !BrokerSubs_StillLinked(broker, next_sub)) {
5608+
/* The write above can drive a re-entrant WS close that frees next_sub.
5609+
* Only re-validate it when a subscription was actually removed during
5610+
* the write (generation changed), so the common case stays O(1). */
5611+
if (next_sub != NULL && broker->subs_gen != subs_gen_snapshot &&
5612+
!BrokerSubs_StillLinked(broker, next_sub)) {
55925613
break;
55935614
}
55945615
sub = next_sub;
@@ -7177,6 +7198,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
71777198
#ifndef WOLFMQTT_STATIC_MEMORY
71787199
BrokerSub* sub = broker->subs;
71797200
BrokerSub* next_sub = NULL;
7201+
word32 subs_gen_snapshot = 0;
71807202
#endif
71817203
/* Fan out to matching subscribers */
71827204
#ifdef WOLFMQTT_STATIC_MEMORY
@@ -7188,8 +7210,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
71887210
/* Snapshot the successor before any MqttPacket_Write: a fan-out
71897211
* write can drive an lws_service spin that frees this client's
71907212
* BrokerSub nodes re-entrantly (LWS_CALLBACK_CLOSED), so reading
7191-
* sub->next afterwards would dereference a freed node. */
7213+
* sub->next afterwards would dereference a freed node. Also snapshot
7214+
* the subs generation so next_sub is only re-validated when a free
7215+
* actually happened. */
71927216
next_sub = sub->next;
7217+
subs_gen_snapshot = broker->subs_gen;
71937218
#endif
71947219
if (sub->client != NULL &&
71957220
sub->client->protocol_level != 0 &&
@@ -7422,8 +7447,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
74227447
}
74237448
}
74247449
/* The write above can drive a re-entrant WS close that frees
7425-
* next_sub; stop the walk if that snapshot is no longer linked. */
7426-
if (next_sub != NULL && !BrokerSubs_StillLinked(broker, next_sub)) {
7450+
* next_sub. Only re-validate it when a subscription was actually
7451+
* removed during the write (generation changed), so the common
7452+
* case stays O(1). */
7453+
if (next_sub != NULL && broker->subs_gen != subs_gen_snapshot &&
7454+
!BrokerSubs_StillLinked(broker, next_sub)) {
74277455
break;
74287456
}
74297457
sub = next_sub;
@@ -8564,6 +8592,7 @@ static void BrokerSubs_FreeAll(MqttBroker* broker)
85648592
WOLFMQTT_FREE(broker->subs->client_id);
85658593
}
85668594
WOLFMQTT_FREE(broker->subs);
8595+
broker->subs_gen++;
85678596
broker->subs = next;
85688597
}
85698598
#endif

src/mqtt_client.c

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,11 @@ void MqttClient_RespList_Remove(MqttClient *client, MqttPendResp *rmResp)
610610
}
611611
}
612612
if (tmpResp) {
613+
#ifdef WOLFMQTT_V5
614+
/* Drop the reserved-quota back-reference so a recycled publish object
615+
* cannot carry a stale pointer into a future pending response. */
616+
tmpResp->recvQuotaStat = NULL;
617+
#endif
613618
/* Fix up the first and last pointers */
614619
if (client->firstPendResp == tmpResp) {
615620
client->firstPendResp = tmpResp->next;
@@ -2916,7 +2921,14 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish,
29162921
publish->packet_id);
29172922
#ifndef WOLFMQTT_NONBLOCK
29182923
if (rc == MQTT_CODE_CONTINUE) {
2919-
/* mark success, let other thread handle response */
2924+
/* No non-blocking re-entry will complete this ack, so
2925+
* report success now. Release the reserved quota since
2926+
* the ack will no longer be tracked, and the pending
2927+
* response is unlinked below so no reference to the
2928+
* caller's publish object remains. */
2929+
#ifdef WOLFMQTT_V5
2930+
MqttClient_RestoreRecvQuota(client, publish);
2931+
#endif
29202932
rc = MQTT_CODE_SUCCESS;
29212933
}
29222934
#endif
@@ -2968,13 +2980,13 @@ static int MqttPublishMsg(MqttClient *client, MqttPublish *publish,
29682980
break;
29692981
#endif
29702982
#ifdef WOLFMQTT_MULTITHREAD
2971-
/* Leave a write-only publish's pending response in the list: a
2972-
* build without WOLFMQTT_NONBLOCK reports success here without
2973-
* the ack, and unlinking it now would prevent the reading thread
2974-
* from completing it and releasing the reserved Receive Maximum
2975-
* unit, leaking quota. The reader removes it when the ack
2976-
* arrives. */
2977-
if (!writeOnly && wm_SemLock(&client->lockClient) == 0) {
2983+
/* Remove the pending response before returning: a caller told
2984+
* SUCCESS may free its publish object, so no reference to it may
2985+
* remain in the list. A write-only publish under
2986+
* WOLFMQTT_NONBLOCK returned CONTINUE above and broke out before
2987+
* here, leaving its entry for the reading thread to complete and
2988+
* remove. */
2989+
if (wm_SemLock(&client->lockClient) == 0) {
29782990
MqttClient_RespList_Remove(client, &publish->pendResp);
29792991
wm_SemUnlock(&client->lockClient);
29802992
}
@@ -3867,6 +3879,15 @@ int MqttClient_CancelMessage(MqttClient *client, MqttObject* msg)
38673879
MqttPacket_TypeDesc(tmpResp->packet_type),
38683880
tmpResp->packet_type, tmpResp->packet_id,
38693881
tmpResp->packetProcessing, tmpResp->packetDone);
3882+
#endif
3883+
#ifdef WOLFMQTT_V5
3884+
/* Release any Receive Maximum unit this response reserved so
3885+
* cancelling a QoS>0 (e.g. write-only) publish does not leak it.
3886+
* lockClient is held; idempotent via recvQuotaHeld. */
3887+
if (tmpResp->recvQuotaStat != NULL) {
3888+
MqttClient_RecvQuotaRelease_Locked(client,
3889+
tmpResp->recvQuotaStat);
3890+
}
38703891
#endif
38713892
MqttClient_RespList_Remove(client, tmpResp);
38723893
break;
@@ -3972,6 +3993,15 @@ int MqttClient_NetDisconnect(MqttClient *client)
39723993
MqttPacket_TypeDesc(tmpResp->packet_type),
39733994
tmpResp->packet_type, tmpResp->packet_id,
39743995
tmpResp->packetProcessing, tmpResp->packetDone);
3996+
#endif
3997+
#ifdef WOLFMQTT_V5
3998+
/* Release any reserved Receive Maximum unit as the list is purged
3999+
* on disconnect, so it is not left held. Idempotent via
4000+
* recvQuotaHeld; lockClient is held. */
4001+
if (tmpResp->recvQuotaStat != NULL) {
4002+
MqttClient_RecvQuotaRelease_Locked(client,
4003+
tmpResp->recvQuotaStat);
4004+
}
39754005
#endif
39764006
MqttClient_RespList_Remove(client, tmpResp);
39774007
}

tests/test_broker_connect.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,74 @@ static int first_disconnect_reason(const byte* buf, size_t len)
12391239
*
12401240
* Pre-fix the broker fanned out twice and the subscriber would see two
12411241
* forwarded PUBLISHes, breaking exactly-once delivery. */
1242+
#ifndef WOLFMQTT_STATIC_MEMORY
1243+
/* The fan-out loop re-validates a snapshotted successor only when the broker's
1244+
* subscription generation changed during the write, keeping the common case
1245+
* O(1). This pins two things: an ordinary PUBLISH is still delivered to a
1246+
* matching subscriber, and removing a subscription (UNSUBSCRIBE) bumps the
1247+
* generation, so a re-entrant free during a later fan-out is still detected. */
1248+
TEST(fanout_subs_generation_bumped_on_unsubscribe)
1249+
{
1250+
MqttBroker broker;
1251+
MqttBrokerNet net;
1252+
int i;
1253+
int sub_pubs;
1254+
word32 gen_after_sub;
1255+
/* CONNECT subscriber "A". */
1256+
static const byte connect_sub[] = {
1257+
0x10, 0x0D, 0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, 0x02, 0x00, 0x3C,
1258+
0x00, 0x01, 'A'
1259+
};
1260+
/* CONNECT publisher "B". */
1261+
static const byte connect_pub[] = {
1262+
0x10, 0x0D, 0x00, 0x04, 'M', 'Q', 'T', 'T', 0x04, 0x02, 0x00, 0x3C,
1263+
0x00, 0x01, 'B'
1264+
};
1265+
/* SUBSCRIBE packet_id=1, filter "x", QoS 0. */
1266+
static const byte subscribe_x[] = {
1267+
0x82, 0x06, 0x00, 0x01, 0x00, 0x01, 'x', 0x00
1268+
};
1269+
/* PUBLISH QoS 0, topic "x", payload "hi". remain = 2+1+2 = 5. */
1270+
static const byte publish_x[] = {
1271+
0x30, 0x05, 0x00, 0x01, 'x', 'h', 'i'
1272+
};
1273+
/* UNSUBSCRIBE packet_id=2, filter "x". remain = 2 + (2+1) = 5. */
1274+
static const byte unsubscribe_x[] = {
1275+
0xA2, 0x05, 0x00, 0x02, 0x00, 0x01, 'x'
1276+
};
1277+
1278+
install_mock_net(&net);
1279+
XMEMSET(&broker, 0, sizeof(broker));
1280+
ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net));
1281+
ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker));
1282+
1283+
reset_mock_clients(2);
1284+
mock_client_input_append(0, connect_sub, sizeof(connect_sub));
1285+
mock_client_input_append(0, subscribe_x, sizeof(subscribe_x));
1286+
mock_client_input_append(1, connect_pub, sizeof(connect_pub));
1287+
mock_client_input_append(1, publish_x, sizeof(publish_x));
1288+
for (i = 0; i < 32; i++) {
1289+
MqttBroker_Step(&broker);
1290+
}
1291+
1292+
/* The common-case fan-out delivered the PUBLISH to the subscriber. */
1293+
sub_pubs = count_packets_of_type(g_clients[0].out_buf,
1294+
g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH);
1295+
ASSERT_EQ(1, sub_pubs);
1296+
gen_after_sub = broker.subs_gen;
1297+
1298+
/* UNSUBSCRIBE removes the BrokerSub and must bump the generation. */
1299+
mock_client_input_append(0, unsubscribe_x, sizeof(unsubscribe_x));
1300+
for (i = 0; i < 16; i++) {
1301+
MqttBroker_Step(&broker);
1302+
}
1303+
ASSERT_TRUE(broker.subs_gen != gen_after_sub);
1304+
1305+
MqttBroker_Stop(&broker);
1306+
MqttBroker_Free(&broker);
1307+
}
1308+
#endif /* !WOLFMQTT_STATIC_MEMORY */
1309+
12421310
TEST(qos2_duplicate_publish_dedup)
12431311
{
12441312
MqttBroker broker;
@@ -4976,6 +5044,9 @@ int main(int argc, char** argv)
49765044
#ifdef WOLFMQTT_V5
49775045
RUN_TEST(connect_v5_emptyid_assigned_id_emitted);
49785046
RUN_TEST(connect_v5_emptyid_clean0_accepted);
5047+
#endif
5048+
#ifndef WOLFMQTT_STATIC_MEMORY
5049+
RUN_TEST(fanout_subs_generation_bumped_on_unsubscribe);
49795050
#endif
49805051
RUN_TEST(qos2_duplicate_publish_dedup);
49815052
RUN_TEST(qos2_phantom_dup_publish_is_fresh);

0 commit comments

Comments
 (0)