Skip to content

Commit 983b051

Browse files
committed
Add regression tests for reset and bind_* after release
After Sqlite3.release(conn, stmt), the statement->statement pointer is NULL. The reset and bind_* NIFs acquire the statement lock but call SQLite functions without checking for NULL → segfault. Also update test comments to reference function names instead of hardcoded NIF line numbers, and rename the describe block to cover the full set of statement functions now under test.
1 parent 0631339 commit 983b051

1 file changed

Lines changed: 76 additions & 16 deletions

File tree

test/exqlite/sqlite3_test.exs

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -860,13 +860,12 @@ defmodule Exqlite.Sqlite3Test do
860860
end
861861
end
862862

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.
863+
# Targets the missing NULL guard in exqlite_last_insert_rowid.
864+
# The function acquires the lock but never checks whether conn->db is NULL
865+
# before calling sqlite3_last_insert_rowid(conn->db).
866+
# After close() sets conn->db = NULL, the very next last_insert_rowid call
867+
# acquires the lock and dereferences the NULL pointer → segfault.
868+
# No concurrency is required; the crash is deterministic.
870869
test "last_insert_rowid after close does not segfault" do
871870
for _ <- 1..100 do
872871
{:ok, conn} = Sqlite3.open(":memory:")
@@ -880,8 +879,8 @@ defmodule Exqlite.Sqlite3Test do
880879
end
881880
end
882881

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
882+
# Targets the TOCTOU in exqlite_transaction_status.
883+
# The function checks !conn->db OUTSIDE the lock, then
885884
# acquires the lock and calls sqlite3_get_autocommit(conn->db) inside it.
886885
# If close() completes (setting conn->db = NULL) between that NULL check
887886
# and the lock acquisition, transaction_status calls
@@ -899,7 +898,7 @@ defmodule Exqlite.Sqlite3Test do
899898
end
900899
end
901900

902-
# Targets the TOCTOU in exqlite_changes (unfixed NIF line 463):
901+
# Targets the TOCTOU in exqlite_changes:
903902
# conn->db is checked outside the lock; a concurrent close() can set
904903
# it to NULL between the check and the sqlite3_changes() call inside
905904
# the lock → sqlite3_changes(NULL) → segfault.
@@ -914,7 +913,7 @@ defmodule Exqlite.Sqlite3Test do
914913
end
915914
end
916915

917-
# Targets the missing NULL guard in exqlite_serialize (unfixed NIF line 999):
916+
# Targets the missing NULL guard in exqlite_serialize:
918917
# The function acquires the lock but calls sqlite3_serialize(conn->db, ...)
919918
# without checking for NULL → sqlite3_serialize(NULL, ...) → segfault.
920919
test "serialize after close does not segfault" do
@@ -925,7 +924,7 @@ defmodule Exqlite.Sqlite3Test do
925924
end
926925
end
927926

928-
# Targets exqlite_enable_load_extension (unfixed NIF line 1258):
927+
# Targets exqlite_enable_load_extension:
929928
# No lock is held and no NULL check is performed; after close sets
930929
# conn->db = NULL, sqlite3_enable_load_extension(NULL, ...) → segfault.
931930
test "enable_load_extension after close does not segfault" do
@@ -936,7 +935,7 @@ defmodule Exqlite.Sqlite3Test do
936935
end
937936
end
938937

939-
# Targets the missing NULL guard in exqlite_deserialize (unfixed NIF):
938+
# Targets the missing NULL guard in exqlite_deserialize:
940939
# Acquires the lock then calls sqlite3_deserialize(conn->db, ...) without
941940
# a NULL check → sqlite3_deserialize(NULL, ...) → segfault.
942941
test "deserialize after close does not segfault" do
@@ -966,7 +965,7 @@ defmodule Exqlite.Sqlite3Test do
966965
assert {:ok, :ok} = result, "close deadlocked after failed deserialize (lock not released)"
967966
end
968967

