-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Open
Labels
Description
I have found these related issues/pull requests
N/A
Description
sqlx-sqlite currently assumes that SQLite column metadata strings are always valid UTF-8:
StatementHandle::column_name()callssqlite3_column_name()and converts the returned bytes withfrom_utf8_unchecked.VirtualStatement::prepare_next()copies that&strintoUStr/SqliteColumnmetadata.SqliteColumn::name()then exposes it through the safe publicColumn::name() -> &strAPI.describe()also calls the samecolumn_name()helper.
That is unsound for SQLite databases whose schema contains invalid UTF-8 names.
SQLite explicitly documents that schema names (including column names) may contain invalid UTF and that SQLite will continue operating normally because it treats those names as byte sequences. In practice, sqlite3_column_name() can return those bytes unchanged.
Because Rust &str must always be valid UTF-8, constructing one with from_utf8_unchecked from schema-derived bytes violates the str invariant and can lead to undefined behavior later.
Reproduction steps
- Create a SQLite database with an invalid-UTF-8 column name using raw SQLite C APIs.
- Open that database through safe
sqlxAPIs and run a normal query. sqlxconstructs column metadata during statement preparation and converts the invalid bytes into&strwithfrom_utf8_unchecked.
use std::ffi::{c_char, CString};
use std::path::PathBuf;
use std::ptr::null_mut;
use sqlx::{Column, Row, SqlitePool};
unsafe fn create_db_with_invalid_column_name(path: &PathBuf) {
use libsqlite3_sys::{
sqlite3, sqlite3_close, sqlite3_exec, sqlite3_open,
};
let mut db: *mut sqlite3 = null_mut();
let path_c = CString::new(path.to_string_lossy().as_bytes()).unwrap();
assert_eq!(sqlite3_open(path_c.as_ptr(), &mut db), 0);
// Column name bytes are: 62 61 64 ff 63 6f 6c == "bad\xffcol"
let create_sql = b"CREATE TABLE ok_table(\"bad\xffcol\" INTEGER);\0";
let insert_sql = b"INSERT INTO ok_table VALUES(123);\0";
assert_eq!(sqlite3_exec(db, create_sql.as_ptr() as *const c_char, None, null_mut(), null_mut()), 0);
assert_eq!(sqlite3_exec(db, insert_sql.as_ptr() as *const c_char, None, null_mut(), null_mut()), 0);
assert_eq!(sqlite3_close(db), 0);
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let path = std::env::temp_dir().join("sqlx-invalid-column-name.db");
let _ = std::fs::remove_file(&path);
unsafe { create_db_with_invalid_column_name(&path) };
let url = format!("sqlite://{}", path.display());
let pool = SqlitePool::connect(&url).await?;
let row = sqlx::query("SELECT * FROM ok_table")
.fetch_one(&pool)
.await?;
// Current main reaches here with a logically-invalid `&str`.
// Any downstream use is now operating on a `str` that may not be valid UTF-8.
let name = row.columns()[0].name();
println!("column name bytes: {:x?}", name.as_bytes());
println!("column name debug: {:?}", name);
Ok(())
}SQLx version
main branch
Enabled SQLx features
default
Database server and version
SQLite
Operating system
MacOS
Rust version
1.94.0 (4a4ef493e 2026-03-02)
Reactions are currently unavailable