From 3c3f0731dbb9d738442ab04814e653fd728e82ac Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Thu, 9 Oct 2025 13:55:21 +0200 Subject: [PATCH 01/17] Set SQLITE_OPEN_FULLMUTEX when opening a connection (cherry picked from commit 30f4d57767bcdff2f728f496607980f6df091e33) --- libsql/src/local/connection.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index 4c54f5acd0..ca1601a6f3 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -51,6 +51,7 @@ impl Connection { pub(crate) fn connect(db: &Database) -> Result { let mut raw = std::ptr::null_mut(); let db_path = db.db_path.clone(); + let flags: c_int = ffi::SQLITE_OPEN_FULLMUTEX | db.flags.bits(); let err = unsafe { ffi::sqlite3_open_v2( std::ffi::CString::new(db_path.as_str()) @@ -58,7 +59,7 @@ impl Connection { .as_c_str() .as_ptr() as *const _, &mut raw, - db.flags.bits() as c_int, + flags, std::ptr::null(), ) }; From b4c4c9dbc43ad8df762087f4e0fb9c7aa3b02d15 Mon Sep 17 00:00:00 2001 From: Bart Zonneveld Date: Tue, 8 Sep 2026 13:52:35 +0200 Subject: [PATCH 02/17] Exempt GET /metrics from admin API auth The admin listener serves both the admin API and Prometheus metrics. Annotation-based scraping cannot attach credentials, so guarding /metrics behind LIBSQL_ADMIN_AUTH_KEY would blind the fleet. Keep GET /metrics open; all other admin routes remain guarded. (cherry picked from commit 96b0b7fcac904a5d219b11d105cd224dbb12dd63) --- libsql-server/src/http/admin/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index 6953696312..2d8de1cdd1 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -212,6 +212,10 @@ async fn auth_middleware( next: Next, ) -> Result { if let Some(ref auth) = auth { + if request.method() == hyper::Method::GET && request.uri().path() == "/metrics" { + return Ok(next.run(request).await); + } + let Some(auth_header) = request.headers().get("authorization") else { return Err(StatusCode::UNAUTHORIZED); }; From 739bfaff628603f325841f0b829464ac1636852a Mon Sep 17 00:00:00 2001 From: Bart Zonneveld Date: Tue, 8 Sep 2026 16:06:26 +0200 Subject: [PATCH 03/17] Commit rust_suite lockfile with idna_adapter 1.1.0 The Extensions Tests job fresh-resolves rust_suite deps on every run; icu 2.3.0 (via idna_adapter 1.2) now requires rustc 1.88 while the repo pins 1.85.0, failing CI on any branch of this tag. Pinning idna_adapter to 1.1.0 (unicode-rs backend) drops icu from the graph. (cherry picked from commit e6a394a75f8494e90ff65de5edd3cc8e68bbdc9d) --- libsql-sqlite3/test/rust_suite/Cargo.lock | 2550 +++++++++++++++++++++ 1 file changed, 2550 insertions(+) create mode 100644 libsql-sqlite3/test/rust_suite/Cargo.lock diff --git a/libsql-sqlite3/test/rust_suite/Cargo.lock b/libsql-sqlite3/test/rust_suite/Cargo.lock new file mode 100644 index 0000000000..36639e9309 --- /dev/null +++ b/libsql-sqlite3/test/rust_suite/Cargo.lock @@ -0,0 +1,2550 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.8.56", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-fs-ext" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc48200a1a0fa6fba138b1802ad7def18ec1cdd92f7b2a04e21f1bd887f7b9" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 1.0.11", + "windows-sys 0.48.0", +] + +[[package]] +name = "cap-primitives" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b6df5b295dca8d56f35560be8c391d59f0420f72e546997154e24e765e6451" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 1.0.11", + "ipnet", + "maybe-owned", + "rustix 0.37.28", + "windows-sys 0.48.0", + "winx 0.35.1", +] + +[[package]] +name = "cap-rand" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25555efacb0b5244cf1d35833d55d21abc916fff0eaad254b8e2453ea9b8ab" +dependencies = [ + "ambient-authority", + "rand", +] + +[[package]] +name = "cap-std" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3373a62accd150b4fcba056d4c5f3b552127f0ec86d3c8c102d60b978174a012" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 1.0.11", + "rustix 0.37.28", +] + +[[package]] +name = "cap-time-ext" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95002993b7baee6b66c8950470e59e5226a23b3af39fc59c47fe416dd39821a" +dependencies = [ + "cap-primitives", + "once_cell", + "rustix 0.37.28", + "winx 0.35.1", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob 0.3.4", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "182b82f78049f54d3aee5a19870d356ef754226665a695ce2fcdd5d55379718e" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c027bf04ecae5b048d3554deb888061bc26f426afff47bf06d6ac933dce0a6" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.13.2", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649f70038235e4c81dba5680d7e5ae83e1081f567232425ab98b55b03afd9904" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d1c5ee2611c6a0bdc8d42d5d3dc5ce8bf53a8040561e26e88b9b21f966417" + +[[package]] +name = "cranelift-control" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da66a68b1f48da863d1d53209b8ddb1a6236411d2d72a280ffa8c2f734f7219e" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd897422dbb66621fa558f4d9209875530c53e3c8f4b13b2849fbb667c431a6" +dependencies = [ + "serde", +] + +[[package]] +name = "cranelift-frontend" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05db883114c98cfcd6959f72278d2fec42e01ea6a6982cfe4f20e88eebe86653" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84559de86e2564152c87e299c8b2559f9107e9c6d274b24ebeb04fb0a5f4abf8" + +[[package]] +name = "cranelift-native" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f40b57f187f0fe1ffaf281df4adba2b4bc623a0f6651954da9f3c184be72761" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.96.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3eab6084cc789b9dd0b1316241efeb2968199fee709f4bb4fe0fb0923bb468b" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools", + "log", + "smallvec", + "wasmparser 0.103.0", + "wasmtime-types", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "file-per-thread-logger" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f2e425d9790201ba4af4630191feac6dcc98765b118d4d18e91d23c2353866" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d167b646a876ba8fda6b50ac645cfd96242553cbaf0ca4fccaa39afcbf0801f" +dependencies = [ + "io-lifetimes 1.0.11", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags 2.13.1", + "debugid", + "fxhash", + "serde", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +dependencies = [ + "fallible-iterator", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279259b0ac81c89d11c290495fdcfa96ea3643b7df311c138b6fe8ca5237f0f8" +dependencies = [ + "idna_mapping", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna_mapping" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c13906586a4b339310541a274dd927aff6fcbb5b8e3af90634c4b31681c792" +dependencies = [ + "unicode-joining-type", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "io-extras" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde93d48f0d9277f977a333eca8313695ddd5301dc96f7e02aeddcb0dd99096f" +dependencies = [ + "io-lifetimes 1.0.11", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.3", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a5c0b993601cad796222ea076565c5d9f337d35592f8622c753724f06d7271" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7b5e473765060536a660eed127f758cf1a810c73e49063264959c60d1727d9" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + +[[package]] +name = "libsql-ffi" +version = "0.9.30" +dependencies = [ + "bindgen", + "cc", + "cmake", + "glob 0.3.4", + "libsql-wasmtime-bindings", +] + +[[package]] +name = "libsql-rusqlite" +version = "0.9.30" +dependencies = [ + "bitflags 2.13.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsql-ffi", + "smallvec", +] + +[[package]] +name = "libsql-sys" +version = "0.9.30" +dependencies = [ + "bytes", + "libsql-ffi", + "libsql-rusqlite", + "once_cell", + "tracing", + "zerocopy 0.7.35", +] + +[[package]] +name = "libsql-wasmtime-bindings" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4794ff21e37f83839dad45f8c7977b071315f18705cf73badc9850b9fb6b6f" +dependencies = [ + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "libsql_rust_suite" +version = "0.2.0" +dependencies = [ + "hex", + "home", + "itertools", + "libsql-sys", + "psm", + "rustc-hash", + "tempfile", + "wabt", + "which", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "crc32fast", + "hashbrown 0.13.2", + "indexmap 1.9.3", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.56", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regalloc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a52e724646c6c0800fc456ec43b4165d2f91fba88ceaca06d9e0b400023478" +dependencies = [ + "hashbrown 0.13.2", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes 1.0.11", + "itoa", + "libc", + "linux-raw-sys 0.3.8", + "once_cell", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-interface" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10081a99cbecbc363d381b9503563785f0b02735fccbb0d4c1a2cb3d39f7e7fe" +dependencies = [ + "bitflags 2.13.1", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes 2.0.4", + "rustix 0.38.44", + "windows-sys 0.48.0", + "winx 0.36.4", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tinyvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-joining-type" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d00a78170970967fdb83f9d49b92f959ab2bb829186b113e4f4604ad98e180" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wabt" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b5f5d6984ca42df66280baa8a15ac188a173ddaf4580b574a98931c01920e7" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "wabt-sys", +] + +[[package]] +name = "wabt-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b064c81821100adb4b71923cecfc67fef083db21c3bbd454b0162c7ffe63eeaa" +dependencies = [ + "cc", + "cmake", + "glob 0.2.11", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi-cap-std-sync" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d29c5da3b5cfc9212a7fa824224875cb67fb89d2a8392db655e4c59b8ab2ae7" +dependencies = [ + "anyhow", + "async-trait", + "cap-fs-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "io-extras", + "io-lifetimes 1.0.11", + "is-terminal", + "once_cell", + "rustix 0.37.28", + "system-interface", + "tracing", + "wasi-common", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasi-common" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bd905dcec1448664bf63d42d291cbae0feeea3ad41631817b8819e096d76bd" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "cap-rand", + "cap-std", + "io-extras", + "log", + "rustix 0.37.28", + "thiserror", + "tracing", + "wasmtime", + "wiggle", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.0", +] + +[[package]] +name = "wasmparser" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c437373cac5ea84f1113d648d51f71751ffbe3d90c00ae67618cf20d0b5ee7b" +dependencies = [ + "indexmap 1.9.3", + "url", +] + +[[package]] +name = "wasmparser" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +dependencies = [ + "bitflags 2.13.1", + "indexmap 2.14.2", + "semver", +] + +[[package]] +name = "wasmtime" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634357e8668774b24c80b210552f3f194e2342a065d6d83845ba22c5817d0770" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bumpalo", + "cfg-if", + "fxprof-processed-profile", + "indexmap 1.9.3", + "libc", + "log", + "object", + "once_cell", + "paste", + "psm", + "rayon", + "serde", + "serde_json", + "target-lexicon", + "wasmparser 0.103.0", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit", + "wasmtime-runtime", + "wat", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33c73c24ce79b0483a3b091a9acf88871f4490b88998e8974b22236264d304c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107809b2d9f5b2fd3ddbaddb3bb92ff8048b62f4030debf1408119ffd38c6cb" +dependencies = [ + "anyhow", + "base64", + "bincode", + "directories-next", + "file-per-thread-logger", + "log", + "rustix 0.37.28", + "serde", + "sha2", + "toml", + "windows-sys 0.48.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ba489850d9c91c6c5b9e1696ee89e7a69d9796236a005f7e9131b6746e13b6" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-component-util" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa88f9e77d80f828c9d684741a9da649366c6d1cceb814755dd9cab7112d1d1" + +[[package]] +name = "wasmtime-cranelift" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5800616a28ed6bd5e8b99ea45646c956d798ae030494ac0689bc3e45d3b689c1" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "gimli", + "log", + "object", + "target-lexicon", + "thiserror", + "wasmparser 0.103.0", + "wasmtime-cranelift-shared", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-cranelift-shared" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e4030b959ac5c5d6ee500078977e813f8768fa2b92fc12be01856cd0c76c55" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-native", + "gimli", + "object", + "target-lexicon", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-environ" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec815d01a8d38aceb7ed4678f9ba551ae6b8a568a63810ac3ad9293b0fd01c8" +dependencies = [ + "anyhow", + "cranelift-entity", + "gimli", + "indexmap 1.9.3", + "log", + "object", + "serde", + "target-lexicon", + "thiserror", + "wasmparser 0.103.0", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-fiber" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c5127908fdf720614891ec741c13dd70c844e102caa393e2faca1ee68e9bfb" +dependencies = [ + "cc", + "cfg-if", + "rustix 0.37.28", + "wasmtime-asm-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-jit" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2712eafe829778b426cad0e1769fef944898923dd29f0039e34e0d53ba72b234" +dependencies = [ + "addr2line", + "anyhow", + "bincode", + "cfg-if", + "cpp_demangle", + "gimli", + "ittapi", + "log", + "object", + "rustc-demangle", + "serde", + "target-lexicon", + "wasmtime-environ", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fb78eacf4a6e47260d8ef8cc81ea8ddb91397b2e848b3fb01567adebfe89b5" +dependencies = [ + "object", + "once_cell", + "rustix 0.37.28", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1364900b05f7d6008516121e8e62767ddb3e176bdf4c84dfa85da1734aeab79" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a16ffe4de9ac9669175c0ea5c6c51ffc596dfb49320aaa6f6c57eff58cef069" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "indexmap 1.9.3", + "libc", + "log", + "mach", + "memfd", + "memoffset", + "paste", + "rand", + "rustix 0.37.28", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "windows-sys 0.48.0", +] + +[[package]] +name = "wasmtime-types" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19961c9a3b04d5e766875a5c467f6f5d693f508b3e81f8dc4a1444aa94f041c9" +dependencies = [ + "cranelift-entity", + "serde", + "thiserror", + "wasmparser 0.103.0", +] + +[[package]] +name = "wasmtime-wasi" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21080ff62878f1d7c53d9571053dbe96552c0f982f9f29eac65ea89974fabfd7" +dependencies = [ + "anyhow", + "libc", + "wasi-cap-std-sync", + "wasi-common", + "wasmtime", + "wiggle", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421f0d16cc5c612b35ae53a0be3d3124c72296f18e5be3468263c745d56d37ab" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "258.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7555c008cca87f2ac58d9f83ccda7e7b44611093ce28eb28f052e7c78024b9bf" +dependencies = [ + "wast 258.0.0", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "wiggle" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34e40b7b17a920d03449ca78b0319984379eed01a9a11c1def9c3d3832d85a" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 1.3.2", + "thiserror", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda132eaa84fe5f15d23a55a912f8417385aee65d0141d78a3b65e46201ed" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn 1.0.109", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ca1a344a0ba781e2a94b27be5bb78f23e43d52336bd663b810d49d7189ad334" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winx" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c52a121f0fbf9320d5f2a9a5d82f6cb7557eda5e8b47fc3e7f359ec866ae960" +dependencies = [ + "bitflags 1.3.2", + "io-lifetimes 1.0.11", + "windows-sys 0.48.0", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-parser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca2581061573ef6d1754983d7a9b3ed5871ef859d52708ea9a0f5af32919172" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 1.9.3", + "log", + "pulldown-cmark", + "unicode-xid", + "url", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror", + "wast 35.0.2", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive 0.8.56", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] From 2250017bd38eb35a3cfcc624a5accb9b81a2bb2d Mon Sep 17 00:00:00 2001 From: Si Le Date: Thu, 21 May 2026 15:53:05 -0400 Subject: [PATCH 04/17] Add diagnostic logging for non-gRPC HTTP responses Intercepts raw HTTP responses before tonic-web gRPC-web framing to capture the actual response body when the server returns a non-gRPC response. This helps diagnose 'Invalid header bit N expected 0 or 1' errors by logging the HTTP status, content-type, and response body preview. (cherry picked from commit 11e180eadd9bd483fd55258219cb50078a12947a) (cherry picked from commit 1691763b33e5d39e1deeed7c9336fd14c7821226) --- libsql/src/replication/client.rs | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/libsql/src/replication/client.rs b/libsql/src/replication/client.rs index 90a7ab4565..330306a526 100644 --- a/libsql/src/replication/client.rs +++ b/libsql/src/replication/client.rs @@ -140,6 +140,7 @@ impl GrpcChannel { .pool_idle_timeout(None) .pool_max_idle_per_host(3) .build(connector); + let client = DiagnosticResponseService::new(client); let client = GrpcWebClientService::new(client); let classifier = GrpcErrorsAsFailures::new().with_success(GrpcCode::FailedPrecondition); @@ -183,6 +184,76 @@ impl Service> for GrpcChannel { } } +/// Diagnostic middleware that intercepts raw HTTP responses before gRPC-web +/// framing. When the response status is not 200 or the content-type is not +/// grpc-web, it buffers and logs the response body so we can diagnose +/// "Invalid header bit N" errors from tonic-web. +#[derive(Clone)] +struct DiagnosticResponseService { + inner: S, +} + +impl DiagnosticResponseService { + fn new(inner: S) -> Self { + Self { inner } + } +} + +impl Service> for DiagnosticResponseService +where + S: Service, Response = http::Response, Error = hyper::Error> + + Clone + + Send + + 'static, + S::Future: Send + 'static, + ReqBody: Send + 'static, +{ + type Response = http::Response; + type Error = hyper::Error; + type Future = + Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let uri = req.uri().clone(); + let fut = self.inner.call(req); + Box::pin(async move { + let resp = fut.await?; + let status = resp.status(); + let content_type = resp + .headers() + .get(http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + let is_grpc = content_type.contains("grpc"); + + if status != http::StatusCode::OK || !is_grpc { + // Buffer the body to log it, then re-create the response + let (parts, body) = resp.into_parts(); + let body_bytes = hyper::body::to_bytes(body).await.unwrap_or_default(); + let preview_len = std::cmp::min(body_bytes.len(), 1024); + let body_preview = String::from_utf8_lossy(&body_bytes[..preview_len]); + tracing::warn!( + status = %status, + uri = %uri, + content_type = %content_type, + body_len = body_bytes.len(), + body_preview = %body_preview, + "[libsql diagnostic] non-gRPC HTTP response — will cause 'Invalid header bit' error" + ); + Ok(http::Response::from_parts(parts, hyper::Body::from(body_bytes))) + } else { + tracing::trace!(status = %status, uri = %uri, "[libsql diagnostic] gRPC response OK"); + Ok(resp) + } + }) + } +} + #[derive(Clone)] /// Contains token and namespace headers to append to every request. pub struct GrpcInterceptor { From 2aa1300cf5947dc3c62ff831cd1bbf51de79af01 Mon Sep 17 00:00:00 2001 From: Aryan Suvarna Date: Mon, 28 Sep 2026 15:52:09 -0400 Subject: [PATCH 05/17] Fix out-of-bounds read when formatting overlong vector text elements vectorParseSqliteText stores each vector element in a 1025-byte stack buffer whose final byte must remain NUL. The length guard used `>`, so a 1025-character element overwrote that terminator before the guard fired; the following error path then formatted the buffer with `%s`, reading past the end of the stack buffer. Reject the element once it reaches MAX_FLOAT_CHAR_SZ characters so the terminator is preserved, regenerate the bundled amalgamations, and add boundary regression tests for every vector function that parses TEXT. (cherry picked from commit 553cb83a9c1349eaff48d41f6b9866cb60c3469e) (cherry picked from commit bf91f74a79ad295ef05d131db4fc3dbc819992ff) --- .../SQLite3MultipleCiphers/src/sqlite3.c | 2 +- libsql-ffi/bundled/src/sqlite3.c | 2 +- libsql-sqlite3/src/vector.c | 2 +- libsql-sqlite3/test/libsql_vector.test | 52 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c index 62a917fb9b..ec643b2198 100644 --- a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c +++ b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c @@ -214930,7 +214930,7 @@ static int vectorParseSqliteText( continue; } if( this != ',' && this != ']' ){ - if( iBuf > MAX_FLOAT_CHAR_SZ ){ + if( iBuf >= MAX_FLOAT_CHAR_SZ ){ *pzErrMsg = sqlite3_mprintf("vector: float string length exceeded %d characters: '%s'", MAX_FLOAT_CHAR_SZ, valueBuf); goto error; } diff --git a/libsql-ffi/bundled/src/sqlite3.c b/libsql-ffi/bundled/src/sqlite3.c index 62a917fb9b..ec643b2198 100644 --- a/libsql-ffi/bundled/src/sqlite3.c +++ b/libsql-ffi/bundled/src/sqlite3.c @@ -214930,7 +214930,7 @@ static int vectorParseSqliteText( continue; } if( this != ',' && this != ']' ){ - if( iBuf > MAX_FLOAT_CHAR_SZ ){ + if( iBuf >= MAX_FLOAT_CHAR_SZ ){ *pzErrMsg = sqlite3_mprintf("vector: float string length exceeded %d characters: '%s'", MAX_FLOAT_CHAR_SZ, valueBuf); goto error; } diff --git a/libsql-sqlite3/src/vector.c b/libsql-sqlite3/src/vector.c index 51f8af5d05..ee1c025528 100644 --- a/libsql-sqlite3/src/vector.c +++ b/libsql-sqlite3/src/vector.c @@ -217,7 +217,7 @@ static int vectorParseSqliteText( continue; } if( this != ',' && this != ']' ){ - if( iBuf > MAX_FLOAT_CHAR_SZ ){ + if( iBuf >= MAX_FLOAT_CHAR_SZ ){ *pzErrMsg = sqlite3_mprintf("vector: float string length exceeded %d characters: '%s'", MAX_FLOAT_CHAR_SZ, valueBuf); goto error; } diff --git a/libsql-sqlite3/test/libsql_vector.test b/libsql-sqlite3/test/libsql_vector.test index 793358e068..3cfe69fedc 100644 --- a/libsql-sqlite3/test/libsql_vector.test +++ b/libsql-sqlite3/test/libsql_vector.test @@ -201,6 +201,56 @@ do_execsql_test vector-1-conversion-f8 { {[-20,-35.25,1.0625,1.63281,2.20313,2.76563,10.1875,99.5,104.5,110]} A0C10DC2883FD13F0D4031402341C742D142DC4206 } +foreach {name sql} { + vector {SELECT vector_extract(vector($input)) = vector_extract(vector('[1]'))} + vector32 {SELECT vector_extract(vector32($input)) = vector_extract(vector32('[1]'))} + vector64 {SELECT vector_extract(vector64($input)) = vector_extract(vector64('[1]'))} + vector8 {SELECT vector_extract(vector8($input)) = vector_extract(vector8('[1]'))} + vector16 {SELECT vector_extract(vector16($input)) = vector_extract(vector16('[1]'))} + vectorb16 {SELECT vector_extract(vectorb16($input)) = vector_extract(vectorb16('[1]'))} + vector1bit {SELECT vector_extract(vector1bit($input)) = vector_extract(vector1bit('[1]'))} + extract {SELECT vector_extract($input) = '[1]'} + cos-left {SELECT vector_distance_cos($input, '[1]') = 0} + cos-right {SELECT vector_distance_cos('[1]', $input) = 0} + l2-left {SELECT vector_distance_l2($input, '[1]') = 0} + l2-right {SELECT vector_distance_l2('[1]', $input) = 0} +} { + foreach length {1023 1024} { + set element "[string repeat 0 [expr {$length - 1}]]1" + set input [format {[%s]} $element] + do_execsql_test vector-1-text-length-$name-$length $sql {1} + } + + foreach length {1025 1026 5000} { + set input [format {[%s]} [string repeat 1 $length]] + do_catchsql_test vector-1-text-length-$name-$length $sql [list 1 \ + "vector: float string length exceeded 1024 characters: '[string repeat 1 1024]'" + ] + } + + set element "1[string repeat x 1023]" + set input [format {[%s]} $element] + do_catchsql_test vector-1-text-length-$name-invalid-1024 $sql [list 1 \ + "vector: invalid float at position 0: '$element'" + ] + + set input [format {[%sx]} $element] + do_catchsql_test vector-1-text-length-$name-invalid-1025 $sql [list 1 \ + "vector: float string length exceeded 1024 characters: '$element'" + ] +} + +set element "[string repeat 0 1023]1" +set input [format {[%s,%s]} $element $element] +do_execsql_test vector-1-text-length-multiple-elements { + SELECT vector_extract(vector($input)); +} {{[1,1]}} + +set input [format {[1,%sx]} $element] +do_catchsql_test vector-1-text-length-invalid-second-element { + SELECT vector($input); +} [list 1 "vector: float string length exceeded 1024 characters: '$element'"] + proc error_messages {sql} { set ret "" set stmt [sqlite3_prepare db $sql -1 dummy] @@ -239,3 +289,5 @@ do_test vector-1-func-errors { {vector_distance: vectors must have the same type: 1 != 2} {vector_distance: l2 distance is not supported for float1bit vectors} }] + +finish_test From 6f0873e02faf53676ce3a14e2d401627447baa49 Mon Sep 17 00:00:00 2001 From: Aryan Suvarna Date: Tue, 29 Sep 2026 19:55:44 -0400 Subject: [PATCH 06/17] Bump pinned Rust toolchain to 1.98.1 libsql-sqlite3/test/rust_suite has no committed Cargo.lock, so CI resolves its transitive dependencies fresh on every run. Several of those now require a newer compiler than the pinned 1.85.0 (icu_* and wasm-encoder/wast declare rust-version 1.88; yoke-derive 0.8.3 declares none but uses str::from_utf8 as an inherent method, stabilized in 1.87). This breaks the Extensions Tests job and the rusttestwasm step of make-sqlite3 on main. Pin current stable (1.98.1) rather than the minimum that compiles today (1.88.0, verified green in CI), so crate MSRV bumps do not break CI again in the near term. Document the constraint next to the unlocked test crate. (cherry picked from commit e48d754dbe05818f15c58f2cb794eef842250cde) (cherry picked from commit a78b64c5ca417a9b1e8e41d57efb968c5870731d) --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c2324b9b48..2449619493 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.85.0" +channel = "1.98.1" From 199c6273a4bdf16a52ee299c9f6614093b4e586e Mon Sep 17 00:00:00 2001 From: Aryan Suvarna Date: Wed, 30 Sep 2026 10:33:25 -0400 Subject: [PATCH 07/17] Fix lints surfaced by Rust 1.98.1 under -D warnings CI compiles with RUSTFLAGS="-D warnings", so lints added since 1.85.0 fail the build: - mismatched_lifetime_syntaxes (new in 1.89): eleven signatures elide a lifetime on the input side (&self / &str) but hide it on the output type (Vec, PageHdrIter, CursorStep, Cow). Spell the output lifetime as '_ as the compiler suggests. No semantic change; this is the lifetime rustc already inferred. - unused_assignments: `frameno` in bottomless-cli's restore loop was only ever copied into BatchReader::new and then incremented, never read. BatchReader tracks its own next_frame_no and the function returns the separate last_received_frame_no, so the local was dead since it was introduced in 4a71b2072a. Remove it and pass first_frame_no directly. Verified locally on 1.98.1 with the same flags as CI: cargo check --all-targets --all-features, cargo fmt --check, and cargo check -p libsql --no-default-features for core/replication/remote. (cherry picked from commit c22cc620e8378bced5c3c0f4fa24d95a14756387) (cherry picked from commit f35e15e23338ab91306c69fa0be6e05738888738) --- bottomless-cli/src/replicator_extras.rs | 4 +--- libsql-server/src/query_analysis.rs | 2 +- libsql-sys/src/wal/mod.rs | 2 +- libsql/src/hrana/cursor.rs | 2 +- libsql/src/hrana/hyper.rs | 2 +- libsql/src/local/impls.rs | 2 +- libsql/src/local/statement.rs | 2 +- libsql/src/replication/connection.rs | 2 +- libsql/src/statement.rs | 4 ++-- libsql/src/sync/statement.rs | 2 +- vendored/rusqlite/src/column.rs | 2 +- 11 files changed, 12 insertions(+), 14 deletions(-) diff --git a/bottomless-cli/src/replicator_extras.rs b/bottomless-cli/src/replicator_extras.rs index 94670bd07f..9586020c54 100644 --- a/bottomless-cli/src/replicator_extras.rs +++ b/bottomless-cli/src/replicator_extras.rs @@ -382,9 +382,8 @@ impl Replicator { let frame = tokio::fs::File::open(&obj).await?; let frame_buf_reader = BufReader::new(frame); - let mut frameno = first_frame_no; let mut reader = bottomless::read::BatchReader::new( - frameno, + first_frame_no, frame_buf_reader, page_size as usize, compression_kind, @@ -411,7 +410,6 @@ impl Replicator { ); pending_pages.flush(db).await?; } - frameno += 1; last_received_frame_no += 1; } db.flush().await?; diff --git a/libsql-server/src/query_analysis.rs b/libsql-server/src/query_analysis.rs index 5762b86d4c..5478047eba 100644 --- a/libsql-server/src/query_analysis.rs +++ b/libsql-server/src/query_analysis.rs @@ -233,7 +233,7 @@ impl StmtKind { } } -fn to_ascii_lower(s: &str) -> Cow { +fn to_ascii_lower(s: &str) -> Cow<'_, str> { if s.chars().all(|c| char::is_ascii_lowercase(&c)) { Cow::Borrowed(s) } else { diff --git a/libsql-sys/src/wal/mod.rs b/libsql-sys/src/wal/mod.rs index 11f9a5260f..7e70ee39a6 100644 --- a/libsql-sys/src/wal/mod.rs +++ b/libsql-sys/src/wal/mod.rs @@ -131,7 +131,7 @@ impl PageHeaders { Self { inner } } - pub fn iter(&self) -> PageHdrIter { + pub fn iter(&self) -> PageHdrIter<'_> { // TODO: move LIBSQL_PAGE_SIZE PageHdrIter::new(self.as_ptr(), 4096) } diff --git a/libsql/src/hrana/cursor.rs b/libsql/src/hrana/cursor.rs index aa0b1191b1..4aa62c9832 100644 --- a/libsql/src/hrana/cursor.rs +++ b/libsql/src/hrana/cursor.rs @@ -147,7 +147,7 @@ where }) } - pub async fn next_step(&mut self) -> Result> { + pub async fn next_step(&mut self) -> Result> { CursorStep::new(self).await } diff --git a/libsql/src/hrana/hyper.rs b/libsql/src/hrana/hyper.rs index 300602c27e..3ea1ad631e 100644 --- a/libsql/src/hrana/hyper.rs +++ b/libsql/src/hrana/hyper.rs @@ -276,7 +276,7 @@ impl crate::statement::Stmt for crate::hrana::Statement { self.cols.len() } - fn columns(&self) -> Vec { + fn columns(&self) -> Vec> { //FIXME: there are several blockers here: // 1. We cannot know the column types before sending a query, so this method will never return results right // away. diff --git a/libsql/src/local/impls.rs b/libsql/src/local/impls.rs index b86405610e..5714d4761a 100644 --- a/libsql/src/local/impls.rs +++ b/libsql/src/local/impls.rs @@ -160,7 +160,7 @@ impl Stmt for LibsqlStmt { self.0.column_count() } - fn columns(&self) -> Vec { + fn columns(&self) -> Vec> { self.0.columns() } } diff --git a/libsql/src/local/statement.rs b/libsql/src/local/statement.rs index c31e751734..e8e0b415e1 100644 --- a/libsql/src/local/statement.rs +++ b/libsql/src/local/statement.rs @@ -339,7 +339,7 @@ impl Statement { /// If associated DB schema can be altered concurrently, you should make /// sure that current statement has already been stepped once before /// calling this method. - pub fn columns(&self) -> Vec { + pub fn columns(&self) -> Vec> { let n = self.column_count(); let mut cols = Vec::with_capacity(n); for i in 0..n { diff --git a/libsql/src/replication/connection.rs b/libsql/src/replication/connection.rs index 418ae03465..e21d01511e 100644 --- a/libsql/src/replication/connection.rs +++ b/libsql/src/replication/connection.rs @@ -797,7 +797,7 @@ impl Stmt for RemoteStatement { } } - fn columns(&self) -> Vec { + fn columns(&self) -> Vec> { if let Some(stmt) = self.local_statement.as_ref() { return stmt.columns(); } diff --git a/libsql/src/statement.rs b/libsql/src/statement.rs index 861fdf8023..e5704f42fe 100644 --- a/libsql/src/statement.rs +++ b/libsql/src/statement.rs @@ -24,7 +24,7 @@ pub(crate) trait Stmt { fn column_count(&self) -> usize; - fn columns(&self) -> Vec; + fn columns(&self) -> Vec>; } /// A cached prepared statement. @@ -103,7 +103,7 @@ impl Statement { } /// Fetch the list of columns for the prepared statement. - pub fn columns(&self) -> Vec { + pub fn columns(&self) -> Vec> { self.inner.columns() } } diff --git a/libsql/src/sync/statement.rs b/libsql/src/sync/statement.rs index b3de1338ef..7b31577672 100644 --- a/libsql/src/sync/statement.rs +++ b/libsql/src/sync/statement.rs @@ -67,7 +67,7 @@ impl Stmt for SyncedStatement { self.inner.column_count() } - fn columns(&self) -> Vec { + fn columns(&self) -> Vec> { self.inner.columns() } } diff --git a/vendored/rusqlite/src/column.rs b/vendored/rusqlite/src/column.rs index 4413a62bcb..64c1cac9c0 100644 --- a/vendored/rusqlite/src/column.rs +++ b/vendored/rusqlite/src/column.rs @@ -136,7 +136,7 @@ impl Statement<'_> { /// calling this method. #[cfg(feature = "column_decltype")] #[cfg_attr(docsrs, doc(cfg(feature = "column_decltype")))] - pub fn columns(&self) -> Vec { + pub fn columns(&self) -> Vec> { let n = self.column_count(); let mut cols = Vec::with_capacity(n); for i in 0..n { From e216a5d1d4759acacbe1b0bfde3f45abf3b3a6e1 Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Tue, 6 Oct 2026 18:46:32 +0200 Subject: [PATCH 08/17] Fix invalid UTF-8 handling in SQLite parser Port gwenn/lemon-rs@14f422a to replace the unchecked UTF-8 conversion with a lossy conversion. Bump the vendored parser to 0.13.1 and add regression coverage for invalid input.\n\nAddresses CVE-2025-47736 / GHSA-8m95-fffc-h4c5. (cherry picked from commit 4feb2b2e4637b3e80e71cf85cda40c417516ca00) --- Cargo.lock | 2 +- vendored/sqlite3-parser/Cargo.toml | 2 +- vendored/sqlite3-parser/src/dialect/mod.rs | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c091acdc86..845243ca71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3114,7 +3114,7 @@ dependencies = [ [[package]] name = "libsql-sqlite3-parser" -version = "0.13.0" +version = "0.13.1" dependencies = [ "bitflags 2.6.0", "cc", diff --git a/vendored/sqlite3-parser/Cargo.toml b/vendored/sqlite3-parser/Cargo.toml index 0381ac1d99..2715330be6 100644 --- a/vendored/sqlite3-parser/Cargo.toml +++ b/vendored/sqlite3-parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libsql-sqlite3-parser" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["gwenn"] description = "SQL parser (as understood by SQLite) (libsql fork)" diff --git a/vendored/sqlite3-parser/src/dialect/mod.rs b/vendored/sqlite3-parser/src/dialect/mod.rs index e0266f10e4..cc06c1a9a0 100644 --- a/vendored/sqlite3-parser/src/dialect/mod.rs +++ b/vendored/sqlite3-parser/src/dialect/mod.rs @@ -58,7 +58,7 @@ impl TokenType { } fn from_bytes(bytes: &[u8]) -> String { - unsafe { str::from_utf8_unchecked(bytes).to_owned() } + String::from_utf8_lossy(bytes).to_string() } include!(concat!(env!("OUT_DIR"), "/keywords.rs")); @@ -403,3 +403,13 @@ impl TokenType { } } } + +#[cfg(test)] +mod tests { + use super::from_bytes; + + #[test] + fn converts_invalid_utf8_lossily() { + assert_eq!(from_bytes(&[0xC0, 0x80]), "��"); + } +} From 76c4f37e060986b5ad02605936b5b9f0350a0649 Mon Sep 17 00:00:00 2001 From: Raphael Lopes Feitoza Date: Wed, 7 Oct 2026 15:39:57 -0400 Subject: [PATCH 09/17] Reject unsafe namespace path components (cherry picked from commit 595c9ea98038b7885d79aeb8dbe9c4121a77a53f) --- libsql-server/src/connection/config.rs | 32 ++++++++--- libsql-server/src/namespace/meta_store.rs | 8 +-- libsql-server/src/namespace/name.rs | 54 +++++++++++++++++-- .../src/replication/replicator_client.rs | 5 +- libsql-server/src/schema/db.rs | 3 +- 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/libsql-server/src/connection/config.rs b/libsql-server/src/connection/config.rs index 970014415d..a75f78fc0e 100644 --- a/libsql-server/src/connection/config.rs +++ b/libsql-server/src/connection/config.rs @@ -65,9 +65,11 @@ impl Default for DatabaseConfig { } } -impl From<&metadata::DatabaseConfig> for DatabaseConfig { - fn from(value: &metadata::DatabaseConfig) -> Self { - DatabaseConfig { +impl TryFrom<&metadata::DatabaseConfig> for DatabaseConfig { + type Error = crate::Error; + + fn try_from(value: &metadata::DatabaseConfig) -> Result { + Ok(DatabaseConfig { block_reads: value.block_reads, block_writes: value.block_writes, block_reason: value.block_reason.clone(), @@ -79,16 +81,16 @@ impl From<&metadata::DatabaseConfig> for DatabaseConfig { allow_attach: value.allow_attach, max_row_size: value.max_row_size.unwrap_or_else(default_max_row_size), is_shared_schema: value.shared_schema.unwrap_or(false), - // namespace name is coming from primary, we assume it's valid shared_schema_name: value .shared_schema_name - .clone() - .map(NamespaceName::new_unchecked), + .as_ref() + .map(|name| NamespaceName::from_string(name.clone())) + .transpose()?, durability_mode: match value.durability_mode { None => DurabilityMode::default(), Some(m) => DurabilityMode::from(metadata::DurabilityMode::try_from(m)), }, - } + }) } } @@ -112,6 +114,22 @@ impl From<&DatabaseConfig> for metadata::DatabaseConfig { } } +#[cfg(test)] +mod namespace_tests { + use super::*; + + #[test] + fn shared_schema_names_are_checked_in_replication_and_json() { + let wire = metadata::DatabaseConfig { + shared_schema_name: Some("../outside".into()), + ..metadata::DatabaseConfig::from(&DatabaseConfig::default()) + }; + assert!(DatabaseConfig::try_from(&wire).is_err()); + let json = r#"{"block_reads":false,"block_writes":false,"block_reason":null,"max_db_pages":100,"heartbeat_url":null,"bottomless_db_id":null,"shared_schema_name":"../outside"}"#; + assert!(serde_json::from_str::(json).is_err()); + } +} + /// Durability mode specifies the `PRAGMA SYNCHRONOUS` setting for the connection #[derive(PartialEq, Clone, Copy, Debug, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] diff --git a/libsql-server/src/namespace/meta_store.rs b/libsql-server/src/namespace/meta_store.rs index 70b419ebe9..2c0916cd77 100644 --- a/libsql-server/src/namespace/meta_store.rs +++ b/libsql-server/src/namespace/meta_store.rs @@ -274,7 +274,7 @@ impl MetaStoreInner { }; let config = match metadata::DatabaseConfig::decode(&v[..]) { - Ok(c) => Arc::new(DatabaseConfig::from(&c)), + Ok(c) => Arc::new(DatabaseConfig::try_from(&c)?), Err(e) => { tracing::warn!("unable to convert config: {}", e); continue; @@ -633,21 +633,21 @@ impl MetaStoreHandle { let config = match fs::read(config_path) { Ok(data) => { let c = metadata::DatabaseConfig::decode(&data[..])?; - DatabaseConfig::from(&c) + DatabaseConfig::try_from(&c)? } Err(err) if err.kind() == io::ErrorKind::NotFound => DatabaseConfig::default(), Err(err) => return Err(Error::IOError(err)), }; Ok(Self { - namespace: NamespaceName::new_unchecked("testmetastore"), + namespace: NamespaceName::from("testmetastore"), inner: HandleState::Internal(Arc::new(Mutex::new(Arc::new(config)))), }) } pub fn internal() -> Self { MetaStoreHandle { - namespace: NamespaceName::new_unchecked("testmetastore"), + namespace: NamespaceName::from("testmetastore"), inner: HandleState::Internal(Arc::new(Mutex::new(Arc::new(DatabaseConfig::default())))), } } diff --git a/libsql-server/src/namespace/name.rs b/libsql-server/src/namespace/name.rs index 51a3eb4905..66dc29a638 100644 --- a/libsql-server/src/namespace/name.rs +++ b/libsql-server/src/namespace/name.rs @@ -1,4 +1,7 @@ -use std::fmt; +use std::{ + fmt, + path::{Component, Path}, +}; use bytes::Bytes; use serde::{de::Visitor, Deserialize}; @@ -45,8 +48,16 @@ impl NamespaceName { } fn validate(s: &str) -> crate::Result<()> { - if s.is_empty() { - tracing::warn!("invalid namespace: empty namespace"); + // Names must be a single path component on both Unix and Windows. + // Keep harmless punctuation and Unicode rather than imposing an identifier alphabet. + let mut components = Path::new(s).components(); + if s.is_empty() + || s.chars().any(|c| matches!(c, '/' | '\\' | '\0')) + || (cfg!(windows) && s.contains(':')) + || !matches!(components.next(), Some(Component::Normal(_))) + || components.next().is_some() + { + tracing::warn!("invalid namespace name"); return Err(crate::error::Error::InvalidNamespace); } @@ -67,9 +78,42 @@ impl NamespaceName { pub fn as_slice(&self) -> &[u8] { &self.0 } +} - pub(crate) fn new_unchecked(s: impl AsRef) -> Self { - Self(Bytes::copy_from_slice(s.as_ref().as_bytes())) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_single_safe_path_components_are_names() { + for name in [ + "", + ".", + "..", + "../victim", + "a/b", + "a\\b", + "/tmp/victim", + "a\0b", + ] { + assert!( + NamespaceName::from_string(name.to_owned()).is_err(), + "{name:?}" + ); + assert!(NamespaceName::from_bytes(Bytes::copy_from_slice(name.as_bytes())).is_err()); + assert!(serde_json::from_str::(&format!("{name:?}")).is_err()); + } + for name in ["tenant", "a..b", "hello world", "café!", "a:b"] { + if cfg!(windows) && name.contains(':') { + continue; + } + assert_eq!( + NamespaceName::from_string(name.to_owned()) + .unwrap() + .as_str(), + name + ); + } } } diff --git a/libsql-server/src/replication/replicator_client.rs b/libsql-server/src/replication/replicator_client.rs index fb8154824d..9fb498e1ed 100644 --- a/libsql-server/src/replication/replicator_client.rs +++ b/libsql-server/src/replication/replicator_client.rs @@ -185,7 +185,10 @@ impl ReplicatorClient for Client { } self.meta_store_handle - .store(DatabaseConfig::from(config)) + .store( + DatabaseConfig::try_from(config) + .map_err(|e| Status::new(Code::InvalidArgument, e.to_string()))?, + ) .await .map_err(|e| Error::Internal(e.into()))?; diff --git a/libsql-server/src/schema/db.rs b/libsql-server/src/schema/db.rs index ec8dcad840..fb2981af6d 100644 --- a/libsql-server/src/schema/db.rs +++ b/libsql-server/src/schema/db.rs @@ -127,7 +127,8 @@ pub(super) fn register_schema_migration_job( }; let config_bytes = row.get_ref(1)?.as_blob().unwrap(); // TODO: handle corrupted meta - let config = DatabaseConfig::from(&metadata::DatabaseConfig::decode(config_bytes).unwrap()); + let config = DatabaseConfig::try_from(&metadata::DatabaseConfig::decode(config_bytes).unwrap()) + .map_err(|e| Error::Registration(Box::new(e)))?; if !config.is_shared_schema { return Err(Error::NotASchema(schema.clone())); } From fa980e6cfc8e6e0de3f99725e4387599b0839ad2 Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Mon, 5 Oct 2026 17:00:18 +0200 Subject: [PATCH 10/17] Validate heap profile IDs in admin API (cherry picked from commit aee7757d7cb87bd9604ba95262d9a5508f1d2c43) --- libsql-server/src/http/admin/mod.rs | 98 +++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index 2d8de1cdd1..4020c9d7c1 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -32,6 +32,8 @@ use crate::LIBSQL_PAGE_SIZE; pub mod stats; +const HEAP_PROFILE_DIR: &str = "heap_profile"; + #[derive(Clone)] struct Metrics { handle: Option, @@ -573,7 +575,7 @@ async fn enable_profile_heap(Json(req): Json) -> crate max_trackers: req.max_trackers.unwrap_or(200), tracker_event_buffer_size: req.tracker_event_buffer_size.unwrap_or(5_000), sample_rate: req.sample_rate.unwrap_or(1.0), - profile_dir: PathBuf::from("heap_profile"), + profile_dir: PathBuf::from(HEAP_PROFILE_DIR), }) .map_err(|e| crate::Error::Anyhow(anyhow::anyhow!("{e}"))) }) @@ -582,11 +584,24 @@ async fn enable_profile_heap(Json(req): Json) -> crate Ok(path.file_name().unwrap().to_str().unwrap().to_string()) } -async fn disable_profile_heap(Path(profile): Path) -> impl axum::response::IntoResponse { +fn heap_profile_path(profile: &str) -> crate::Result { + let Some(timestamp) = profile.strip_prefix("rip-") else { + return Err(Error::InvalidPath(profile.to_owned())); + }; + if timestamp.is_empty() || !timestamp.bytes().all(|c| c.is_ascii_digit()) { + return Err(Error::InvalidPath(profile.to_owned())); + } + + Ok(PathBuf::from(HEAP_PROFILE_DIR).join(profile)) +} + +async fn disable_profile_heap( + Path(profile): Path, +) -> crate::Result { + let profile_dir = heap_profile_path(&profile)?; let (tx, rx) = tokio::sync::mpsc::channel::(1); tokio::task::spawn_blocking(move || { rheaper::disable_tracking(); - let profile_dir = PathBuf::from("heap_profile").join(&profile); let sink = PollSender::new(tx).sink_map_err(|_| std::io::Error::from(ErrorKind::BrokenPipe)); let writer = tokio_util::io::SyncIoBridge::new(SinkWriter::new(CopyToBytes::new(sink))); @@ -605,11 +620,84 @@ async fn disable_profile_heap(Path(profile): Path) -> impl axum::respons tokio_stream::wrappers::ReceiverStream::new(rx).map(|b| Result::<_, Infallible>::Ok(b)); let body = StreamBody::new(stream); - body + Ok(body) } async fn delete_profile_heap(Path(profile): Path) -> crate::Result<()> { - let profile_dir = PathBuf::from("heap_profile").join(&profile); + let profile_dir = heap_profile_path(&profile)?; tokio::fs::remove_dir_all(&profile_dir).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use hyper::Method; + use tower::ServiceExt; + + #[test] + fn heap_profile_path_accepts_generated_profile_ids() { + assert_eq!( + heap_profile_path("rip-1720000000").unwrap(), + PathBuf::from(HEAP_PROFILE_DIR).join("rip-1720000000") + ); + } + + #[test] + fn heap_profile_path_rejects_invalid_profile_ids() { + for profile in [ + "", + ".", + "..", + "../rip-1720000000", + "/data", + "rip-1720000000/..", + "rip-1720000000/profile", + "rip-1720000000\\profile", + "profile", + "rip-", + "rip-invalid", + "rip-+1720000000", + ] { + assert!( + matches!(heap_profile_path(profile), Err(Error::InvalidPath(path)) if path == profile), + "unexpected result for {profile:?}" + ); + } + } + + #[tokio::test] + async fn heap_profile_routes_reject_percent_encoded_traversal() { + let app = axum::Router::new() + .route( + "/profile/heap/disable/:id", + axum::routing::post(disable_profile_heap), + ) + .route( + "/profile/heap/:id", + axum::routing::delete(delete_profile_heap), + ); + + for (method, uri) in [ + (Method::POST, "/profile/heap/disable/%2e%2e"), + (Method::POST, "/profile/heap/disable/rip-1720000000%2F.."), + (Method::DELETE, "/profile/heap/%2e%2e"), + (Method::DELETE, "/profile/heap/%2Fdata"), + (Method::DELETE, "/profile/heap/rip-1720000000%2F.."), + ] { + let response = app + .clone() + .oneshot( + Request::builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{uri}"); + } + } +} From 8f52e136de10194e63db0a01291501ad31e7d0a8 Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Thu, 8 Oct 2026 12:51:08 +0200 Subject: [PATCH 11/17] Treat heap profile IDs as opaque basenames (cherry picked from commit 5f4fa6d1ad61491468e88d835a9989b86a23d8f9) --- libsql-server/src/http/admin/mod.rs | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index 4020c9d7c1..bb1ac8453a 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use std::cell::OnceCell; use std::convert::Infallible; use std::io::ErrorKind; -use std::path::PathBuf; +use std::path::{Component, Path as FsPath, PathBuf}; use std::sync::Arc; use std::time::Duration; use tokio::sync::Notify; @@ -585,10 +585,15 @@ async fn enable_profile_heap(Json(req): Json) -> crate } fn heap_profile_path(profile: &str) -> crate::Result { - let Some(timestamp) = profile.strip_prefix("rip-") else { - return Err(Error::InvalidPath(profile.to_owned())); - }; - if timestamp.is_empty() || !timestamp.bytes().all(|c| c.is_ascii_digit()) { + let mut components = FsPath::new(profile).components(); + let is_safe_basename = !profile.is_empty() + && !profile + .chars() + .any(|c| matches!(c, '/' | '\\' | '\0' | ':')) + && matches!(components.next(), Some(Component::Normal(_))) + && components.next().is_none(); + + if !is_safe_basename { return Err(Error::InvalidPath(profile.to_owned())); } @@ -636,15 +641,22 @@ mod tests { use tower::ServiceExt; #[test] - fn heap_profile_path_accepts_generated_profile_ids() { - assert_eq!( - heap_profile_path("rip-1720000000").unwrap(), - PathBuf::from(HEAP_PROFILE_DIR).join("rip-1720000000") - ); + fn heap_profile_path_accepts_safe_basenames() { + for profile in [ + "rip-1720000000", + "future-profile-format", + "profile 1", + "café", + ] { + assert_eq!( + heap_profile_path(profile).unwrap(), + PathBuf::from(HEAP_PROFILE_DIR).join(profile) + ); + } } #[test] - fn heap_profile_path_rejects_invalid_profile_ids() { + fn heap_profile_path_rejects_unsafe_basenames() { for profile in [ "", ".", @@ -654,10 +666,8 @@ mod tests { "rip-1720000000/..", "rip-1720000000/profile", "rip-1720000000\\profile", - "profile", - "rip-", - "rip-invalid", - "rip-+1720000000", + "rip-1720000000\0profile", + "C:profile", ] { assert!( matches!(heap_profile_path(profile), Err(Error::InvalidPath(path)) if path == profile), From 24ff9c1f07fcd6b26e03cdfe03dcba108976b8dc Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Thu, 8 Oct 2026 13:19:45 +0200 Subject: [PATCH 12/17] Check heap profile exists before disabling tracking (cherry picked from commit 61e0984cd62443d319c27025e4b0db2a0d83e36a) --- libsql-server/src/http/admin/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index bb1ac8453a..532ca804b2 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -604,6 +604,9 @@ async fn disable_profile_heap( Path(profile): Path, ) -> crate::Result { let profile_dir = heap_profile_path(&profile)?; + // Fail before disabling tracking so a wrong ID does not stop the live + // session and stream an empty archive. + tokio::fs::metadata(&profile_dir).await?; let (tx, rx) = tokio::sync::mpsc::channel::(1); tokio::task::spawn_blocking(move || { rheaper::disable_tracking(); @@ -710,4 +713,25 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{uri}"); } } + + #[tokio::test] + async fn disable_profile_heap_fails_for_missing_profile() { + let app = axum::Router::new().route( + "/profile/heap/disable/:id", + axum::routing::post(disable_profile_heap), + ); + + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri("/profile/heap/disable/rip-0-missing") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + } } From 9711cc966b6fbc347d0cc253fecf3c0bc560484a Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Thu, 8 Oct 2026 13:43:15 +0200 Subject: [PATCH 13/17] Stop heap tracking before checking the profile directory (cherry picked from commit 91966862030ac09dea3878bd483b5846afeed192) --- libsql-server/src/http/admin/mod.rs | 72 +++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/libsql-server/src/http/admin/mod.rs b/libsql-server/src/http/admin/mod.rs index 532ca804b2..7fa4f900a3 100644 --- a/libsql-server/src/http/admin/mod.rs +++ b/libsql-server/src/http/admin/mod.rs @@ -604,12 +604,15 @@ async fn disable_profile_heap( Path(profile): Path, ) -> crate::Result { let profile_dir = heap_profile_path(&profile)?; - // Fail before disabling tracking so a wrong ID does not stop the live - // session and stream an empty archive. + // Always stop tracking first: this must remain possible even if the + // profile directory is gone or the ID was lost. `disable_tracking` is a + // no-op when nothing is enabled, so a retry with the right ID streams the + // already-finalized profile. + tokio::task::spawn_blocking(rheaper::disable_tracking).await?; + // Fail explicitly instead of streaming an empty archive for a wrong ID. tokio::fs::metadata(&profile_dir).await?; let (tx, rx) = tokio::sync::mpsc::channel::(1); tokio::task::spawn_blocking(move || { - rheaper::disable_tracking(); let sink = PollSender::new(tx).sink_map_err(|_| std::io::Error::from(ErrorKind::BrokenPipe)); let writer = tokio_util::io::SyncIoBridge::new(SinkWriter::new(CopyToBytes::new(sink))); @@ -734,4 +737,67 @@ mod tests { assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } + + #[tokio::test] + async fn disable_profile_heap_stops_live_session_and_streams_profile() { + // The tracker is process-global: this must stay the only test that + // enables tracking. + let profile_dir = rheaper::enable_tracking(rheaper::TrackerConfig { + max_stack_depth: 1, + max_trackers: 1, + tracker_event_buffer_size: 1, + sample_rate: 0.0, + profile_dir: PathBuf::from(HEAP_PROFILE_DIR), + }) + .unwrap(); + let profile = profile_dir + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned(); + + let app = axum::Router::new().route( + "/profile/heap/disable/:id", + axum::routing::post(disable_profile_heap), + ); + let response = app + .oneshot( + Request::builder() + .method(Method::POST) + .uri(format!("/profile/heap/disable/{profile}")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let status = response.status(); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + std::fs::remove_dir_all(&profile_dir).unwrap(); + + assert_eq!(status, StatusCode::OK); + let mut archive = tar::Archive::new(&body[..]); + let entries: Vec = archive + .entries() + .unwrap() + .map(|e| e.unwrap().path().unwrap().to_string_lossy().into_owned()) + .collect(); + assert!( + entries.iter().any(|e| e.trim_end_matches('/') == profile), + "tar entries: {entries:?}" + ); + // Tracking is off again, so enabling must succeed and be cleaned up. + let again = rheaper::enable_tracking(rheaper::TrackerConfig { + max_stack_depth: 1, + max_trackers: 1, + tracker_event_buffer_size: 1, + sample_rate: 0.0, + profile_dir: PathBuf::from(HEAP_PROFILE_DIR), + }) + .unwrap(); + rheaper::disable_tracking(); + std::fs::remove_dir_all(&again).unwrap(); + // Only removes the parent if nothing else is in it. + let _ = std::fs::remove_dir(HEAP_PROFILE_DIR); + } } From cc07580a18e1522b6b5400ddd580e9a06014cba3 Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn Date: Tue, 13 Oct 2026 21:25:04 +0200 Subject: [PATCH 14/17] Remove obsolete libSQL binary release workflow (cherry picked from commit 422db4a2d714ce9f5225f7674f7b53d52527cf3c) --- .github/workflows/release-libsql.yml | 207 --------------------------- 1 file changed, 207 deletions(-) delete mode 100644 .github/workflows/release-libsql.yml diff --git a/.github/workflows/release-libsql.yml b/.github/workflows/release-libsql.yml deleted file mode 100644 index 1d9e3e1e55..0000000000 --- a/.github/workflows/release-libsql.yml +++ /dev/null @@ -1,207 +0,0 @@ -name: Create and publish libsql binaries - -on: - push: - #branches: ['main'] - tags: - - libsql-sqlite3-v*.*.* - -jobs: - - start-runner: - name: Start self-hosted EC2 runner - runs-on: ubuntu-latest - outputs: - label: ${{ steps.start-ec2-runner.outputs.label }} - ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }} - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Start EC2 runner - id: start-ec2-runner - uses: machulav/ec2-github-runner@v2 - with: - mode: start - github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} - ec2-image-id: ami-0e811aad4d084edb9 - ec2-instance-type: t4g.2xlarge - subnet-id: subnet-05010d45bfb86f5fb - security-group-id: sg-01bd3845621443669 - - build_binaries: - needs: start-runner - strategy: - matrix: - os: - - ubuntu-latest - - macos-14 - - ${{ needs.start-runner.outputs.label }} - variant: - - 'wasm-udf-dynamic' - - 'wasm-udf-wasmedge' - - 'wasm-udf' - - '' - - runs-on: ${{ matrix.os }} - steps: - #Debug output + special rustup config for the ubuntu/aarch64 runner - - name: show step matrix details - run: | - env - set -ex - uname -a - echo "os: ${{ matrix.os }}" - echo "variant: ${{ matrix.variant }}" - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > ./rustup.sh - chmod a+x rustup.sh - ./rustup.sh -y - if [[ "${{ matrix.os}}" == "${{ needs.start-runner.outputs.label }}" ]]; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH - echo "PATH=$PATH:/root/.cargo/bin" >> $GITHUB_PATH - fi - pwd - whoami - - #for windows only, use correct newlines - - name: Set git to use LF - if: matrix.os == 'windows-latest' - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - - uses: actions/checkout@v3 - - - uses: dtolnay/rust-toolchain@stable - - - name: setup msys2 for windows - if: matrix.os == 'windows-latest' - uses: msys2/setup-msys2@v2 - with: - update: true - install: mingw-w64-x86_64-toolchain mingw-w64-x86_64-sqlite3 gcc make base-devel mingw-w64-x86_64-rust zip - msystem: mingw64 - path-type: inherit - - - name: run configure - working-directory: libsql-sqlite3 - run: | - set -ex - mkdir build - pushd build - case "${{ matrix.variant }}" in - "wasm-udf-dynamic") - ../configure --enable-wasm-runtime-dynamic - ;; - "wasm-udf-wasmedge") - # Fix for https://github.com/WasmEdge/WasmEdge/issues/2622 - if [[ "${{ matrix.os}}" == "${{ needs.start-runner.outputs.label }}" ]]; then - curl -SLO https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh - chmod a+x install.sh - ./install.sh --dist manylinux2014 -D - else - curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash - fi - source ~/.wasmedge/env - ../configure --enable-wasm-runtime-wasmedge - ;; - "wasm-udf") - ../configure --enable-wasm-runtime - ;; - *) - ../configure - ;; - esac - popd - - - name: build - working-directory: libsql-sqlite3 - run: | - set -ex - if [[ "${{ matrix.variant }}" == "wasm-udf-wasmedge" ]]; then - source ~/.wasmedge/env || echo "no wasmedge installed" - fi - pushd build - make - make sqlite3.c - ls -la - popd - - - name: package artefacts (for named variants) - working-directory: libsql-sqlite3 - if: matrix.variant != '' - run: | - set -ex - uname -a - OS=$(uname -s) - ARCH=$(uname -m) - cd build - tar cvzf libsql-${{ github.ref_name }}-${{ matrix.variant }}-$OS-$ARCH.tar.gz .libs libsql sqlite3 - tar cvzf libsql-amalgamation-${{ github.ref_name }}-${{ matrix.variant }}-$OS-$ARCH.tar.gz sqlite3.c sqlite3.h - ls -la - cd .. - - - name: package artefacts - working-directory: libsql-sqlite3 - if: matrix.variant == '' - run: | - set -ex - uname -a - OS=$(uname -s) - ARCH=$(uname -m) - cd build - tar cvzf libsql-${{ github.ref_name }}-$OS-$ARCH.tar.gz .libs libsql sqlite3 - tar cvzf libsql-amalgamation-${{ github.ref_name }}-$OS-$ARCH.tar.gz sqlite3.c sqlite3.h - ls -la - cd .. - - - name: Push artefacts into release draft - working-directory: libsql-sqlite3 - uses: softprops/action-gh-release@v1 - if: ${{startsWith(github.ref, 'refs/tags/') }} - with: - fail_on_unmatched_files: false - files: | - build/libsql-${{ github.ref_name }}-${{ matrix.variant }}-Linux-x86_64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-${{ matrix.variant }}-Linux-x86_64.tar.gz - build/libsql-${{ github.ref_name }}-Linux-x86_64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-Linux-x86_64.tar.gz - build/libsql-${{ github.ref_name }}-${{ matrix.variant }}-Linux-aarch64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-${{ matrix.variant }}-Linux-aarch64.tar.gz - build/libsql-${{ github.ref_name }}-Linux-aarch64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-Linux-aarch64.tar.gz - build/libsql-${{ github.ref_name }}-${{ matrix.variant }}-Darwin-x86_64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-${{ matrix.variant }}-Darwin-x86_64.tar.gz - build/libsql-${{ github.ref_name }}-Darwin-x86_64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-Darwin-x86_64.tar.gz - build/libsql-${{ github.ref_name }}-${{ matrix.variant }}-Darwin-arm64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-${{ matrix.variant }}-Darwin-arm64.tar.gz - build/libsql-${{ github.ref_name }}-Darwin-arm64.tar.gz - build/libsql-amalgamation-${{ github.ref_name }}-Darwin-arm64.tar.gz - - stop-runner: - name: Stop self-hosted EC2 runner - needs: - - start-runner # required to get output from the start-runner job - - build_binaries # required to wait when the main job is done - runs-on: ubuntu-latest - if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - name: Stop EC2 runner - uses: machulav/ec2-github-runner@v2 - with: - mode: stop - github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} - label: ${{ needs.start-runner.outputs.label }} - ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }} From 7ca8e7601910a1d361eec6958f79a75cce5eb8d9 Mon Sep 17 00:00:00 2001 From: Tomasz Szymczyszyn <90851802+tszymczyszyn-shopify@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:03:47 +0900 Subject: [PATCH 15/17] Never report a namespace whose entry was emptied as missing (#56) * Never report a namespace whose entry was emptied as missing NamespaceStore::with mapped an emptied cache entry to NamespaceDoesntExist. Entries are emptied by shutdown (which runs while HTTP and gRPC services still accept requests), by the cache's eviction listener, and by destroy. A request that got the entry before it was emptied answered 404 "namespace doesn't exist" for a namespace the metastore still has. Clients cannot tell that from a namespace that is really gone, and a 404 is the one answer a client may act on as authoritative (for example by creating the namespace again or discarding local state). with now: - answers NamespaceStoreShutdown (503) once the store is shut down, on every path, so a request after shutdown no longer reloads the namespace; - looks the namespace up again when the entry it got was emptied, so an evicted namespace is reloaded instead of reported missing; - drops an emptied entry that is still the cached one, under moka's key lock and only if it is still cached, empty and unlocked, so the next lookup loads the namespace; it never fills an entry itself; - answers NamespaceDoesntExist only when the metastore no longer has the namespace, and the new NamespaceUnavailable (503) otherwise. Also: - fork loads its source through with. create refuses once the store is shut down, and drops a left-empty entry before loading, so its restore option is applied. checkpoint no longer caches an empty entry for a namespace that is not loaded. - gRPC: rpc::namespace_error_status maps both 503 errors to UNAVAILABLE in replication auth, ReplicationLogService and ProxyService, instead of INTERNAL, and neither auth path falls back to another strategy for them. - libsql_server_namespace_emptied_entry counts emptied entries a request meets; looking again, dropping a left-empty entry and giving up are logged at warn. Related upstream issue: tursodatabase/libsql#1772. * Test the give-up path of NamespaceStore::with No test reached the final `Err` of `with`, or the `else` in `fork` that answers for a source emptied between its two loads. Every test exited the loop earlier: from the shutdown check, from the metastore check, or with `Ok`. Putting the original `NamespaceDoesntExist` back on either path passed every test. Split the loop into `with_attempts(namespace, max_attempts, f)`; `with` passes `MAX_EMPTIED_ENTRY_ATTEMPTS`. With one attempt, the emptied entry a request finds is the last one it looks at, so the tail is the only exit. Two tests pin it: `NamespaceUnavailable` while the metastore still has the namespace, `NamespaceDoesntExist` once it does not. For `fork`, holding the destination entry's write lock stops it between loading the source through `with` and reading it from its own lookup. Emptying the source there reaches the `else`, which must answer `NamespaceUnavailable`. `start_waiting_request` is now a thin wrapper over `start_waiting`, which takes the future to run, so these tests reuse the same rendezvous. * Do not spend an attempt on dropping a left-empty entry For an entry that was left empty, `with` used all three attempts: one to find it empty, one to recognise it as still cached and drop it, one to load the namespace. There was no attempt left for the fresh entry, so if that was emptied as well (the cache rejecting a new entry at capacity, for example) the request got a 503 that one more lookup would have avoided. Count only lookups that found an emptied entry. The drop branch clears `emptied`, so it cannot run twice without such a lookup in between, and the loop still ends after at most twice the budget. The new test gives `with_attempts` a budget of two over a left-empty entry. With the old counting the budget was gone after the drop. * Refuse to open a namespace once the store has shut down `with`, `create` and `fork` check `has_shutdown` before they load, but the load itself awaits the metastore and then opens the namespace. A request that passed the check just before shutdown started could open a namespace after `shutdown()` had taken out every cached entry. Nothing would ever shut that namespace down. `reset`, which only replicas call, had no check at all and fills an entry of its own. Check the flag again inside `load_namespace`'s init, which only runs on a cache miss, so one check covers every caller. The error comes back as `Error::Ref`, which the HTTP and gRPC mappings and both auth paths unwrap with `as_ref`. Add the same check at the top of `reset`. A load that was already opening the namespace when shutdown started can still finish. Closing that needs the shutdown-ordering follow-up: stop accepting requests, drain, then take the namespaces out. * Answer UNAVAILABLE from the replica proxy while the store is shut down `ReplicaProxyService::do_auth` sent `NamespaceStoreShutdown` and `NamespaceUnavailable` to its generic arm, which answers `INTERNAL`. Before, a replica shutting down produced a false `NamespaceDoesntExist` there, and the request fell back to the server-wide auth strategy. So this change had altered what clients of a restarting replica see, from a wrong fallback to an error they do not retry, while primaries answer `UNAVAILABLE` for the same condition. Map the two errors through `namespace_error_status`, as the primary's proxy and replication services do. Neither falls back to the user strategy. * Count give-ups and dropped left-empty entries `libsql_server_namespace_emptied_entry` rises on every deploy, because every request that was waiting while shutdown took its namespace out looks again and succeeds or gets a 503 from the shutdown check. The two outcomes an operator would alert on had no metric: a request answered with an error because every lookup found an emptied entry, and a cache entry dropped because something left it empty. Both were log lines only. Add `libsql_server_namespace_emptied_entry_gave_up`, incremented where `with` gives up, and `libsql_server_namespace_left_empty_entry_dropped`, incremented when `evict_if_still_empty` removed the entry (a `Nop` is not counted). Neither has a label. * Test the destroy race against the real destroy The test for a namespace destroyed while a request waited removed the metastore record and emptied the entry by hand. The contract it checks, that the request answers "missing" because the record is gone, depends on `destroy` removing the record before it takes the namespace out. Swapping those two steps in `destroy` failed no test. Run the real `destroy` in a task. It removes the record, then waits for the entry's write lock the test holds. The test waits for the record to go, then takes the namespace out itself and drops the lock, so the request finds the entry empty whichever of the two the lock wakes first. Letting `destroy` take the namespace instead would make the outcome depend on that order. With the steps swapped in `destroy`, the wait for the record times out. * Assert that reloads go into a fresh cache entry The eviction test and both fork tests only compared the namespace's name, which any instance has, including one opened outside the cache or one filled into the emptied entry. Only the left-empty test checked the cache afterwards. After each reload, assert that the cache holds a different entry, that it holds a namespace, and that the emptied entry is still empty. * Test several requests waiting on one left-empty entry Every test had one request on the emptied entry, so `evict_if_still_empty` ran alone and in a fixed order. A rolling restart or an eviction empties an entry under all the requests waiting on it at once, and the identity and lock checks exist for that case. Eight requests wait on one entry that is then left empty. Each must succeed, and all of them must be served by the one namespace the cache holds afterwards, compared by the pointer of its `Stats`. The test runs on the multi-thread runtime so the requests really interleave. 50 consecutive runs passed. * Make comments and logs match what the code guarantees - The doc comment on `evict_if_still_empty` promised that it "cannot open a namespace that the cache no longer holds". It cannot see a `fork` or `reset` that holds the entry but has not locked it yet, and moka's `Op::Remove` removes whatever the cache holds when it runs; its compute lock does not exclude `try_get_with` or `invalidate`. Say so, and log an error when the removed entry is not the one the closure saw. - The retry log fires once per request that was waiting when shutdown or eviction took the namespace. That is expected and the counter carries the signal; log it at debug. The drop and give-up logs stay at warn. - The eviction listener logged "deallocated" before checking whether the entry held anything, so dropping a left-empty entry logged a deallocation that did not happen. - Note the known hazard in `fork` and `reset`: an entry write lock held across a cache call, which moka's inline maintenance can wait for. The fix (reserving the name outside the cache) is a follow-up. * Send a forged proxy auth header in the shutdown test `ProxyService::extract_context` authenticates a request for a namespace reported as missing from `x-proxy-authorization`, a header the client writes. Before this change, a false "missing" during shutdown or eviction let a forged `FullAccess` through for a namespace that has its own keys. The test for the 503 answer sent no header, so a change that brought the fallback back for the two 503 errors would have passed it as long as the request then failed for some other reason. Send the header: with the fallback, `extract_context` returns `Ok`, and the test fails. * Test hygiene for the namespace store tests - `settle`, `wait_until` and `wait_until_async` spun on `yield_now` for up to ten seconds. That burns a core while the awaited event runs on a blocking thread, as `destroy`'s metastore removal does. Sleep a millisecond between checks instead. - The fork tests ran on the current-thread runtime. `fork`'s cleanup on failure calls `block_in_place`, which panics there, so a regression in `fork` would have surfaced as that panic rather than the real error. Run them on the multi-thread runtime. - Note that the reference counting in `settle` and `start_waiting` is what moka 0.12.8 does, and that an upgrade fails these tests with a timeout rather than hanging them. * Leave entries that stay empty to a follow-up and simplify the retry loop The loop in `with` tracked the emptied entry it had found so that, when the next lookup returned the same entry, it could drop that entry from the cache (`evict_if_still_empty`) and reload the namespace. That handled an entry that something left cached but empty, which after the `checkpoint` fix only a failed fork does, and it was the most delicate code in this change: a compute under moka's key lock, with windows it could not close on its own. It is removed here and kept for a separate change, to be reconciled with the cache entry redesign. What remains is a plain loop: look the namespace up, serve it if its entry holds it, otherwise count the emptied entry and look again, up to three times. An entry that stays empty is found on every attempt, so the request gives up with `NamespaceUnavailable` and the namespace answers 503 until the entry is evicted or the server restarts, as `with_gives_up_with_unavailable_when_the_entry_stays_empty` pins. The give-up counter now records only that outcome: giving up because the store shut down, or because the namespace is really gone, is not a failure of the store. Since the give-up path is reachable through `with` itself now, the `with_attempts` test seam and the `left_empty_entry_dropped` counter go too. The fork test that said "emptied while waiting" left the entry cached, which is the stays-empty case; it now evicts the entry as the `with` test does. Also corrects the comment in `reset`: `make_namespace` does reach the cache on a replica, through `with` for a shared schema during the replication handshake. That hazard predates this change. * Remove the record of a create that shutdown interrupted before its load `create` stores the namespace's record, then loads the namespace. Since `load_namespace` refuses to open a namespace once the store has shut down, a shutdown that starts between the two left the record behind with nothing loaded. Every retry then answered "already exists", and the restore option (a dump, say) was never applied. Before this change the same window loaded the namespace, into an entry nothing would shut down. The refusal happens before anything is opened or written, so `create` now removes the record again when its load is refused for that reason. Other load failures are left as they were. * Classify namespace errors for authentication in one place The three gRPC authentication paths each matched the store's error to decide between "missing namespace" (fall back to the user strategy, or skip authentication) and "exists but cannot be served" (UNAVAILABLE, no fallback). Each unwrapped `Error::Ref` on its own, and the replica proxy did not, so a wrapped `NamespaceDoesntExist` would have been INTERNAL there. They now decide on the code `namespace_error_status` returns, which is the one place that classifies these errors and the one that is tested with wrapped errors. `Error::as_ref` unwraps nested `Ref`s, as `into_response` already did. The Hello test for a shut-down store now configures Basic auth and sends no credentials, so that it fails if the UNAVAILABLE case ever falls back to the user strategy. * Name the left-empty test after what it guards `with_gives_up_with_unavailable_when_the_entry_stays_empty` becomes `with_reports_unavailable_not_missing_for_an_entry_left_empty`: the same form as its neighbours, the PR's term for the case, and the contract it pins (a namespace that exists must not be reported missing). * Test that waiters on an evicted entry share one load, and that 503s never read like 404s Eight requests waiting on one entry that is then evicted all look the namespace up again at once; `try_get_with` coalesces them into one load and one fresh entry. Before, nothing asserted that on the eviction path. Clients tell a missing namespace from a temporarily unavailable one by the status, and some also by the message. The two 503 messages must not contain "doesn't exist" or "not found". (cherry picked from commit 851ddea771ef48433aae4d2b0eb037db8cc0f6b1) --- libsql-server/src/error.rs | 6 +- libsql-server/src/metrics.rs | 16 + libsql-server/src/namespace/mod.rs | 2 + libsql-server/src/namespace/store.rs | 776 +++++++++++++++++- libsql-server/src/rpc/mod.rs | 66 ++ libsql-server/src/rpc/proxy.rs | 81 +- libsql-server/src/rpc/replica_proxy.rs | 57 +- libsql-server/src/rpc/replication/auth.rs | 28 +- .../src/rpc/replication/replication_log.rs | 81 +- 9 files changed, 1026 insertions(+), 87 deletions(-) diff --git a/libsql-server/src/error.rs b/libsql-server/src/error.rs index bfe67f47c7..d925d31308 100644 --- a/libsql-server/src/error.rs +++ b/libsql-server/src/error.rs @@ -102,6 +102,8 @@ pub enum Error { UrlParseError(#[from] url::ParseError), #[error("Namespace store has shutdown")] NamespaceStoreShutdown, + #[error("Namespace `{0}` is temporarily unavailable")] + NamespaceUnavailable(String), #[error("Unable to update metastore: {0}")] MetaStoreUpdateFailure(Box), // This is for errors returned by moka @@ -131,9 +133,10 @@ pub enum Error { } impl AsRef for Error { + /// The error behind any number of `Ref` wrappers, like `into_response`. fn as_ref(&self) -> &Self { match self { - Self::Ref(this) => this.as_ref(), + Self::Ref(this) => >::as_ref(this), _ => self, } } @@ -212,6 +215,7 @@ impl IntoResponse for &Error { PrimaryStreamInterupted => self.format_err(StatusCode::INTERNAL_SERVER_ERROR), UrlParseError(_) => self.format_err(StatusCode::BAD_REQUEST), NamespaceStoreShutdown => self.format_err(StatusCode::SERVICE_UNAVAILABLE), + NamespaceUnavailable(_) => self.format_err(StatusCode::SERVICE_UNAVAILABLE), MetaStoreUpdateFailure(_) => self.format_err(StatusCode::INTERNAL_SERVER_ERROR), Ref(this) => this.as_ref().into_response(), ProstDecode(_) => self.format_err(StatusCode::INTERNAL_SERVER_ERROR), diff --git a/libsql-server/src/metrics.rs b/libsql-server/src/metrics.rs index e5de216b79..5f6aee411a 100644 --- a/libsql-server/src/metrics.rs +++ b/libsql-server/src/metrics.rs @@ -37,6 +37,22 @@ pub static STREAM_HANDLES_COUNT: Lazy = Lazy::new(|| { describe_gauge!(NAME, "amount of in-memory stream handles"); register_gauge!(NAME) }); +pub static NAMESPACE_EMPTIED_ENTRY: Lazy = Lazy::new(|| { + const NAME: &str = "libsql_server_namespace_emptied_entry"; + describe_counter!( + NAME, + "number of times a request found its namespace cache entry emptied (by shutdown, eviction or destroy) and looked again" + ); + register_counter!(NAME) +}); +pub static NAMESPACE_EMPTIED_ENTRY_GAVE_UP: Lazy = Lazy::new(|| { + const NAME: &str = "libsql_server_namespace_emptied_entry_gave_up"; + describe_counter!( + NAME, + "number of requests answered `namespace unavailable` because every lookup of their namespace, which still exists, found its cache entry emptied" + ); + register_counter!(NAME) +}); pub static NAMESPACE_LOAD_LATENCY: Lazy = Lazy::new(|| { const NAME: &str = "libsql_server_namespace_load_latency"; describe_histogram!(NAME, "latency is us when loading a namespace"); diff --git a/libsql-server/src/namespace/mod.rs b/libsql-server/src/namespace/mod.rs index ec45b50445..dd806675c9 100644 --- a/libsql-server/src/namespace/mod.rs +++ b/libsql-server/src/namespace/mod.rs @@ -16,6 +16,8 @@ use crate::stats::Stats; use self::meta_store::MetaStoreHandle; pub use self::name::NamespaceName; +#[cfg(test)] +pub(crate) use self::store::test::make_store as make_test_store; pub use self::store::NamespaceStore; pub mod broadcasters; diff --git a/libsql-server/src/namespace/store.rs b/libsql-server/src/namespace/store.rs index 86e9438ccd..763c9c5b93 100644 --- a/libsql-server/src/namespace/store.rs +++ b/libsql-server/src/namespace/store.rs @@ -15,7 +15,9 @@ use crate::broadcaster::BroadcastMsg; use crate::connection::config::DatabaseConfig; use crate::database::DatabaseKind; use crate::error::Error; -use crate::metrics::NAMESPACE_LOAD_LATENCY; +use crate::metrics::{ + NAMESPACE_EMPTIED_ENTRY, NAMESPACE_EMPTIED_ENTRY_GAVE_UP, NAMESPACE_LOAD_LATENCY, +}; use crate::namespace::{NamespaceBottomlessDbId, NamespaceBottomlessDbIdInit, NamespaceName}; use crate::stats::Stats; @@ -27,6 +29,11 @@ use super::{Namespace, ResetCb, ResetOp, ResolveNamespacePathFn, RestoreOption}; type NamespaceEntry = Arc>>; +/// How many times `with` looks up a namespace whose cache entry was emptied +/// under it (by shutdown, eviction or destroy) before giving up. Dropping an +/// entry that was left empty is not a lookup and does not count. +const MAX_EMPTIED_ENTRY_ATTEMPTS: usize = 3; + /// Stores and manage a set of namespaces. pub struct NamespaceStore { pub inner: Arc, @@ -71,9 +78,9 @@ impl NamespaceStore { // On the other, if there's lots of cache pressure, snapshotting // very often will kill the machine's I/O. Box::pin(async move { - tracing::info!("namespace `{name}` deallocated"); // shutdown namespace if let Some(ns) = ns.write().await.take() { + tracing::info!("namespace `{name}` deallocated"); if let Err(e) = ns.shutdown(snapshot_at_shutdown).await { tracing::error!("error deallocating `{name}`: {e}") } @@ -141,11 +148,11 @@ impl NamespaceStore { } pub async fn checkpoint(&self, namespace: NamespaceName) -> crate::Result<()> { - let entry = self - .inner - .store - .get_with(namespace.clone(), async { Default::default() }) - .await; + // Only a loaded namespace has anything to checkpoint. Inserting an empty + // entry for one that is not loaded would leave it cached empty. + let Some(entry) = self.inner.store.get(&namespace).await else { + return Ok(()); + }; let lock = entry.read().await; if let Some(ns) = &*lock { ns.checkpoint().await?; @@ -158,6 +165,10 @@ impl NamespaceStore { namespace: NamespaceName, restore_option: RestoreOption, ) -> anyhow::Result<()> { + if self.inner.has_shutdown.load(Ordering::Relaxed) { + return Err(Error::NamespaceStoreShutdown.into()); + } + // The process for reseting is as follow: // - get a lock on the namespace entry, if the entry exists, then it's a lock on the entry, // if it doesn't exist, insert an empty entry and take a lock on it @@ -168,6 +179,12 @@ impl NamespaceStore { .store .get_with(namespace.clone(), async { Default::default() }) .await; + // This write lock is held across `make_namespace` below. Do not add a + // cache call while it is held: moka runs its maintenance inline in + // cache calls and awaits the eviction listener, which waits for this + // lock. The same holds for `fork`'s `to_lock`. One such call predates + // this comment: on a replica, a namespace with a shared schema loads + // its schema through `with` during the replication handshake. let mut lock = entry.write().await; if let Some(ns) = lock.take() { ns.destroy().await?; @@ -224,6 +241,10 @@ impl NamespaceStore { if !self.inner.metadata.exists(&from).await { return Err(crate::error::Error::NamespaceDoesntExist(from.to_string())); } + // Load the source as `with` does, so a source whose cache entry was + // emptied (by eviction, or left empty) is reloaded rather than failing + // the fork, and its errors are the same as `with`'s. + self.with(from.clone(), |_| ()).await?; let to_entry = self .inner @@ -241,12 +262,20 @@ impl NamespaceStore { } let from_config = self.inner.metadata.handle(from.clone()).await; + // Known hazard: `to_lock` is held across this cache call. moka runs its + // maintenance inline in cache calls and awaits the eviction listener, + // which waits for the evicted entry's write lock. If the cache is at + // capacity and picks the fresh, empty `to_entry` as its victim, the + // listener waits for the lock this task holds. The `with` above makes + // this a cache hit in the normal case, which lowers the exposure + // without removing it. Fixing it means reserving `to` outside the + // cache; see the follow-ups in PR #56. let from_entry = self .load_namespace(&from, from_config.clone(), RestoreOption::Latest) .await?; let from_lock = from_entry.read().await; let Some(from_ns) = &*from_lock else { - return Err(crate::error::Error::NamespaceDoesntExist(from.to_string())); + return Err(self.emptied_entry_error(&from).await); }; struct Bomb { @@ -322,30 +351,65 @@ impl NamespaceStore { where Fun: FnOnce(&Namespace) -> R, { - if namespace != NamespaceName::default() - && !self.inner.metadata.exists(&namespace).await - && !self.inner.allow_lazy_creation - { - return Err(Error::NamespaceDoesntExist(namespace.to_string())); - } + for _ in 0..MAX_EMPTIED_ENTRY_ATTEMPTS { + if self.inner.has_shutdown.load(Ordering::Relaxed) { + return Err(Error::NamespaceStoreShutdown); + } + if namespace != NamespaceName::default() + && !self.inner.metadata.exists(&namespace).await + && !self.inner.allow_lazy_creation + { + return Err(Error::NamespaceDoesntExist(namespace.to_string())); + } - let f = { - let name = namespace.clone(); - move |ns: NamespaceEntry| async move { - let lock = ns.read().await; - match &*lock { - Some(ns) => Ok(f(ns)), - // the namespace was taken out of the entry - None => Err(Error::NamespaceDoesntExist(name.to_string())), + let handle = self.inner.metadata.handle(namespace.to_owned()).await; + let entry = self + .load_namespace(&namespace, handle, RestoreOption::Latest) + .await?; + { + let lock = entry.read().await; + if let Some(ns) = &*lock { + return Ok(f(ns)); } } - }; - let handle = self.inner.metadata.handle(namespace.to_owned()).await; - f(self - .load_namespace(&namespace, handle, RestoreOption::Latest) - .await?) - .await + // The namespace was taken out of the entry after we got it: by + // shutdown, by cache eviction, or by a concurrent destroy. That + // does not mean it is missing, so look it up again. Expected + // during a rolling restart and under cache pressure, once per + // waiting request: the counter is the signal, not the log. + NAMESPACE_EMPTIED_ENTRY.increment(1); + tracing::debug!(%namespace, "namespace cache entry was emptied while in use, looking it up again"); + } + + // Every lookup found an emptied entry. That is also where an entry + // that stays cached but empty ends up (a fork that failed before + // storing its record leaves one behind): the namespace answers + // `NamespaceUnavailable` until the entry is evicted or the server + // restarts. Dropping such an entry so that the next lookup reloads the + // namespace is left to a follow-up. + let error = self.emptied_entry_error(&namespace).await; + if matches!(error, Error::NamespaceUnavailable(_)) { + // Shutdown and a namespace that is really gone are not failures + // of the store, so the counter only records this outcome. + NAMESPACE_EMPTIED_ENTRY_GAVE_UP.increment(1); + } + tracing::warn!(%namespace, %error, "namespace cache entry kept being emptied, giving up"); + Err(error) + } + + /// The error for a namespace whose cache entry was emptied while it was + /// being used. `NamespaceDoesntExist` only when the metastore agrees: a + /// client must be able to trust it, and a namespace that still exists but + /// cannot be served right now is a temporary condition, not a missing one. + async fn emptied_entry_error(&self, namespace: &NamespaceName) -> Error { + if self.inner.has_shutdown.load(Ordering::Relaxed) { + Error::NamespaceStoreShutdown + } else if !self.inner.metadata.exists(namespace).await { + Error::NamespaceDoesntExist(namespace.to_string()) + } else { + Error::NamespaceUnavailable(namespace.to_string()) + } } fn resolve_attach_fn(&self) -> ResolveNamespacePathFn { @@ -391,6 +455,12 @@ impl NamespaceStore { restore_option: RestoreOption, ) -> crate::Result { let init = async { + // Shutdown may have started since the caller checked. A namespace + // opened now would not be in the entries shutdown takes out, so + // nobody would ever shut it down. + if self.inner.has_shutdown.load(Ordering::Relaxed) { + return Err(Error::NamespaceStoreShutdown); + } let ns = self .make_namespace(namespace, db_config, restore_option) .await?; @@ -418,6 +488,10 @@ impl NamespaceStore { restore_option: RestoreOption, db_config: DatabaseConfig, ) -> crate::Result<()> { + if self.inner.has_shutdown.load(Ordering::Relaxed) { + return Err(Error::NamespaceStoreShutdown); + } + if let Some(shared_schema_name) = &db_config.shared_schema_name { // we hold a lock for the duration of the namespace creation let _lock = self @@ -444,7 +518,7 @@ impl NamespaceStore { tracing::debug!("storing db config"); handle.store(db_config).await?; tracing::debug!("completed storing db config, loading namespace"); - self.load_namespace(&namespace, handle, restore_option) + self.load_created(&namespace, handle, restore_option) .await?; tracing::debug!("completed loading namespace"); @@ -452,6 +526,43 @@ impl NamespaceStore { Ok(()) } + /// Loads the namespace whose record `create` has just stored. If shutdown + /// started in between, `load_namespace` refuses before anything is opened + /// or written, and the record is removed again. Left in place, it would + /// make every retry answer "already exists" for a namespace that was never + /// loaded, so its restore option would never be applied. + async fn load_created( + &self, + namespace: &NamespaceName, + handle: MetaStoreHandle, + restore_option: RestoreOption, + ) -> crate::Result<()> { + let error = match self.load_namespace(namespace, handle, restore_option).await { + Ok(_) => return Ok(()), + Err(error) => error, + }; + if matches!(error.as_ref(), Error::NamespaceStoreShutdown) { + let removed = tokio::task::spawn_blocking({ + let metadata = self.inner.metadata.clone(); + let namespace = namespace.clone(); + move || metadata.remove(namespace) + }) + .await; + match removed { + Ok(Ok(_)) => {} + Ok(Err(e)) => tracing::error!( + %namespace, + "failed to remove the record of a namespace whose creation shutdown interrupted: {e}" + ), + Err(e) => tracing::error!( + %namespace, + "failed to remove the record of a namespace whose creation shutdown interrupted: {e}" + ), + } + } + Err(error) + } + pub async fn shutdown(self) -> crate::Result<()> { let mut set = JoinSet::new(); self.inner.has_shutdown.store(true, Ordering::Relaxed); @@ -532,3 +643,610 @@ impl NamespaceStore { .await } } + +#[cfg(test)] +pub(crate) mod test { + use std::path::Path; + + use axum::response::IntoResponse; + use hyper::StatusCode; + use libsql_sys::wal::Sqlite3WalManager; + use tempfile::tempdir; + use tokio::sync::Semaphore; + + use super::*; + use crate::namespace::configurator::{BaseNamespaceConfig, PrimaryConfig, PrimaryConfigurator}; + use crate::namespace::meta_store::metastore_connection_maker; + + pub(crate) async fn make_store(path: &Path) -> NamespaceStore { + let (maker, manager) = metastore_connection_maker(None, path).await.unwrap(); + let meta_store = MetaStore::new( + Default::default(), + path, + maker().unwrap(), + manager, + DatabaseKind::Primary, + ) + .await + .unwrap(); + let base_config = BaseNamespaceConfig { + base_path: path.to_path_buf().into(), + extensions: Arc::new([]), + stats_sender: tokio::sync::mpsc::channel(1).0, + max_response_size: 100_000_000, + max_total_response_size: 100_000_000, + max_concurrent_connections: Arc::new(Semaphore::new(10)), + max_concurrent_requests: 10_000, + encryption_config: None, + connection_creation_timeout: None, + disable_intelligent_throttling: false, + }; + let primary_config = PrimaryConfig { + max_log_size: 1_000_000_000, + max_log_duration: None, + bottomless_replication: None, + scripted_backup: None, + checkpoint_interval: None, + }; + let mut configurators = NamespaceConfigurators::empty(); + configurators.with_primary(PrimaryConfigurator::new( + base_config, + primary_config, + Arc::new(Sqlite3WalManager::default), + )); + + NamespaceStore::new( + false, + false, + 10, + meta_store, + configurators, + DatabaseKind::Primary, + ) + .await + .unwrap() + } + + async fn create(store: &NamespaceStore, name: &str) -> NamespaceName { + let namespace = NamespaceName::from_string(name.to_string()).unwrap(); + store + .create( + namespace.clone(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + namespace + } + + /// Starts `request` while the test holds `entry`'s write lock, and returns + /// once the request holds the same entry and waits for the lock: the + /// cache, the test and the request then share it. + async fn start_waiting( + store: &NamespaceStore, + entry: &NamespaceEntry, + request: impl FnOnce(NamespaceStore) -> Fut, + ) -> tokio::task::JoinHandle + where + Fut: std::future::Future + Send + 'static, + Fut::Output: Send + 'static, + { + settle(store, entry).await; + let request = tokio::spawn(request(store.clone())); + wait_until("the request holds the entry", || { + Arc::strong_count(entry) >= 3 + }) + .await; + request + } + + /// `start_waiting` for a `with` on `namespace`. + async fn start_waiting_request( + store: &NamespaceStore, + namespace: &NamespaceName, + entry: &NamespaceEntry, + ) -> tokio::task::JoinHandle> { + let namespace = namespace.clone(); + start_waiting(store, entry, move |store| async move { + store.with(namespace, |ns| ns.name().clone()).await + }) + .await + } + + /// Lets the cache drop the extra reference it keeps to a fresh entry until + /// its housekeeping runs, so that a third holder of `entry` can only be the + /// request or fork under test. + /// + /// This and `start_waiting` count references: the cache holds one, the + /// test one, and each waiting request one. That is how moka 0.12.8 (the + /// version in Cargo.lock) behaves; a moka upgrade that keeps more + /// references fails these tests with a timeout rather than hanging them. + async fn settle(store: &NamespaceStore, entry: &NamespaceEntry) { + tokio::time::timeout(Duration::from_secs(10), async { + while Arc::strong_count(entry) > 2 { + store.inner.store.run_pending_tasks().await; + tokio::time::sleep(Duration::from_millis(1)).await; + } + }) + .await + .expect("the cache keeps an extra reference to the entry"); + assert_eq!(Arc::strong_count(entry), 2); + } + + /// Yields until `condition` holds; fails the test instead of hanging. + async fn wait_until(what: &str, condition: impl Fn() -> bool) { + wait_until_async(what, || std::future::ready(condition())).await + } + + /// `wait_until` for a condition that is checked asynchronously. + async fn wait_until_async>( + what: &str, + condition: impl Fn() -> Fut, + ) { + tokio::time::timeout(Duration::from_secs(10), async { + while !condition().await { + tokio::time::sleep(Duration::from_millis(1)).await; + } + }) + .await + .unwrap_or_else(|_| panic!("timed out waiting until {what}")); + } + + #[tokio::test] + async fn with_after_shutdown_is_unavailable_not_missing() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + + store.clone().shutdown().await.unwrap(); + + let error = store.with(namespace, |_| ()).await.unwrap_err(); + assert!(matches!(error, Error::NamespaceStoreShutdown), "{error:?}"); + } + + #[tokio::test] + async fn load_namespace_refuses_to_open_a_namespace_once_the_store_has_shut_down() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + // Not cached, so the load would open the namespace. + store.inner.store.invalidate(&namespace).await; + store.inner.has_shutdown.store(true, Ordering::Relaxed); + + let handle = store.inner.metadata.handle(namespace.clone()).await; + let error = store + .load_namespace(&namespace, handle, RestoreOption::Latest) + .await + .unwrap_err(); + + assert!( + matches!(error.as_ref(), Error::NamespaceStoreShutdown), + "{error:?}" + ); + assert!(!store.inner.store.contains_key(&namespace)); + } + + #[tokio::test] + async fn reset_after_shutdown_is_refused() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + store.clone().shutdown().await.unwrap(); + + let error = store + .reset(namespace.clone(), RestoreOption::Latest) + .await + .unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some(Error::NamespaceStoreShutdown) + ), + "{error:?}" + ); + assert!(!store.inner.store.contains_key(&namespace)); + } + + #[tokio::test] + async fn with_reloads_a_namespace_evicted_while_waiting_for_it() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + let entry = store.inner.store.get(&namespace).await.unwrap(); + let mut lock = entry.write().await; + let request = start_waiting_request(&store, &namespace, &entry).await; + + // Evict the entry the request holds, as cache pressure or idleness + // does. The eviction listener waits for the write lock this test + // holds, so evict from another task and wait for the removal. + let eviction = tokio::spawn({ + let store = store.clone(); + let namespace = namespace.clone(); + async move { store.inner.store.invalidate(&namespace).await } + }); + wait_until("the entry is evicted", || { + !store.inner.store.contains_key(&namespace) + }) + .await; + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + assert_eq!(request.await.unwrap().unwrap(), namespace); + eviction.await.unwrap(); + // Reloaded into a fresh entry; the evicted one was not filled. + let cached = store.inner.store.get(&namespace).await.unwrap(); + assert!(!Arc::ptr_eq(&cached, &entry)); + assert!(cached.read().await.is_some()); + assert!(entry.read().await.is_none()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn requests_waiting_on_one_evicted_entry_share_one_reloaded_namespace() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + let entry = store.inner.store.get(&namespace).await.unwrap(); + let mut lock = entry.write().await; + settle(&store, &entry).await; + const REQUESTS: usize = 8; + let requests: Vec<_> = (0..REQUESTS) + .map(|_| { + tokio::spawn({ + let store = store.clone(); + let namespace = namespace.clone(); + async move { store.with(namespace, |ns| ns.stats.clone()).await } + }) + }) + .collect(); + wait_until("every request holds the entry", || { + Arc::strong_count(&entry) >= 2 + REQUESTS + }) + .await; + + // The entry every request holds is evicted: all of them look the + // namespace up again at once. + let eviction = tokio::spawn({ + let store = store.clone(); + let namespace = namespace.clone(); + async move { store.inner.store.invalidate(&namespace).await } + }); + wait_until("the entry is evicted", || { + !store.inner.store.contains_key(&namespace) + }) + .await; + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + let mut stats = Vec::with_capacity(REQUESTS); + for request in requests { + stats.push(request.await.unwrap().unwrap()); + } + eviction.await.unwrap(); + // The lookups coalesced: one namespace, loaded once, into one fresh + // entry, not one load per waiting request. + let cached = store.inner.store.get(&namespace).await.unwrap(); + assert!(!Arc::ptr_eq(&cached, &entry)); + let loaded = cached.read().await.as_ref().unwrap().stats.clone(); + assert!(stats.iter().all(|stats| Arc::ptr_eq(stats, &loaded))); + assert!(entry.read().await.is_none()); + } + + #[tokio::test] + async fn with_reports_missing_for_a_namespace_destroyed_while_waiting_for_it() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + let entry = store.inner.store.get(&namespace).await.unwrap(); + let mut lock = entry.write().await; + let request = start_waiting_request(&store, &namespace, &entry).await; + + // `destroy` removes the metastore record first, then takes the + // namespace out of its entry, for which it waits on the lock this + // test holds. The request must answer "missing" because of the record. + let destroy = tokio::spawn({ + let store = store.clone(); + let namespace = namespace.clone(); + async move { store.destroy(namespace, false).await } + }); + wait_until_async("the record is removed", || async { + !store.exists(&namespace).await + }) + .await; + // Take the namespace out as destroy would once it gets the lock, so + // the request finds the entry empty whichever of the two wakes first. + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + let error = request.await.unwrap().unwrap_err(); + assert!(matches!(error, Error::NamespaceDoesntExist(_)), "{error:?}"); + destroy.await.unwrap().unwrap(); + assert!(!store.inner.store.contains_key(&namespace)); + } + + #[tokio::test] + async fn with_reports_unavailable_not_missing_for_an_entry_left_empty() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + let entry = store.inner.store.get(&namespace).await.unwrap(); + let mut lock = entry.write().await; + let request = start_waiting_request(&store, &namespace, &entry).await; + + // The namespace is taken out but its emptied entry stays cached, as an + // entry left behind by a fork that failed before storing its record: + // every lookup finds it again. + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + // The metastore still has the namespace: giving up is temporary, not + // "missing". + let error = request.await.unwrap().unwrap_err(); + assert!(matches!(error, Error::NamespaceUnavailable(_)), "{error:?}"); + assert!(Arc::ptr_eq( + &store.inner.store.get(&namespace).await.unwrap(), + &entry + )); + } + + #[tokio::test] + async fn create_removes_its_record_when_shutdown_starts_before_its_load() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = NamespaceName::from_string("ns".to_string()).unwrap(); + + // `create`'s steps up to its load, then shutdown starts: the only + // place where a stored record has no loaded namespace behind it. + assert!(!store.inner.metadata.exists(&namespace).await); + let handle = store.inner.metadata.handle(namespace.clone()).await; + handle + .store(Arc::new(DatabaseConfig::default())) + .await + .unwrap(); + store.inner.has_shutdown.store(true, Ordering::Relaxed); + + let error = store + .load_created(&namespace, handle, RestoreOption::Latest) + .await + .unwrap_err(); + assert!( + matches!(error.as_ref(), Error::NamespaceStoreShutdown), + "{error:?}" + ); + assert!(!store.inner.metadata.exists(&namespace).await); + + // A retry against a fresh store on the same files creates the + // namespace instead of answering "already exists". + drop(store); + let store = make_store(tmp.path()).await; + store + .create( + namespace.clone(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + assert!(store.inner.store.get(&namespace).await.is_some()); + } + + #[tokio::test] + async fn create_after_shutdown_is_refused() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + store.clone().shutdown().await.unwrap(); + let namespace = NamespaceName::from_string("ns".to_string()).unwrap(); + + let error = store + .create( + namespace.clone(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap_err(); + + assert!(matches!(error, Error::NamespaceStoreShutdown), "{error:?}"); + assert!(!store.inner.metadata.exists(&namespace).await); + } + + #[tokio::test] + async fn with_reports_shutdown_when_the_store_shuts_down_while_waiting() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + let entry = store.inner.store.get(&namespace).await.unwrap(); + let mut lock = entry.write().await; + let request = start_waiting_request(&store, &namespace, &entry).await; + + // The rolling-restart case: shutdown starts while the request waits, + // then takes the namespace out of the entry the request holds. + let shutdown = tokio::spawn(store.clone().shutdown()); + wait_until("shutdown has started", || { + store.inner.has_shutdown.load(Ordering::Relaxed) + }) + .await; + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + let error = request.await.unwrap().unwrap_err(); + assert!(matches!(error, Error::NamespaceStoreShutdown), "{error:?}"); + shutdown.await.unwrap().unwrap(); + } + + #[tokio::test] + async fn checkpoint_does_not_cache_an_empty_entry_for_an_unloaded_namespace() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + store.inner.store.invalidate(&namespace).await; + + store.checkpoint(namespace.clone()).await.unwrap(); + + assert!(!store.inner.store.contains_key(&namespace)); + assert_eq!( + store + .with(namespace.clone(), |ns| ns.name().clone()) + .await + .unwrap(), + namespace + ); + } + + // `fork`'s cleanup on failure calls `block_in_place`, which panics on + // the current-thread runtime and would hide the real error. + #[tokio::test(flavor = "multi_thread")] + async fn fork_reloads_a_source_emptied_while_waiting_for_it() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let from = create(&store, "from").await; + let to = NamespaceName::from_string("to".to_string()).unwrap(); + let entry = store.inner.store.get(&from).await.unwrap(); + let mut lock = entry.write().await; + settle(&store, &entry).await; + let fork = tokio::spawn({ + let store = store.clone(); + let (from, to) = (from.clone(), to.clone()); + async move { store.fork(from, to, DatabaseConfig::default(), None).await } + }); + wait_until("the fork holds the source entry", || { + Arc::strong_count(&entry) >= 3 + }) + .await; + + // Evict the source entry the fork holds, as cache pressure does: the + // eviction listener waits for the write lock this test holds, so + // evict from another task and wait for the removal. + let eviction = tokio::spawn({ + let store = store.clone(); + let from = from.clone(); + async move { store.inner.store.invalidate(&from).await } + }); + wait_until("the source entry is evicted", || { + !store.inner.store.contains_key(&from) + }) + .await; + lock.take().unwrap().shutdown(false).await.unwrap(); + drop(lock); + + fork.await.unwrap().unwrap(); + eviction.await.unwrap(); + assert_eq!( + store + .with(to.clone(), |ns| ns.name().clone()) + .await + .unwrap(), + to + ); + // The source was reloaded into a fresh entry; the emptied one was not + // filled. + let cached = store.inner.store.get(&from).await.unwrap(); + assert!(!Arc::ptr_eq(&cached, &entry)); + assert!(cached.read().await.is_some()); + assert!(entry.read().await.is_none()); + } + + // `fork`'s cleanup on failure calls `block_in_place`, which panics on + // the current-thread runtime and would hide the real error. + #[tokio::test(flavor = "multi_thread")] + async fn fork_reports_unavailable_when_its_source_is_emptied_between_its_loads() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let from = create(&store, "from").await; + let to = NamespaceName::from_string("to".to_string()).unwrap(); + // Holding the destination entry's lock stops the fork after it loaded + // its source through `with` and before it reads the source from its + // own lookup. + let to_entry: NamespaceEntry = Default::default(); + store.inner.store.insert(to.clone(), to_entry.clone()).await; + let to_lock = to_entry.write().await; + let fork = start_waiting(&store, &to_entry, { + let (from, to) = (from.clone(), to.clone()); + move |store| async move { store.fork(from, to, DatabaseConfig::default(), None).await } + }) + .await; + + // The source is taken out, and its emptied entry stays cached. + let from_entry = store.inner.store.get(&from).await.unwrap(); + from_entry + .write() + .await + .take() + .unwrap() + .shutdown(false) + .await + .unwrap(); + drop(to_lock); + + let error = fork.await.unwrap().unwrap_err(); + assert!(matches!(error, Error::NamespaceUnavailable(_)), "{error:?}"); + } + + #[tokio::test] + async fn an_emptied_entry_is_missing_only_when_the_metastore_agrees() { + let tmp = tempdir().unwrap(); + let store = make_store(tmp.path()).await; + let namespace = create(&store, "ns").await; + + let error = store.emptied_entry_error(&namespace).await; + assert!(matches!(error, Error::NamespaceUnavailable(_)), "{error:?}"); + + tokio::task::spawn_blocking({ + let store = store.clone(); + let namespace = namespace.clone(); + move || store.inner.metadata.remove(namespace) + }) + .await + .unwrap() + .unwrap(); + let error = store.emptied_entry_error(&namespace).await; + assert!(matches!(error, Error::NamespaceDoesntExist(_)), "{error:?}"); + + store.inner.has_shutdown.store(true, Ordering::Relaxed); + let error = store.emptied_entry_error(&namespace).await; + assert!(matches!(error, Error::NamespaceStoreShutdown), "{error:?}"); + } + + /// Clients tell a missing namespace from a temporarily unavailable one by + /// the status, and some also by the message: a 503 must never read like + /// a 404. + #[test] + fn temporarily_unavailable_messages_do_not_claim_the_namespace_is_missing() { + for error in [ + Error::NamespaceStoreShutdown, + Error::NamespaceUnavailable("ns".to_string()), + ] { + let message = error.to_string(); + assert!(!message.contains("doesn't exist"), "{message}"); + assert!(!message.contains("not found"), "{message}"); + } + assert!(Error::NamespaceDoesntExist("ns".to_string()) + .to_string() + .contains("doesn't exist")); + } + + #[test] + fn temporarily_unavailable_namespaces_answer_503() { + for error in [ + Error::NamespaceStoreShutdown, + Error::NamespaceUnavailable("ns".to_string()), + // Wrapped by the cache, once or more. + Error::Ref(Arc::new(Error::NamespaceStoreShutdown)), + Error::Ref(Arc::new(Error::Ref(Arc::new(Error::NamespaceUnavailable( + "ns".to_string(), + ))))), + ] { + assert_eq!( + error.into_response().status(), + StatusCode::SERVICE_UNAVAILABLE + ); + } + assert_eq!( + Error::NamespaceDoesntExist("ns".to_string()) + .into_response() + .status(), + StatusCode::NOT_FOUND + ); + } +} diff --git a/libsql-server/src/rpc/mod.rs b/libsql-server/src/rpc/mod.rs index 2936a8742c..653af6398d 100644 --- a/libsql-server/src/rpc/mod.rs +++ b/libsql-server/src/rpc/mod.rs @@ -23,6 +23,26 @@ pub mod replica_proxy; pub mod replication; pub mod streaming_exec; +/// The gRPC status for an error from `NamespaceStore::with`. `FAILED_PRECONDITION` +/// is only ever a missing namespace. A namespace that exists but cannot be +/// served right now (the store is shutting down, or its cache entry was +/// emptied) is `UNAVAILABLE`, so clients retry it instead of treating it as +/// missing or as an internal failure. The authentication paths decide on the +/// code this returns, so that they classify the error in exactly one place. +pub(crate) fn namespace_error_status(error: &crate::Error) -> Status { + // `Error::as_ref` unwraps the `Error::Ref`s from the namespace cache. + #[allow(clippy::useless_asref)] + match error.as_ref() { + crate::Error::NamespaceDoesntExist(_) => Status::failed_precondition( + libsql_replication::rpc::replication::NAMESPACE_DOESNT_EXIST, + ), + crate::Error::NamespaceStoreShutdown | crate::Error::NamespaceUnavailable(_) => { + Status::unavailable(error.to_string()) + } + _ => Status::internal(error.to_string()), + } +} + pub async fn run_rpc_server( proxy_service: ProxyService, acceptor: A, @@ -149,3 +169,49 @@ fn trace_request(req: &hyper::Request, span: &Span) { } } } + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use tonic::Code; + + use super::namespace_error_status; + use crate::Error; + + #[test] + fn namespace_errors_map_to_grpc_status() { + let cases = [ + ( + Error::NamespaceDoesntExist("ns".into()), + Code::FailedPrecondition, + ), + (Error::NamespaceStoreShutdown, Code::Unavailable), + (Error::NamespaceUnavailable("ns".into()), Code::Unavailable), + ( + Error::Ref(Arc::new(Error::NamespaceUnavailable("ns".into()))), + Code::Unavailable, + ), + ( + Error::Ref(Arc::new(Error::NamespaceDoesntExist("ns".into()))), + Code::FailedPrecondition, + ), + // Nested `Ref`s are unwrapped too. + ( + Error::Ref(Arc::new(Error::Ref(Arc::new( + Error::NamespaceStoreShutdown, + )))), + Code::Unavailable, + ), + (Error::InvalidNamespace, Code::Internal), + ]; + for (error, code) in cases { + assert_eq!(namespace_error_status(&error).code(), code, "{error:?}"); + } + // Replicas match on the message to decide that a namespace is gone. + assert_eq!( + namespace_error_status(&Error::NamespaceDoesntExist("ns".into())).message(), + libsql_replication::rpc::replication::NAMESPACE_DOESNT_EXIST + ); + } +} diff --git a/libsql-server/src/rpc/proxy.rs b/libsql-server/src/rpc/proxy.rs index 1ab5d1d19a..2259da8a75 100644 --- a/libsql-server/src/rpc/proxy.rs +++ b/libsql-server/src/rpc/proxy.rs @@ -13,7 +13,6 @@ use libsql_replication::rpc::proxy::{ describe_result, Ack, DescribeRequest, DescribeResult, Description, DisconnectMessage, ExecReq, ExecResp, ExecuteResults, QueryResult, ResultRows, Row, }; -use libsql_replication::rpc::replication::NAMESPACE_DOESNT_EXIST; use rusqlite::types::ValueRef; use tokio::time::Duration; use uuid::Uuid; @@ -313,13 +312,21 @@ impl ProxyService { let auth = match namespace_jwt_keys { Ok(Ok(Some(key))) => Some(Auth::new(Jwt::new(key))), Ok(Ok(None)) => self.user_auth_strategy.clone(), - Err(e) => match e.as_ref() { - crate::error::Error::NamespaceDoesntExist(_) => None, - _ => Err(tonic::Status::internal(format!( - "Error fetching jwt key for a namespace: {}", - e - )))?, - }, + Err(e) => { + // `namespace_error_status` classifies the store's errors: + // `FAILED_PRECONDITION` is a missing namespace, `UNAVAILABLE` + // one that exists but cannot be served right now. Only a + // missing namespace skips authentication here. + let status = crate::rpc::namespace_error_status(&e); + match status.code() { + tonic::Code::FailedPrecondition => None, + tonic::Code::Unavailable => Err(status)?, + _ => Err(tonic::Status::internal(format!( + "Error fetching jwt key for a namespace: {}", + e + )))?, + } + } Ok(Err(e)) => Err(tonic::Status::internal(format!( "Error fetching jwt key for a namespace: {}", e @@ -585,13 +592,7 @@ impl Proxy for ProxyService { (connection_maker, notifier) }) .await - .map_err(|e| { - if let crate::error::Error::NamespaceDoesntExist(_) = e { - tonic::Status::failed_precondition(NAMESPACE_DOESNT_EXIST) - } else { - tonic::Status::internal(e.to_string()) - } - })?; + .map_err(|e| crate::rpc::namespace_error_status(&e))?; let conn = connection_maker .create() @@ -617,13 +618,7 @@ impl Proxy for ProxyService { .namespaces .with(ctx.namespace().clone(), |ns| ns.db.connection_maker()) .await - .map_err(|e| { - if let crate::error::Error::NamespaceDoesntExist(_) = e { - tonic::Status::failed_precondition(NAMESPACE_DOESNT_EXIST) - } else { - tonic::Status::internal(e.to_string()) - } - })?; + .map_err(|e| crate::rpc::namespace_error_status(&e))?; let conn = { let lock = self.clients.upgradable_read().await; @@ -691,13 +686,7 @@ impl Proxy for ProxyService { .namespaces .with(ctx.namespace().clone(), |ns| ns.db.connection_maker()) .await - .map_err(|e| { - if let crate::error::Error::NamespaceDoesntExist(_) = e { - tonic::Status::failed_precondition(NAMESPACE_DOESNT_EXIST) - } else { - tonic::Status::internal(e.to_string()) - } - })?; + .map_err(|e| crate::rpc::namespace_error_status(&e))?; let DescribeRequest { client_id, stmt } = req.into_inner(); let client_id = Uuid::from_str(&client_id).unwrap(); @@ -755,3 +744,37 @@ impl Proxy for ProxyService { })) } } + +#[cfg(test)] +mod test { + use tempfile::tempdir; + + use super::*; + use crate::connection::config::DatabaseConfig; + use crate::namespace::{make_test_store, NamespaceName, RestoreOption}; + + #[tokio::test] + async fn context_while_the_store_is_shut_down_is_unavailable() { + let tmp = tempdir().unwrap(); + let store = make_test_store(tmp.path()).await; + store + .create( + NamespaceName::default(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + store.clone().shutdown().await.unwrap(); + let service = ProxyService::new(store, None, true); + // A namespace reported as missing is authenticated from this header, + // which the client writes. A false "missing" used to make a forged + // one pass; the request must fail before it is read. + let mut request = tonic::Request::new(()); + Authenticated::FullAccess.upgrade_grpc_request(&mut request); + + let status = service.extract_context(&mut request).await.err().unwrap(); + + assert_eq!(status.code(), tonic::Code::Unavailable, "{status:?}"); + } +} diff --git a/libsql-server/src/rpc/replica_proxy.rs b/libsql-server/src/rpc/replica_proxy.rs index f6efec2739..84be3d36b5 100644 --- a/libsql-server/src/rpc/replica_proxy.rs +++ b/libsql-server/src/rpc/replica_proxy.rs @@ -47,13 +47,22 @@ impl ReplicaProxyService { let auth = match namespace_jwt_keys { Ok(Some(key)) => Ok(Auth::new(Jwt::new(key))), - Ok(None) | Err(crate::error::Error::NamespaceDoesntExist(_)) => { - Ok(self.user_auth_strategy.clone()) + Ok(None) => Ok(self.user_auth_strategy.clone()), + Err(e) => { + // `namespace_error_status` classifies the store's errors: + // `FAILED_PRECONDITION` is a missing namespace, `UNAVAILABLE` + // one that exists but cannot be served right now. Only a + // missing namespace falls back to the user strategy. + let status = super::namespace_error_status(&e); + match status.code() { + tonic::Code::FailedPrecondition => Ok(self.user_auth_strategy.clone()), + tonic::Code::Unavailable => Err(status), + _ => Err(Status::internal(format!( + "Can't fetch jwt key for a namespace: {}", + e + ))), + } } - Err(e) => Err(Status::internal(format!( - "Can't fetch jwt key for a namespace: {}", - e - ))), }?; let auth_context = @@ -127,3 +136,39 @@ impl Proxy for ReplicaProxyService { client.describe(req).await } } + +#[cfg(test)] +mod test { + use tempfile::tempdir; + + use super::*; + use crate::auth::Disabled; + use crate::connection::config::DatabaseConfig; + use crate::namespace::{make_test_store, NamespaceName, RestoreOption}; + + #[tokio::test] + async fn auth_while_the_store_is_shut_down_is_unavailable() { + let tmp = tempdir().unwrap(); + let store = make_test_store(tmp.path()).await; + store + .create( + NamespaceName::default(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + store.clone().shutdown().await.unwrap(); + let service = ReplicaProxyService::new( + Channel::from_static("http://127.0.0.1:1").connect_lazy(), + Uri::from_static("http://127.0.0.1:1"), + store, + Auth::new(Disabled::new()), + true, + ); + + let status = service.do_auth(&mut Request::new(())).await.unwrap_err(); + + assert_eq!(status.code(), tonic::Code::Unavailable, "{status:?}"); + } +} diff --git a/libsql-server/src/rpc/replication/auth.rs b/libsql-server/src/rpc/replication/auth.rs index 7c726e6d6d..e0289ae5b1 100644 --- a/libsql-server/src/rpc/replication/auth.rs +++ b/libsql-server/src/rpc/replication/auth.rs @@ -1,5 +1,4 @@ -use libsql_replication::rpc::replication::NAMESPACE_DOESNT_EXIST; -use tonic::Status; +use tonic::{Code, Status}; use crate::auth::parsers::parse_grpc_auth_header; use crate::auth::{Auth, Jwt}; @@ -18,18 +17,21 @@ pub async fn authenticate( let auth = match namespace_jwt_keys { Ok(Ok(Some(key))) => Some(Auth::new(Jwt::new(key))), Ok(Ok(None)) => user_auth_strategy.clone(), - Err(e) => match e.as_ref() { - crate::error::Error::NamespaceDoesntExist(_) if allow_user_auth_fallback => { - user_auth_strategy.clone() + Err(e) => { + // `namespace_error_status` is the one place that classifies the + // store's errors: `FAILED_PRECONDITION` is a missing namespace, + // `UNAVAILABLE` one that exists but cannot be served right now. + // Only a missing namespace may fall back to the user strategy. + let status = crate::rpc::namespace_error_status(&e); + match status.code() { + Code::FailedPrecondition if allow_user_auth_fallback => user_auth_strategy.clone(), + Code::FailedPrecondition | Code::Unavailable => return Err(status), + _ => Err(Status::internal(format!( + "Error fetching jwt key for a namespace: {}", + e + )))?, } - crate::error::Error::NamespaceDoesntExist(_) => { - return Err(tonic::Status::failed_precondition(NAMESPACE_DOESNT_EXIST)) - } - _ => Err(Status::internal(format!( - "Error fetching jwt key for a namespace: {}", - e - )))?, - }, + } Ok(Err(e)) => Err(Status::internal(format!( "Error fetching jwt key for a namespace: {}", e diff --git a/libsql-server/src/rpc/replication/replication_log.rs b/libsql-server/src/rpc/replication/replication_log.rs index 1f7585f34f..67b1a7bf9f 100644 --- a/libsql-server/src/rpc/replication/replication_log.rs +++ b/libsql-server/src/rpc/replication/replication_log.rs @@ -11,8 +11,8 @@ pub use libsql_replication::rpc::replication as rpc; use libsql_replication::rpc::replication::log_offset::WalFlavor; use libsql_replication::rpc::replication::replication_log_server::ReplicationLog; use libsql_replication::rpc::replication::{ - Frame, Frames, HelloRequest, HelloResponse, LogOffset, NAMESPACE_DOESNT_EXIST, - NEED_SNAPSHOT_ERROR_MSG, NO_HELLO_ERROR_MSG, SESSION_TOKEN_KEY, + Frame, Frames, HelloRequest, HelloResponse, LogOffset, NEED_SNAPSHOT_ERROR_MSG, + NO_HELLO_ERROR_MSG, SESSION_TOKEN_KEY, }; use md5::{Digest, Md5}; use tokio_stream::StreamExt as _; @@ -154,13 +154,7 @@ impl ReplicationLogService { Ok((logger, config, version, stats, config_changed)) }) .await - .map_err(|e| { - if let crate::error::Error::NamespaceDoesntExist(_) = e.as_ref() { - Status::failed_precondition(NAMESPACE_DOESNT_EXIST) - } else { - Status::internal(e.to_string()) - } - })??; + .map_err(|e| crate::rpc::namespace_error_status(&e))??; if verify_session { self.verify_session_token(req, version)?; @@ -455,3 +449,72 @@ mod snapshot_stream { } } } + +#[cfg(test)] +mod test { + use tempfile::tempdir; + + use super::*; + use crate::connection::config::DatabaseConfig; + use crate::namespace::{make_test_store, RestoreOption}; + + async fn shut_down_store(path: &std::path::Path) -> NamespaceStore { + let store = make_test_store(path).await; + store + .create( + NamespaceName::default(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + store.clone().shutdown().await.unwrap(); + store + } + + #[tokio::test] + async fn hello_to_an_internal_service_while_the_store_is_shut_down_is_unavailable() { + let tmp = tempdir().unwrap(); + let store = shut_down_store(tmp.path()).await; + // An internal service skips authentication for requests that were not + // proxied, so the error comes from logger_from_namespace. + let service = ReplicationLogService::new(store, None, None, true, false, true); + + let status = service + .hello(tonic::Request::new(HelloRequest::new())) + .await + .unwrap_err(); + + assert_eq!(status.code(), tonic::Code::Unavailable, "{status:?}"); + } + + #[tokio::test] + async fn hello_while_the_store_is_shut_down_is_unavailable() { + let tmp = tempdir().unwrap(); + let store = make_test_store(tmp.path()).await; + store + .create( + NamespaceName::default(), + RestoreOption::Latest, + DatabaseConfig::default(), + ) + .await + .unwrap(); + // An external service authenticates through the namespace first. A + // user strategy that would reject this request (Basic auth, and the + // request carries no credentials) shows that the shut-down store does + // not fall back to it: falling back would answer UNAUTHENTICATED or + // INTERNAL, as a missing namespace does. + let user_auth = Auth::new(crate::auth::HttpBasic::new("dXNlcjpwYXNz".to_string())); + let service = + ReplicationLogService::new(store.clone(), None, Some(user_auth), true, false, false); + store.clone().shutdown().await.unwrap(); + + let status = service + .hello(tonic::Request::new(HelloRequest::new())) + .await + .unwrap_err(); + + assert_eq!(status.code(), tonic::Code::Unavailable, "{status:?}"); + } +} From c93d0fb0b3155cc4162ad4c0646f683712110034 Mon Sep 17 00:00:00 2001 From: Mostafa Yassine Date: Tue, 20 Oct 2026 15:54:29 -0400 Subject: [PATCH 16/17] libsql: add regression test for idempotent Connection::disconnect (cherry picked from commit eaabc928478aba68633a0b07890a0d4fa4bdd201) --- libsql/src/local/connection.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index ca1601a6f3..3583192d79 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -822,6 +822,24 @@ mod tests { OpenFlags, }; + /// `LibsqlConnection::drop` calls `disconnect()`, then the inner + /// `Connection::drop` calls it again. The second call must not reach + /// `sqlite3_close_v2` with a handle that the first call already freed. + #[test] + fn disconnect_is_idempotent() { + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path().join("local.db"); + let db = Database::new(path.to_str().unwrap().to_string(), OpenFlags::default()); + let mut conn = Connection::connect(&db).unwrap(); + assert!(!conn.raw.is_null()); + + conn.disconnect(); + assert!(conn.raw.is_null()); + + conn.disconnect(); + assert!(conn.raw.is_null()); + } + #[tokio::test] pub async fn test_kek() { let temp_dir = tempfile::tempdir().unwrap(); From 6cb046c85926c2689c34f806b9bb73a8f5790d5b Mon Sep 17 00:00:00 2001 From: Mostafa Yassine Date: Tue, 20 Oct 2026 21:09:38 -0400 Subject: [PATCH 17/17] libsql: fold double-close repro into a unit test (cherry picked from commit 5863c874215ceb6e7fc1f0903945e179afbe9578) --- libsql/src/local/connection.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index 3583192d79..3d06b5ac52 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -840,6 +840,27 @@ mod tests { assert!(conn.raw.is_null()); } + /// Drops a connection through the public API so both `Drop` impls run. + /// Passes on any allocator; under Guard Malloc or valgrind it faults in + /// `sqlite3SafetyCheckSickOrOk` if `disconnect()` closes the handle twice. + #[tokio::test] + async fn dropping_public_connection_closes_once() { + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path().join("local.db"); + for _ in 0..20 { + let db = crate::Builder::new_local(path.to_str().unwrap()) + .build() + .await + .unwrap(); + let conn = db.connect().unwrap(); + conn.execute("CREATE TABLE IF NOT EXISTS t(x INTEGER)", ()) + .await + .unwrap(); + drop(conn); + drop(db); + } + } + #[tokio::test] pub async fn test_kek() { let temp_dir = tempfile::tempdir().unwrap();