@@ -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
23342483bool 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
1108611237finalize :
1108711238 if (rc != SQLITE_OK ) printf ("%s (%d)\n" , (db ) ? sqlite3_errmsg (db ) : "N/A" , rc );
0 commit comments