Skip to content

Zero-Length ClientId Unique Assignment Is Incomplete #491

@LiD0209

Description

@LiD0209

Zero-Length ClientId Unique Assignment Is Incomplete

Summary

This document shows that wolfMQTT's MQTT 3.1.1 handling of accepted zero-length ClientIds is incomplete. The broker may accept an empty ClientId without assigning a unique replacement in the MQTT 3.1.1 path, while the generated ClientId logic is tied to MQTT v5-specific conditions and does not uniformly enforce the required CleanSession=1 constraint.

English Standard Text

The relevant MQTT 3.1.1 rules are in Section 3.1.3 CONNECT Payload, Client Identifier.

Original English text with context:

The Client Identifier (ClientId) identifies the Client to the Server.
Each Client connecting to the Server has a unique ClientId.
The ClientId MUST be used by Clients and by Servers to identify state that
they hold relating to this MQTT Session between the Client and the Server
[MQTT-3.1.3-2].

The special zero-length ClientId rule is:

A Server MAY allow a Client to supply a ClientId that has a length of zero
bytes. However if it does so the Server MUST treat this as a special case and
assign a unique ClientId to that Client. It MUST then process the CONNECT
packet as if the Client had provided that unique ClientId [MQTT-3.1.3-6].

The CleanSession linkage rules are:

If the Client supplies a zero-byte ClientId, the Client MUST also set
CleanSession to 1 [MQTT-3.1.3-7].

If the Client supplies a zero-byte ClientId with CleanSession set to 0,
the Server MUST respond to the CONNECT Packet with a CONNACK return code
0x02 (Identifier rejected) and then close the Network Connection
[MQTT-3.1.3-8].

Meaning in this issue:

  • A Server is not required to allow a zero-length ClientId. That part is MAY.
  • If the Server does allow it, assigning a unique ClientId is MUST.
  • After assignment, the Server must process the connection as if the Client had supplied that unique ID.
  • For MQTT 3.1.1, zero-length ClientId is only valid with CleanSession=1.
  • Zero-length ClientId with CleanSession=0 must be rejected with CONNACK 0x02 and the connection must be closed.

Expected MQTT 3.1.1 Behavior

For a MQTT 3.1.1 CONNECT packet with a zero-length ClientId:

ClientId length CleanSession Server policy Expected behavior
0 1 Server does not allow zero-length ClientId reject with CONNACK 0x02, then close
0 1 Server allows zero-length ClientId assign a unique ClientId and process CONNECT as if that ID was supplied
0 0 any policy reject with CONNACK 0x02, then close

The key point is that accepting zero-length ClientId without assigning a unique ClientId is not one of the allowed MQTT 3.1.1 outcomes.

Code Description

1. CONNECT decoding accepts the ClientId field as a length-prefixed string

File: wolfMQTT-master/src/mqtt_packet.c:1038

tmp = MqttDecode_String(rx_payload, &mc_connect->client_id, NULL,
        (word32)(rx_buf_len - (rx_payload - rx_buf)));
if (tmp < 0) {
    return tmp;
}

MqttDecode_Connect() decodes the ClientId but does not enforce the zero-length special-case rules. It does not reject ClientId length == 0 && CleanSession == 0, and it does not assign a unique ClientId.

2. Broker stores the decoded ClientId length, including zero length

File: wolfMQTT-master/src/mqtt_broker.c:2708

if (mc.client_id) {
    word16 id_len = 0;
    if (MqttDecode_Num((byte*)mc.client_id - MQTT_DATA_LEN_SIZE,
            &id_len, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) {
        ...
        BROKER_STORE_STR(bc->client_id, mc.client_id, id_len,
            BROKER_MAX_CLIENT_ID_LEN);
    }
}

For a zero-length ClientId, id_len is 0. The broker stores that value and continues. There is no MQTT 3.1.1 branch here that changes an empty ClientId into a generated unique ID.

3. Empty ClientId skips normal uniqueness/session handling

File: wolfMQTT-master/src/mqtt_broker.c:2743

/* Client ID uniqueness and clean session handling */
bc->clean_session = mc.clean_session;
if (BROKER_STR_VALID(bc->client_id)) {
    BrokerClient* old;
    ...
}

The uniqueness and existing-session logic is guarded by BROKER_STR_VALID(bc->client_id). A zero-length ClientId is not a valid non-empty string, so this block is skipped. That means the accepted MQTT 3.1.1 empty-ClientId path is not processed as a generated unique ClientId.

4. Default CONNACK state accepts the connection

File: wolfMQTT-master/src/mqtt_broker.c:2917

ack.flags = 0;
ack.return_code = MQTT_CONNECT_ACK_CODE_ACCEPTED;

File: wolfMQTT-master/src/mqtt_broker.c:3023

rc = MqttEncode_ConnectAck(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), &ack);

