Skip to content

Commit 54692cd

Browse files
committed
Add regression tests for exqlite_changes, serialize, enable_load_extension, deserialize, set_update_hook
Each test was confirmed to crash with SIGSEGV (exit 139) on the unfixed NIF. - "concurrent close and changes" — TOCTOU: conn->db NULL check is outside the lock; concurrent close() can NULL the pointer between the check and the sqlite3_changes() call inside the lock. - "serialize after close" — sequential NULL-deref: no NULL guard before sqlite3_serialize(conn->db, ...) after close() sets conn->db = NULL. - "enable_load_extension after close" — no lock held at all, no NULL guard; sqlite3_enable_load_extension(NULL, ...) after close() → crash. - "deserialize after close" — sequential NULL-deref: no NULL guard before sqlite3_deserialize(conn->db, ...) after close(). - "set_update_hook after close" — sequential NULL-deref: no NULL guard before sqlite3_update_hook(conn->db, ...) after close().
1 parent 602f122 commit 54692cd

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

test/exqlite/sqlite3_test.exs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,5 +898,67 @@ defmodule Exqlite.Sqlite3Test do
898898
assert_receive {:b, _}, 1000
899899
end
900900
end
901+
902+
# Targets the TOCTOU in exqlite_changes (unfixed NIF line 463):
903+
# conn->db is checked outside the lock; a concurrent close() can set
904+
# it to NULL between the check and the sqlite3_changes() call inside
905+
# the lock → sqlite3_changes(NULL) → segfault.
906+
test "concurrent close and changes does not segfault" do
907+
for _ <- 1..500 do
908+
{:ok, conn} = Sqlite3.open(":memory:")
909+
parent = self()
910+
spawn(fn -> send(parent, {:a, Sqlite3.close(conn)}) end)
911+
spawn(fn -> send(parent, {:b, Sqlite3.changes(conn)}) end)
912+
assert_receive {:a, :ok}, 1000
913+
assert_receive {:b, _}, 1000
914+
end
915+
end
916+
917+
# Targets the missing NULL guard in exqlite_serialize (unfixed NIF line 999):
918+
# The function acquires the lock but calls sqlite3_serialize(conn->db, ...)
919+
# without checking for NULL → sqlite3_serialize(NULL, ...) → segfault.
920+
test "serialize after close does not segfault" do
921+
for _ <- 1..50 do
922+
{:ok, conn} = Sqlite3.open(":memory:")
923+
:ok = Sqlite3.close(conn)
924+
assert {:error, _} = Sqlite3.serialize(conn, "main")
925+
end
926+
end
927+
928+
# Targets exqlite_enable_load_extension (unfixed NIF line 1258):
929+
# No lock is held and no NULL check is performed; after close sets
930+
# conn->db = NULL, sqlite3_enable_load_extension(NULL, ...) → segfault.
931+
test "enable_load_extension after close does not segfault" do
932+
for _ <- 1..50 do
933+
{:ok, conn} = Sqlite3.open(":memory:")
934+
:ok = Sqlite3.close(conn)
935+
assert {:error, _} = Sqlite3.enable_load_extension(conn, false)
936+
end
937+
end
938+
939+
# Targets the missing NULL guard in exqlite_deserialize (unfixed NIF):
940+
# Acquires the lock then calls sqlite3_deserialize(conn->db, ...) without
941+
# a NULL check → sqlite3_deserialize(NULL, ...) → segfault.
942+
test "deserialize after close does not segfault" do
943+
{:ok, src} = Sqlite3.open(":memory:")
944+
{:ok, data} = Sqlite3.serialize(src, "main")
945+
:ok = Sqlite3.close(src)
946+
for _ <- 1..50 do
947+
{:ok, conn} = Sqlite3.open(":memory:")
948+
:ok = Sqlite3.close(conn)
949+
assert {:error, _} = Sqlite3.deserialize(conn, "main", data)
950+
end
951+
end
952+
953+
# Targets the missing NULL guard in exqlite_set_update_hook (unfixed NIF):
954+
# Acquires the lock then calls sqlite3_update_hook(conn->db, ...) without
955+
# a NULL check → sqlite3_update_hook(NULL, ...) → segfault.
956+
test "set_update_hook after close does not segfault" do
957+
for _ <- 1..50 do
958+
{:ok, conn} = Sqlite3.open(":memory:")
959+
:ok = Sqlite3.close(conn)
960+
assert {:error, _} = Sqlite3.set_update_hook(conn, self())
961+
end
962+
end
901963
end
902964
end

0 commit comments

Comments
 (0)