Skip to content

Commit 03667b4

Browse files
authored
Merge pull request #484 from embhorn/fenrir-tests
Add fenrir suggested test cases
2 parents c08b73f + 3271c99 commit 03667b4

9 files changed

Lines changed: 1789 additions & 135 deletions

File tree

AGENTS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,50 @@ Uses `.clang-format` with LLVM base style:
147147
- Tab indentation (4-space tabs)
148148
- K&R inspired style
149149

150+
## Test Integrity
151+
Never modify, delete, skip, or weaken tests to make them pass.
152+
Never fabricate, adjust, or derive expected values from the code under test just to force a pass; fixed expected values are acceptable when they come from an independent oracle, such as committed test vectors or other externally verified results.
153+
A passing test suite achieved by changing the tests (not the implementation) is not a passing result.
154+
Fix the code. If the code cannot be fixed within scope, escalate.
155+
156+
Do not use the code under test as its only oracle where an independent oracle is required, especially for crypto, KDFs, canonical encodings, and other security-sensitive transformations. In those cases, tests should use known external test vectors, cross-validation against an independent implementation, or bit-exact comparison against a trusted reference path. For example, a test that only encrypts with function A and decrypts with function A is insufficient to validate the correctness of the cryptographic primitive.
157+
158+
Roundtrip/property tests are still acceptable where they match the behavior being validated, such as encode/decode or serialize/parse flows already used elsewhere in this repository, but they should not be the sole oracle when stronger independent validation is needed.
159+
160+
## No Fabrication
161+
Never report status, results, or completion that does not reflect work actually performed.
162+
If you are uncertain whether a step succeeded, say so explicitly. Do not paper over uncertainty with confident-sounding output.
163+
164+
## Exit Code Discipline
165+
Every shell command's exit code must be checked.
166+
Never proceed after a silent failure.
167+
A command that failed and was ignored is not a completed step.
168+
169+
## MQTT Specification Discipline
170+
Wire format and protocol behavior are governed by the published MQTT specifications. Treat the spec as the source of truth, not the code.
171+
172+
Relevant specifications:
173+
- MQTT v3.1.1 — OASIS Standard (`mqtt-v3.1.1-os`)
174+
- MQTT v5.0 — OASIS Standard (`mqtt-v5.0`)
175+
- MQTT-SN v1.2 — OASIS
176+
177+
When implementing or testing a normative requirement, cite it in a comment so reviewers can verify against the spec:
178+
- MQTT v3.1.1 / v5.0: bracketed conformance identifiers, e.g. `[MQTT-3.8.1-1]`, `[MQTT-2.3.1-1]`.
179+
- When a rule has no bracketed identifier, reference the section number, e.g. `MQTT 5.0 section 3.15.2`.
180+
- MQTT-SN v1.2: `MQTT-SN 1.2 section X.Y`.
181+
182+
Match the version scope of the change. MQTT v5.0 adds packets and fields (AUTH, Reason Codes, Properties) that do not exist in v3.1.1; do not apply a v5-only rule to v3.1.1 code paths or vice versa. Guard v5-only logic with `WOLFMQTT_V5` and v3.1.1-only logic accordingly.
183+
184+
## Test Oracle Discipline
185+
Do not use the code under test as its own oracle for wire-format behavior. For encoder or decoder tests, use one of:
186+
- A hand-constructed byte sequence that matches the spec wire format, built by reading the relevant section (not captured from encoder output). Comment the byte layout so the fixture is auditable.
187+
- Values from the spec's worked examples or conformance annex.
188+
- A cross-check against an independent implementation (e.g. mosquitto) captured once and committed as a fixed byte array.
189+
190+
Roundtrip tests (encode then decode, or vice versa) are acceptable for regression and structural coverage, but they cannot be the sole oracle for a wire-format rule — a bug present in both encoder and decoder will still roundtrip. Pair roundtrip coverage with at least one independent fixture per rule.
191+
192+
Tests must be fully offline and must not fetch vectors from the network at runtime.
193+
150194
## Dependencies
151195

152196
- **wolfSSL** - Required for TLS support

scripts/broker.test

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,93 @@ else
297297
fi
298298
fi # has_will
299299

300+
# --- Test 6b: Keep-alive timeout window precision ---
301+
echo ""
302+
echo "--- Test 6b: Keep-alive timeout window ---"
303+
if [ "$has_will" = "no" ]; then
304+
echo "SKIP: Keep-alive timeout window (not built with will support)"
305+
else
306+
# Validate that the broker's 1.5x keep_alive multiplier (mqtt_broker.c:
307+
# keep_alive_sec * 3 / 2) is honored. Test 6 above only checks that the LWT
308+
# is eventually delivered, so silent mutations (e.g. *1, *2, *3) would still
309+
# pass. This test measures wall-clock time from client freeze to LWT
310+
# delivery and asserts a tight bound that catches significant multiplier
311+
# changes.
312+
#
313+
# k=4 is used to widen the discrimination window. The broker checks timing
314+
# at integer-second resolution, so the keepalive fire happens at
315+
# last_rx_int + threshold + 1 wall seconds, where threshold = floor(k*mult).
316+
#
317+
# Expected fire window (relative to client freeze):
318+
# mult threshold fire window (s) +will_delay (v5)
319+
# 1.0 4 4-5 9-10
320+
# 1.5 6 6-7 11-12 <- spec compliant
321+
# 2.0 8 8-9 13-14
322+
# 3.0 12 12-13 17-18
323+
# 4.0 16 16-17 21-22
324+
v5_active=no
325+
if ./$sub_bin -h 2>&1 | grep -q "Max packet size"; then
326+
v5_active=yes
327+
fi
328+
if [ "$v5_active" = "yes" ]; then
329+
LO_BOUND=10
330+
HI_BOUND=14
331+
else
332+
LO_BOUND=5
333+
HI_BOUND=9
334+
fi
335+
rm -f "${TMP_DIR}/t6b_watcher.ready" "${TMP_DIR}/t6b_client.ready"
336+
t6b_setup_ok=1
337+
timeout 30 ./$sub_bin -T -h 127.0.0.1 -p $port -n "wolfMQTT/example/lwttopic" \
338+
-i "t6b_watcher" -R "${TMP_DIR}/t6b_watcher.ready" \
339+
>"${TMP_DIR}/t6b_sub.log" 2>&1 &
340+
T6B_WATCHER_PID=$!
341+
TEST_PIDS+=($T6B_WATCHER_PID)
342+
if ! wait_for_file "${TMP_DIR}/t6b_watcher.ready" 5; then
343+
echo "FAIL: Keep-alive timeout window (watcher did not become ready)"
344+
FAIL=1
345+
t6b_setup_ok=0
346+
fi
347+
./$sub_bin -T -h 127.0.0.1 -p $port -n "test/ka2_trigger" -i "ka2_client" -l -k 4 \
348+
-R "${TMP_DIR}/t6b_client.ready" >"${TMP_DIR}/t6b_client.log" 2>&1 &
349+
T6B_CLIENT_PID=$!
350+
TEST_PIDS+=($T6B_CLIENT_PID)
351+
if ! wait_for_file "${TMP_DIR}/t6b_client.ready" 5; then
352+
echo "FAIL: Keep-alive timeout window (client did not become ready)"
353+
FAIL=1
354+
t6b_setup_ok=0
355+
fi
356+
if [ $t6b_setup_ok -eq 1 ]; then
357+
kill -STOP $T6B_CLIENT_PID 2>/dev/null
358+
T6B_T_START=$(date +%s)
359+
T6B_ELAPSED=-1
360+
T6B_DEADLINE=$((T6B_T_START + HI_BOUND + 5))
361+
while [ $(date +%s) -lt $T6B_DEADLINE ]; do
362+
if grep -q "ka2_client" "${TMP_DIR}/t6b_sub.log" 2>/dev/null; then
363+
T6B_ELAPSED=$(($(date +%s) - T6B_T_START))
364+
break
365+
fi
366+
sleep 0.1
367+
done
368+
fi
369+
kill -9 $T6B_CLIENT_PID 2>/dev/null
370+
wait $T6B_CLIENT_PID 2>/dev/null || true
371+
kill $T6B_WATCHER_PID 2>/dev/null
372+
wait $T6B_WATCHER_PID 2>/dev/null || true
373+
TEST_PIDS=()
374+
if [ $t6b_setup_ok -eq 1 ]; then
375+
if [ $T6B_ELAPSED -lt 0 ]; then
376+
echo "FAIL: Keep-alive timeout window (no LWT within $((HI_BOUND + 5))s)"
377+
FAIL=1
378+
elif [ $T6B_ELAPSED -lt $LO_BOUND ] || [ $T6B_ELAPSED -gt $HI_BOUND ]; then
379+
echo "FAIL: Keep-alive timeout window (elapsed=${T6B_ELAPSED}s, expected ${LO_BOUND}-${HI_BOUND}s)"
380+
FAIL=1
381+
else
382+
echo "PASS: Keep-alive timeout window (elapsed=${T6B_ELAPSED}s, expected ${LO_BOUND}-${HI_BOUND}s)"
383+
fi
384+
fi
385+
fi # has_will
386+
300387
fi # skip_plain (tests 1-6)
301388

