Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/SQLitePCLRaw.core/raw.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,8 @@ static public int sqlite3_prepare_v2(sqlite3 db, string sql, out sqlite3_stmt st
var ba = sql.to_utf8_with_z();
var sp = new ReadOnlySpan<byte>(ba);
int rc = sqlite3_prepare_v2(db, sp, out stmt, out var sp_tail);
tail = utf8_span_to_string(sp_tail.Slice(0, sp_tail.Length - 1));
// Audit #1: provider now returns an empty span on error; guard Slice(0, -1).
tail = sp_tail.Length > 0 ? utf8_span_to_string(sp_tail.Slice(0, sp_tail.Length - 1)) : "";
return rc;
}

Expand Down Expand Up @@ -874,7 +875,8 @@ static public int sqlite3_prepare_v3(sqlite3 db, string sql, uint flags, out sql
var ba = sql.to_utf8_with_z();
var sp = new ReadOnlySpan<byte>(ba);
int rc = sqlite3_prepare_v3(db, sp, flags, out stmt, out var sp_tail);
tail = utf8_span_to_string(sp_tail.Slice(0, sp_tail.Length - 1));
// Audit #1: provider now returns an empty span on error; guard Slice(0, -1).
tail = sp_tail.Length > 0 ? utf8_span_to_string(sp_tail.Slice(0, sp_tail.Length - 1)) : "";
return rc;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v2(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v2(db, p_sql, sql.Length, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand All @@ -301,11 +301,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v3(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v3(db, p_sql, sql.Length, flags, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v2(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v2(db, p_sql, sql.Length, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand All @@ -304,11 +304,12 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v3(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v3(db, p_sql, sql.Length, flags, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
// Follow-up to 854eeb0: funcptrs_win's v3 span variant was missed in that commit — same p_tail bounds guard as the other three provider variants.
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v2(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v2(db, p_sql, sql.Length, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand All @@ -300,11 +300,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v3(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v3(db, p_sql, sql.Length, flags, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v2(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v2(db, p_sql, sql.Length, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand All @@ -303,11 +303,11 @@ unsafe int ISQLite3Provider.sqlite3_prepare_v3(sqlite3 db, ReadOnlySpan<byte> sq
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v3(db, p_sql, sql.Length, flags, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand Down
142 changes: 142 additions & 0 deletions src/common/tests_xunit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,148 @@ public void test_prepare_tail_string()
}
}

// Regression test for the ArgumentOutOfRangeException variant of the
// long-standing prepare-race bug class (see issues #108, #321, #430,
// #479, #588). The provider's span overload of sqlite3_prepare_v2
// computes `(int)(p_tail - p_sql)` and uses it as a Slice start.
//
// On SQLITE_MISUSE paths (unsafe db handle, null zSql), native returns
// without writing *pzTail, leaving the caller's `out byte* p_tail`
// local at its .locals-init default of 0. For a non-empty SQL input,
// p_sql is a real non-null pointer, so `p_tail - p_sql` is a 64-bit
// signed difference that truncates to an int whose sign depends on
// bit 31 of the low 32 bits of p_sql. When bit 31 is clear (the sql
// buffer sits at a typical high managed-heap address on x64), the
// cast produces a large negative int, and `sql.Slice(negative, ...)`
// throws ArgumentOutOfRangeException instead of cleanly returning
// the error rc to the caller.
//
// We trigger the unsafe-db path deterministically via manual_close_v2,
// and we force the sql buffer into the bit-31-clear address range by
// growing the heap with a rolling allocator before each iteration.
// On a fresh process the heap can start either side of 2^31, so
// without the rolling growth the test is stochastic. With it, every
// iteration fires the bug on unpatched builds.
//
// The patched provider bounds-checks p_tail against
// [p_sql, p_sql + sql.Length] before computing the slice, so this
// returns rc=SQLITE_MISUSE + tail=Empty instead of throwing.
[Fact]
public void test_prepare_v2_span_tolerates_uninitialised_pzTail()
{
var rolling = new byte[256][];
var rnd = new Random(42);
for (var i = 0; i < 512; i++)
{
rolling[i % rolling.Length] = new byte[rnd.Next(1024, 128 * 1024)];

var db = ugly.open(":memory:");
db.manual_close_v2();
try
{
var sql = u.to_utf8("SELECT 1;");

int rc = raw.sqlite3_prepare_v2(db, sql.AsSpan(), out var stmt, out var tail);

Assert.Equal(raw.SQLITE_MISUSE, rc);
Assert.True(tail.IsEmpty);
stmt?.Dispose();
}
finally
{
db.Dispose();
}
}
GC.KeepAlive(rolling);
}

[Fact]
public void test_prepare_v3_span_tolerates_uninitialised_pzTail()
{
var rolling = new byte[256][];
var rnd = new Random(42);
for (var i = 0; i < 512; i++)
{
rolling[i % rolling.Length] = new byte[rnd.Next(1024, 128 * 1024)];

var db = ugly.open(":memory:");
db.manual_close_v2();
try
{
var sql = u.to_utf8("SELECT 1;");

int rc = raw.sqlite3_prepare_v3(db, sql.AsSpan(), 0, out var stmt, out var tail);

Assert.Equal(raw.SQLITE_MISUSE, rc);
Assert.True(tail.IsEmpty);
stmt?.Dispose();
}
finally
{
db.Dispose();
}
}
GC.KeepAlive(rolling);
}

// Audit #1: the string overloads of prepare_v2/v3 used to throw
// ArgumentOutOfRangeException on the same MISUSE path because they
// unconditionally sliced the (now empty) tail span. These tests
// assert they return rc cleanly with an empty tail string instead.
[Fact]
public void test_prepare_v2_string_tolerates_uninitialised_pzTail()
{
var rolling = new byte[256][];
var rnd = new Random(42);
for (var i = 0; i < 512; i++)
{
rolling[i % rolling.Length] = new byte[rnd.Next(1024, 128 * 1024)];

var db = ugly.open(":memory:");
db.manual_close_v2();
try
{
int rc = raw.sqlite3_prepare_v2(db, "SELECT 1;", out var stmt, out string tail);

Assert.Equal(raw.SQLITE_MISUSE, rc);
Assert.Equal("", tail);
stmt?.Dispose();
}
finally
{
db.Dispose();
}
}
GC.KeepAlive(rolling);
}

[Fact]
public void test_prepare_v3_string_tolerates_uninitialised_pzTail()
{
var rolling = new byte[256][];
var rnd = new Random(42);
for (var i = 0; i < 512; i++)
{
rolling[i % rolling.Length] = new byte[rnd.Next(1024, 128 * 1024)];

var db = ugly.open(":memory:");
db.manual_close_v2();
try
{
int rc = raw.sqlite3_prepare_v3(db, "SELECT 1;", 0, out var stmt, out string tail);

Assert.Equal(raw.SQLITE_MISUSE, rc);
Assert.Equal("", tail);
stmt?.Dispose();
}
finally
{
db.Dispose();
}
}
GC.KeepAlive(rolling);
}

[Fact]
public void test_bind_parameter_index()
{
Expand Down
16 changes: 8 additions & 8 deletions src/providers/provider.tt
Original file line number Diff line number Diff line change
Expand Up @@ -528,11 +528,11 @@ namespace SQLitePCL
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v2(db, p_sql, sql.Length, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand All @@ -559,11 +559,11 @@ namespace SQLitePCL
fixed (byte* p_sql = sql)
{
var rc = NativeMethods.sqlite3_prepare_v3(db, p_sql, sql.Length, flags, out stm, out var p_tail);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
if (len_remain > 0)
if (p_tail != null && p_tail >= p_sql && p_tail <= p_sql + sql.Length)
{
tail = sql.Slice(len_consumed, len_remain);
var len_consumed = (int) (p_tail - p_sql);
int len_remain = sql.Length - len_consumed;
tail = len_remain > 0 ? sql.Slice(len_consumed, len_remain) : ReadOnlySpan<byte>.Empty;
}
else
{
Expand Down
48 changes: 3 additions & 45 deletions src/tests/my_batteries_v2.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2014-2019 SourceGear, LLC
/*
Copyright 2014-2026 SourceGear, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -14,55 +14,13 @@ You may obtain a copy of the License at
limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Reflection;
using System.IO;

namespace SQLitePCL
{
public static class Batteries_V2
{
class MyGetFunctionPointer : IGetFunctionPointer
{
readonly IntPtr _dll;
public MyGetFunctionPointer(IntPtr dll)
{
_dll = dll;
}

public IntPtr GetFunctionPointer(string name)
{
if (NativeLibrary.TryGetExport(_dll, name, out var f))
{
//System.Console.WriteLine("{0}.{1} : {2}", _dll, name, f);
return f;
}
else
{
return IntPtr.Zero;
}
}
}
static IGetFunctionPointer MakeDynamic(string name, int flags)
{
// TODO should this be GetExecutingAssembly()?
var assy = typeof(SQLitePCL.raw).Assembly;
var dll = SQLitePCL.NativeLibrary.Load(name, assy, flags);
var gf = new MyGetFunctionPointer(dll);
return gf;
}
static void DoDynamic_cdecl(string name, int flags)
{
var gf = MakeDynamic(name, flags);
SQLitePCL.SQLite3Provider_dynamic_cdecl.Setup(name, gf);
SQLitePCL.raw.SetProvider(new SQLite3Provider_dynamic_cdecl());
}

public static void Init()
{
DoDynamic_cdecl("e_sqlite3", NativeLibrary.WHERE_PLAIN);
raw.SetProvider(new SQLite3Provider_e_sqlite3());
}
}
}

Loading