SQLite User Forum

sqlite c-api inconsistencies.
Login

sqlite c-api inconsistencies.

(1) By Pavan Nambi (Pavan-Nambi) on 2026-05-01 02:42:35 [link] [source]

cont.d from forumpost/2243800e99115e0a

first of all sorry about continuing discussion in that thread and also sorry about not giving specific versions. i am (sort of) copying the same message from that discussion to here so it will make sense to other readers.

Regression: 128-byte mutex alignment introduces UB in dynamic mutex allocation

note: this is not in any release yet.

Build testfixture with alignment UBSan:

  ./configure
  make testfixture CFLAGS='-O1 -g -fsanitize=alignment -fno-sanitize-recover=alignment -fno-omit-frame-pointer'

  for {set i 0} {$i < 200000} {incr i} {
    alloc_dealloc_mutex
  }
  puts DONE

output: 5 out of 10 runs, in some runs it do not show any UB.

  ./testfixture /Users/pavan/Documents/github/sqlitecapi/plans/repro_mutex_alloc_loop.tcl
sqlite3.c:30828:32: runtime error: member access within misaligned address 0x000106125750 for type 'sqlite3_mutex' (aka 'struct sqlite3_mutex'), which requires 128 byte alignment
0x000106125750: note: pointer points here
 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00
              ^ 
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior sqlite3.c:30828:32 
[1]    30049 abort      ./testfixture 

bisect for this is: 1786fcd5b4ee6cd9b4780f3687dfaec5b90ef047 (2026-04-28 15:12:40 UTC) - here 5/10 runs fail

d64a1dbe0fb2d9286806d833a3146b21d5bf1636 - all runs pass.

versions and info:

#define SQLITE_SOURCE_ID      "2026-04-30 18:23:58 4aac1057eeaf6c29a4893e9c080497c780b0963e810c501532d79eba1b457f27"

fossil status
repository:   /Users/pavan/sqlite/src.fossil
local-root:   /Users/pavan/sqlite/
config-db:    /Users/pavan/.config/fossil.db
checkout:     4aac1057eeaf6c29a4893e9c080497c780b0963e 2026-04-30 18:23:58 UTC
parent:       6f7a5ff22db10f889596239aae5f6a1130cbdfe7 2026-04-30 16:52:56 UTC
tags:         trunk
comment:      The value for /f and other filename substitutions in SQLITE_PS1 is now "memory" if open on an in-memory database.
              (user: drh)

