Skip to content

Commit 308d646

Browse files
authored
Fix NULL-deref, use-after-free, and lock-misuse bugs in sqlite3_nif.c (#342)
Several functions in sqlite3_nif.c read conn->db or statement->statement outside of the connection mutex, or after it could have been freed by a concurrent close(). This leads to segfaults in production when close() races with any of the other NIFs. exqlite_interrupt had a use-after-free window: close() could call sqlite3_close_v2() and null conn->db between the NULL check and the sqlite3_interrupt() call. Fixed by adding an interrupt_mutex to connection_t so close() holds that lock while nulling conn->db, and interrupt() holds it while calling sqlite3_interrupt(). exqlite_close, exqlite_changes, exqlite_transaction_status, exqlite_serialize, exqlite_deserialize, exqlite_set_update_hook, exqlite_enable_load_extension, exqlite_last_insert_rowid, and exqlite_execute all had their conn->db NULL checks outside the lock, making them vulnerable to a concurrent close(). Moved the checks inside the lock. exqlite_deserialize had a lock leak on malloc failure and a buffer leak if sqlite3_deserialize() failed. exqlite_enable_load_extension was missing the lock entirely and had a missing unlock on one error path. All twelve statement NIFs (reset, bind_parameter_count, bind_parameter_index, bind_text, bind_blob, bind_integer, bind_float, bind_null, step, columns, multi_step, and the statement branch of errmsg) dereferenced statement->statement without checking for NULL, which crashes after Sqlite3.release/2. They now return {:error, :invalid_statement} instead. Each fix is preceded by a regression test commit that demonstrates the crash or documents the unsafe pattern.
1 parent 66185e1 commit 308d646

3 files changed

Lines changed: 387 additions & 40 deletions

File tree

c_src/sqlite3_nif.c

Lines changed: 138 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ typedef struct connection
5252
{
5353
sqlite3* db;
5454
ErlNifMutex* mutex;
55+
ErlNifMutex* interrupt_mutex;
5556
ErlNifPid update_hook_pid;
5657
} connection_t;
5758

@@ -336,8 +337,14 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
336337
enif_mutex_destroy(mutex);
337338
return make_error_tuple(env, am_out_of_memory);
338339
}
339-
conn->db = db;
340-
conn->mutex = mutex;
340+
conn->db = db;
341+
conn->mutex = mutex;
342+
conn->interrupt_mutex = enif_mutex_create("exqlite:interrupt");
343+
if (conn->interrupt_mutex == NULL) {
344+
// conn->db and conn->mutex are set; the destructor will clean them up.
345+
enif_release_resource(conn);
346+
return make_error_tuple(env, am_failed_to_create_mutex);
347+
}
341348

342349
result = enif_make_resource(env, conn);
343350
enif_release_resource(conn);
@@ -364,16 +371,17 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
364371
return make_error_tuple(env, am_invalid_connection);
365372
}
366373

367-
// DB is already closed, nothing to do here
368-
if (conn->db == NULL) {
369-
return am_ok;
370-
}
371-
372374
// close connection in critical section to avoid race-condition
373375
// cases. Cases such as query timeout and connection pooling
374376
// attempting to close the connection
375377
connection_acquire_lock(conn);
376378

379+
// DB is already closed, nothing to do here.
380+
if (conn->db == NULL) {
381+
connection_release_lock(conn);
382+
return am_ok;
383+
}
384+
377385
int autocommit = sqlite3_get_autocommit(conn->db);
378386
if (autocommit == 0) {
379387
rc = sqlite3_exec(conn->db, "ROLLBACK;", NULL, NULL, NULL);
@@ -383,18 +391,26 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
383391
}
384392
}
385393

394+
// Hold interrupt_mutex across close+NULL so that any concurrent
395+
// exqlite_interrupt() either completes its sqlite3_interrupt() call
396+
// before we start closing, or blocks until we've both closed and
397+
// NULLed conn->db (then sees NULL and skips).
398+
//
386399
// note: _v2 may not fully close the connection, hence why we check if
387400
// any transaction is open above, to make sure other connections aren't blocked.
388401
// v1 is guaranteed to close or error, but will return error if any
389402
// unfinalized statements, which we likely have, as we rely on the destructors
390403
// to later run to clean those up
404+
enif_mutex_lock(conn->interrupt_mutex);
391405
rc = sqlite3_close_v2(conn->db);
392406
if (rc != SQLITE_OK) {
407+
enif_mutex_unlock(conn->interrupt_mutex);
393408
connection_release_lock(conn);
394409
return make_sqlite3_error_tuple(env, rc, conn->db);
395410
}
396-
397411
conn->db = NULL;
412+
enif_mutex_unlock(conn->interrupt_mutex);
413+
398414
connection_release_lock(conn);
399415

