Skip to main content

sqlite_embedded/
guard.rs

1//! Resource and statement guards for a connection that runs SQL sent by an untrusted peer.
2//!
3//! An engine that executes an Envoy's SQL on its own threads bounds the work of each command,
4//! the database's size, the largest value and each statement's result size, and refuses statements that would change the
5//! VFS's journal, page or locking assumptions or open another database file.
6use std::{
7	ffi::CStr,
8	os::raw::{c_char, c_int, c_void},
9	sync::atomic::{AtomicU64, Ordering},
10};
11
12use anyhow::{Result, ensure};
13use libsqlite3_sys::{
14	SQLITE_ATTACH, SQLITE_DENY, SQLITE_LIMIT_LENGTH, SQLITE_OK, SQLITE_PRAGMA, sqlite3,
15	sqlite3_limit, sqlite3_progress_handler, sqlite3_set_authorizer,
16};
17
18/// SQLite virtual machine instructions between two progress callbacks.
19const PROGRESS_INTERVAL: c_int = 1000;
20
21/// Pragmas a guarded connection must not set. Reading them stays allowed.
22const DENIED_PRAGMA_WRITES: &[&str] = &[
23	"temp_store",
24	"temp_store_directory",
25	"data_store_directory",
26	"max_page_count",
27	"page_size",
28	"journal_mode",
29	"synchronous",
30	"locking_mode",
31	"cache_spill",
32	"auto_vacuum",
33	"journal_size_limit",
34];
35
36#[derive(Clone, Copy, Debug)]
37pub struct ConnectionGuard {
38	/// The database's largest size in pages.
39	pub max_page_count: u32,
40	/// The largest string or blob in bytes.
41	pub max_length_bytes: i32,
42	/// Progress callbacks one command may take before SQLite interrupts it. Each callback is
43	/// `PROGRESS_INTERVAL` virtual machine instructions.
44	pub max_progress_callbacks: u64,
45	/// The most logical bytes one statement's result may hold. Each value costs 8 bytes plus
46	/// its text or blob length, and the statement stops as soon as its result passes the cap.
47	pub max_result_bytes: u64,
48}
49
50impl Default for ConnectionGuard {
51	fn default() -> Self {
52		Self {
53			max_page_count: 65_536,
54			max_length_bytes: 24 * 1024 * 1024,
55			max_progress_callbacks: 100_000,
56			max_result_bytes: 24 * 1024 * 1024,
57		}
58	}
59}
60
61/// A guard installed on one open connection. It must outlive the connection, because SQLite
62/// keeps a pointer to it for the progress callback.
63pub(crate) struct InstalledGuard {
64	config: ConnectionGuard,
65	progress_callbacks: AtomicU64,
66}
67
68impl InstalledGuard {
69	pub(crate) fn new(config: ConnectionGuard) -> Box<Self> {
70		Box::new(Self {
71			config,
72			progress_callbacks: AtomicU64::new(0),
73		})
74	}
75
76	/// Sets the size cap, then installs the length limit, the progress handler and the
77	/// authorizer. The size cap is set first because the authorizer then refuses to change it.
78	pub(crate) fn install(&self, db: *mut sqlite3) -> Result<()> {
79		crate::vfs::sqlite_exec(
80			db,
81			&format!("PRAGMA max_page_count = {};", self.config.max_page_count),
82		)
83		.map_err(anyhow::Error::msg)?;
84		unsafe {
85			sqlite3_limit(db, SQLITE_LIMIT_LENGTH, self.config.max_length_bytes);
86			sqlite3_progress_handler(
87				db,
88				PROGRESS_INTERVAL,
89				Some(progress),
90				self as *const Self as *mut c_void,
91			);
92		}
93		let rc = unsafe { sqlite3_set_authorizer(db, Some(authorize), std::ptr::null_mut()) };
94		ensure!(
95			rc == SQLITE_OK,
96			"failed to install the SQLite authorizer: {rc}"
97		);
98		Ok(())
99	}
100
101	/// The result byte cap for each statement on this connection.
102	pub(crate) fn max_result_bytes(&self) -> u64 {
103		self.config.max_result_bytes
104	}
105
106	/// Starts a new command's work budget.
107	pub(crate) fn reset(&self) {
108		self.progress_callbacks.store(0, Ordering::Relaxed);
109	}
110}
111
112extern "C" fn progress(context: *mut c_void) -> c_int {
113	let guard = unsafe { &*(context as *const InstalledGuard) };
114	let callbacks = guard.progress_callbacks.fetch_add(1, Ordering::Relaxed) + 1;
115	c_int::from(callbacks > guard.config.max_progress_callbacks)
116}
117
118extern "C" fn authorize(
119	_context: *mut c_void,
120	action: c_int,
121	first: *const c_char,
122	second: *const c_char,
123	_database: *const c_char,
124	_trigger: *const c_char,
125) -> c_int {
126	let text = |value: *const c_char| -> Option<String> {
127		if value.is_null() {
128			return None;
129		}
130		Some(
131			unsafe { CStr::from_ptr(value) }
132				.to_string_lossy()
133				.into_owned(),
134		)
135	};
136	match action {
137		// An empty filename attaches a private temporary database, which stays in process.
138		SQLITE_ATTACH if text(first).is_some_and(|name| !name.is_empty()) => SQLITE_DENY,
139		SQLITE_PRAGMA if second.is_null() => SQLITE_OK,
140		SQLITE_PRAGMA => {
141			let name = text(first).unwrap_or_default().to_ascii_lowercase();
142			if DENIED_PRAGMA_WRITES.contains(&name.as_str()) {
143				SQLITE_DENY
144			} else {
145				SQLITE_OK
146			}
147		}
148		_ => SQLITE_OK,
149	}
150}