System info: ```

sqlite3_db_readonly() returns 0 after sqlite3_deserialize(..., SQLITE_DESERIALIZE_READONLY)

sqlite3 src :memory:
  src eval {
    CREATE TABLE t(x);
    INSERT INTO t VALUES(1);
  }
  set blob [src serialize main]

  sqlite3 dst :memory:
  dst deserialize -readonly 1 $blob

  set ro [sqlite3_db_readonly dst main]
  set rc [catch {dst eval {INSERT INTO t VALUES(2)}} msg]

  puts "db_readonly=$ro"
  puts "write_catch_rc=$rc"
  puts "write_msg=$msg"

observed:

  db_readonly=0
  write_catch_rc=1
  write_msg=attempt to write a readonly database

in specs i was expecting db_readonly to be 1

sqlite docs say

> The sqlite3_db_readonly(D,N) interface returns 1 if the database N of connection D is read-only, 0 if it is read/write, or -1 if N is not the name of a database on connection D.

just another question:

also another issue i just want to ask what's the expected output here....if this should give api misuse

repro: https://gist.github.com/Pavan-Nambi/e2db9f211d5607876d1caa2c4071ead1

currently this gives

  backup_step rc=0
  deserialize rc=11 (SQLITE_CORRUPT)
  errcode=11 ex=11 msg=malformed database schema (x) - invalid rootpage

(2) By Dan Kennedy (dan) on 2026-05-01 11:23:36 in reply to 1 [link] [source]

just another question:

repro: https://gist.github.com/Pavan-Nambi/e2db9f211d5607876d1caa2c4071ead1

currently this gives

 backup_step rc=0
 deserialize rc=11 (SQLITE_CORRUPT)
 errcode=11 ex=11 msg=malformed database schema (x) - invalid rootpage

As you say, using the API like this is explicitly documented as leading to malfunctions. From https://sqlite.org/c3ref/backup_finish.html :

However, the application must guarantee that the destination database connection is not passed to any other API (by any thread) after sqlite3_backup_init() is called and before the corresponding call to sqlite3_backup_finish(). SQLite does not currently check to see if the application incorrectly accesses the destination database connection and so no error code is reported, but the operations may malfunction nevertheless.

In this particular case, when sqlite3_deserialize() on the destination db midway through the backup, SQLite tries to read the schema of the destination db. But because it is midway through the backup, some of the page numbers in the sqlite_schema table are invalid, and SQLite is reporting this as corruption.

So I think this one is just "not a bug".

Dan.

(3) By Stephan Beal (stephan) on 2026-05-01 12:39:51 in reply to 1 [link] [source]

in specs i was expecting db_readonly to be 1

Not for in-memory dbs. See /forumpost/9cc735eb6940e316 for an older discussion about that.

(4) By Pavan Nambi (Pavan-Nambi) on 2026-05-01 13:35:07 in reply to 1 [link] [source]

Thanks! From next time I will try to check open issues more throughly,

(5.1) By Pavan Nambi (Pavan-Nambi) on 2026-05-13 18:45:41 edited from 5.0 in reply to 4 [source]

Hi, sorry i didn't got enough time to continue this in last few days.

i ran into another invariant. which seems to me like a bug please correct me if i am doing something wrong here, thanks

build sqlite with -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK

here is a minimal c repro - https://gist.github.com/Pavan-Nambi/98617f5706038849d5f9f28524494fbc

this crashes on mac os

sqlite_version=3.54.0
sqlite_sourceid=2026-05-11 00:53:44 5e916b2a8fd41ffc42c29bfc9b5333b7a579f37fe094bd0b6b00e2f176c4e3fe
case=non-null-error-pointer
changegroup_new rc=0
changegroup_schema rc=0
change_begin rc=0
change_int64 pk rc=0
calling change_finish with pzErr=&zErr on invalid INSERT...
change_finish rc=1 zErr=invalid change: column 1 is undefined
case=null-error-pointer
changegroup_new rc=0
changegroup_schema rc=0
change_begin rc=0
change_int64 pk rc=0
calling change_finish with pzErr=NULL on invalid INSERT...
[1]    9463 segmentation fault  ./repro

with ASAN

calling change_finish with pzErr=NULL on invalid INSERT...
/Users/pavan/sqlite/sqlite3.c:239596:13: runtime error: store to null pointer of type 'char *'
    #0 0x00010266b5e0 in sqlite3changegroup_change_finish sqlite3.c:239596
    #1 0x000102571224 in run_case repro_changegroup_finish_null_pzerr.c:44
    #2 0x000102570b68 in main repro_changegroup_finish_null_pzerr.c:76
    #3 0x000198c01d50 in start+0x1c0c (dyld:arm64e+0x8d50)

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /Users/pavan/sqlite/sqlite3.c:239596:13 
AddressSanitizer:DEADLYSIGNAL
=================================================================
==9593==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x00010266b61c bp 0x00016d88ebd0 sp 0x00016d88d660 T0)
==9593==The signal is caused by a WRITE memory access.
==9593==Hint: address points to the zero page.
    #0 0x00010266b61c in sqlite3changegroup_change_finish sqlite3.c:239596
    #1 0x000102571224 in run_case repro_changegroup_finish_null_pzerr.c:44
    #2 0x000102570b68 in main repro_changegroup_finish_null_pzerr.c:76
    #3 0x000198c01d50 in start+0x1c0c (dyld:arm64e+0x8d50)

==9593==Register values:
 x[0] = 0x0000000000000000   x[1] = 0x0000000000000103   x[2] = 0x0000000000000103   x[3] = 0x000000010ac00640  
 x[4] = 0x0000000063000000   x[5] = 0x0000000000000000   x[6] = 0x0000000000000000   x[7] = 0x0000000000000000  
 x[8] = 0x0000604000000890   x[9] = 0x000000016d88d680  x[10] = 0x0000000000000000  x[11] = 0x0000000000000002  
x[12] = 0x0000000000000000  x[13] = 0x0000000103970ac1  x[14] = 0x000000016d88c740  x[15] = 0x0000000103970ab8  
x[16] = 0x0000000198fd0884  x[17] = 0x00000001038c85d8  x[18] = 0x0000000000000000  x[19] = 0x000000016d88d6a0  
x[20] = 0x0000000205ee8e08  x[21] = 0x0000000205c3ce00  x[22] = 0xfffffffffffffff0  x[23] = 0x0000000205eec520  
x[24] = 0x0000000000000001  x[25] = 0x000000016d88f030  x[26] = 0x0000000205eec530  x[27] = 0x0000000000000000  
x[28] = 0x0000000000000000     fp = 0x000000016d88ebd0     lr = 0x000000010266b5e4     sp = 0x000000016d88d660  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV sqlite3.c:239596 in sqlite3changegroup_change_finish
==9593==ABORTING
[1]    9593 abort      ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=print_stacktrace=1 ./repro

i was expecting sqlite3changegroup_change_finish(grp, 0, NULL) to return SQLITE_ERROR for the invalid constructed change, without writing an error message and without crashing.

(6) By Pavan Nambi (Pavan-Nambi) on 2026-05-14 01:46:35 in reply to 1 [link] [source]

hi, the above issues appears to be fixed now , here is another mismatch i ran into, please correct me if i am doing something wrong here.

here is a c repro : https://gist.github.com/Pavan-Nambi/9ba8437888be57172b93ced55753125f

i get:


case 1: valid parameter index
  bind index: 1
  sqlite3_bind_null rc=0 expected=0
  sqlite3_bind_text(NULL) rc=0 expected=0
  destructor_calls=0 expected=0
  destructor_arg=0x1 expected unchanged=0x1

case 2: out-of-range parameter index
  bind index: 2
  sqlite3_bind_null rc=25 expected=25
  sqlite3_bind_text(NULL) rc=25 expected=25
  destructor_calls=1 expected=0
  destructor_arg=0x0 expected unchanged=0x1

I'm expecting destructor_calls to remain 0 in both cases, because the third parameter to sqlite3_bind_text() is NULL

reference: https://sqlite.org/c3ref/bind_blob.html

(7) By Pavan Nambi (Pavan-Nambi) on 2026-05-14 08:03:19 in reply to 1 [link] [source]

not sure if this is docs problem or code or if i am just misunderstanding this..

but docs say

The sqlite3_uri_int64(F,P,D) routine converts the value of P into a 64-bit signed integer and returns that integer, or D if P does not exist. If the value of P is something other than an integer, then zero is returned.


  #include "sqlite3.h"
  #include <stdio.h>

  static void print_i64(const char *label, sqlite3_int64 v){
    printf("%s=%lld\n", label, (long long)v);
  }

  int main(void){
  const char *params[] = {
      "valid", "42",
      "invalid", "abc",
      "empty", "",
      "suffix", "123xyz",
      0
    };

    sqlite3_filename f;

    sqlite3_initialize();

    f = sqlite3_create_filename(
      "test.db",         
      "test.db-journal",  
      "test.db-wal",      
      4,               
      params
    );

    if( f==0 ){
      printf("sqlite3_create_filename returned NULL\n");
      return 1;
    }

    printf("sqlite_version=%s\n", sqlite3_libversion());
    printf("sqlite_sourceid=%s\n", sqlite3_sourceid());

    print_i64("valid_default777",
      sqlite3_uri_int64(f, "valid", 777));

    print_i64("missing_default777",
      sqlite3_uri_int64(f, "missing", 777));

    print_i64("invalid_alpha_default777",
      sqlite3_uri_int64(f, "invalid", 777));

    print_i64("invalid_empty_default777",
      sqlite3_uri_int64(f, "empty", 777));

    print_i64("invalid_suffix_default777",
      sqlite3_uri_int64(f, "suffix", 777));

    sqlite3_free_filename(f);
    sqlite3_shutdown();
    return 0;
  }

i'm expecting invalid-ones to be 0

output is:

sqlite_version=3.54.0
sqlite_sourceid=2026-05-13 20:37:30 f81d6d7bc8943729f678a3b62921a96764b15b9cc11d8a5753e48210a1b59617
valid_default777=42
missing_default777=777
invalid_alpha_default777=777
invalid_empty_default777=777
invalid_suffix_default777=777

(8) By Richard Hipp (drh) on 2026-05-14 11:10:46 in reply to 7 [link] [source]

Documentation fix is in the source tree now and will appear on the website in the next release.

(9) By Pavan Nambi (Pavan-Nambi) on 2026-05-14 11:17:16 in reply to 8 [link] [source]

thanks!

just flagging this here as it might have been missed.. - https://sqlite.org/forum/forumpost/a9d160c662548fe6 (this is not resolved yet imo) - im not sure if its docs issue or code.

am i doing something wrong here? this crashes as UAF with asan

(this is on trunk)

 sqlite3 db :memory:
  db eval { CREATE TABLE t(id INTEGER PRIMARY KEY, x TEXT); }

  sqlite3changegroup grp
  grp schema db main
  puts "schema ok"

  db close
  puts "db close ok"

  grp change_begin INSERT t 0

output:



schema ok
db close ok
=================================================================
==30803==ERROR: AddressSanitizer: heap-use-after-free on address 0x6180000004f1 at pc 0x0001048d2b2c bp 0x00016b92fba0 sp 0x00016b92fb98
READ of size 1 at 0x6180000004f1 thread T0
    #0 0x0001048d2b28 in sqlite3SafetyCheckOk sqlite3.c:38432
    #1 0x000104d01d04 in sqlite3LockAndPrepare sqlite3.c:147696
    #2 0x0001048e218c in sqlite3_prepare_v2 sqlite3.c:147794
    #3 0x000104ec0bf0 in sessionTableInfo sqlite3.c:233719
    #4 0x000104ebf97c in sessionInitTable sqlite3.c:233839
    #5 0x000104edb068 in sessionChangesetFindTable sqlite3.c:238674
    #6 0x000104ed9374 in sqlite3changegroup_change_begin sqlite3.c:239506
    #7 0x000104654158 in test_changegroup_cmd test_session.c:1722
    #8 0x0001058ea848 in TclNRRunCallbacks+0x4c (libtcl8.6.dylib:arm64+0x12848)
    #9 0x0001058eb5d8 in TclEvalEx+0x670 (libtcl8.6.dylib:arm64+0x135d8)
    #10 0x0001058ecf64 in Tcl_GlobalEval+0x34 (libtcl8.6.dylib:arm64+0x14f64)
    #11 0x00010480ae20 in main tclsqlite-ex.c:7838
    #12 0x000190629d50 in start+0x1c0c (dyld:arm64e+0x8d50)

0x6180000004f1 is located 113 bytes inside of 816-byte region [0x618000000480,0x6180000007b0)
freed by thread T0 here:
    #0 0x000106175bf8 in __sanitizer_mz_free+0xfc (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41bf8)
    #1 0x00010487bbdc in sqlite3MemFree sqlite3.c:28098
    #2 0x000104876388 in sqlite3_free sqlite3.c:31887
    #3 0x0001049b8438 in sqlite3LeaveMutexAndCloseZombie sqlite3.c:187830
    #4 0x000104e88ebc in sqlite3Close sqlite3.c:187665
    #5 0x000104e8841c in sqlite3_close sqlite3.c:187708
    #6 0x00010485cb84 in delDatabaseRef tclsqlite-ex.c:3853
    #7 0x0001048406e0 in DbDeleteCmd tclsqlite-ex.c:3912
    #8 0x0001058e8fc0 in Tcl_DeleteCommandFromToken+0x104 (libtcl8.6.dylib:arm64+0x10fc0)
    #9 0x0001058e9bc8 in Tcl_DeleteCommand+0x28 (libtcl8.6.dylib:arm64+0x11bc8)
    #10 0x00010482f844 in DbObjCmd tclsqlite-ex.c:5982
    #11 0x0001058ea848 in TclNRRunCallbacks+0x4c (libtcl8.6.dylib:arm64+0x12848)
    #12 0x0001058eb5d8 in TclEvalEx+0x670 (libtcl8.6.dylib:arm64+0x135d8)
    #13 0x0001058ecf64 in Tcl_GlobalEval+0x34 (libtcl8.6.dylib:arm64+0x14f64)
    #14 0x00010480ae20 in main tclsqlite-ex.c:7838
    #15 0x000190629d50 in start+0x1c0c (dyld:arm64e+0x8d50)

previously allocated by thread T0 here:
    #0 0x0001061757dc in __sanitizer_mz_malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x417dc)
    #1 0x00019081df40 in _malloc_zone_malloc_instrumented_or_legacy+0x94 (libsystem_malloc.dylib:arm64e+0x36f40)
    #2 0x00010487bb7c in sqlite3MemMalloc sqlite3.c:28066
    #3 0x00010487fa50 in mallocWithAlarm sqlite3.c:31765
    #4 0x000104876178 in sqlite3Malloc sqlite3.c:31790
    #5 0x00010487a2c8 in sqlite3MallocZero sqlite3.c:32069
    #6 0x000104e946d8 in openDatabase sqlite3.c:189743
    #7 0x000104e977a0 in sqlite3_open_v2 sqlite3.c:190056
    #8 0x000104809c4c in DbMain tclsqlite-ex.c:7618
    #9 0x0001058ea848 in TclNRRunCallbacks+0x4c (libtcl8.6.dylib:arm64+0x12848)
    #10 0x0001058eb5d8 in TclEvalEx+0x670 (libtcl8.6.dylib:arm64+0x135d8)
    #11 0x0001058ecf64 in Tcl_GlobalEval+0x34 (libtcl8.6.dylib:arm64+0x14f64)
    #12 0x00010480ae20 in main tclsqlite-ex.c:7838
    #13 0x000190629d50 in start+0x1c0c (dyld:arm64e+0x8d50)

SUMMARY: AddressSanitizer: heap-use-after-free sqlite3.c:38432 in sqlite3SafetyCheckOk
Shadow bytes around the buggy address:
  0x618000000200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x618000000280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x618000000300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x618000000380: 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x618000000400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x618000000480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd[fd]fd
  0x618000000500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x618000000580: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x618000000600: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x618000000680: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x618000000700: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==30803==ABORTING