From 82d5c841e68b53ad06fe331d77df099b7b7f2993 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 29 May 2026 23:11:52 +0200 Subject: [PATCH] fix(parallel): destroy thread parser before slab reclaim in LSP-cross loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LSP-cross loop in parallel_resolve called cbm_slab_reclaim() without first destroying the thread-local tree-sitter parser, violating the documented slab contract. The parser's lexer holds slab-allocated state (notably lexer.included_ranges, a 24-byte allocation that lands in the ≤64B slab bucket); reclaiming the slab leaves that pointer dangling. Workers don't trip on it because they don't reparse afterward, but the main thread participates in cbm_parallel_for and any later cbm_extract_file on the main thread — including the sequential pass_definitions in the incremental pipeline — hits a heap-use-after-free in ts_lexer_goto on macOS-ASan. Two-part fix: - pass_parallel.c: destroy the thread parser before cbm_slab_reclaim() in the per-file LSP-cross loop, matching the worker extract loop. - pass_definitions.c: defensively drop the thread parser at pass entry so the incremental sequential path cannot inherit stale state from any prior run. --- src/pipeline/pass_definitions.c | 5 +++++ src/pipeline/pass_parallel.c | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 779758c84..246be5072 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -333,6 +333,11 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t /* Ensure extraction library is initialized */ cbm_init(); + /* Defensive: a prior pipeline run may have left a thread-local parser whose + * lexer holds pointers into a slab that has since been reclaimed. Drop it + * here so the first cbm_extract_file below recreates a fresh parser. */ + cbm_destroy_thread_parser(); + int total_defs = 0; int total_calls = 0; int total_imports = 0; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 52075d31e..3ce678931 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2128,6 +2128,12 @@ static void resolve_worker(int worker_id, void *ctx_ptr) { } } free(filtered); + /* Contract: cbm_slab_reclaim() requires the thread parser to be + * destroyed first; otherwise its lexer holds slab pointers + * (lexer.included_ranges) that get freed underneath it, causing + * a heap-use-after-free on the next ts_lexer_goto. The next + * cbm_extract_file on this thread will recreate the parser. */ + cbm_destroy_thread_parser(); cbm_slab_reclaim(); uint64_t lsp_elapsed_ns = extract_now_ns() - lsp_t0; atomic_fetch_add_explicit(&rc->time_ns_cross_lsp, lsp_elapsed_ns,