400416
return am_ok;
@@ -427,6 +443,11 @@ exqlite_execute(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
427443

428444
connection_acquire_lock(conn);
429445

446+
if (conn->db == NULL) {
447+
connection_release_lock(conn);
448+
return make_error_tuple(env, am_connection_closed);
449+
}
450+
430451
rc = sqlite3_exec(conn->db, (char*)bin.data, NULL, NULL, NULL);
431452
if (rc != SQLITE_OK) {
432453
connection_release_lock(conn);
@@ -456,11 +477,11 @@ exqlite_changes(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
456477
return make_error_tuple(env, am_invalid_connection);
457478
}
458479

480+
connection_acquire_lock(conn);
459481
if (conn->db == NULL) {
482+
connection_release_lock(conn);
460483
return make_error_tuple(env, am_connection_closed);
461484
}
462-
463-
connection_acquire_lock(conn);
464485
int changes = sqlite3_changes(conn->db);
465486
connection_release_lock(conn);
466487
return make_ok_tuple(env, enif_make_int(env, changes));
@@ -539,6 +560,10 @@ exqlite_reset(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
539560
}
540561

541562
statement_acquire_lock(statement);
563+
if (statement->statement == NULL) {
564+
statement_release_lock(statement);
565+
return make_error_tuple(env, am_invalid_statement);
566+
}
542567
sqlite3_reset(statement->statement);
543568
statement_release_lock(statement);
544569
return am_ok;
@@ -556,6 +581,10 @@ exqlite_bind_parameter_count(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]
556581
}
557582

558583
statement_acquire_lock(statement);
584+
if (statement->statement == NULL) {
585+
statement_release_lock(statement);
586+
return make_error_tuple(env, am_invalid_statement);
587+
}
559588
int bind_parameter_count = sqlite3_bind_parameter_count(statement->statement);
560589
statement_release_lock(statement);
561590
return enif_make_int(env, bind_parameter_count);
@@ -580,6 +609,10 @@ exqlite_bind_parameter_index(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]
580609
}
581610

582611
statement_acquire_lock(statement);
612+
if (statement->statement == NULL) {
613+
statement_release_lock(statement);
614+
return make_error_tuple(env, am_invalid_statement);
615+
}
583616
int index = sqlite3_bind_parameter_index(statement->statement, (const char*)name.data);
584617
statement_release_lock(statement);
585618
return enif_make_int(env, index);
@@ -607,6 +640,10 @@ exqlite_bind_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
607640
}
608641

609642
statement_acquire_lock(statement);
643+
if (statement->statement == NULL) {
644+
statement_release_lock(statement);
645+
return make_error_tuple(env, am_invalid_statement);
646+
}
610647
int rc = sqlite3_bind_text(statement->statement, idx, (char*)text.data, text.size, SQLITE_TRANSIENT);
611648
statement_release_lock(statement);
612649
return enif_make_int(env, rc);
@@ -634,6 +671,10 @@ exqlite_bind_blob(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
634671
}
635672

636673
statement_acquire_lock(statement);
674+
if (statement->statement == NULL) {
675+
statement_release_lock(statement);
676+
return make_error_tuple(env, am_invalid_statement);
677+
}
637678
int rc = sqlite3_bind_blob(statement->statement, idx, (char*)blob.data, blob.size, SQLITE_TRANSIENT);
638679
statement_release_lock(statement);
639680
return enif_make_int(env, rc);
@@ -661,6 +702,10 @@ exqlite_bind_integer(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
661702
}
662703

663704
statement_acquire_lock(statement);
705+
if (statement->statement == NULL) {
706+
statement_release_lock(statement);
707+
return make_error_tuple(env, am_invalid_statement);
708+
}
664709
int rc = sqlite3_bind_int64(statement->statement, idx, i);
665710
statement_release_lock(statement);
666711
return enif_make_int(env, rc);
@@ -688,6 +733,10 @@ exqlite_bind_float(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
688733
}
689734

