@@ -1295,20 +1295,31 @@ void Spin(Gbs& Gb, LockableVector<HQUIC>& Connections, std::vector<HQUIC>* Liste
12951295 }
12961296 }
12971297 } else {
1298- // Pool creation failed -- tear down the pre-allocated
1299- // wrappers (their Connection is null, so dtor is a no-op
1300- // for ConnectionClose) and close any connections msquic
1301- // did manage to create if CLOSE_ON_FAILURE wasn't set.
1302- for (uint16_t i = 0 ; i < PoolSize; i++) {
1303- delete Wrappers[i];
1304- }
1298+ // Pool creation failed. Order matters here: any
1299+ // connections msquic did manage to create have their
1300+ // ClientContext pointing at our Wrappers (we passed those
1301+ // in via PoolConfig.Context). ConnectionClose is sync and
1302+ // fires SHUTDOWN_COMPLETE through our handler before
1303+ // returning, and that handler dereferences the wrapper
1304+ // (reads ctx->ThreadID). So we must close the connections
1305+ // FIRST while the wrappers are still alive, then delete
1306+ // the wrappers. Doing it in the opposite order is a
1307+ // heap-use-after-free -- ASan caught exactly that.
1308+ //
1309+ // If CLOSE_ON_FAILURE is set, msquic already closed the
1310+ // created connections during PoolCreate (and fired any
1311+ // events with the wrappers still alive at the time), so
1312+ // we skip the manual close, matching the original code.
13051313 if (!(PoolConfig.Flags & QUIC_CONNECTION_POOL_FLAG_CLOSE_ON_FAILURE )) {
13061314 for (uint16_t i = 0 ; i < PoolSize; i++) {
13071315 if (PoolConnections[i] != nullptr ) {
13081316 MsQuicTable.ConnectionClose (PoolConnections[i]);
13091317 }
13101318 }
13111319 }
1320+ for (uint16_t i = 0 ; i < PoolSize; i++) {
1321+ delete Wrappers[i];
1322+ }
13121323 }
13131324 }
13141325 break ;
0 commit comments