Skip to content

Commit 432562a

Browse files
committed
Fix use-after-free in exqlite_interrupt: add interrupt_mutex; NULL guard execute
exqlite_interrupt() read conn->db and called sqlite3_interrupt() without any lock, while a concurrent close() could sqlite3_close_v2() and NULL conn->db between the read and the call → use-after-free / segfault. The connection lock cannot be used here: running queries hold it for their full duration, and interrupt() must not block waiting for them to finish. Add a dedicated interrupt_mutex to connection_t. close() acquires it after sqlite3_close_v2() sets conn->db = NULL, and interrupt() acquires it before reading conn->db. This eliminates the TOCTOU entirely. Also add a conn->db == NULL guard inside the lock in exqlite_execute so that a racing call to execute after close returns an error rather than crashing.
1 parent eff42e3 commit 432562a

2 files changed

Lines changed: 43 additions & 18 deletions

File tree

c_src/sqlite3_nif.c

Lines changed: 38 additions & 10 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);
@@ -398,7 +405,12 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
398405
return make_sqlite3_error_tuple(env, rc, conn->db);
399406
}
400407

408+
// Acquire interrupt_mutex so any concurrent exqlite_interrupt() finishes
409+
// before we NULL out conn->db, eliminating the TOCTOU / use-after-free.
410+
enif_mutex_lock(conn->interrupt_mutex);
401411
conn->db = NULL;
412+
enif_mutex_unlock(conn->interrupt_mutex);
413+
402414
connection_release_lock(conn);
403415

404416
return am_ok;
@@ -431,6 +443,11 @@ exqlite_execute(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
431443

432444
connection_acquire_lock(conn);
433445

446+
if (conn->db == NULL) {
447+
connection_release_lock(conn);
448+
return make_error_tuple(env, am_connection_closed);
449+
}
450+
434451
rc = sqlite3_exec(conn->db, (char*)bin.data, NULL, NULL, NULL);
435452
if (rc != SQLITE_OK) {
436453
connection_release_lock(conn);
@@ -1171,6 +1188,11 @@ connection_type_destructor(ErlNifEnv* env, void* arg)
11711188
enif_mutex_destroy(conn->mutex);
11721189
conn->mutex = NULL;
11731190
}
1191+
1192+
if (conn->interrupt_mutex) {
1193+
enif_mutex_destroy(conn->interrupt_mutex);
1194+
conn->interrupt_mutex = NULL;
1195+
}
11741196
}
11751197

11761198
void
@@ -1477,14 +1499,20 @@ exqlite_interrupt(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
14771499
return make_error_tuple(env, am_invalid_connection);
14781500
}
14791501

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

14891517
return am_ok;
14901518
}

test/exqlite/sqlite3_test.exs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -659,11 +659,9 @@ defmodule Exqlite.Sqlite3Test do
659659
:ok = Sqlite3.close(conn)
660660
:ok = Sqlite3.bind(statement, ["this is a test"])
661661

662-
{:error, message} =
662+
assert {:error, :connection_closed} =
663663
Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)")
664664

665-
assert message == "Sqlite3 was invoked incorrectly."
666-
667665
assert :done == Sqlite3.step(conn, statement)
668666
end
669667
end
@@ -845,11 +843,10 @@ defmodule Exqlite.Sqlite3Test do
845843
:ok = Sqlite3.close(conn)
846844
end
847845

848-
# Targets the TOCTOU in exqlite_interrupt:
849-
# The function checks conn->db == NULL outside the lock, then calls
850-
# sqlite3_interrupt(conn->db) without holding the lock. A concurrent
851-
# close() can sqlite3_close_v2() and NULL conn->db between the check and
852-
# the call → sqlite3_interrupt on a freed pointer → use-after-free segfault.
846+
# Verifies that concurrent interrupt/1 and close/1 are race-free.
847+
# exqlite_interrupt acquires interrupt_mutex (shared with close()) before
848+
# reading conn->db, and close() acquires it (while holding the conn lock,
849+
# after the running query has finished) before nulling conn->db.
853850
test "concurrent interrupt and close does not segfault" do
854851
for _ <- 1..500 do
855852
{:ok, conn} = Sqlite3.open(":memory:")

0 commit comments

Comments
 (0)