969-
# Targets the missing NULL guard in exqlite_set_update_hook (unfixed NIF):
968+
# Targets the missing NULL guard in exqlite_set_update_hook:
970969
# Acquires the lock then calls sqlite3_update_hook(conn->db, ...) without
971970
# a NULL check → sqlite3_update_hook(NULL, ...) → segfault.
972971
test "set_update_hook after close does not segfault" do
@@ -978,7 +977,7 @@ defmodule Exqlite.Sqlite3Test do
978977
end
979978
end
980979

981-
describe ".step, .columns, .multi_step after release" do
980+
describe ".step, .columns, .multi_step, .reset, .bind_* after release" do
982981
# Targets statement use-after-release in exqlite_step.
983982
# After Sqlite3.release(conn, stmt), statement->statement is set to NULL
984983
# under the connection lock. exqlite_step acquires the connection lock
@@ -1013,7 +1012,7 @@ defmodule Exqlite.Sqlite3Test do
10131012

10141013
# Targets statement use-after-release in exqlite_multi_step.
10151014
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1016-
# The pre-lock check at line 773 catches the sequential case, but there
1015+
# The pre-lock check catches the sequential case, but there
10171016
# is a TOCTOU window between that check and the sqlite3_step() call
10181017
# inside the connection lock. This test verifies the expected error
10191018
# return and documents the race condition.
@@ -1027,5 +1026,66 @@ defmodule Exqlite.Sqlite3Test do
10271026
:ok = Sqlite3.close(conn)
10281027
end
10291028
end
1029+
1030+
# Targets statement use-after-release in exqlite_reset.
1031+
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1032+
# exqlite_reset acquires the statement lock and calls sqlite3_reset(NULL)
1033+
# without checking for NULL → segfault.
1034+
test "reset after release does not segfault" do
1035+
for _ <- 1..50 do
1036+
{:ok, conn} = Sqlite3.open(":memory:")
1037+
:ok = Sqlite3.execute(conn, "create table t (x integer)")
1038+
{:ok, stmt} = Sqlite3.prepare(conn, "select * from t")
1039+
:ok = Sqlite3.release(conn, stmt)
1040+
assert {:error, _} = Sqlite3.reset(stmt)
1041+
:ok = Sqlite3.close(conn)
1042+
end
1043+
end
1044+
1045+
# Targets statement use-after-release in exqlite_bind_parameter_count.
1046+
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1047+
# exqlite_bind_parameter_count acquires the statement lock and calls
1048+
# sqlite3_bind_parameter_count(NULL) → segfault.
1049+
test "bind_parameter_count after release does not segfault" do
1050+
for _ <- 1..50 do
1051+
{:ok, conn} = Sqlite3.open(":memory:")
1052+
{:ok, stmt} = Sqlite3.prepare(conn, "select ?")
1053+
:ok = Sqlite3.release(conn, stmt)
1054+
assert {:error, _} = Sqlite3.bind_parameter_count(stmt)
1055+
:ok = Sqlite3.close(conn)
1056+
end
1057+
end
1058+
1059+
# Targets statement use-after-release in the bind_* NIFs.
1060+
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1061+
# Each bind_* NIF acquires the statement lock and calls the SQLite bind
1062+
# function without checking for NULL → segfault.
1063+
test "bind_text after release does not segfault" do
1064+
for _ <- 1..50 do
1065+
{:ok, conn} = Sqlite3.open(":memory:")
1066+
{:ok, stmt} = Sqlite3.prepare(conn, "select ?")
1067+
:ok = Sqlite3.release(conn, stmt)
1068+
try do
1069+
Sqlite3.bind_text(stmt, 1, "hello")
1070+
rescue
1071+
_ -> :ok
1072+
end
1073+
:ok = Sqlite3.close(conn)
1074+
end
1075+
end
1076+
1077+
test "bind_integer after release does not segfault" do
1078+
for _ <- 1..50 do
1079+
{:ok, conn} = Sqlite3.open(":memory:")
1080+
{:ok, stmt} = Sqlite3.prepare(conn, "select ?")
1081+
:ok = Sqlite3.release(conn, stmt)
1082+
try do
1083+
Sqlite3.bind_integer(stmt, 1, 42)
1084+
rescue
1085+
_ -> :ok
1086+
end
1087+
:ok = Sqlite3.close(conn)
1088+
end
1089+
end
10301090
end
10311091
end

0 commit comments

Comments
 (0)