Commit 89772c1d15 for frr
commit 89772c1d158e042dbb61dbcfd3fd1b28f3b9e691
Author: Christian Hopps <chopps@labn.net>
Date: Sun Sep 27 11:09:40 2026 +0000
lib: mgmt_msg: fix use-after-free when handler disconnects mid-batch
When a connection is dropped (e.g., due to an error) the
notify_disconnect callback may delete underlying state (e.g., the
adapter state in mgmtd case). When this happens in the msg handling
loop (common), and there are more messages queued (uncommon) the latter
messages still get handled, but with now deleted state (use-after-free).
Mark the connection as in_handler while running the loop and
defer calling the enotify_disconnect callback until the loop exits.
Additionally msg_conn_disconnect() now flushes the unsent write queue
and the unhandled read queue, and cancels any proc-msgs event.
Previously a reconnecting client could replay the previous connections
messages.
Finally, free up any yet-to-process queued messages when we free the
mgmt_msg state.
Signed-off-by: Christian Hopps <chopps@labn.net>
diff --git a/lib/mgmt_msg.c b/lib/mgmt_msg.c
index 8754e7b9d9..856a8bbe3f 100644
--- a/lib/mgmt_msg.c
+++ b/lib/mgmt_msg.c
@@ -169,7 +169,8 @@ bool mgmt_msg_procbufs(struct mgmt_msg_state *ms,
left = stream_get_endp(work);
MGMT_MSG_TRACE(dbgtag, "Processing stream of len %zu", left);
/*
- * Q: if the handler disconnects should we stop/flush?
+ * If the handler disconnects, msg_conn_disconnect() flushes the
+ * inq so the next pop returns NULL and we exit the loop.
*/
mhdr = (struct mgmt_msg_hdr *)STREAM_DATA(work);
handle_msg(MGMT_MSG_MARKER_VERSION(mhdr->marker), (uint8_t *)(mhdr + 1),
@@ -412,6 +413,39 @@ size_t mgmt_msg_reset_writes(struct mgmt_msg_state *ms)
s = stream_fifo_pop(&ms->outq), nproc++)
stream_free(s);
+ /* Also drop unqueued message[s]. */
+ if (ms->outs) {
+ stream_free(ms->outs);
+ ms->outs = NULL;
+ nproc++;
+ }
+
+ return nproc;
+}
+
+/**
+ * Reset the read state, freeing any received but unprocessed messages and
+ * discarding any partially read message.
+ *
+ * Args:
+ * ms: mgmt_msg_state for this process.
+ *
+ * Returns:
+ * Number of queued (unprocessed) message streams that were freed.
+ */
+size_t mgmt_msg_reset_reads(struct mgmt_msg_state *ms)
+{
+ struct stream *s;
+ size_t nproc = 0;
+
+ for (s = stream_fifo_pop(&ms->inq); s;
+ s = stream_fifo_pop(&ms->inq), nproc++)
+ stream_free(s);
+
+ /* Drop any unfinished message. */
+ if (ms->ins)
+ stream_reset(ms->ins);
+
return nproc;
}
@@ -431,6 +465,7 @@ void mgmt_msg_init(struct mgmt_msg_state *ms, size_t max_read_buf,
void mgmt_msg_destroy(struct mgmt_msg_state *ms)
{
+ mgmt_msg_reset_reads(ms);
mgmt_msg_reset_writes(ms);
if (ms->ins)
stream_free(ms->ins);
@@ -487,11 +522,28 @@ static void msg_conn_read(struct event *event)
static void msg_conn_proc_msgs(struct event *event)
{
struct msg_conn *conn = EVENT_ARG(event);
+ bool more;
+
+ /*
+ * A handler may disconnect the connection; the disconnect callback can
+ * free `conn` (e.g., server side adapter deletion) so defer calling it
+ * until we are out of the handler loop.
+ */
+ conn->in_handler = true;
+ more = mgmt_msg_procbufs(&conn->mstate,
+ (void (*)(uint8_t, uint8_t *, size_t, void *))conn->handle_msg,
+ conn, conn->debug);
+ conn->in_handler = false;
+
+ if (conn->disconnect_pending) {
+ conn->disconnect_pending = false;
+ /* NOTE: may free `conn` */
+ if (conn->notify_disconnect)
+ (void)(*conn->notify_disconnect)(conn);
+ return;
+ }
- if (mgmt_msg_procbufs(&conn->mstate,
- (void (*)(uint8_t, uint8_t *, size_t,
- void *))conn->handle_msg,
- conn, conn->debug))
+ if (more)
/* there's more, schedule handling more */
msg_conn_sched_proc_msgs(conn);
}
@@ -531,9 +583,24 @@ void msg_conn_disconnect(struct msg_conn *conn, bool reconnect)
/* We need to unschedule any pending events on this fd */
event_cancel(&conn->read_ev);
event_cancel(&conn->write_ev);
+ event_cancel(&conn->proc_msg_ev);
- /* Notify client through registered callback (if any) */
- if (conn->notify_disconnect)
+ /*
+ * Drop any queued but unsent or unprocessed messages, they
+ * belong to the old connection and must not be replayed on a
+ * new one (or delivered to a handler after a disconnect).
+ */
+ mgmt_msg_reset_writes(&conn->mstate);
+ mgmt_msg_reset_reads(&conn->mstate);
+
+ /*
+ * Notify client through registered callback (if any). The
+ * callback may free `conn`, so if we're inside the message
+ * handler loop defer it until that loop has exited.
+ */
+ if (conn->in_handler)
+ conn->disconnect_pending = true;
+ else if (conn->notify_disconnect)
(void)(*conn->notify_disconnect)(conn);
}
diff --git a/lib/mgmt_msg.h b/lib/mgmt_msg.h
index 7eb88fbfa7..4784a886eb 100644
--- a/lib/mgmt_msg.h
+++ b/lib/mgmt_msg.h
@@ -67,6 +67,7 @@ extern bool mgmt_msg_procbufs(struct mgmt_msg_state *ms,
void *user, bool debug);
extern enum mgmt_msg_rsched mgmt_msg_read(struct mgmt_msg_state *ms, int fd,
bool debug);
+extern size_t mgmt_msg_reset_reads(struct mgmt_msg_state *ms);
extern size_t mgmt_msg_reset_writes(struct mgmt_msg_state *ms);
extern int mgmt_msg_send_msg(struct mgmt_msg_state *ms, uint8_t version,
void *msg, size_t len,
@@ -98,6 +99,8 @@ struct msg_conn {
void *user;
uint short_circuit_depth;
bool is_short_circuit; /* true when the message being handled is SC */
+ bool in_handler; /* true while inside the proc-msgs handler loop */
+ bool disconnect_pending; /* disconnected in_handler, notify deferred */
bool is_client;
bool debug;
};
diff --git a/mgmtd/mgmt_be_adapter.c b/mgmtd/mgmt_be_adapter.c
index 972593ef71..fbd0bbe3d4 100644
--- a/mgmtd/mgmt_be_adapter.c
+++ b/mgmtd/mgmt_be_adapter.c
@@ -567,7 +567,6 @@ static void be_adapter_process_msg(uint8_t version, uint8_t *data, size_t msg_le
_dbg("Got %s from '%s' txn-id %" PRIu64, mgmt_msg_code_name(msg->code), adapter->name,
msg->refer_id);
- assert(adapter->id != MGMTD_BE_CLIENT_ID_MAX || msg->code == MGMT_MSG_CODE_SUBSCRIBE);
if (adapter->id == MGMTD_BE_CLIENT_ID_MAX && msg->code != MGMT_MSG_CODE_SUBSCRIBE) {
_log_err("backend client '%s' sent message type %s without subscribing first",
adapter->name, mgmt_msg_code_name(msg->code));