302389
# --- Test 7: Username/password auth ---

src/mqtt_broker.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,19 @@
3535

3636
#ifdef WOLFMQTT_BROKER
3737

38-
#define BROKER_FORCE_ZERO(mem, len) Mqtt_ForceZero(mem, (word32)(len))
38+
/* Secure memory zeroing - uses volatile pointer to prevent the compiler
39+
* from optimizing away the stores (dead-store elimination). */
40+
static void MqttBroker_ForceZero(void* mem, word32 len)
41+
{
42+
volatile byte* p = (volatile byte*)mem;
43+
word32 i;
44+
for (i = 0; i < len; i++) {
45+
p[i] = 0;
46+
}
47+
}
48+
49+
#define BROKER_FORCE_ZERO(mem, len) \
50+
MqttBroker_ForceZero(mem, (word32)(len))
3951

4052
/* -------------------------------------------------------------------------- */
4153
/* Platform includes */
@@ -2701,6 +2713,13 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
27012713
return rc;
27022714
}
27032715

2716+
#ifdef WOLFMQTT_V5
2717+
/* Initialize early so every `goto send_connack` below produces a CONNACK
2718+
* matching the client's protocol level (v5 CONNACK has a Properties
2719+
* length and uses v5 reason codes). */
2720+
ack.protocol_level = mc.protocol_level;
2721+
#endif
2722+
27042723
/* Store client ID */
27052724
#ifdef WOLFMQTT_STATIC_MEMORY
27062725
bc->client_id[0] = '\0';
@@ -2917,7 +2936,6 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
29172936
ack.flags = 0;
29182937
ack.return_code = MQTT_CONNECT_ACK_CODE_ACCEPTED;
29192938
#ifdef WOLFMQTT_V5
2920-
ack.protocol_level = mc.protocol_level;
29212939
ack.props = NULL;
29222940
#endif
29232941

src/mqtt_client.c

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,19 @@
2626

2727
#include "wolfmqtt/mqtt_client.h"
2828

29-
#define CLIENT_FORCE_ZERO(mem, len) Mqtt_ForceZero(mem, (word32)(len))
29+
/* Secure memory zeroing - uses volatile pointer to prevent the compiler
30+
* from optimizing away the stores (dead-store elimination). */
31+
static void MqttClient_ForceZero(void* mem, word32 len)
32+
{
33+
volatile byte* p = (volatile byte*)mem;
34+
word32 i;
35+
for (i = 0; i < len; i++) {
36+
p[i] = 0;
37+
}
38+
}
39+
40+
#define CLIENT_FORCE_ZERO(mem, len) \
41+
MqttClient_ForceZero(mem, (word32)(len))
3042