Unless another branch changes ack.return_code, the broker sends CONNACK success. The zero-length MQTT 3.1.1 ClientId path does not set MQTT_CONNECT_ACK_CODE_REFUSED_ID and does not assign a unique ClientId before this point.

5. Unique assignment exists only in a MQTT v5 conditional branch

File: wolfMQTT-master/src/mqtt_broker.c:2966

#ifdef WOLFMQTT_V5
    if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 &&
        ack.return_code == MQTT_CONNECT_ACK_CODE_ACCEPTED) {
        MqttProp* prop;

        /* If client sent empty client ID, generate one and inform client */
        if (!BROKER_STR_VALID(bc->client_id)) {
            char auto_id[32];
            int id_len = XSNPRINTF(auto_id, (int)sizeof(auto_id),
                "auto-%04x", broker->next_packet_id++);

File: wolfMQTT-master/src/mqtt_broker.c:2979

if (id_len > 0) {
    BROKER_STORE_STR(bc->client_id, auto_id, (word16)id_len,
        BROKER_MAX_CLIENT_ID_LEN);
}

File: wolfMQTT-master/src/mqtt_broker.c:2983

if (BROKER_STR_VALID(bc->client_id)) {
    prop = MqttProps_Add(&ack.props);
    if (prop != NULL) {
        prop->type = MQTT_PROP_ASSIGNED_CLIENT_ID;
        prop->data_str.str = bc->client_id;
        prop->data_str.len = (word16)XSTRLEN(bc->client_id);
    }
}

This assignment is conditional on:

  • WOLFMQTT_V5 being compiled;
  • the connection protocol level being MQTT v5 or later;
  • the CONNACK return code still being accepted;
  • the stored bc->client_id being empty.

It does not apply to MQTT 3.1.1 accepted CONNECT packets.

Dynamic Test Evidence

The existing raw CONNECT test sends zero-length ClientId packets.

Observed MQTT 3.1.1 behavior:

mqtt311_emptyid_clean1 -> CONNACK 20020000, return_code=0, connection remains alive
mqtt311_emptyid_clean0 -> CONNACK 20020000, return_code=0, connection remains alive

Interpretation:

  • mqtt311_emptyid_clean1: the broker accepts zero-length ClientId, but the MQTT 3.1.1 path does not assign a visible or internally non-empty unique ClientId before normal processing.
  • mqtt311_emptyid_clean0: the broker accepts a combination that MQTT 3.1.1 requires it to reject with 0x02 and close.

Observed MQTT v5 behavior:

mqtt5_emptyid_clean1 -> accepted and assigned client id such as auto-xxxx
mqtt5_emptyid_clean0 -> accepted and assigned client id such as auto-xxxx

This confirms the issue: unique assignment exists in a MQTT v5 accepted branch, but it is not a uniform MQTT 3.1.1 behavior and is not bound to the required CleanSession=1 condition.

Issue Mapping

Issue aspect Why it is real
MQTT 3.1.1 assignment path The code has a generated ClientId path, but it is under WOLFMQTT_V5 and protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5. MQTT 3.1.1 empty ClientId acceptance does not use this path.
Conditional assignment logic The assignment logic is conditional on build flags, protocol level, accepted return code, and empty stored ClientId. It is not a single MQTT 3.1.1 special-case branch that handles every accepted zero-length ClientId.

Inconsistency Reason

MQTT 3.1.1 gives the Server a choice: it may reject a zero-length ClientId, or it may allow it. But once wolfMQTT accepts a zero-length MQTT 3.1.1 ClientId, the standard requires the Server to assign a unique ClientId and process the CONNECT as if that unique ClientId had been supplied.

wolfMQTT does not implement that MQTT 3.1.1 special case uniformly:

  • The MQTT 3.1.1 path can send CONNACK success for an empty ClientId.
  • The normal ClientId uniqueness block is skipped because the stored ClientId remains empty.
  • The only generated-ID branch is MQTT v5 specific.
  • The generated-ID branch is also not guarded by CleanSession=1, so it can assign an ID even for invalid empty ClientId + CleanSession=0 cases in the MQTT v5 path.

Therefore, MQTT 3.1.1 zero-length ClientId handling is incomplete: accepted empty ClientIds are not consistently assigned unique IDs, and invalid CleanSession=0 combinations are not rejected.

Conclusion

For MQTT 3.1.1, the unique-assignment behavior should be treated as part of the zero-length ClientId compliance gap:

wolfMQTT accepts zero-length ClientId in MQTT 3.1.1, but the required unique
ClientId assignment is not implemented in the MQTT 3.1.1 accepted path.
The only generated-ID logic is conditional and MQTT v5-specific.

These issues are closely related to the zero-length ClientId / CleanSession validation gap, but they describe a different part of the same standard rule: not only must CleanSession=0 be rejected, an accepted zero-length ClientId must also receive a unique ClientId.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions