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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-03 02:09:53 +01:00
1 parent 6b97b569db
commit c263d9656d
3 files changed
+23 -25

No files matched your search

+3 -4
View File
@@ -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,
+11 -12
View File
@@ -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) {
+9 -9
View File
@@ -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);