3143
/* DOCUMENTED BUILD OPTIONS:
3244
*
@@ -3013,6 +3025,7 @@ int MqttClient_NetDisconnect(MqttClient *client)
30133025
{
30143026
#ifdef WOLFMQTT_MULTITHREAD
30153027
MqttPendResp *tmpResp;
3028+
MqttPendResp *nextResp;
30163029
int rc;
30173030
#endif
30183031

@@ -3027,7 +3040,6 @@ int MqttClient_NetDisconnect(MqttClient *client)
30273040
#ifdef WOLFMQTT_DEBUG_CLIENT
30283041
PRINTF("Net Disconnect: Removing pending responses");
30293042
#endif
3030-
MqttPendResp *nextResp;
30313043
for (tmpResp = client->firstPendResp;
30323044
tmpResp != NULL;
30333045
tmpResp = nextResp) {

src/mqtt_packet.c

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,18 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect)
801801
}
802802
#endif
803803

804-
remain_len += (int)XSTRLEN(mc_connect->client_id) + MQTT_DATA_LEN_SIZE;
804+
/* MQTT UTF-8 strings are limited to 65535 bytes [MQTT-1.5.3]. Check here
805+
* (before writing the fixed header) so a later MqttEncode_String failure
806+
* cannot corrupt tx_payload via `tx_payload += -1`. */
807+
{
808+
size_t str_len = XSTRLEN(mc_connect->client_id);
809+
if (str_len > (size_t)0xFFFF) {
810+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
811+
}
812+
remain_len += (int)str_len + MQTT_DATA_LEN_SIZE;
813+
}
805814
if (mc_connect->enable_lwt) {
815+
size_t str_len;
806816
/* Verify all required fields are present */
807817
if (mc_connect->lwt_msg == NULL ||
808818
mc_connect->lwt_msg->topic_name == NULL ||
@@ -811,8 +821,12 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect)
811821
{
812822
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
813823
}
824+
str_len = XSTRLEN(mc_connect->lwt_msg->topic_name);
825+
if (str_len > (size_t)0xFFFF) {
826+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
827+
}
814828

815-
remain_len += (int)XSTRLEN(mc_connect->lwt_msg->topic_name);
829+
remain_len += (int)str_len;
816830
remain_len += MQTT_DATA_LEN_SIZE;
817831
/* LWT payload uses word16 length prefix, validate it fits */
818832
if (mc_connect->lwt_msg->total_len > (word32)0xFFFF) {
@@ -841,10 +855,18 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect)
841855
#endif
842856
}
843857
if (mc_connect->username) {
844-
remain_len += (int)XSTRLEN(mc_connect->username) + MQTT_DATA_LEN_SIZE;
858+
size_t str_len = XSTRLEN(mc_connect->username);
859+
if (str_len > (size_t)0xFFFF) {
860+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
861+
}
862+
remain_len += (int)str_len + MQTT_DATA_LEN_SIZE;
845863
}
846864
if (mc_connect->password) {
847-
remain_len += (int)XSTRLEN(mc_connect->password) + MQTT_DATA_LEN_SIZE;
865+
size_t str_len = XSTRLEN(mc_connect->password);
866+
if (str_len > (size_t)0xFFFF) {
867+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
868+
}
869+
remain_len += (int)str_len + MQTT_DATA_LEN_SIZE;
848870
}
849871

850872
/* Encode fixed header */
@@ -1302,9 +1324,18 @@ int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, MqttPublish *publish,
13021324
if (tx_buf == NULL || publish == NULL) {
13031325
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
13041326
}
1327+
/* MQTT UTF-8 strings are limited to 65535 bytes [MQTT-1.5.3]. Check here
1328+
* before writing the fixed header so a later MqttEncode_String failure
1329+
* cannot corrupt tx_payload via `tx_payload += -1`. */
1330+
{
1331+
size_t str_len = XSTRLEN(publish->topic_name);
1332+
if (str_len > (size_t)0xFFFF) {
1333+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
1334+
}
13051335

1306-
/* Determine packet length */
1307-
variable_len = (int)XSTRLEN(publish->topic_name) + MQTT_DATA_LEN_SIZE;
1336+
/* Determine packet length */
1337+
variable_len = (int)str_len + MQTT_DATA_LEN_SIZE;
1338+
}
13081339
if (publish->qos > MQTT_QOS_0) {
13091340
if (publish->packet_id == 0) {
13101341
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PACKET_ID);
@@ -1721,7 +1752,12 @@ int MqttEncode_Subscribe(byte *tx_buf, int tx_buf_len,
17211752
for (i = 0; i < subscribe->topic_count; i++) {
17221753
topic = &subscribe->topics[i];
17231754
if ((topic != NULL) && (topic->topic_filter != NULL)) {
1724-
remain_len += (int)XSTRLEN(topic->topic_filter) + MQTT_DATA_LEN_SIZE;
1755+
/* MQTT UTF-8 strings are limited to 65535 bytes [MQTT-1.5.3] */
1756+
size_t str_len = XSTRLEN(topic->topic_filter);
1757+
if (str_len > (size_t)0xFFFF) {
1758+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
1759+
}
1760+
remain_len += (int)str_len + MQTT_DATA_LEN_SIZE;
17251761
remain_len++; /* For QoS */
17261762
}
17271763
else {
@@ -2016,8 +2052,12 @@ int MqttEncode_Unsubscribe(byte *tx_buf, int tx_buf_len,
20162052
for (i = 0; i < unsubscribe->topic_count; i++) {
20172053
topic = &unsubscribe->topics[i];
20182054
if ((topic != NULL) && (topic->topic_filter != NULL)) {
2019-
remain_len += (int)XSTRLEN(topic->topic_filter) +
2020-
MQTT_DATA_LEN_SIZE;
2055+
/* MQTT UTF-8 strings are limited to 65535 bytes [MQTT-1.5.3] */
2056+
size_t str_len = XSTRLEN(topic->topic_filter);
2057+
if (str_len > (size_t)0xFFFF) {
2058+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_BAD_ARG);
2059+
}
2060+
remain_len += (int)str_len + MQTT_DATA_LEN_SIZE;
20212061
}
20222062
else {
20232063
/* Topic count is invalid */
@@ -2675,7 +2715,8 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth)
26752715
/* Decode variable header */
26762716
auth->reason_code = *rx_payload++;
26772717
if ((auth->reason_code == MQTT_REASON_SUCCESS) ||
2678-
(auth->reason_code == MQTT_REASON_CONT_AUTH))
2718+
(auth->reason_code == MQTT_REASON_CONT_AUTH) ||
2719+
(auth->reason_code == MQTT_REASON_REAUTH))
26792720
{
26802721
/* Decode Length of Properties */
26812722
if (rx_buf_len < (rx_payload - rx_buf)) {
@@ -2700,28 +2741,29 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth)
27002741
if (tmp < 0)
27012742
return tmp;
27022743
rx_payload += tmp;
2703-
}
2704-
else if (auth->reason_code != MQTT_REASON_SUCCESS) {
2705-
/* The Reason Code and Property Length can be omitted if the
2706-
Reason Code is 0x00 (Success) and there are no Properties.
2707-
In this case the AUTH has a Remaining Length of 0. */
2708-
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
2709-
}
2710-
if (auth->props != NULL) {
2711-
/* Must have Authentication Method */
2744+
}
2745+
else if (auth->reason_code != MQTT_REASON_SUCCESS) {
2746+
/* The Reason Code and Property Length can be omitted if
2747+
the Reason Code is 0x00 (Success) and there are no
2748+
Properties. In this case the AUTH has a Remaining
2749+
Length of 0. */
2750+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
2751+
}
2752+
if (auth->props != NULL) {
2753+
/* Must have Authentication Method */
27122754

2713-
/* Must have Authentication Data */
2755+
/* Must have Authentication Data */
27142756

2715-
/* May have zero or more User Property pairs */
2757+
/* May have zero or more User Property pairs */
2758+
}
2759+
else {
2760+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
2761+
}
27162762
}
27172763
else {
27182764
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
27192765
}
27202766
}
2721-
else {
2722-
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER);
2723-
}
2724-
}
27252767
else {
27262768
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
27272769
}

tests/fuzz/gen_corpus.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ def main():
180180
connect + gen_subscribe("test/#"))
181181
write_seed("seq_connect_subscribe_wildcard.bin",
182182
connect + gen_subscribe("+/data/#"))
183+
# SUBSCRIBE options byte with QoS bits = 0b11 (reserved). Exercises the
184+
# BrokerHandle_Subscribe cap that clamps surfaced QoS=3 down to QoS 2
185+
# before BrokerSubs_Add / SUBACK.
186+
write_seed("seq_connect_subscribe_qos3.bin",
187+
connect + gen_subscribe("test/#", qos=3))
183188
write_seed("seq_connect_unsubscribe.bin",
184189
connect + gen_unsubscribe("test/#"))
185190
write_seed("seq_connect_pingreq.bin",

0 commit comments

Comments
 (0)