Skip to main content

moonpool_explorer/
coverage.rs

1//! Coverage tracking for exploration novelty detection.
2//!
3//! Tracks which assertion paths have been explored across all timelines.
4//! The [`ExploredMap`] lives in shared memory and accumulates coverage
5//! from every timeline. Each timeline gets a fresh [`CoverageBitmap`]
6//! that is merged into the explored map after the timeline finishes.
7
8/// Size of coverage bitmaps in bytes (8192 bit positions).
9pub const COVERAGE_MAP_SIZE: usize = 1024;
10
11/// Per-timeline coverage bitmap, cleared before each split.
12///
13/// Tracks which assertion paths were hit during this timeline's execution.
14/// After the timeline finishes, the parent merges this into the [`ExploredMap`].
15pub struct CoverageBitmap {
16    ptr: *mut u8,
17}
18
19impl CoverageBitmap {
20    /// Wrap a shared-memory pointer as a coverage bitmap.
21    ///
22    /// # Safety
23    ///
24    /// `ptr` must point to at least [`COVERAGE_MAP_SIZE`] bytes of valid,
25    /// writable shared memory.
26    pub unsafe fn new(ptr: *mut u8) -> Self {
27        Self { ptr }
28    }
29
30    /// Set the bit at the given index (mod total bits).
31    pub fn set_bit(&self, index: usize) {
32        let bit_index = index % (COVERAGE_MAP_SIZE * 8);
33        let byte = bit_index / 8;
34        let bit = bit_index % 8;
35        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes (constructor invariant).
36        // The modular arithmetic (bit_index % (COVERAGE_MAP_SIZE * 8)) guarantees
37        // byte = bit_index / 8 < COVERAGE_MAP_SIZE, so ptr.add(byte) is in bounds.
38        unsafe {
39            *self.ptr.add(byte) |= 1 << bit;
40        }
41    }
42
43    /// Clear all bits to zero.
44    pub fn clear(&self) {
45        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes of writable shared
46        // memory (constructor invariant). write_bytes zeroes exactly that region.
47        unsafe {
48            std::ptr::write_bytes(self.ptr, 0, COVERAGE_MAP_SIZE);
49        }
50    }
51
52    /// Get a pointer to the underlying data.
53    #[must_use]
54    pub fn as_ptr(&self) -> *const u8 {
55        self.ptr
56    }
57}
58
59/// Cross-process coverage map, OR'd by all timelines.
60///
61/// Lives in `MAP_SHARED` memory so all forked timelines contribute.
62/// A bit set to 1 means "this assertion path has been explored."
63pub struct ExploredMap {
64    ptr: *mut u8,
65}
66
67impl ExploredMap {
68    /// Wrap a shared-memory pointer as a explored map.
69    ///
70    /// # Safety
71    ///
72    /// `ptr` must point to at least [`COVERAGE_MAP_SIZE`] bytes of valid,
73    /// writable shared memory.
74    pub unsafe fn new(ptr: *mut u8) -> Self {
75        Self { ptr }
76    }
77
78    /// Merge a timeline's coverage bitmap into this explored map (bitwise OR).
79    pub fn merge_from(&self, other: &CoverageBitmap) {
80        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
81        // (constructor invariants of ExploredMap and CoverageBitmap).
82        // Loop bound 0..COVERAGE_MAP_SIZE ensures all ptr.add(i) are in bounds.
83        unsafe {
84            for i in 0..COVERAGE_MAP_SIZE {
85                *self.ptr.add(i) |= *other.as_ptr().add(i);
86            }
87        }
88    }
89
90    /// Count the number of set bits in the explored map (population count).
91    ///
92    /// Returns the total number of unique assertion paths explored across
93    /// all timelines.
94    #[must_use]
95    pub fn count_bits_set(&self) -> u32 {
96        let mut count: u32 = 0;
97        // Safety: self.ptr points to COVERAGE_MAP_SIZE bytes (constructor invariant).
98        // Loop bound 0..COVERAGE_MAP_SIZE ensures ptr.add(i) is in bounds.
99        unsafe {
100            for i in 0..COVERAGE_MAP_SIZE {
101                count += (*self.ptr.add(i)).count_ones();
102            }
103        }
104        count
105    }
106
107    /// Check if a timeline's bitmap contains any bits not yet in the explored map.
108    #[must_use]
109    pub fn has_new_bits(&self, other: &CoverageBitmap) -> bool {
110        // Safety: both pointers are valid for COVERAGE_MAP_SIZE bytes
111        // (constructor invariants of ExploredMap and CoverageBitmap).
112        // Loop bound 0..COVERAGE_MAP_SIZE ensures all ptr.add(i) are in bounds.
113        unsafe {
114            for i in 0..COVERAGE_MAP_SIZE {
115                let explored = *self.ptr.add(i);
116                let child = *other.as_ptr().add(i);
117                // Child has bits that explored map doesn't
118                if (child & !explored) != 0 {
119                    return true;
120                }
121            }
122        }
123        false
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::shared_mem;
131
132    #[test]
133    fn test_set_bit_and_check() {
134        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
135        let explored_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
136        let bm = unsafe { CoverageBitmap::new(ptr) };
137        let vm = unsafe { ExploredMap::new(explored_ptr) };
138
139        // Initially no new bits
140        assert!(!vm.has_new_bits(&bm));
141
142        // Set a bit in bitmap
143        bm.set_bit(42);
144        assert!(vm.has_new_bits(&bm));
145
146        // Merge into explored map
147        vm.merge_from(&bm);
148        // Now no new bits (already merged)
149        assert!(!vm.has_new_bits(&bm));
150
151        unsafe {
152            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
153            shared_mem::free_shared(explored_ptr, COVERAGE_MAP_SIZE);
154        }
155    }
156
157    #[test]
158    fn test_clear() {
159        let ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
160        let bm = unsafe { CoverageBitmap::new(ptr) };
161
162        bm.set_bit(0);
163        bm.set_bit(100);
164        bm.set_bit(8000);
165
166        bm.clear();
167
168        // Verify all zeroed
169        unsafe {
170            for i in 0..COVERAGE_MAP_SIZE {
171                assert_eq!(*ptr.add(i), 0);
172            }
173            shared_mem::free_shared(ptr, COVERAGE_MAP_SIZE);
174        }
175    }
176
177    #[test]
178    fn test_merge_accumulates() {
179        let bm1_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
180        let bm2_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
181        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
182
183        let bm1 = unsafe { CoverageBitmap::new(bm1_ptr) };
184        let bm2 = unsafe { CoverageBitmap::new(bm2_ptr) };
185        let vm = unsafe { ExploredMap::new(vm_ptr) };
186
187        bm1.set_bit(10);
188        bm2.set_bit(20);
189
190        vm.merge_from(&bm1);
191        // bm2 has bit 20 which is new
192        assert!(vm.has_new_bits(&bm2));
193
194        vm.merge_from(&bm2);
195        // Now neither has new bits
196        assert!(!vm.has_new_bits(&bm1));
197        assert!(!vm.has_new_bits(&bm2));
198
199        unsafe {
200            shared_mem::free_shared(bm1_ptr, COVERAGE_MAP_SIZE);
201            shared_mem::free_shared(bm2_ptr, COVERAGE_MAP_SIZE);
202            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
203        }
204    }
205
206    #[test]
207    fn test_count_bits_set() {
208        let vm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
209        let bm_ptr = shared_mem::alloc_shared(COVERAGE_MAP_SIZE).expect("alloc failed");
210        let vm = unsafe { ExploredMap::new(vm_ptr) };
211        let bm = unsafe { CoverageBitmap::new(bm_ptr) };
212
213        assert_eq!(vm.count_bits_set(), 0);
214
215        bm.set_bit(0);
216        bm.set_bit(42);
217        bm.set_bit(8000);
218        vm.merge_from(&bm);
219
220        assert_eq!(vm.count_bits_set(), 3);
221
222        unsafe {
223            shared_mem::free_shared(vm_ptr, COVERAGE_MAP_SIZE);
224            shared_mem::free_shared(bm_ptr, COVERAGE_MAP_SIZE);
225        }
226    }
227}