- Fix CVE-2026-82720, Use-after-free in DoH stream cleanup code path.

Thanks to Yuqi Qiu and Xiang Li from Nankai University, AOSP Lab,
  for the report.
This commit is contained in:
W.C.A. Wijngaards
2026-09-16 09:24:29 +02:00
parent 8c2e0fd6cc
commit eba3d35ad4
2 changed files with 92 additions and 72 deletions
+71 -72
View File
@@ -1097,6 +1097,18 @@ mesh_state_make_unique(struct mesh_state* mstate)
mstate->unique = mstate;
}
/** pop a reply from the reply list, if there are any. */
static struct mesh_reply*
mesh_reply_list_pop_first(struct mesh_state* mstate)
{
if(mstate->reply_list) {
struct mesh_reply* r = mstate->reply_list;
mstate->reply_list = r->next;
return r;
}
return NULL;
}
void
mesh_state_cleanup(struct mesh_state* mstate)
{
@@ -1112,15 +1124,30 @@ mesh_state_cleanup(struct mesh_state* mstate)
}
/* drop unsent replies */
if(!mstate->replies_sent) {
struct mesh_reply* rep = mstate->reply_list;
struct mesh_reply* rep;
struct mesh_cb* cb;
/* in tcp_req_info, the mstates linked are removed, but
* the reply_list is now NULL, so the remove-from-empty-list
* takes no time and also it does not do the mesh accounting */
mstate->reply_list = NULL;
for(; rep; rep=rep->next) {
/* Pop items from the list, that means there is no iterator.
* And then items can be removed from the reply list, from
* like comm_point_drop_reply and comm_point_close calls.
* As the tcp_req_info and http2 code drops the entire
* connection. That could delete mesh_reply items previous and
* after the current state. The previous items are already
* popped. And the next items can be altered, like to when a
* connection has more replies on the reply list.
* The current item is also popped so the code needs to
* remove its references. */
while((rep = mesh_reply_list_pop_first(mstate)) != NULL) {
infra_wait_limit_dec(mesh->env->infra_cache,
&rep->query_reply, mesh->env->cfg);
if(rep->query_reply.c->tcp_req_info)
tcp_req_info_remove_mesh_state(
rep->query_reply.c->tcp_req_info,
mstate);
else if(rep->query_reply.c->use_h2)
http2_stream_remove_mesh_state(rep->h2_stream);
else if(rep->query_reply.doq_stream)
doq_stream_remove_mesh_state(
rep->query_reply.doq_stream);
comm_point_drop_reply(&rep->query_reply);
log_assert(mesh->num_reply_addrs > 0);
mesh->num_reply_addrs--;
@@ -1484,12 +1511,6 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep,
struct timeval end_time;
struct timeval duration;
int secure;
/* briefly set the replylist to null in case the
* meshsendreply calls tcpreqinfo sendreply that
* comm_point_drops because of size, and then the
* null stops the mesh state remove and thus
* reply_list modification and accounting */
struct mesh_reply* rlist = m->reply_list;
/* rpz: apply actions */
rcode = mesh_is_udp(r) && mesh_is_rpz_respip_tcponly_action(m)
@@ -1546,9 +1567,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep,
sldns_buffer_write_at(r_buffer, 0, &r->qid, sizeof(uint16_t));
sldns_buffer_write_at(r_buffer, 12, r->qname,
m->s.qinfo.qname_len);
m->reply_list = NULL;
comm_point_send_reply(&r->query_reply);
m->reply_list = rlist;
} else if(rcode) {
m->s.qinfo.qname = r->qname;
m->s.qinfo.local_alias = r->local_alias;
@@ -1570,9 +1589,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep,
}
error_encode(r_buffer, rcode, &m->s.qinfo, r->qid,
r->qflags, &r->edns);
m->reply_list = NULL;
comm_point_send_reply(&r->query_reply);
m->reply_list = rlist;
} else {
size_t udp_size = r->edns.udp_size;
r->edns.edns_version = EDNS_ADVERTISED_VERSION;
@@ -1608,9 +1625,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep,
error_encode(r_buffer, LDNS_RCODE_SERVFAIL,
&m->s.qinfo, r->qid, r->qflags, &r->edns);
}
m->reply_list = NULL;
comm_point_send_reply(&r->query_reply);
m->reply_list = rlist;
}
infra_wait_limit_dec(m->s.env->infra_cache, &r->query_reply,
m->s.env->cfg);
@@ -1762,6 +1777,7 @@ void mesh_query_done(struct mesh_state* mstate)
struct reply_info* rep = (mstate->s.return_msg?
mstate->s.return_msg->rep:NULL);
struct timeval tv = {0, 0};
struct mesh_area* mesh = mstate->s.env->mesh;
int i = 0;
/* No need for the serve expired timer anymore; we are going to reply. */
if(mstate->s.serve_expired_data) {
@@ -1786,7 +1802,18 @@ void mesh_query_done(struct mesh_state* mstate)
&& (!rep || rep->security != sec_status_secure))
dns_error_reporting(&mstate->s, rep);
for(r = mstate->reply_list; r; r = r->next) {
while((r = mesh_reply_list_pop_first(mstate)) != NULL) {
/* it was not detached (because it had a reply list), could be now */
if(!mstate->reply_list && !mstate->cb_list
&& mstate->super_set.count == 0) {
mesh->num_detached_states++;
}
/* if not replies any more in mstate, it is no longer a reply_state */
if(!mstate->reply_list && !mstate->cb_list) {
log_assert(mesh->num_reply_states > 0);
mesh->num_reply_states--;
}
if(mesh_is_udp(r)) {
/* For UDP queries, the old replies are discarded.
* This stops a large volume of old replies from
@@ -1801,22 +1828,18 @@ void mesh_query_done(struct mesh_state* mstate)
((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
mstate->s.env->cfg->discard_timeout) {
/* Drop the reply, it is too old */
/* briefly set the reply_list to NULL, so that the
* tcp req info cleanup routine that calls the mesh
* to deregister the meshstate for it is not done
* because the list is NULL and also accounting is not
* done there, but instead we do that here. */
struct mesh_reply* reply_list = mstate->reply_list;
verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
infra_wait_limit_dec(mstate->s.env->infra_cache,
&r->query_reply, mstate->s.env->cfg);
mstate->reply_list = NULL;
if(r->query_reply.c->use_h2)
if(r->query_reply.c->tcp_req_info)
tcp_req_info_remove_mesh_state(
r->query_reply.c->tcp_req_info,
mstate);
else if(r->query_reply.c->use_h2)
http2_stream_remove_mesh_state(r->h2_stream);
else if(r->query_reply.doq_stream)
doq_stream_remove_mesh_state(r->query_reply.doq_stream);
comm_point_drop_reply(&r->query_reply);
mstate->reply_list = reply_list;
log_assert(mstate->s.env->mesh->num_reply_addrs > 0);
mstate->s.env->mesh->num_reply_addrs--;
mstate->s.env->mesh->num_queries_discard_timeout++;
@@ -1841,22 +1864,17 @@ void mesh_query_done(struct mesh_state* mstate)
/* if this query is determined to be dropped during the
* mesh processing, this is the point to take that action. */
if(mstate->s.is_drop) {
/* briefly set the reply_list to NULL, so that the
* tcp req info cleanup routine that calls the mesh
* to deregister the meshstate for it is not done
* because the list is NULL and also accounting is not
* done there, but instead we do that here. */
struct mesh_reply* reply_list = mstate->reply_list;
infra_wait_limit_dec(mstate->s.env->infra_cache,
&r->query_reply, mstate->s.env->cfg);
mstate->reply_list = NULL;
if(r->query_reply.c->use_h2) {
if(r->query_reply.c->tcp_req_info) {
tcp_req_info_remove_mesh_state(
r->query_reply.c->tcp_req_info, mstate);
} else if(r->query_reply.c->use_h2) {
http2_stream_remove_mesh_state(r->h2_stream);
} else if(r->query_reply.doq_stream) {
doq_stream_remove_mesh_state(r->query_reply.doq_stream);
}
comm_point_drop_reply(&r->query_reply);
mstate->reply_list = reply_list;
log_assert(mstate->s.env->mesh->num_reply_addrs > 0);
mstate->s.env->mesh->num_reply_addrs--;
} else {
@@ -1897,18 +1915,6 @@ void mesh_query_done(struct mesh_state* mstate)
}
}
/* Mesh area accounting */
if(mstate->reply_list) {
mstate->reply_list = NULL;
if(!mstate->reply_list && !mstate->cb_list) {
/* was a reply state, not anymore */
log_assert(mstate->s.env->mesh->num_reply_states > 0);
mstate->s.env->mesh->num_reply_states--;
}
if(!mstate->reply_list && !mstate->cb_list &&
mstate->super_set.count == 0)
mstate->s.env->mesh->num_detached_states++;
}
mstate->replies_sent = 1;
while((c = mstate->cb_list) != NULL) {
@@ -2498,7 +2504,6 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m,
}
}
static int
apply_respip_action(struct module_qstate* qstate,
const struct query_info* qinfo, struct respip_client_info* cinfo,
@@ -2631,7 +2636,18 @@ mesh_serve_expired_callback(void* arg)
if(verbosity >= VERB_ALGO)
log_dns_msg("Serve expired lookup", &qstate->qinfo, msg->rep);
for(r = mstate->reply_list; r; r = r->next) {
while((r = mesh_reply_list_pop_first(mstate)) != NULL) {
/* it was not detached (because it had a reply list), could be now */
if(!mstate->reply_list && !mstate->cb_list
&& mstate->super_set.count == 0) {
mesh->num_detached_states++;
}
/* if not replies any more in mstate, it is no longer a reply_state */
if(!mstate->reply_list && !mstate->cb_list) {
log_assert(mesh->num_reply_states > 0);
mesh->num_reply_states--;
}
if(mesh_is_udp(r)) {
struct timeval old;
timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time);
@@ -2639,22 +2655,17 @@ mesh_serve_expired_callback(void* arg)
((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
mstate->s.env->cfg->discard_timeout) {
/* Drop the reply, it is too old */
/* briefly set the reply_list to NULL, so that the
* tcp req info cleanup routine that calls the mesh
* to deregister the meshstate for it is not done
* because the list is NULL and also accounting is not
* done there, but instead we do that here. */
struct mesh_reply* reply_list = mstate->reply_list;
verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
infra_wait_limit_dec(mstate->s.env->infra_cache,
&r->query_reply, mstate->s.env->cfg);
mstate->reply_list = NULL;
if(r->query_reply.c->use_h2)
if(r->query_reply.c->tcp_req_info)
tcp_req_info_remove_mesh_state(
r->query_reply.c->tcp_req_info, mstate);
else if(r->query_reply.c->use_h2)
http2_stream_remove_mesh_state(r->h2_stream);
else if(r->query_reply.doq_stream)
doq_stream_remove_mesh_state(r->query_reply.doq_stream);
comm_point_drop_reply(&r->query_reply);
mstate->reply_list = reply_list;
log_assert(mstate->s.env->mesh->num_reply_addrs > 0);
mstate->s.env->mesh->num_reply_addrs--;
mstate->s.env->mesh->num_queries_discard_timeout++;
@@ -2712,18 +2723,6 @@ mesh_serve_expired_callback(void* arg)
}
}
/* Mesh area accounting */
if(mstate->reply_list) {
mstate->reply_list = NULL;
if(!mstate->reply_list && !mstate->cb_list) {
log_assert(mesh->num_reply_states > 0);
mesh->num_reply_states--;
if(mstate->super_set.count == 0) {
mesh->num_detached_states++;
}
}
}
while((c = mstate->cb_list) != NULL) {
/* take this cb off the list; so that the list can be
* changed, eg. by adds from the callback routine */
+21
View File
@@ -3230,6 +3230,26 @@ static int http2_submit_settings(struct http2_session* h2_session)
}
#endif /* HAVE_NGHTTP2 */
/** Clear http2 stream mesh states */
static void http2_session_clear_meshstate(struct http2_session* h2_session)
{
#ifdef HAVE_NGHTTP2
/* Since the session gets closed, remove the mesh state references. */
struct http2_stream* h2_stream;
for(h2_stream = h2_session->first_stream; h2_stream;
h2_stream = h2_stream->next) {
if(h2_stream->mesh_state) {
mesh_state_remove_reply(h2_stream->mesh,
h2_stream->mesh_state, h2_session->c,
h2_stream, NULL);
h2_stream->mesh_state = NULL;
}
}
#else
(void)h2_session;
#endif /* HAVE_NGHTTP2 */
}
#ifdef HAVE_NGHTTP2
/** Delete http2 stream. After session delete or stream close callback */
static void http2_stream_delete(struct http2_session* h2_session,
@@ -6936,6 +6956,7 @@ comm_point_drop_reply(struct comm_reply* repinfo)
if(repinfo->c->type == comm_http) {
if(repinfo->c->h2_session) {
repinfo->c->h2_session->is_drop = 1;
http2_session_clear_meshstate(repinfo->c->h2_session);
if(!repinfo->c->h2_session->postpone_drop)
reclaim_http_handler(repinfo->c);
return;