690735
statement_acquire_lock(statement);
736+
if (statement->statement == NULL) {
737+
statement_release_lock(statement);
738+
return make_error_tuple(env, am_invalid_statement);
739+
}
691740
int rc = sqlite3_bind_double(statement->statement, idx, f);
692741
statement_release_lock(statement);
693742
return enif_make_int(env, rc);
@@ -710,6 +759,10 @@ exqlite_bind_null(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
710759
}
711760

712761
statement_acquire_lock(statement);
762+
if (statement->statement == NULL) {
763+
statement_release_lock(statement);
764+
return make_error_tuple(env, am_invalid_statement);
765+
}
713766
int rc = sqlite3_bind_null(statement->statement, idx);
714767
statement_release_lock(statement);
715768
return enif_make_int(env, rc);
@@ -756,6 +809,11 @@ exqlite_multi_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
756809

757810
connection_acquire_lock(conn);
758811

812+
if (statement->statement == NULL) {
813+
connection_release_lock(conn);
814+
return make_error_tuple(env, am_invalid_statement);
815+
}
816+
759817
ERL_NIF_TERM rows = enif_make_list_from_array(env, NULL, 0);
760818
for (int i = 0; i < chunk_size; i++) {
761819
ERL_NIF_TERM row;
@@ -817,6 +875,11 @@ exqlite_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
817875

818876
connection_acquire_lock(conn);
819877

878+
if (statement->statement == NULL) {
879+
connection_release_lock(conn);
880+
return make_error_tuple(env, am_invalid_statement);
881+
}
882+
820883
int rc = sqlite3_step(statement->statement);
821884
switch (rc) {
822885
case SQLITE_ROW:
@@ -866,6 +929,10 @@ exqlite_columns(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
866929
}
867930

868931
statement_acquire_lock(statement);
932+
if (statement->statement == NULL) {
933+
statement_release_lock(statement);
934+
return make_error_tuple(env, am_invalid_statement);
935+
}
869936
size = sqlite3_column_count(statement->statement);
870937

871938
if (size == 0) {
@@ -920,6 +987,10 @@ exqlite_last_insert_rowid(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
920987
}
921988

922989
connection_acquire_lock(conn);
990+
if (conn->db == NULL) {
991+
connection_release_lock(conn);
992+
return make_error_tuple(env, am_connection_closed);
993+
}
923994
sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db);
924995
connection_release_lock(conn);
925996
return make_ok_tuple(env, enif_make_int64(env, last_rowid));
@@ -947,11 +1018,13 @@ exqlite_transaction_status(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
9471018
// and then re-opens a new connection. There is a condition where by
9481019
// the connection's database is not set but the calling elixir / erlang
9491020
// pass an incomplete reference.
1021+
// Check must be inside the lock: a concurrent close() can set conn->db = NULL
1022+
// between the pre-lock check and the sqlite3_get_autocommit() call → segfault.
1023+
connection_acquire_lock(conn);
9501024
if (!conn->db) {
1025+
connection_release_lock(conn);
9511026
return make_ok_tuple(env, am_error);
9521027
}
953-
954-
connection_acquire_lock(conn);
9551028
int autocommit = sqlite3_get_autocommit(conn->db);
9561029
connection_release_lock(conn);
9571030

@@ -986,6 +1059,11 @@ exqlite_serialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
9861059

9871060
connection_acquire_lock(conn);
9881061

1062+
if (conn->db == NULL) {
1063+
connection_release_lock(conn);
1064+
return make_error_tuple(env, am_connection_closed);
1065+
}
1066+
9891067
buffer = sqlite3_serialize(conn->db, (char*)database_name.data, &buffer_size, 0);
9901068
if (!buffer) {
9911069
connection_release_lock(conn);
@@ -1032,15 +1110,22 @@ exqlite_deserialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
10321110

10331111
connection_acquire_lock(conn);
10341112

1113+
if (conn->db == NULL) {
1114+
connection_release_lock(conn);
1115+
return make_error_tuple(env, am_connection_closed);
1116+
}
1117+
10351118
size = serialized.size;
10361119
buffer = sqlite3_malloc(size);
10371120
if (!buffer) {
1121+
connection_release_lock(conn);
10381122
return make_error_tuple(env, am_deserialization_failed);
10391123
}
10401124

10411125
memcpy(buffer, serialized.data, size);
1042-
rc = sqlite3_deserialize(conn->db, "main", buffer, size, size, flags);
1126+
rc = sqlite3_deserialize(conn->db, (const char*)database_name.data, buffer, size, size, flags);
10431127
if (rc != SQLITE_OK) {
1128+
sqlite3_free(buffer);
10441129
connection_release_lock(conn);
10451130
return make_sqlite3_error_tuple(env, rc, conn->db);
10461131
}
@@ -1102,6 +1187,11 @@ connection_type_destructor(ErlNifEnv* env, void* arg)
11021187
enif_mutex_destroy(conn->mutex);
11031188
conn->mutex = NULL;
11041189
}
1190+
1191+
if (conn->interrupt_mutex) {
1192+
enif_mutex_destroy(conn->interrupt_mutex);
1193+
conn->interrupt_mutex = NULL;
1194+
}
11051195
}
11061196

11071197
void
@@ -1245,10 +1335,18 @@ exqlite_enable_load_extension(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[
12451335
return make_error_tuple(env, am_invalid_enable_load_extension_value);
12461336
}
12471337

1338+
connection_acquire_lock(conn);
1339+
if (conn->db == NULL) {
1340+
connection_release_lock(conn);
1341+
return make_error_tuple(env, am_connection_closed);
1342+
}
12481343
rc = sqlite3_enable_load_extension(conn->db, enable_load_extension_value);
12491344
if (rc != SQLITE_OK) {
1250-
return make_sqlite3_error_tuple(env, rc, conn->db);
1345+
ERL_NIF_TERM err = make_sqlite3_error_tuple(env, rc, conn->db);
1346+
connection_release_lock(conn);
1347+
return err;
12511348
}
1349+
connection_release_lock(conn);
12521350
return am_ok;
12531351
}
12541352

@@ -1312,6 +1410,11 @@ exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
13121410

13131411
connection_acquire_lock(conn);
13141412

1413+
if (conn->db == NULL) {
1414+
connection_release_lock(conn);
1415+
return make_error_tuple(env, am_connection_closed);
1416+
}
1417+
13151418
// Passing the connection as the third argument causes it to be
13161419
// passed as the first argument to update_callback. This allows us
13171420
// to extract the hook pid and reset the hook if the pid is not alive.
@@ -1395,14 +1498,20 @@ exqlite_interrupt(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
13951498
return make_error_tuple(env, am_invalid_connection);
13961499
}
13971500

1398-
// DB is already closed, nothing to do here
1399-
if (conn->db == NULL) {
1400-
return am_ok;
1501+
// We deliberately do NOT hold the connection lock here. A running
1502+
// query holds the lock for its entire duration; acquiring it in
1503+
// interrupt() would block until the query finishes, which defeats
1504+
// the purpose of interrupting it.
1505+
//
1506+
// interrupt_mutex is a dedicated lightweight lock shared with close().
1507+
// close() holds the connection lock (so any running query has already
1508+
// released it) then acquires interrupt_mutex before nulling conn->db.
1509+
// interrupt() acquires interrupt_mutex here, so the two cannot overlap.
1510+
enif_mutex_lock(conn->interrupt_mutex);
1511+
if (conn->db != NULL) {
1512+
sqlite3_interrupt(conn->db);
14011513
}
1402-
1403-
// connection_acquire_lock(conn);
1404-
sqlite3_interrupt(conn->db);
1405-
// connection_release_lock(conn);
1514+
enif_mutex_unlock(conn->interrupt_mutex);
14061515

14071516
return am_ok;
14081517
}
@@ -1416,10 +1525,18 @@ exqlite_errmsg(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
14161525

14171526
if (enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
14181527
connection_acquire_lock(conn);
1528+
if (conn->db == NULL) {
1529+
connection_release_lock(conn);
1530+
return make_error_tuple(env, am_connection_closed);
1531+
}
14191532
msg = sqlite3_errmsg(conn->db);
14201533
connection_release_lock(conn);
14211534
} else if (enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
14221535
statement_acquire_lock(statement);
1536+
if (statement->statement == NULL) {
1537+
statement_release_lock(statement);
1538+
return am_nil;
1539+
}
14231540
msg = sqlite3_errmsg(sqlite3_db_handle(statement->statement));
14241541
statement_release_lock(statement);
14251542
} else {

0 commit comments

Comments
 (0)