Skip to content

Commit 974c6cb

Browse files
authored
fix: harden table initialization against stale config and error cleanup
- Skip stale table settings for dropped tables during init by checking database_table_exists() before creating triggers - Add col_name to cloudsync_table_settings PRIMARY KEY in SQLite to match PostgreSQL and prevent REPLACE INTO from silently overwriting column-level settings - Zero-initialize column arrays (col_name, col_id, col_merge_stmt, col_value_stmt) to prevent SIGSEGV when error cleanup calls databasevm_finalize on uninitialized pointers - Free column arrays in table_free unconditionally so they are not leaked when table_add_to_context_cb fails before incrementing ncols
1 parent 1dbfb60 commit 974c6cb

File tree

5 files changed

+190
-36
lines changed

5 files changed

+190
-36
lines changed

src/cloudsync.c

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -720,37 +720,35 @@ void table_free (cloudsync_table_context *table) {
720720
DEBUG_DBFUNCTION("table_free %s", (table) ? (table->name) : "NULL");
721721
if (!table) return;
722722

723-
if (table->ncols > 0) {
724-
if (table->col_name) {
725-
for (int i=0; i<table->ncols; ++i) {
726-
cloudsync_memory_free(table->col_name[i]);
727-
}
728-
cloudsync_memory_free(table->col_name);
729-
}
730-
if (table->col_merge_stmt) {
731-
for (int i=0; i<table->ncols; ++i) {
732-
databasevm_finalize(table->col_merge_stmt[i]);
733-
}
734-
cloudsync_memory_free(table->col_merge_stmt);
735-
}
736-
if (table->col_value_stmt) {
737-
for (int i=0; i<table->ncols; ++i) {
738-
databasevm_finalize(table->col_value_stmt[i]);
739-
}
740-
cloudsync_memory_free(table->col_value_stmt);
723+
if (table->col_name) {
724+
for (int i=0; i<table->ncols; ++i) {
725+
cloudsync_memory_free(table->col_name[i]);
741726
}
742-
if (table->col_id) {
743-
cloudsync_memory_free(table->col_id);
727+
cloudsync_memory_free(table->col_name);
728+
}
729+
if (table->col_merge_stmt) {
730+
for (int i=0; i<table->ncols; ++i) {
731+
databasevm_finalize(table->col_merge_stmt[i]);
744732
}
745-
if (table->col_algo) {
746-
cloudsync_memory_free(table->col_algo);
733+
cloudsync_memory_free(table->col_merge_stmt);
734+
}
735+
if (table->col_value_stmt) {
736+
for (int i=0; i<table->ncols; ++i) {
737+
databasevm_finalize(table->col_value_stmt[i]);
747738
}
748-
if (table->col_delimiter) {
749-
for (int i=0; i<table->ncols; ++i) {
750-
if (table->col_delimiter[i]) cloudsync_memory_free(table->col_delimiter[i]);
751-
}
752-
cloudsync_memory_free(table->col_delimiter);
739+
cloudsync_memory_free(table->col_value_stmt);
740+
}
741+
if (table->col_id) {
742+
cloudsync_memory_free(table->col_id);
743+
}
744+
if (table->col_algo) {
745+
cloudsync_memory_free(table->col_algo);
746+
}
747+
if (table->col_delimiter) {
748+
for (int i=0; i<table->ncols; ++i) {
749+
if (table->col_delimiter[i]) cloudsync_memory_free(table->col_delimiter[i]);
753750
}
751+
cloudsync_memory_free(table->col_delimiter);
754752
}
755753

756754
if (table->block_value_read_stmt) databasevm_finalize(table->block_value_read_stmt);
@@ -1079,16 +1077,16 @@ bool table_add_to_context (cloudsync_context *data, table_algo algo, const char
10791077

10801078
// a table with only pk(s) is totally legal
10811079
if (ncols > 0) {
1082-
table->col_name = (char **)cloudsync_memory_alloc((uint64_t)(sizeof(char *) * ncols));
1080+
table->col_name = (char **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(char *) * ncols));
10831081
if (!table->col_name) goto abort_add_table;
1084-
1085-
table->col_id = (int *)cloudsync_memory_alloc((uint64_t)(sizeof(int) * ncols));
1082+
1083+
table->col_id = (int *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(int) * ncols));
10861084
if (!table->col_id) goto abort_add_table;
1087-
1088-
table->col_merge_stmt = (dbvm_t **)cloudsync_memory_alloc((uint64_t)(sizeof(void *) * ncols));
1085+
1086+
table->col_merge_stmt = (dbvm_t **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(void *) * ncols));
10891087
if (!table->col_merge_stmt) goto abort_add_table;
1090-
1091-
table->col_value_stmt = (dbvm_t **)cloudsync_memory_alloc((uint64_t)(sizeof(void *) * ncols));
1088+
1089+
table->col_value_stmt = (dbvm_t **)cloudsync_memory_zeroalloc((uint64_t)(sizeof(void *) * ncols));
10921090
if (!table->col_value_stmt) goto abort_add_table;
10931091

10941092
table->col_algo = (col_algo_t *)cloudsync_memory_zeroalloc((uint64_t)(sizeof(col_algo_t) * ncols));

src/cloudsync.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
extern "C" {
1919
#endif
2020

21-
#define CLOUDSYNC_VERSION "1.0.6"
21+
#define CLOUDSYNC_VERSION "1.0.7"
2222
#define CLOUDSYNC_MAX_TABLENAME_LEN 512
2323

2424
#define CLOUDSYNC_VALUE_NOTSET -1

src/dbutils.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ int dbutils_settings_table_load_callback (void *xdata, int ncols, char **values,
363363

364364
// Table-level algo setting (col_name == "*")
365365
if (strcmp(key, "algo") == 0 && col_name && strcmp(col_name, "*") == 0) {
366+
// Skip stale settings for tables that no longer exist
367+
if (!database_table_exists(data, table_name, cloudsync_schema(data))) {
368+
DEBUG_SETTINGS("skipping stale settings for dropped table: %s", table_name);
369+
continue;
370+
}
366371
table_algo algo = cloudsync_algo_from_name(value);
367372
char fbuf[2048];
368373
int frc = dbutils_table_settings_get_value(data, table_name, "*", "filter", fbuf, sizeof(fbuf));

src/sqlite/sql_sqlite.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const char * const SQL_INSERT_SITE_ID_ROWID =
5656
"INSERT INTO cloudsync_site_id (rowid, site_id) VALUES (?, ?);";
5757

5858
const char * const SQL_CREATE_TABLE_SETTINGS_TABLE =
59-
"CREATE TABLE IF NOT EXISTS cloudsync_table_settings (tbl_name TEXT NOT NULL COLLATE NOCASE, col_name TEXT NOT NULL COLLATE NOCASE, key TEXT, value TEXT, PRIMARY KEY(tbl_name,key));";
59+
"CREATE TABLE IF NOT EXISTS cloudsync_table_settings (tbl_name TEXT NOT NULL COLLATE NOCASE, col_name TEXT NOT NULL COLLATE NOCASE, key TEXT, value TEXT, PRIMARY KEY(tbl_name,col_name,key));";
6060

6161
const char * const SQL_CREATE_SCHEMA_VERSIONS_TABLE =
6262
"CREATE TABLE IF NOT EXISTS cloudsync_schema_versions (hash INTEGER PRIMARY KEY, seq INTEGER NOT NULL)";

test/unit.c

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2330,6 +2330,155 @@ bool do_test_error_handling(void) {
23302330
return result;
23312331
}
23322332

2333+
// Test that stale table settings don't crash on reload.
2334+
// Exercises the error cleanup path in table_add_to_context_cb:
2335+
// if a table is dropped without cloudsync_cleanup(), its settings
2336+
// persist as orphans. On re-init the zero-initialized column arrays
2337+
// must prevent databasevm_finalize from being called on garbage pointers.
2338+
bool do_test_stale_table_settings(bool cleanup_databases) {
2339+
bool result = false;
2340+
char dbpath[256];
2341+
time_t timestamp = time(NULL);
2342+
2343+
#ifdef __ANDROID__
2344+
snprintf(dbpath, sizeof(dbpath), "%s/cloudsync-test-stale-%ld.sqlite", ".", timestamp);
2345+
#else
2346+
snprintf(dbpath, sizeof(dbpath), "%s/cloudsync-test-stale-%ld.sqlite", getenv("HOME"), timestamp);
2347+
#endif
2348+
2349+
// Phase 1: create database, table, and init cloudsync
2350+
sqlite3 *db = NULL;
2351+
int rc = sqlite3_open(dbpath, &db);
2352+
if (rc != SQLITE_OK) return false;
2353+
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
2354+
sqlite3_cloudsync_init(db, NULL, NULL);
2355+
2356+
rc = sqlite3_exec(db, "CREATE TABLE cloud (id TEXT PRIMARY KEY NOT NULL, value TEXT, extra INTEGER);", NULL, NULL, NULL);
2357+
if (rc != SQLITE_OK) goto cleanup;
2358+
2359+
rc = sqlite3_exec(db, "SELECT cloudsync_init('cloud');", NULL, NULL, NULL);
2360+
if (rc != SQLITE_OK) goto cleanup;
2361+
2362+
// Phase 2: drop the table WITHOUT calling cloudsync_cleanup
2363+
// This leaves stale entries in cloudsync_table_settings
2364+
sqlite3_exec(db, "SELECT cloudsync_terminate();", NULL, NULL, NULL);
2365+
sqlite3_close(db);
2366+
db = NULL;
2367+
2368+
// Reopen and drop the table directly (bypassing cloudsync)
2369+
rc = sqlite3_open(dbpath, &db);
2370+
if (rc != SQLITE_OK) goto cleanup;
2371+
rc = sqlite3_exec(db, "DROP TABLE IF EXISTS cloud;", NULL, NULL, NULL);
2372+
if (rc != SQLITE_OK) goto cleanup;
2373+
sqlite3_close(db);
2374+
db = NULL;
2375+
2376+
// Phase 3: reopen the database — sqlite3_cloudsync_init will load
2377+
// stale settings and try to re-create triggers for the dropped table.
2378+
// Without the fix this would crash (SIGSEGV) or return an error.
2379+
rc = sqlite3_open(dbpath, &db);
2380+
if (rc != SQLITE_OK) goto cleanup;
2381+
rc = sqlite3_cloudsync_init(db, NULL, NULL);
2382+
if (rc != SQLITE_OK) goto cleanup;
2383+
2384+
// If we reach here without crashing, the fix works
2385+
result = true;
2386+
2387+
cleanup:
2388+
if (db) {
2389+
sqlite3_exec(db, "SELECT cloudsync_terminate();", NULL, NULL, NULL);
2390+
sqlite3_close(db);
2391+
}
2392+
if (cleanup_databases) {
2393+
file_delete_internal(dbpath);
2394+
// also clean up WAL and SHM files
2395+
char walpath[280];
2396+
snprintf(walpath, sizeof(walpath), "%s-wal", dbpath);
2397+
file_delete_internal(walpath);
2398+
snprintf(walpath, sizeof(walpath), "%s-shm", dbpath);
2399+
file_delete_internal(walpath);
2400+
}
2401+
return result;
2402+
}
2403+
2404+
// Authorizer state for do_test_context_cb_error_cleanup.
2405+
// Denies INSERT on a specific table after allowing a set number of INSERTs.
2406+
static const char *g_deny_insert_table = NULL;
2407+
static int g_deny_insert_remaining = 0;
2408+
2409+
static int deny_insert_authorizer(void *pUserData, int action, const char *zArg1,
2410+
const char *zArg2, const char *zDbName, const char *zTrigger) {
2411+
(void)pUserData; (void)zArg2; (void)zDbName; (void)zTrigger;
2412+
if (action == SQLITE_INSERT && g_deny_insert_table &&
2413+
zArg1 && strcmp(zArg1, g_deny_insert_table) == 0) {
2414+
if (g_deny_insert_remaining > 0) {
2415+
g_deny_insert_remaining--;
2416+
return SQLITE_OK;
2417+
}
2418+
return SQLITE_DENY;
2419+
}
2420+
return SQLITE_OK;
2421+
}
2422+
2423+
// Test that the error cleanup path in table_add_to_context_cb doesn't crash
2424+
// when databasevm_prepare fails for the merge statement.
2425+
// Before the zeroalloc fix, col_value_stmt[index] contained uninitialized
2426+
// garbage and databasevm_finalize was called on it → SIGSEGV.
2427+
bool do_test_context_cb_error_cleanup(void) {
2428+
sqlite3 *db = NULL;
2429+
cloudsync_context *ctx = NULL;
2430+
bool result = false;
2431+
2432+
int rc = sqlite3_open(":memory:", &db);
2433+
if (rc != SQLITE_OK) return false;
2434+
sqlite3_cloudsync_init(db, NULL, NULL);
2435+
2436+
// Create table with PK and non-PK columns
2437+
rc = sqlite3_exec(db,
2438+
"CREATE TABLE ctx_err (id TEXT PRIMARY KEY NOT NULL, val TEXT, num INTEGER);",
2439+
NULL, NULL, NULL);
2440+
if (rc != SQLITE_OK) goto cleanup;
2441+
2442+
// Init cloudsync on the table (creates meta table, triggers, settings)
2443+
rc = sqlite3_exec(db, "SELECT cloudsync_init('ctx_err');", NULL, NULL, NULL);
2444+
if (rc != SQLITE_OK) goto cleanup;
2445+
2446+
// Create a fresh context — ctx_err is not in its table list,
2447+
// so table_add_to_context won't short-circuit at table_lookup.
2448+
ctx = cloudsync_context_create(db);
2449+
if (!ctx) goto cleanup;
2450+
2451+
// Install authorizer that denies INSERT on the base table.
2452+
// Allow 1 INSERT (the sentinel prepared in table_add_stmts),
2453+
// then deny the next (the merge UPSERT prepared in the callback).
2454+
// This causes databasevm_prepare to fail inside table_add_to_context_cb,
2455+
// triggering the error cleanup path.
2456+
g_deny_insert_table = "ctx_err";
2457+
g_deny_insert_remaining = 1;
2458+
sqlite3_set_authorizer(db, deny_insert_authorizer, NULL);
2459+
2460+
// Must return false (callback error) without crashing
2461+
bool added = table_add_to_context(ctx, table_algo_crdt_cls, "ctx_err");
2462+
2463+
// Remove authorizer before any further operations
2464+
sqlite3_set_authorizer(db, NULL, NULL);
2465+
g_deny_insert_table = NULL;
2466+
2467+
if (added) goto cleanup; // should have failed
2468+
2469+
result = true;
2470+
2471+
cleanup:
2472+
sqlite3_set_authorizer(db, NULL, NULL);
2473+
g_deny_insert_table = NULL;
2474+
if (ctx) cloudsync_context_free(ctx);
2475+
if (db) {
2476+
sqlite3_exec(db, "SELECT cloudsync_terminate();", NULL, NULL, NULL);
2477+
sqlite3_close(db);
2478+
}
2479+
return result;
2480+
}
2481+
23332482
// Test cloudsync_terminate function
23342483
bool do_test_terminate(void) {
23352484
sqlite3 *db = NULL;
@@ -11082,6 +11231,8 @@ int main (int argc, const char * argv[]) {
1108211231
result += test_report("Delete/Resurrect Order:", do_test_delete_resurrect_ordering(3, print_result, cleanup_databases));
1108311232
result += test_report("Large Composite PK Test:", do_test_large_composite_pk(2, print_result, cleanup_databases));
1108411233
result += test_report("Schema Hash Mismatch:", do_test_schema_hash_mismatch(2, print_result, cleanup_databases));
11234+
result += test_report("Stale Table Settings:", do_test_stale_table_settings(cleanup_databases));
11235+
result += test_report("CB Error Cleanup:", do_test_context_cb_error_cleanup());
1108511236

1108611237
finalize:
1108711238
if (rc != SQLITE_OK) printf("%s (%d)\n", (db) ? sqlite3_errmsg(db) : "N/A", rc);

0 commit comments

Comments
 (0)