Skip to main content

base64_ng/v2/assurance/
default_provider.rs

1//! Bounded local best-effort provider used by portable deployments and tests.
2
3mod helpers;
4
5use helpers::{check_limit, effective_pages, next_generation, report_from_state};
6
7use alloc::vec::Vec;
8use core::{
9    cell::{RefCell, UnsafeCell},
10    marker::PhantomData,
11    sync::atomic::{AtomicUsize, Ordering},
12};
13
14use super::{
15    AttestationEvidence, DisposalResult, JournalDisposition, PhysicalProtection,
16    ProtectedMemoryProvider, ProtectionError, ProtectionRequest, ProviderAccess, ProviderHealth,
17    ProviderLimits, ProviderOperationResult, ProviderReport, QuarantineRecord, ResourceKind,
18    TeardownCursor, WipeConfirmation, WipeEvidence,
19};
20
21static NEXT_PROVIDER_IDENTITY: AtomicUsize = AtomicUsize::new(1);
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23enum SlotLifecycle {
24    Free,
25    Reserved,
26    Active,
27    Quarantined,
28    PermanentlyQuarantined,
29}
30
31struct Slot {
32    generation: usize,
33    lifecycle: SlotLifecycle,
34    logical_bytes: usize,
35    effective_pages: usize,
36    retries: usize,
37    storage: Option<Vec<u8>>,
38    cursor: TeardownCursor,
39}
40
41impl Slot {
42    const fn free() -> Self {
43        Self {
44            generation: 1,
45            lifecycle: SlotLifecycle::Free,
46            logical_bytes: 0,
47            effective_pages: 0,
48            retries: 0,
49            storage: None,
50            cursor: TeardownCursor::new(),
51        }
52    }
53
54    fn reset(&mut self) {
55        self.generation = self.generation.checked_add(1).unwrap_or(0);
56        self.lifecycle = SlotLifecycle::Free;
57        self.logical_bytes = 0;
58        self.effective_pages = 0;
59        self.retries = 0;
60        self.storage = None;
61        self.cursor = TeardownCursor::new();
62    }
63}
64
65struct ProviderState<const SLOTS: usize> {
66    health: ProviderHealth,
67    health_generation: usize,
68    protection_generation: usize,
69    slots: [Slot; SLOTS],
70}
71
72impl<const SLOTS: usize> Drop for ProviderState<SLOTS> {
73    fn drop(&mut self) {
74        for slot in &mut self.slots {
75            if let Some(storage) = slot.storage.as_deref_mut() {
76                crate::wipe_bytes(storage);
77            }
78        }
79    }
80}
81
82#[doc(hidden)]
83/// Reservation made before a best-effort allocation exists.
84pub struct BestEffortReservation {
85    slot: usize,
86    generation: usize,
87    request: ProtectionRequest,
88}
89
90#[doc(hidden)]
91/// Unique active allocation handle.
92pub struct BestEffortHandle {
93    slot: usize,
94    generation: usize,
95    reserved_pages: usize,
96    bytes: Vec<u8>,
97    _not_thread_or_unwind_safe: PhantomData<(UnsafeCell<()>, &'static mut dyn FnMut())>,
98}
99
100/// Finite local provider with best-effort wiping and no OS memory protection.
101///
102/// The provider is intentionally `!Sync`. It pre-reserves one registry slot
103/// and all count/byte/page budget before allocating zeroed storage. Its default
104/// hooks are conclusive, so quarantine is reachable only through recovery of a
105/// previously transferred record or fault-injected provider wrappers.
106pub struct BestEffortProvider<const SLOTS: usize> {
107    identity: usize,
108    limits: ProviderLimits,
109    state: RefCell<ProviderState<SLOTS>>,
110}
111impl<const SLOTS: usize> BestEffortProvider<SLOTS> {
112    /// Creates a bounded provider.
113    // Rust 1.97 renamed this operation; retain the 1.90 MSRV spelling until
114    // the workspace MSRV can use `try_update`.
115    #[allow(deprecated)]
116    pub fn new(limits: ProviderLimits) -> Result<Self, ProtectionError> {
117        if limits.page_size == 0
118            || limits.max_identities > SLOTS
119            || limits.max_registry_entries > SLOTS
120            || limits.max_retry_attempts == 0
121            || limits.max_maintenance_work == 0
122        {
123            return Err(ProtectionError::InvalidLimits);
124        }
125        let identity = NEXT_PROVIDER_IDENTITY
126            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
127                value.checked_add(1)
128            })
129            .map_err(|_| ProtectionError::ProviderUnavailable)?;
130        Ok(Self {
131            identity,
132            limits,
133            state: RefCell::new(ProviderState {
134                health: ProviderHealth::Healthy,
135                health_generation: 1,
136                protection_generation: 1,
137                slots: core::array::from_fn(|_| Slot::free()),
138            }),
139        })
140    }
141
142    /// Runs bounded in-process recovery for quarantined entries.
143    ///
144    /// Every wipe retry restarts at byte zero. This default provider has no
145    /// fallible later hooks; successful retries close their slots. Health is
146    /// not upgraded automatically.
147    pub fn maintain(&self) -> usize {
148        let mut state = self.state.borrow_mut();
149        let mut work = 0;
150        let mut shutdown = false;
151        for slot in &mut state.slots {
152            if work >= self.limits.max_maintenance_work {
153                break;
154            }
155            if slot.lifecycle != SlotLifecycle::Quarantined {
156                continue;
157            }
158            work += 1;
159            if slot.retries >= self.limits.max_retry_attempts {
160                slot.lifecycle = SlotLifecycle::PermanentlyQuarantined;
161                shutdown = true;
162                continue;
163            }
164            slot.retries += 1;
165            if let Some(storage) = slot.storage.as_deref_mut() {
166                crate::wipe_bytes(storage);
167            }
168            slot.storage = None;
169            slot.reset();
170        }
171        if shutdown {
172            state.health = ProviderHealth::Shutdown;
173            state.health_generation = next_generation(state.health_generation);
174        }
175        work
176    }
177
178    /// Restores admission only after quarantine is empty and no tombstone exists.
179    pub fn restore_health_after_self_check(&self) -> Result<(), ProtectionError> {
180        let mut state = self.state.borrow_mut();
181        if state.slots.iter().any(|slot| {
182            matches!(
183                slot.lifecycle,
184                SlotLifecycle::Quarantined | SlotLifecycle::PermanentlyQuarantined
185            )
186        }) {
187            return Err(ProtectionError::ProviderUnavailable);
188        }
189        let health_generation = next_generation(state.health_generation);
190        let protection_generation = next_generation(state.protection_generation);
191        if health_generation == 0 || protection_generation == 0 {
192            state.health = ProviderHealth::Shutdown;
193            state.health_generation = health_generation;
194            state.protection_generation = protection_generation;
195            return Err(ProtectionError::ProviderUnavailable);
196        }
197        state.health = ProviderHealth::Healthy;
198        state.health_generation = health_generation;
199        state.protection_generation = protection_generation;
200        Ok(())
201    }
202
203    fn release_reservation(&self, reservation: &BestEffortReservation) {
204        let mut state = self.state.borrow_mut();
205        if let Some(slot) = state.slots.get_mut(reservation.slot)
206            && slot.generation == reservation.generation
207            && slot.lifecycle == SlotLifecycle::Reserved
208        {
209            slot.reset();
210        }
211    }
212}
213
214#[allow(unsafe_code)]
215unsafe impl<const SLOTS: usize> ProtectedMemoryProvider for BestEffortProvider<SLOTS> {
216    type Handle = BestEffortHandle;
217    type Reservation = BestEffortReservation;
218
219    fn provider_identity(&self) -> usize {
220        self.identity
221    }
222    fn provider_generation(&self) -> usize {
223        1
224    }
225
226    fn health_generation(&self) -> usize {
227        self.state.borrow().health_generation
228    }
229
230    fn protection_generation(&self) -> usize {
231        self.state.borrow().protection_generation
232    }
233
234    fn health(&self) -> ProviderHealth {
235        self.state.borrow().health
236    }
237
238    fn limits(&self) -> ProviderLimits {
239        self.limits
240    }
241
242    fn report(&self) -> ProviderReport {
243        let state = self.state.borrow();
244        let mut report = ProviderReport {
245            health: state.health,
246            health_generation: state.health_generation,
247            protection_generation: state.protection_generation,
248            active_and_reserved: 0,
249            quarantined: 0,
250            permanently_quarantined: 0,
251            tombstoned: 0,
252            charged_logical_bytes: 0,
253            charged_effective_pages: 0,
254        };
255        for slot in &state.slots {
256            match slot.lifecycle {
257                SlotLifecycle::Reserved | SlotLifecycle::Active => report.active_and_reserved += 1,
258                SlotLifecycle::Quarantined => report.quarantined += 1,
259                SlotLifecycle::PermanentlyQuarantined => {
260                    report.permanently_quarantined += 1;
261                }
262                SlotLifecycle::Free => continue,
263            }
264            report.charged_logical_bytes += slot.logical_bytes;
265            report.charged_effective_pages += slot.effective_pages;
266        }
267        report
268    }
269
270    fn reserve(
271        &self,
272        _access: &ProviderAccess,
273        request: ProtectionRequest,
274    ) -> Result<Self::Reservation, ProtectionError> {
275        if request.requires_attestation() {
276            return Err(ProtectionError::ProtectionUnavailable);
277        }
278        let mut state = self.state.borrow_mut();
279        if state.health != ProviderHealth::Healthy {
280            return Err(ProtectionError::ProviderUnavailable);
281        }
282        let report = report_from_state(&state);
283        let used_identities = report
284            .active_and_reserved
285            .checked_add(report.quarantined)
286            .and_then(|used| used.checked_add(report.permanently_quarantined))
287            .and_then(|used| used.checked_add(report.tombstoned))
288            .ok_or(ProtectionError::ProtectionResourceExhausted(
289                ResourceKind::Identities,
290            ))?;
291        let limit_result = check_limit(
292            used_identities,
293            1,
294            self.limits.max_identities,
295            ResourceKind::Identities,
296        )
297        .and_then(|()| {
298            check_limit(
299                used_identities,
300                1,
301                self.limits.max_registry_entries,
302                ResourceKind::RegistryEntries,
303            )
304        })
305        .and_then(|()| {
306            check_limit(
307                report.charged_logical_bytes,
308                request.logical_bytes(),
309                self.limits.max_logical_bytes,
310                ResourceKind::LogicalBytes,
311            )
312        })
313        .and_then(|()| {
314            check_limit(
315                report.charged_effective_pages,
316                request.reserved_pages(),
317                self.limits.max_effective_pages,
318                ResourceKind::EffectivePages,
319            )
320        });
321        if let Err(error) = limit_result {
322            state.health = ProviderHealth::Exhausted;
323            state.health_generation = next_generation(state.health_generation);
324            return Err(error);
325        }
326        let Some((slot_index, slot)) = state
327            .slots
328            .iter_mut()
329            .enumerate()
330            .find(|(_, slot)| slot.lifecycle == SlotLifecycle::Free && slot.generation != 0)
331        else {
332            state.health = ProviderHealth::Exhausted;
333            state.health_generation = next_generation(state.health_generation);
334            return Err(ProtectionError::ProtectionResourceExhausted(
335                ResourceKind::RegistryEntries,
336            ));
337        };
338        slot.lifecycle = SlotLifecycle::Reserved;
339        slot.logical_bytes = request.logical_bytes();
340        slot.effective_pages = request.reserved_pages();
341        Ok(BestEffortReservation {
342            slot: slot_index,
343            generation: slot.generation,
344            request,
345        })
346    }
347
348    fn materialize(
349        &self,
350        _access: &ProviderAccess,
351        reservation: Self::Reservation,
352    ) -> Result<Self::Handle, ProtectionError> {
353        let mut bytes = Vec::new();
354        if bytes
355            .try_reserve_exact(reservation.request.logical_bytes())
356            .is_err()
357        {
358            self.release_reservation(&reservation);
359            return Err(ProtectionError::ProviderUnavailable);
360        }
361        bytes.resize(reservation.request.logical_bytes(), 0);
362        let actual_pages =
363            effective_pages(bytes.as_ptr() as usize, bytes.len(), self.limits.page_size)?;
364        if actual_pages > reservation.request.reserved_pages() {
365            crate::wipe_bytes(&mut bytes);
366            self.release_reservation(&reservation);
367            return Err(ProtectionError::ActualRangeExceededReservation);
368        }
369        let mut state = self.state.borrow_mut();
370        let Some(slot) = state.slots.get_mut(reservation.slot) else {
371            crate::wipe_bytes(&mut bytes);
372            return Err(ProtectionError::ProviderUnavailable);
373        };
374        if slot.generation != reservation.generation || slot.lifecycle != SlotLifecycle::Reserved {
375            crate::wipe_bytes(&mut bytes);
376            return Err(ProtectionError::ProviderUnavailable);
377        }
378        slot.lifecycle = SlotLifecycle::Active;
379        slot.effective_pages = actual_pages;
380        Ok(BestEffortHandle {
381            slot: reservation.slot,
382            generation: reservation.generation,
383            reserved_pages: actual_pages,
384            bytes,
385            _not_thread_or_unwind_safe: PhantomData,
386        })
387    }
388
389    fn logical_len(&self, _access: &ProviderAccess, handle: &Self::Handle) -> usize {
390        handle.bytes.len()
391    }
392
393    fn physical_protection(
394        &self,
395        _access: &ProviderAccess,
396        _handle: &Self::Handle,
397    ) -> PhysicalProtection {
398        PhysicalProtection::ProtectionConfirmedAbsent
399    }
400
401    fn bytes<'handle>(
402        &self,
403        _access: &ProviderAccess,
404        handle: &'handle Self::Handle,
405    ) -> &'handle [u8] {
406        &handle.bytes
407    }
408
409    fn bytes_mut<'handle>(
410        &self,
411        _access: &ProviderAccess,
412        handle: &'handle mut Self::Handle,
413    ) -> &'handle mut [u8] {
414        &mut handle.bytes
415    }
416
417    fn confirm_wipe(
418        &self,
419        _access: &ProviderAccess,
420        _handle: &Self::Handle,
421        _attestation: Option<AttestationEvidence>,
422        cursor: &mut TeardownCursor,
423    ) -> WipeConfirmation {
424        cursor.disposition = JournalDisposition::Applied;
425        WipeConfirmation {
426            result: ProviderOperationResult::Applied,
427            evidence: WipeEvidence::WipedBestEffort,
428        }
429    }
430
431    fn remove_protection(
432        &self,
433        _access: &ProviderAccess,
434        _handle: &mut Self::Handle,
435        cursor: &mut TeardownCursor,
436    ) -> ProviderOperationResult {
437        cursor.disposition = JournalDisposition::Applied;
438        ProviderOperationResult::Applied
439    }
440
441    fn reconcile_accounting(
442        &self,
443        _access: &ProviderAccess,
444        _handle: &mut Self::Handle,
445        cursor: &mut TeardownCursor,
446    ) -> ProviderOperationResult {
447        cursor.disposition = JournalDisposition::Applied;
448        ProviderOperationResult::Applied
449    }
450
451    fn dispose(
452        &self,
453        _access: &ProviderAccess,
454        mut handle: Self::Handle,
455        cursor: &mut TeardownCursor,
456    ) -> DisposalResult<Self::Handle> {
457        cursor.disposition = JournalDisposition::Applied;
458        crate::wipe_bytes(&mut handle.bytes);
459        let mut state = self.state.borrow_mut();
460        if let Some(slot) = state.slots.get_mut(handle.slot)
461            && slot.generation == handle.generation
462            && slot.lifecycle == SlotLifecycle::Active
463        {
464            slot.reset();
465        }
466        DisposalResult::Applied
467    }
468
469    fn quarantine(&self, _access: &ProviderAccess, handle: Self::Handle, record: QuarantineRecord) {
470        let mut state = self.state.borrow_mut();
471        state.health = ProviderHealth::Degraded;
472        state.health_generation = next_generation(state.health_generation);
473        if let Some(slot) = state.slots.get_mut(handle.slot)
474            && slot.generation == handle.generation
475        {
476            slot.lifecycle = SlotLifecycle::Quarantined;
477            slot.retries = record.retry_attempt;
478            slot.cursor = record.cursor;
479            slot.logical_bytes = handle.bytes.len();
480            slot.effective_pages = handle.reserved_pages;
481            slot.storage = Some(handle.bytes);
482        }
483    }
484}