From c263d9656df52f1fd9dad27dfc49dd890fd2aa5d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 3 Nov 2026 02:09:53 +0100 Subject: [PATCH] fix(store): a failed resolver stage keeps its SQL until the writer finalizes The direct writer's tables point at CREATE text the stage owns, and finalize runs whether the stage worked or not. On failure the stage was freed first, so finalize built sqlite_master from freed memory: heap-use-after-free in importance and pipeline_semantic_manifest_repro, whose rejected publish reaches it. The caller already frees the stage on every path, so it now gets the stage back either way. Signed-off-by: Martin Vogel --- src/pipeline/pipeline.c | 7 +++---- src/store/store_resolver_evidence.c | 23 +++++++++++------------ src/store/store_resolver_evidence.h | 18 +++++++++--------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index fc2664224..998251829 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -4176,10 +4176,9 @@ static bool resolver_stage_tables(cbm_db_writer_t *writer, void *context) { const cbm_resolve_file_t *files = cbm_gbuf_resolver_files(generation->gbuf, &file_count); const cbm_resolve_summary_t *summaries = cbm_gbuf_resolver_summaries(generation->gbuf, &summary_count); - write->stage = - cbm_store_resolver_stage_write(writer, generation->project, rows, count, files, file_count, - summaries, summary_count, generation->workers); - return write->stage != NULL; + return cbm_store_resolver_stage_write(writer, generation->project, rows, count, files, + file_count, summaries, summary_count, generation->workers, + &write->stage); } static int publish_generation_impl(const cbm_pipeline_generation_t *generation, diff --git a/src/store/store_resolver_evidence.c b/src/store/store_resolver_evidence.c index 2ad5672bc..c90391d24 100644 --- a/src/store/store_resolver_evidence.c +++ b/src/store/store_resolver_evidence.c @@ -1674,7 +1674,9 @@ static bool evidence_run(sqlite3 *db, cbm_db_writer_t *direct, const char *proje cbm_free(CBM_MEM_CLASS_STORE, cache); cbm_arena_destroy(&w->owned); cbm_free(CBM_MEM_CLASS_STORE, writer); - if (ok && out) + /* The writer's tables keep pointing at the stage's SQL until finalize, which runs whether + * this worked or not, so a caller who asked for the stage frees it either way. */ + if (out) *out = stage; else cbm_store_resolver_stage_free(stage); @@ -1687,17 +1689,14 @@ bool cbm_store_resolver_evidence(sqlite3 *db, const char *project, const cbm_res return db && evidence_run(db, NULL, project, rows, count, files, file_count, summaries, summary_count, commit_batches, workers, NULL); } -cbm_resolver_stage_t *cbm_store_resolver_stage_write(cbm_db_writer_t *writer, const char *project, - const cbm_resolve_record_t *rows, size_t count, - const cbm_resolve_file_t *files, - size_t file_count, - const cbm_resolve_summary_t *summaries, - size_t summary_count, int workers) { - cbm_resolver_stage_t *stage = NULL; - return writer && evidence_run(NULL, writer, project, rows, count, files, file_count, summaries, - summary_count, false, workers, &stage) - ? stage - : NULL; +bool cbm_store_resolver_stage_write(cbm_db_writer_t *writer, const char *project, + const cbm_resolve_record_t *rows, size_t count, + const cbm_resolve_file_t *files, size_t file_count, + const cbm_resolve_summary_t *summaries, size_t summary_count, + int workers, cbm_resolver_stage_t **stage) { + return writer && stage && + evidence_run(NULL, writer, project, rows, count, files, file_count, summaries, + summary_count, false, workers, stage); } bool cbm_store_resolver_stage_finish(sqlite3 *db, const char *project, const cbm_resolver_stage_t *stage) { diff --git a/src/store/store_resolver_evidence.h b/src/store/store_resolver_evidence.h index 5e317a0c3..d630519b1 100644 --- a/src/store/store_resolver_evidence.h +++ b/src/store/store_resolver_evidence.h @@ -15,16 +15,16 @@ bool cbm_store_resolver_evidence(sqlite3 *db, const char *project, const cbm_res /* The same write when the store is a new file still being built: facts, supports, * occurrences and their text go straight into writer's pages, tens of millions of rows * that SQL would take one at a time. The tables are ordinary and the views over them - * read back the same values. NULL on failure, which also fails the file. What is - * returned holds no fact and no digest, only what the store then inserts, and it - * borrows the same rows as this call until freed. */ + * read back the same values. False on failure, which also fails the file. *stage holds + * no fact and no digest, only what the store then inserts, and it borrows the same rows + * as this call until freed. Free it after the writer's finalize, whatever this returned: + * the writer's tables keep pointing at SQL it owns. */ typedef struct cbm_resolver_stage cbm_resolver_stage_t; -cbm_resolver_stage_t *cbm_store_resolver_stage_write(cbm_db_writer_t *writer, const char *project, - const cbm_resolve_record_t *rows, size_t count, - const cbm_resolve_file_t *files, - size_t file_count, - const cbm_resolve_summary_t *summaries, - size_t summary_count, int workers); +bool cbm_store_resolver_stage_write(cbm_db_writer_t *writer, const char *project, + const cbm_resolve_record_t *rows, size_t count, + const cbm_resolve_file_t *files, size_t file_count, + const cbm_resolve_summary_t *summaries, size_t summary_count, + int workers, cbm_resolver_stage_t **stage); /* The rest of the write, on the finished file. */ bool cbm_store_resolver_stage_finish(sqlite3 *db, const char *project, const cbm_resolver_stage_t *stage);