Skip to content

Commit 3bf5cb9

Browse files
committed
Add regression tests for exqlite_close, last_insert_rowid, transaction_status
Each test targets a distinct race condition or NULL-deref in sqlite3_nif.c and was confirmed to crash with SIGSEGV (exit 139) on the unfixed NIF. 1. "concurrent double close" — TOCTOU in exqlite_close: two threads both pass the `conn->db == NULL` check outside the lock; one closes the db and NULLs the pointer; the second then calls sqlite3_get_autocommit(NULL) inside the lock → crash. 2. "last_insert_rowid after close" — missing NULL guard in exqlite_last_insert_rowid: acquires the lock but calls sqlite3_last_insert_rowid(conn->db) without checking for NULL; deterministic crash after a sequential close(). 3. "concurrent close and transaction_status" — TOCTOU in exqlite_transaction_status: !conn->db is checked outside the lock, sqlite3_get_autocommit(conn->db) is called inside it; concurrent close() can free conn->db between them → crash.
1 parent 66185e1 commit 3bf5cb9

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

test/exqlite/sqlite3_test.exs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,5 +844,59 @@ defmodule Exqlite.Sqlite3Test do
844844
Process.sleep(100)
845845
:ok = Sqlite3.close(conn)
846846
end
847+
848+
# Targets the two-concurrent-close TOCTOU:
849+
# Both threads pass the `conn->db == NULL` check outside the lock.
850+
# One closes and sets conn->db = NULL; the other then calls
851+
# sqlite3_get_autocommit(NULL) inside the lock → segfault.
852+
test "concurrent double close does not segfault" do
853+
for _ <- 1..200 do
854+
{:ok, conn} = Sqlite3.open(":memory:")
855+
parent = self()
856+
spawn(fn -> send(parent, {:a, Sqlite3.close(conn)}) end)
857+
spawn(fn -> send(parent, {:b, Sqlite3.close(conn)}) end)
858+
assert_receive {:a, :ok}, 1000
859+
assert_receive {:b, :ok}, 1000
860+
end
861+
end
862+
863+
# Targets the missing NULL guard in exqlite_last_insert_rowid (line 923
864+
# in the unfixed NIF). The function acquires the lock but never checks
865+
# whether conn->db is NULL before calling sqlite3_last_insert_rowid(conn->db).
866+
# After close() sets conn->db = NULL (inside its own lock and then releases),
867+
# the very next last_insert_rowid call acquires the lock and dereferences the
868+
# NULL pointer → segfault. No concurrency is required; the crash is
869+
# deterministic.
870+
test "last_insert_rowid after close does not segfault" do
871+
for _ <- 1..100 do
872+
{:ok, conn} = Sqlite3.open(":memory:")
873+
:ok = Sqlite3.execute(conn, "create table t (id integer primary key)")
874+
:ok = Sqlite3.execute(conn, "insert into t values (1)")
875+
{:ok, 1} = Sqlite3.last_insert_rowid(conn)
876+
:ok = Sqlite3.close(conn)
877+
# Unfixed: lock acquired, sqlite3_last_insert_rowid(NULL) → segfault.
878+
# Fixed: lock acquired, NULL check fires → {:error, :connection_closed}.
879+
assert {:error, :connection_closed} = Sqlite3.last_insert_rowid(conn)
880+
end
881+
end
882+
883+
# Targets the TOCTOU in exqlite_transaction_status (lines 950–955 in the
884+
# unfixed NIF). The function checks !conn->db OUTSIDE the lock, then
885+
# acquires the lock and calls sqlite3_get_autocommit(conn->db) inside it.
886+
# If close() completes (setting conn->db = NULL) between that NULL check
887+
# and the lock acquisition, transaction_status calls
888+
# sqlite3_get_autocommit(NULL) → segfault.
889+
# This is a distinct bug from the double-close TOCTOU (Test 1): it involves
890+
# two *different* functions whose NULL checks are both outside their locks.
891+
test "concurrent close and transaction_status does not segfault" do
892+
for _ <- 1..500 do
893+
{:ok, conn} = Sqlite3.open(":memory:")
894+
parent = self()
895+
spawn(fn -> send(parent, {:a, Sqlite3.close(conn)}) end)
896+
spawn(fn -> send(parent, {:b, Sqlite3.transaction_status(conn)}) end)
897+
assert_receive {:a, :ok}, 1000
898+
assert_receive {:b, _}, 1000
899+
end
900+
end
847901
end
848902
end

0 commit comments

Comments
 (0)