1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//! Phase 35 (M6) — bounded miri decommit/recommit cycle (`alloc-decommit`).
//!
//! A deliberately SMALL test sized for miri (~1000× slower than native): it
//! allocates just enough large-ish small blocks to spill past the primordial
//! segment into ONE fresh `Small` segment, frees everything so that fresh
//! segment empties and decommits, then re-allocates and writes/reads back to
//! prove the recommit path is sound. Under miri `os::decommit_pages` /
//! `recommit_pages` are no-ops, so this verifies the BOOKKEEPING (live_count
//! zero-crossing → decommit hook, the reset, and reuse correctness), not RSS —
//! exactly what the design (§5) asks miri to cover. No UAF, no OOB write during
//! the reset, no access to "freed" pages on reuse.
//!
//! Kept separate from `decommit_soak` (whose N is large for a native RSS soak)
//! so the miri target is fast. Block size is chosen so the primordial fills in
//! a few thousand blocks rather than tens of thousands.
#![cfg(all(feature = "alloc-core", feature = "alloc-decommit"))]
use core::alloc::Layout;
use sefer_alloc::alloc_core::AllocCore;
#[test]
fn decommit_recommit_cycle_bookkeeping() {
let before = AllocCore::dbg_decommit_count();
let mut ac = AllocCore::new().expect("primordial");
// 2 KiB blocks: ~2K per 4 MiB payload, so a few thousand spills one fresh
// segment. Small enough for miri to finish quickly.
let layout = Layout::from_size_align(2048, 8).unwrap();
// Two rounds: round 1 fills + empties (→ decommit); round 2 reuses.
for round in 0..2 {
let mut ptrs = Vec::new();
// Enough to overflow the primordial into >= 2 fresh Small segments, so
// at least one fresh `Small` segment is NON-current when emptied (the
// current carve target is never decommitted). ~2K blocks/segment at 2 KiB,
// so ~6000 spans the primordial + 2 fresh segments.
for i in 0..6000usize {
let p = ac.alloc(layout);
assert!(!p.is_null(), "alloc null i={i} round={round}");
// Touch a few bytes (recommitted pages on reuse must read back).
unsafe {
let b = (i & 0xFF) as u8;
p.write(b);
p.add(2047).write(b);
}
ptrs.push((p, (i & 0xFF) as u8));
}
for &(p, b) in &ptrs {
unsafe {
assert_eq!(p.read(), b, "byte0 readback round={round}");
assert_eq!(p.add(2047).read(), b, "byteN readback round={round}");
}
}
// Free all → empties the fresh non-current Small segment → decommit.
for &(p, _) in &ptrs {
ac.dealloc(p, layout);
}
}
let after = AllocCore::dbg_decommit_count();
assert!(
after > before,
"decommit hook never fired (before={before}, after={after})"
);
}