Skip to content

core/ext: NUL-terminate text values for the C API - #10728

Merged
jussisaurio merged 1 commit into
tursodatabase:mainfrom
abrahamperz:fix/value-text-nul-terminate-7982
Oct 26, 2026
Merged

jussisaurio merged 1 commit into
tursodatabase:mainfrom
abrahamperz:fix/value-text-nul-terminate-7982

Conversation

@abrahamperz

@abrahamperz abrahamperz commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7982.

sqlite3_value_text (and the other text accessors that share its path) returned a pointer that wasn't NUL-terminated, so a C caller running strlen on it read past the end of the buffer.

The SQLite C API promises a NUL-terminated string here. The compat layer handed back the pointer of a Rust &str whose buffer (TextValue, built by new_boxed) was exactly len bytes, with nothing after it.

How

TextValue now allocates len + 1 bytes with a trailing 0. len still counts only the text, so to_text() and as_str() don't change and nothing on the Rust side sees the extra byte. free() releases len + 1, and new_with_message allocates the same way, so a stored error message is also a valid C string.

Testing

text_value_pointer_is_nul_terminated checks that the byte at offset len is 0 for an empty string, ASCII text, text with spaces and text with an embedded NUL. Without the fix that read runs past the buffer and the test segfaults. bindings/c still compiles.

sqlite3_value_text must return a pointer to a NUL-terminated string (the
SQLite API contract), but the C compat layer handed back the pointer of a
Rust &str whose backing buffer (TextValue, built by new_boxed) was exactly
`len` bytes with no terminator. A C caller running strlen on it read past
the buffer.

Allocate TextValue storage with a trailing NUL (len + 1 bytes); `len` still
counts only the text, so to_text()/as_str() are unchanged. free() releases
len + 1, and the error-message path (new_with_message) allocates the same
way so its stored message is also a valid C string.

Tests: text_value_pointer_is_nul_terminated checks the byte at `len` is NUL
for several inputs. Without the fix that read runs past the buffer (SIGSEGV);
with it, the test passes.

Fixes tursodatabase#7982
@abrahamperz
abrahamperz marked this pull request as ready for review October 25, 2026 11:00
@turso-github-handyman

turso-github-handyman Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Autofixer review

🔄 Live status: Autofixer edits this comment as the review runs. Each finished review is also posted as its own GitHub review.

Autofixer reviews pull requests from outside contributors on its own. Its review of c64537323068 is here; each reviewed commit gets its own review, so the earlier ones stay in this pull request's timeline.

✅ Verdict: looks good to merge
Autofixer adds the autofixer: merge-recommended label while CI is green, the branch merges cleanly, and no maintainer has requested changes.

An automated review by Autofixer's agents; maintainers decide what to merge. Autofixer started this review on its own, as it does for pull requests from outside contributors. Push a fix to get a re-review (up to 3 reviews in all); a pull request whose blocking findings stay unaddressed for 7 days is closed. Maintainers: add autofixer: no-review to stop these reviews, or autofixer: review-requested to review it as requested.

@turso-github-handyman turso-github-handyman Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autofixer review of c64537323068

✅ Verdict: looks good to merge
Autofixer adds the autofixer: merge-recommended label while CI is green, the branch merges cleanly, and no maintainer has requested changes.

In short: A C function registered via sqlite3_create_function_v2 got strlen(sqlite3_value_text('AAA'))=6 on main and segfaulted on ''. Now 3 and 0, like real SQLite. Review: TextValue buffers in turso_ext are now len+1 bytes.

Least sure about: extensions/core/src/types.rs:216: With a sized allocator (jemalloc sdallocx), a wrong-size free between an old-ABI extension and new core could corrupt the heap. No valgrind or jemalloc build here to observe it. Can't be checked in the audit's container: there is no valgrind or sized-dealloc allocator in this container. …

Doubts: 4 doubts checked (4 refuted), 1 can't be checked here


Not blocking

2 minor findings
  • Minor extensions/core/src/types.rs:197 Adds 4 explanatory comment blocks (one with an em-dash), against CLAUDE.md's 'Do not add comments'.
    Suggested fix: Drop the comments. The len+1 alloc/free pair speaks for itself.
  • Minor extensions/core/src/types.rs:550 The test reads Value buffers directly and never goes through sqlite3_value_text in a C function, which is the issue's reported path.
    Suggested fix: Add a bindings/c/tests/compat test: a create_function_v2 callback that returns strlen(sqlite3_value_text) for 'AAA' and ''.
