Skip to content

fix: stale cloudsync_table_settings crash on reopen#40

Merged
andinux merged 5 commits intomainfrom
fix/stale-table-settings-dropped-meta
Apr 15, 2026
Merged

fix: stale cloudsync_table_settings crash on reopen#40
andinux merged 5 commits intomainfrom
fix/stale-table-settings-dropped-meta

Conversation

@andinux
Copy link
Copy Markdown
Collaborator

@andinux andinux commented Apr 15, 2026

Summary

  • Fix a double-free crash when reopening a database whose base table and <table>_cloudsync meta-table were dropped without calling cloudsync_cleanup.
  • Distinguish real failures from empty results in cloudsync_dbversion_rebuild, so init errors are surfaced instead of silently leaving db_version_stmt unset.
  • Bump to 1.0.14, update changelog, add two unit tests.

Details

Two independent bugs were involved:

  1. src/cloudsync.ccloudsync_dbversion_rebuild returned DBRES_NOMEM when cloudsync_dbversion_build_query yielded a NULL SQL string. That NULL happens benignly when cloudsync_table_settings still has a row but sqlite_master no longer has any matching *_cloudsync table (e.g. after a manual DROP TABLE). The build helper now returns rc via an out-param so the caller can distinguish "no meta-tables" (rc == OK && sql == NULL, treated like count == 0) from a real database_select_text failure (propagated via cloudsync_set_dberror).

  2. src/sqlite/cloudsync_sqlite.c — on the init-failure branch, dbsync_register_functions manually called cloudsync_context_free(data), but the pointer is already owned by SQLite via the cloudsync_version function's destructor (registered earlier in the same function). sqlite3_close then freed it a second time → pointer being freed was not allocated SIGABRT. Removed the manual free; SQLite cleans up on close.

Test plan

  • do_test_stale_table_settings_dropped_meta — reproduces the original crash (drop base table + meta-table, reopen, load extension, create new tracked table).
  • do_test_dbversion_rebuild_error — installs an sqlite3_set_authorizer denying reads of sqlite_master so SQL_DBVERSION_BUILD_QUERY prepare fails with SQLITE_AUTH; asserts cloudsync_dbversion_rebuild returns non-OK and records an error on the context.
  • Full make unittest suite passes, including the memory-leak check.

andinux and others added 5 commits April 15, 2026 10:30
Dropping a table and its <table>_cloudsync meta-table without calling
cloudsync_cleanup left stale rows in cloudsync_table_settings. On the
next connection, loading the extension crashed with a double-free.

Two bugs:
- cloudsync_dbversion_rebuild returned DBRES_NOMEM when the build query
  yielded NULL (no *_cloudsync tables in sqlite_master), failing init.
  Treat NULL the same as count == 0: no prepared stmt, rebuilt later.
- On init failure, dbsync_register_functions manually freed the context
  that SQLite already owns via the cloudsync_version destructor, causing
  a double-free on sqlite3_close. Let SQLite release it instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rsion_rebuild

Previously cloudsync_dbversion_build_query collapsed all failures into a
NULL return, and the stale-settings fix then treated every NULL as
"no meta-tables" and returned DBRES_OK. A genuine failure from
database_select_text (SQLITE_NOMEM materializing the generated SQL, a
prepare/step error, etc.) would leave data->db_version_stmt unset and
silently succeed. Subsequent writes would fall back to
CLOUDSYNC_MIN_DB_VERSION via cloudsync_dbversion_next, potentially
emitting duplicate or rewound db_version values on a database that
still has synced tables.

Change cloudsync_dbversion_build_query to return rc with the SQL via an
out-param. cloudsync_dbversion_rebuild now surfaces non-OK results
through cloudsync_set_dberror, and only the rc == OK && sql == NULL
case (generator produced a NULL row because sqlite_master has no
*_cloudsync tables) is treated like count == 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install an sqlite3 authorizer that denies reads of sqlite_master, so
sqlite3_prepare_v2 of SQL_DBVERSION_BUILD_QUERY fails with SQLITE_AUTH.
On a context with a non-empty cloudsync_table_settings, this must now
return a non-OK rc and set cloudsync_errcode/errmsg — before the review
fix it was silently collapsed into DBRES_OK, leaving db_version_stmt
unset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eopen

Mirrors the SQLite unit test do_test_stale_table_settings_dropped_meta.
Drops the base table and its <table>_cloudsync meta-table without
calling cloudsync_cleanup, reconnects to force a fresh backend, and
verifies that cloudsync_init on a new table still succeeds. Before the
shared fix in cloudsync_dbversion_rebuild this aborted with
"An error occurred while trying to initialize context" because
SQL_DBVERSION_BUILD_QUERY's string_agg over zero rows produced a NULL
SQL string that was misreported as DBRES_NOMEM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andinux andinux merged commit 60d5893 into main Apr 15, 2026
53 of 54 checks passed
@andinux andinux deleted the fix/stale-table-settings-dropped-meta branch April 15, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant