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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Single-threaded isolation of the `reclaim_offset` integration (task #37).
//!
//! The isolated ring test (`remote_ring_unit.rs`) proved the *ring* correct as a
//! data structure. It did NOT exercise `reclaim_offset` — the owner-side logic
//! that turns a drained offset back into a `BinTable` free-list node (page-map
//! class lookup + double-free guard + push). That logic is single-threaded
//! owner code; if it is buggy, the multi-thread crash would reproduce here with
//! **no threads at all**.
//!
//! Protocol per round: alloc K blocks, push each block's offset into its
//! segment ring (simulating a cross-thread free), drain+reclaim, then re-alloc K
//! and assert every pointer is valid, distinct, and in range. A logic bug in
//! `reclaim_offset` (wrong class, double-add, bad offset) corrupts the free list
//! and this test crashes/asserts WITHOUT any concurrency.
//!
//! GREEN here ⟹ the reclaim logic is sound single-threaded ⟹ the race_repro
//! crash is a concurrency/ordering bug (not a logic bug), directing the fix.
#![cfg(all(feature = "alloc-core", feature = "alloc-xthread"))]
use core::alloc::Layout;
use std::collections::HashSet;
use sefer_alloc::alloc_core::AllocCore;
fn seg_of(p: *mut u8) -> usize {
// Match os::segment_base_of: mask to SEGMENT alignment. SEGMENT is 4 MiB
// (1<<22) in this build; derive defensively by masking low 22 bits.
(p as usize) & !((1usize << 22) - 1)
}
#[test]
fn reclaim_offset_single_threaded_roundtrip() {
let mut ac = AllocCore::new().expect("primordial");
let layout = Layout::from_size_align(8, 8).unwrap(); // class 0
const K: usize = 200;
const ROUNDS: usize = 50;
for round in 0..ROUNDS {
// 1. Alloc K blocks.
let mut ptrs = Vec::with_capacity(K);
for _ in 0..K {
let p = ac.alloc(layout);
assert!(!p.is_null(), "alloc returned null in round {round}");
ptrs.push(p);
}
// Distinct.
let set: HashSet<usize> = ptrs.iter().map(|&p| p as usize).collect();
assert_eq!(
set.len(),
K,
"alloc handed out a duplicate in round {round}"
);
// 2. Simulate cross-thread free: push each offset (with its size class)
// into its segment ring. The test allocates only `layout` (8/8) →
// class 0, so the cross-thread freer's class is 0 here.
const CLASS_IDX: usize = 0;
let mut pushed = 0usize;
for &p in &ptrs {
if ac.dbg_push_to_ring(p, CLASS_IDX) {
pushed += 1;
}
}
// The ring is bounded (RING_CAP=256); with K=200 all should fit per
// segment if blocks span few segments. Don't assert all pushed — some
// may overflow if many land in one segment's ring; that's a bounded
// leak, not a bug. But we DID push at least some.
assert!(pushed > 0, "no offsets pushed in round {round}");
// 3. Drain + reclaim into the BinTables.
ac.dbg_drain_all_rings();
// 4. Re-alloc K blocks. Reclaimed blocks should be reused; the rest
// carved. Every pointer must be valid and distinct from the others
// handed out THIS round (a corrupt free list would hand out a
// garbage pointer or loop).
let mut ptrs2 = Vec::with_capacity(K);
for _ in 0..K {
let p = ac.alloc(layout);
assert!(!p.is_null(), "re-alloc null in round {round}");
ptrs2.push(p);
}
let set2: HashSet<usize> = ptrs2.iter().map(|&p| p as usize).collect();
assert_eq!(
set2.len(),
K,
"re-alloc handed out a DUPLICATE in round {round} — free-list \
corruption (a block listed twice)"
);
// 5. Sanity: each re-alloc'd pointer is SEGMENT-aligned-base-consistent
// (points inside a real segment, not garbage).
for &p in &ptrs2 {
let base = seg_of(p);
assert!(p as usize >= base, "pointer below its segment base");
assert!(
(p as usize) - base < (1usize << 22),
"pointer outside its segment (corrupt offset) in round {round}"
);
}
// 6. Free everything own-thread to recycle for the next round (keeps
// the working set bounded). Own-thread dealloc routes straight to the
// BinTable.
for &p in &ptrs2 {
ac.dealloc(p, layout);
}
}
}