use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::RealmId;
use crate::btree::overflow::{
OVERFLOW_ROOT_HEADER_LEN, collect_chain, decode_overflow, encode_overflow,
overflow_page_capacity, overflow_root_capacity, read_chain, read_root_page,
};
use crate::crypto::CipherId;
use crate::crypto::kdf::derive_mk;
use crate::pager::format::data_page::ENVELOPE_OVERHEAD;
use crate::pager::{PageKind, Pager, PagerConfig};
use crate::vfs::memory::MemVfs;
use proptest::prelude::*;
use proptest::test_runner::{TestCaseResult, TestRunner};
const PAGE: usize = 4096;
const REALM: RealmId = RealmId::new([0x3C; 16]);
const POOL: usize = 6;
const FIRST_PAGE: u64 = 4;
fn cases() -> u32 {
std::env::var("PAGEDB_PROPTEST_CASES")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(32)
}
fn walk_timeout() -> Duration {
Duration::from_millis(2_000 + u64::from(cases()) * 100)
}
fn config() -> ProptestConfig {
ProptestConfig {
cases: cases(),
failure_persistence: None,
..ProptestConfig::default()
}
}
fn block_on<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
}
async fn fresh_pager() -> Arc<Pager<MemVfs>> {
let mk = derive_mk(&[0x3D; 32], &[0u8; 16], 0).unwrap();
let config = PagerConfig {
page_size: PAGE,
buffer_pool_pages: 64,
segment_cache_pages: 64,
cipher_id: CipherId::Aes256Gcm,
mk_epoch: 0,
main_db_file_id: [0x3E; 16],
main_db_path: "/main.db".into(),
anchor_budget: 1_000_000,
dek_lru_capacity: 16,
observer_retry_count: 0,
metrics_enabled: false,
};
Arc::new(Pager::open(MemVfs::new(), mk, config).await.unwrap())
}
fn run_bounded<S>(
label: &'static str,
strategy: S,
timeout: Duration,
test: impl Fn(S::Value) -> TestCaseResult + Send + 'static,
) where
S: Strategy + Send + 'static,
S::Value: std::fmt::Debug,
{
let last_input = Arc::new(Mutex::new(String::from("<none>")));
let thread_input = Arc::clone(&last_input);
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let mut runner = TestRunner::new(config());
let outcome = runner.run(&strategy, move |value| {
*thread_input
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = format!("{value:?}");
test(value)
});
let _ = sender.send(outcome.map_err(|error| error.to_string()));
});
match receiver.recv_timeout(timeout) {
Ok(Ok(())) => {}
Ok(Err(message)) => panic!("{label}: {message}"),
Err(_) => {
let input = last_input
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
panic!("{label}: did not terminate within {timeout:?}; last input: {input}");
}
}
}
fn forged_root_body(refcount: u32, next: u64, declared_data_len: u32, data: &[u8]) -> Vec<u8> {
let mut body = vec![0u8; PAGE - ENVELOPE_OVERHEAD];
body[0..4].copy_from_slice(&refcount.to_le_bytes());
body[4..12].copy_from_slice(&next.to_le_bytes());
body[12..16].copy_from_slice(&declared_data_len.to_le_bytes());
let take = data.len().min(body.len() - OVERFLOW_ROOT_HEADER_LEN);
body[OVERFLOW_ROOT_HEADER_LEN..OVERFLOW_ROOT_HEADER_LEN + take].copy_from_slice(&data[..take]);
body
}
fn pool_page_id(slot: usize) -> u64 {
match slot % (POOL + 1) {
0 => 0,
occupied => FIRST_PAGE + occupied as u64 - 1,
}
}
type ChainSpec = (Vec<(usize, u16)>, u64);
fn chain_spec() -> impl Strategy<Value = ChainSpec> {
(
prop::collection::vec((0usize..=POOL, 0u16..=600), 1..=POOL),
prop_oneof![
Just(0u64),
Just(u64::MAX),
Just(u64::MAX / 2),
Just(u64::from(u32::MAX)),
0u64..8192,
],
)
}
async fn lay_out_chain(pager: &Arc<Pager<MemVfs>>, pages: &[(usize, u16)]) {
let root_capacity = overflow_root_capacity(PAGE);
let chain_capacity = overflow_page_capacity(PAGE);
for (index, &(next_slot, declared_len)) in pages.iter().enumerate() {
let page_id = FIRST_PAGE + index as u64;
let next = pool_page_id(next_slot);
if index == 0 {
let data = vec![0xAB_u8; usize::from(declared_len).min(root_capacity)];
let body = forged_root_body(1, next, u32::from(declared_len), &data);
pager
.write_main_page(page_id, REALM, PageKind::OverflowRoot, &body)
.await
.unwrap();
} else {
let data = vec![0xCD_u8; usize::from(declared_len).min(chain_capacity)];
let mut body = vec![0u8; PAGE - ENVELOPE_OVERHEAD];
encode_overflow(&mut body, next, &data).unwrap();
pager
.write_main_page(page_id, REALM, PageKind::Overflow, &body)
.await
.unwrap();
}
}
}
#[test]
fn generated_overflow_chains_terminate_or_report_corruption() {
run_bounded(
"overflow read_chain over a generated pointer graph",
chain_spec(),
walk_timeout(),
|(pages, total_len)| {
block_on(async {
let pager = fresh_pager().await;
lay_out_chain(&pager, &pages).await;
let _ = read_root_page(&pager, REALM, FIRST_PAGE).await;
let _ = read_chain(&pager, REALM, FIRST_PAGE, total_len).await;
let _ = collect_chain(&pager, REALM, FIRST_PAGE).await;
});
Ok(())
},
);
}
#[test]
fn absurd_total_len_over_a_valid_chain_is_rejected_not_allocated() {
run_bounded(
"overflow read_chain with an absurd total_len",
(
0usize..=POOL,
prop_oneof![Just(u64::MAX), (1u64 << 40)..u64::MAX],
),
walk_timeout(),
|(next_slot, total_len)| {
let outcome = block_on(async {
let pager = fresh_pager().await;
let body = forged_root_body(1, pool_page_id(next_slot), 4, b"data");
pager
.write_main_page(FIRST_PAGE, REALM, PageKind::OverflowRoot, &body)
.await
.unwrap();
read_chain(&pager, REALM, FIRST_PAGE, total_len).await
});
prop_assert!(
outcome.is_err(),
"a total_len of {total_len} cannot be satisfied by a one-page chain"
);
Ok(())
},
);
}
#[test]
fn wholly_random_overflow_bodies_never_panic() {
run_bounded(
"overflow chain over wholly random page bodies",
(
prop::collection::vec(
prop::collection::vec(any::<u8>(), PAGE - ENVELOPE_OVERHEAD),
1..=3,
),
any::<u64>(),
),
walk_timeout(),
|(bodies, total_len)| {
block_on(async {
let pager = fresh_pager().await;
for (index, body) in bodies.iter().enumerate() {
let kind = if index == 0 {
PageKind::OverflowRoot
} else {
PageKind::Overflow
};
pager
.write_main_page(FIRST_PAGE + index as u64, REALM, kind, body)
.await
.unwrap();
}
let _ = read_root_page(&pager, REALM, FIRST_PAGE).await;
let _ = read_chain(&pager, REALM, FIRST_PAGE, total_len).await;
let _ = collect_chain(&pager, REALM, FIRST_PAGE).await;
});
Ok(())
},
);
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_never_panic_the_chain_page_decoder(
body in prop::collection::vec(any::<u8>(), 0..=(PAGE - ENVELOPE_OVERHEAD)),
) {
if let Ok((_, data)) = decode_overflow(&body) {
prop_assert!(data.len() + 12 <= body.len());
}
}
#[test]
fn hostile_chain_page_data_len_is_bounded_by_the_body(
next in any::<u64>(),
declared_len in any::<u32>(),
body_len in 0usize..=(PAGE - ENVELOPE_OVERHEAD),
) {
let mut body = vec![0u8; body_len];
if body.len() >= 12 {
body[0..8].copy_from_slice(&next.to_le_bytes());
body[8..12].copy_from_slice(&declared_len.to_le_bytes());
}
if let Ok((got_next, data)) = decode_overflow(&body) {
prop_assert_eq!(got_next, next);
prop_assert_eq!(data.len() as u64, u64::from(declared_len));
prop_assert!(12 + data.len() <= body.len());
}
}
}