Both sanitizer lanes are red on main, not on any one PR
Reproduced on pristine origin/main d06a3792 with no other commits applied:
cmake -S . -B build-sanitize -G Ninja -DVLLM_CPP_BUILD_TESTS=ON \
-DVLLM_CPP_CUDA=OFF -DVLLM_CPP_SANITIZE=thread
cmake --build build-sanitize --target test_llm_engine
setarch $(uname -m) -R ./build-sanitize/tests/test_llm_engine # exit 66
(setarch -R is needed locally: TSan otherwise aborts with unexpected memory mapping before running a single test.)
WARNING: ThreadSanitizer: data race
#8 PromRegistry::SeriesFor(...) src/vllm/v1/metrics/prometheus.cpp:134
#9 PromRegistry::SetGauge(...) src/vllm/v1/metrics/prometheus.cpp:165
SUMMARY: ThreadSanitizer: data race in operator delete(void*, unsigned long)
CI agrees: on PR #307 the ASan lane fails six tests — test_load_direct_upload, test_llama_embedding_fold, test_laguna_nvfp4_loader, test_llm_engine, test_openai_api_server, test_capi — with heap-use-after-free.
Root cause
SeriesFor hands out a reference into a std::vector<Series> and then grows that same vector:
PromRegistry::Series& PromRegistry::SeriesFor(Family& fam, const std::vector<std::string>& labelvalues) {
for (auto& s : fam.series) {
if (s.labelvalues == labelvalues) return s;
}
...
fam.series.push_back(std::move(s)); // reallocates: every previously returned Series& dangles
return fam.series.back();
}
std::vector reallocation invalidates all references to its elements, so any Series& obtained before a later SeriesFor call on the same family is dangling — that is the heap-use-after-free. Callers (IncCounter, SetGauge, Observe, Prime) all go through it.
Separately, PromRegistry contains no mutex or atomic at all (grep -c 'mutex|lock_guard|atomic' = 0 in both the header and the .cpp), while /metrics is now served concurrently with the engine's own recording. A scrape iterating fam.series while a writer push_backs is a data race on the container itself, independent of the dangling-reference bug.
Suggested direction
Stability of references is the actual requirement, so a container that provides it (std::deque, or std::vector<std::unique_ptr<Series>>, or returning an index rather than a reference), plus a lock or per-series atomics around the concurrent recording path. Not attempted here — this belongs to the metrics row, not the one that found it.
Provenance
Bisected by elimination, not assumed: PR #307's spec-only commit c3db7804 passed both sanitizer lanes; the failures appear only after merges that pulled in 9c55e3df ("wire /metrics to the engine the server actually serves from", #277), and then reproduce on pristine main with none of #307 applied. Found while gating container images (#170).
Both sanitizer lanes are red on
main, not on any one PRReproduced on pristine
origin/maind06a3792with no other commits applied:(
setarch -Ris needed locally: TSan otherwise aborts withunexpected memory mappingbefore running a single test.)CI agrees: on PR #307 the ASan lane fails six tests —
test_load_direct_upload,test_llama_embedding_fold,test_laguna_nvfp4_loader,test_llm_engine,test_openai_api_server,test_capi— withheap-use-after-free.Root cause
SeriesForhands out a reference into astd::vector<Series>and then grows that same vector:std::vectorreallocation invalidates all references to its elements, so anySeries&obtained before a laterSeriesForcall on the same family is dangling — that is theheap-use-after-free. Callers (IncCounter,SetGauge,Observe,Prime) all go through it.Separately,
PromRegistrycontains no mutex or atomic at all (grep -c 'mutex|lock_guard|atomic'= 0 in both the header and the .cpp), while/metricsis now served concurrently with the engine's own recording. A scrape iteratingfam.serieswhile a writerpush_backs is a data race on the container itself, independent of the dangling-reference bug.Suggested direction
Stability of references is the actual requirement, so a container that provides it (
std::deque, orstd::vector<std::unique_ptr<Series>>, or returning an index rather than a reference), plus a lock or per-series atomics around the concurrent recording path. Not attempted here — this belongs to the metrics row, not the one that found it.Provenance
Bisected by elimination, not assumed: PR #307's spec-only commit
c3db7804passed both sanitizer lanes; the failures appear only after merges that pulled in9c55e3df("wire /metrics to the engine the server actually serves from", #277), and then reproduce on pristinemainwith none of #307 applied. Found while gating container images (#170).