2 follow-ups: problems outside this change
  • sqlite3_value_text (bindings/c) does not convert non-text values: INTEGER, REAL and BLOB values return a NULL pointer, unlike SQLite's text conversion. …: C scalar function f registered via sqlite3_create_function_v2 that prints sqlite3_value_text(argv[0]); then: SELECT f(42), f(1.5), f(x'414243');
  • sqlite3_exec in bindings/c splits SQL on ';' without handling CREATE TRIGGER ... BEGIN ...; ...; END bodies, so any script with a multi-statement trigger fails: sqlite3_exec(db, "create table u(a); create trigger tr after insert on u begin select 1; select 2; end; insert into u values(1);", 0,0,&e)
The doubts the audit worked through (5)
  1. Refuted: extensions/core/src/types.rs:216: free() now frees len+1. Any TextValue buffer allocated with exactly len bytes would be freed with the wrong size.
    → Only new_boxed and new_with_message create owned buffers, and both now allocate len+1. No other from_raw on text. The compat suite (69 tests) and leak_tests pass. grep -rn "from_raw_parts\|from_raw(" extensions/core/src/types.rs core/ext bindings/c/src/lib.rs | grep -iv "blob\|cstr" | head -20
  2. Refuted: extensions/core/src/types.rs:216: An extension .so built with the old turso_ext allocates len bytes, but new core frees len+1, which is a sized-dealloc mismatch (the ABI concern the issue raised).
    → Loading the base-built liblimbo_regexp into head tursodb returns correct results. ExtensionApi already gains fields without any versioning, so there is no cross-version ABI promise to break. printf ".load /tmp/base/target/debug/liblimbo_regexp\nSELECT regexp_replace('abc','b','xyz'), regexp_substr('hello world','w.*');\n" | timeout 60 ./target/debug/tursodb -q :memory: 2>&1 | tail -3; ech
  3. Refuted: extensions/core/src/types.rs:195: Text results from dynamic extensions built from head and freed by core could break, and so could ext error messages.
    → Head-built regexp/crypto .so files return axyzc|world|YWJj. The error path gives 'Extension error' with no crash. printf '.load target/debug/liblimbo_regexp\n.load target/debug/liblimbo_crypto\nSELECT regexp_replace(%s), regexp_substr(%s), crypto_encode(%s);\nSELECT crypto_encode(%s);\n' "'abc','b','xyz'" "'hello
  4. Refuted: extensions/core/src/types.rs:550: The new unit test might not fail without the fix.
    → With the base types.rs plus the new test module, the test hits SIGSEGV. With the head version it passes, both with and without core_only. timeout 600 cargo test -p turso_ext --lib text_value_pointer 2>&1 | grep -E "test result|SIG|panicked" | head
  5. Can't be checked here: extensions/core/src/types.rs:216: With a sized allocator (jemalloc sdallocx), a wrong-size free between an old-ABI extension and new core could corrupt the heap. No valgrind or jemalloc build here to observe it.
    → There is no valgrind or sized-dealloc allocator in this container. Running an old-ABI .so under valgrind or ASan would settle it.
Audit notes

Approve: the fix NUL-terminates TextValue buffers at the single allocation point, with alloc and free updated together. The oracle and the neighboring suites pass. Only minor test-placement and comment nits remain.

An automated review by Autofixer's agents; maintainers decide what to merge. Autofixer started this review on its own, as it does for pull requests from outside contributors. Push a fix to get a re-review (up to 3 reviews in all); a pull request whose blocking findings stay unaddressed for 7 days is closed. Maintainers: add autofixer: no-review to stop these reviews, or autofixer: review-requested to review it as requested.

@turso-github-handyman turso-github-handyman Bot added autofixer: follow-ups-recommended Autofixer's audit found follow-ups; they are filed as issues when this merges autofixer: merge-recommended Autofixer's independent audit recommends merging this PR as is and removed autofixer: merge-recommended Autofixer's independent audit recommends merging this PR as is labels Oct 26, 2026
@jussisaurio
jussisaurio merged commit 4ef1b8e into tursodatabase:main Oct 26, 2026
215 checks passed
@turso-github-handyman turso-github-handyman Bot removed the autofixer: merge-recommended Autofixer's independent audit recommends merging this PR as is label Oct 26, 2026
@turso-github-handyman

Copy link
Copy Markdown

Autofixer follow-ups

@turso-github-handyman turso-github-handyman Bot added autofixer: follow-ups-filed Autofixer checked this PR's follow-ups and filed the ones that reproduce and removed autofixer: follow-ups-recommended Autofixer's audit found follow-ups; they are filed as issues when this merges labels Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofixer: auto-review autofixer: follow-ups-filed Autofixer checked this PR's follow-ups and filed the ones that reproduce extensionlib

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite3_value_text returns a non-NUL-terminated buffer, violating the SQLite API contract

2 participants