diff --git a/core/src/task/download_task.cpp b/core/src/task/download_task.cpp index a613332..e471943 100644 --- a/core/src/task/download_task.cpp +++ b/core/src/task/download_task.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,9 @@ struct DownloadTaskState : std::enable_shared_from_this { std::mutex mu; std::shared_mutex workers_mu; std::mutex deferred_mu; + // Notified (holding mu) whenever seg_finished removes an entry from `workers`; quiesce() + // waits on it instead of clearing the map itself — see quiesce()'s comment. + std::condition_variable workers_drained_cv; EngineState state = EngineState::probing; std::optional last_error; @@ -574,6 +578,7 @@ void DownloadTaskState::seg_finished(std::uint32_t seg_idx, Resultsecond); workers.erase(it); } + workers_drained_cv.notify_all(); // quiesce() may be waiting for `workers` to empty out if (retired.load()) { // engine shutting down / already terminal — no more callbacks if (w->buf) (void)w->buf->flush(); @@ -1184,12 +1189,24 @@ void DownloadTaskState::do_refresh_url(std::string url, std::vectortransfer.cancel(); - workers.clear(); + { + std::shared_lock wl(workers_mu); + for (auto &[idx, w] : workers) + w->transfer.cancel(); + } + // Do not clear `workers` here: transfer.cancel() only requests the HttpClient worker + // thread stop the transfer, asynchronously -- it does not wait for that to happen. A + // worker's SegWorker (and its WriteBuffer) may still be in active use by a curl write + // callback running on that other thread right now. Clearing the map out from under it + // was a real, ASan-caught heap-use-after-free (ring buffer freed here while + // SparseFile::write_at() on the HttpClient worker thread was still writing through it). + // Every worker removes and flushes itself, safely, via seg_finished once HttpClient + // actually confirms the transfer has stopped (same path every other exit uses; see the + // `retired` branch there) -- just wait for that to happen for all of them. Bounded by + // however long a cancelled curl transfer takes to unwind, not user-controllable. + workers_drained_cv.wait(lk, [this] { return workers.empty(); }); } EngineState DownloadTaskState::snapshot_state() {