Skip to content

Commit 6053bde

Browse files
committed
Add regression tests for statement NULL-dereference
After Sqlite3.release(conn, stmt) sets statement->statement = NULL: - exqlite_step: calls sqlite3_step(NULL) — returns SQLITE_MISUSE by SQLite's internal guard, so the test already passes, but the guard should be explicit in the NIF. - exqlite_columns: calls sqlite3_column_count(NULL) which returns 0, causing columns/2 to return {:ok, []} instead of {:error, _}. This test is RED — it currently fails with {:ok, []}. - exqlite_multi_step: the pre-lock !statement->statement check at line 773 catches the sequential case; the test documents the expected {:error, _} behavior and the TOCTOU race window. The bind_*/reset/errmsg NULL guards share the same pattern and will be fixed in the same GREEN commit.
1 parent 4aa6a44 commit 6053bde

1 file changed

Lines changed: 51 additions & 0 deletions

File tree

test/exqlite/sqlite3_test.exs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,4 +977,55 @@ defmodule Exqlite.Sqlite3Test do
977977
end
978978
end
979979
end
980+
981+
describe ".step, .columns, .multi_step after release" do
982+
# Targets statement use-after-release in exqlite_step.
983+
# After Sqlite3.release(conn, stmt), statement->statement is set to NULL
984+
# under the connection lock. exqlite_step acquires the connection lock
985+
# and then calls sqlite3_step(statement->statement) without checking for
986+
# NULL → sqlite3_step(NULL) → segfault.
987+
test "step after release does not segfault" do
988+
for _ <- 1..50 do
989+
{:ok, conn} = Sqlite3.open(":memory:")
990+
:ok = Sqlite3.execute(conn, "create table t (x integer)")
991+
{:ok, stmt} = Sqlite3.prepare(conn, "select * from t")
992+
:ok = Sqlite3.release(conn, stmt)
993+
assert {:error, _} = Sqlite3.step(conn, stmt)
994+
:ok = Sqlite3.close(conn)
995+
end
996+
end
997+
998+
# Targets statement use-after-release in exqlite_columns.
999+
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1000+
# exqlite_columns acquires the statement lock (= connection lock) and
1001+
# calls sqlite3_column_count(statement->statement) without checking for
1002+
# NULL → sqlite3_column_count(NULL) → segfault.
1003+
test "columns after release does not segfault" do
1004+
for _ <- 1..50 do
1005+
{:ok, conn} = Sqlite3.open(":memory:")
1006+
:ok = Sqlite3.execute(conn, "create table t (x integer)")
1007+
{:ok, stmt} = Sqlite3.prepare(conn, "select * from t")
1008+
:ok = Sqlite3.release(conn, stmt)
1009+
assert {:error, _} = Sqlite3.columns(conn, stmt)
1010+
:ok = Sqlite3.close(conn)
1011+
end
1012+
end
1013+
1014+
# Targets statement use-after-release in exqlite_multi_step.
1015+
# After Sqlite3.release(conn, stmt), statement->statement is NULL.
1016+
# The pre-lock check at line 773 catches the sequential case, but there
1017+
# is a TOCTOU window between that check and the sqlite3_step() call
1018+
# inside the connection lock. This test verifies the expected error
1019+
# return and documents the race condition.
1020+
test "multi_step after release does not segfault" do
1021+
for _ <- 1..50 do
1022+
{:ok, conn} = Sqlite3.open(":memory:")
1023+
:ok = Sqlite3.execute(conn, "create table t (x integer)")
1024+
{:ok, stmt} = Sqlite3.prepare(conn, "select * from t")
1025+
:ok = Sqlite3.release(conn, stmt)
1026+
assert {:error, _} = Sqlite3.multi_step(conn, stmt, 10)
1027+
:ok = Sqlite3.close(conn)
1028+
end
1029+
end
1030+
end
9801031
end

0 commit comments

Comments
 (0)