Skip to main content

onnx_runtime_memory_api/
binding.rs

1//! Registry-issued memory bindings and lifetime pins.
2//!
3//! This module is intentionally narrower than a process memory manager. It owns
4//! registration identity, current-mechanism selection, binding/allocation
5//! identity, owning allocation handles, and the `Arc`s required to keep one
6//! mechanism usable. It does not own allocation policy, reservations, leases,
7//! queue scheduling, or reclamation of quarantined ownership.
8//!
9//! Release ownership is split deliberately:
10//!
11//! * [`BoundAllocation`] is non-RAII Phase-3 metadata whose only release path is
12//!   the explicit [`MemoryBinding::release`] migration adapter.
13//! * [`OwningAllocation`] is the Phase-4 owner: not `Clone`, not `Copy`, one
14//!   consuming release, and a `Drop` that quarantines rather than frees.
15//! * [`crate::deferred::PreparedAllocationRelease`] is final ownership that has
16//!   already been detached from the live record and may be queued.
17
18use std::collections::HashMap;
19use std::fmt::Debug;
20use std::num::NonZeroU64;
21use std::ptr::NonNull;
22use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
23use std::sync::{Arc, Mutex, MutexGuard};
24
25use crate::deferred::{
26    AllocationReleaseOutcome, AllocationReleaseState, DeferredReleaseDisposition,
27    DeferredReleaseQueue, PreparedAllocationRelease, PreparedReleasePins, QuarantineReason,
28    QuarantinedAllocation,
29};
30use crate::{
31    AllocationCommitRange, DeviceAllocator, DeviceKey, MemoryError, SharedDevicePrefix,
32    SharedPrefixCommitInfo,
33};
34
35static NEXT_REGISTRY_ID: AtomicU64 = AtomicU64::new(1);
36
37#[derive(Clone, Copy, PartialEq, Eq, Hash)]
38struct OpaqueIdentity {
39    registry: NonZeroU64,
40    serial: NonZeroU64,
41}
42
43impl Debug for OpaqueIdentity {
44    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        formatter
46            .debug_tuple("opaque")
47            .field(&self.registry)
48            .field(&self.serial)
49            .finish()
50    }
51}
52
53macro_rules! opaque_identity {
54    ($name:ident) => {
55        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
56        pub struct $name(OpaqueIdentity);
57
58        impl Debug for $name {
59            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60                formatter
61                    .debug_tuple(stringify!($name))
62                    .field(&self.0)
63                    .finish()
64            }
65        }
66    };
67}
68
69opaque_identity!(AuthorityIdentity);
70opaque_identity!(ProviderContextIdentity);
71opaque_identity!(MechanismIdentity);
72opaque_identity!(BindingId);
73
74/// Opaque registry-issued generation of one [`MemoryBinding`].
75#[derive(Clone, Copy, PartialEq, Eq, Hash)]
76pub struct BindingGeneration(NonZeroU64);
77
78impl Debug for BindingGeneration {
79    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        formatter
81            .debug_tuple("BindingGeneration")
82            .field(&self.0)
83            .finish()
84    }
85}
86
87/// Opaque registry-issued generation of one allocation at one binding.
88///
89/// Generations are never derived from a pointer. Reusing the same virtual
90/// address therefore cannot make metadata for an earlier allocation current.
91#[derive(Clone, Copy, PartialEq, Eq, Hash)]
92pub struct AllocationGeneration(NonZeroU64);
93
94impl Debug for AllocationGeneration {
95    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        formatter
97            .debug_tuple("AllocationGeneration")
98            .field(&self.0)
99            .finish()
100    }
101}
102
103/// Complete identity of one manager-issued binding.
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
105pub struct BindingIdentity {
106    id: BindingId,
107    generation: BindingGeneration,
108    device: DeviceKey,
109    mechanism: MechanismIdentity,
110    provider_context: ProviderContextIdentity,
111    authority: AuthorityIdentity,
112}
113
114impl BindingIdentity {
115    pub const fn id(self) -> BindingId {
116        self.id
117    }
118
119    pub const fn generation(self) -> BindingGeneration {
120        self.generation
121    }
122
123    pub const fn device(self) -> DeviceKey {
124        self.device
125    }
126
127    pub const fn mechanism(self) -> MechanismIdentity {
128        self.mechanism
129    }
130
131    pub const fn provider_context(self) -> ProviderContextIdentity {
132        self.provider_context
133    }
134
135    pub const fn authority(self) -> AuthorityIdentity {
136        self.authority
137    }
138}
139
140/// Identity of one allocation made through a binding.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142pub struct AllocationIdentity {
143    binding: BindingIdentity,
144    generation: AllocationGeneration,
145}
146
147impl AllocationIdentity {
148    pub const fn binding(self) -> BindingIdentity {
149        self.binding
150    }
151
152    pub const fn generation(self) -> AllocationGeneration {
153        self.generation
154    }
155}
156
157/// Resource retained by a registered provider context or authority.
158///
159/// The registry treats this value as an opaque lifetime pin. Concrete managers
160/// may store CUDA contexts, provider libraries, accounting authorities, or a
161/// composite resource owner here.
162pub trait BindingResource: Send + Sync + Debug {}
163
164impl<T> BindingResource for T where T: Send + Sync + Debug {}
165
166/// Whether one registered allocator is self-contained or an explicitly trusted
167/// composition of multiple inner mechanism interfaces.
168#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
169pub enum MechanismCoherence {
170    /// One allocator implementation supplies its own ordinary and optional
171    /// capability interfaces.
172    SelfContained,
173    /// The registrar explicitly attested that a transparent/composite wrapper
174    /// routes allocation, capabilities, and canonical release coherently.
175    TrustedComposite,
176}
177
178/// Lifecycle of one registered mechanism.
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum MechanismLifecycle {
181    /// New allocations and capability operations are accepted.
182    Active,
183    /// New work is rejected, but existing allocations may still be explicitly
184    /// released through their pinned original mechanism.
185    Retired,
186    /// The device/context was lost. All operations, including explicit release,
187    /// are rejected until external context/process termination is confirmed.
188    DeviceLost,
189    /// External context/process termination was observed. Metadata is terminal;
190    /// no allocator callback is made while entering this state.
191    Terminated,
192}
193
194/// Read-only lifecycle information. Taking a snapshot never invokes a provider.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct MechanismSnapshot {
197    pub identity: MechanismIdentity,
198    pub device: DeviceKey,
199    pub provider_context: ProviderContextIdentity,
200    pub authority: AuthorityIdentity,
201    pub coherence: MechanismCoherence,
202    pub lifecycle: MechanismLifecycle,
203    pub live_allocations: usize,
204    pub active_operations: usize,
205    /// Allocations whose live record was retired into a prepared release that
206    /// has not settled yet. These are [`AllocationReleaseState::Queued`].
207    pub queued_releases: usize,
208    /// Allocations whose ownership was retained instead of released.
209    pub quarantined_allocations: usize,
210    /// Bytes still owned by quarantined ownership.
211    pub quarantined_bytes: u64,
212}
213
214impl MechanismSnapshot {
215    /// Whether any ownership is still outstanding in any non-terminal or
216    /// retained state. Removal is unsafe while this is true.
217    pub const fn retains_ownership(&self) -> bool {
218        self.live_allocations != 0 || self.queued_releases != 0 || self.quarantined_allocations != 0
219    }
220}
221
222/// Binding/registration failure before any caller-provided device action runs.
223#[derive(Debug, thiserror::Error)]
224pub enum BindingError {
225    #[error("binding identity space is exhausted")]
226    IdentityExhausted,
227    #[error("the {kind} belongs to another binding registry")]
228    ForeignRegistry { kind: &'static str },
229    #[error("cannot register {subject} for {actual:?}; its registered device is {expected:?}")]
230    DeviceMismatch {
231        subject: &'static str,
232        expected: DeviceKey,
233        actual: DeviceKey,
234    },
235    #[error("provider context {0:?} is not registered")]
236    UnregisteredProviderContext(ProviderContextIdentity),
237    #[error("provider context {0:?} still has a registered mechanism")]
238    ProviderContextInUse(ProviderContextIdentity),
239    #[error("authority {0:?} is not registered")]
240    UnregisteredAuthority(AuthorityIdentity),
241    #[error("authority {0:?} still has a registered mechanism")]
242    AuthorityInUse(AuthorityIdentity),
243    #[error("mechanism {0:?} is not registered")]
244    UnregisteredMechanism(MechanismIdentity),
245    #[error("device {0:?} has no selected memory mechanism")]
246    NoSelectedMechanism(DeviceKey),
247    #[error("mechanism {mechanism:?} is {lifecycle:?}; {operation} is not permitted")]
248    InactiveMechanism {
249        mechanism: MechanismIdentity,
250        lifecycle: MechanismLifecycle,
251        operation: &'static str,
252    },
253    #[error("device {device:?} was lost: {reason}")]
254    DeviceLost { device: DeviceKey, reason: Arc<str> },
255    #[error("binding mismatch: expected {expected:?}, but metadata belongs to {actual:?}")]
256    BindingMismatch {
257        expected: BindingId,
258        actual: BindingId,
259    },
260    #[error("allocation metadata {0:?} is stale or was already explicitly released")]
261    StaleAllocation(AllocationIdentity),
262    #[error(
263        "allocation {identity:?} still has {views} outstanding view(s); physical release is not \
264         permitted while a borrowed view or alias may still be used"
265    )]
266    OutstandingViews {
267        identity: AllocationIdentity,
268        views: usize,
269    },
270    #[error(
271        "release of allocation {identity:?} left {retained_bytes} byte(s) in the {state} state: \
272         {reason}"
273    )]
274    ReleaseQuarantined {
275        identity: AllocationIdentity,
276        state: AllocationReleaseState,
277        reason: QuarantineReason,
278        retained_bytes: u64,
279    },
280    #[error(
281        "mechanism {mechanism:?} still owns {quarantined} quarantined allocation(s); removal \
282         would lose ownership that was deliberately retained"
283    )]
284    QuarantinedOwnership {
285        mechanism: MechanismIdentity,
286        quarantined: usize,
287    },
288    #[error("view range {offset}..{end} exceeds allocation size {allocation_bytes}")]
289    ViewOutOfBounds {
290        offset: usize,
291        end: usize,
292        allocation_bytes: usize,
293    },
294    #[error("binding registry lock was poisoned while {operation}")]
295    LockPoisoned { operation: &'static str },
296    #[error(
297        "provider context {context:?} still has {active_operations} active mechanism operation(s)"
298    )]
299    ContextNotQuiescent {
300        context: ProviderContextIdentity,
301        active_operations: usize,
302    },
303    #[error(transparent)]
304    Memory(#[from] MemoryError),
305}
306
307#[derive(Debug)]
308struct IdentitySource {
309    registry: NonZeroU64,
310    next: AtomicU64,
311}
312
313impl IdentitySource {
314    fn new() -> Result<Self, BindingError> {
315        let registry = next_nonzero(&NEXT_REGISTRY_ID)?;
316        Ok(Self {
317            registry,
318            next: AtomicU64::new(1),
319        })
320    }
321
322    fn opaque(&self) -> Result<OpaqueIdentity, BindingError> {
323        Ok(OpaqueIdentity {
324            registry: self.registry,
325            serial: next_nonzero(&self.next)?,
326        })
327    }
328
329    fn binding_generation(&self) -> Result<BindingGeneration, BindingError> {
330        Ok(BindingGeneration(next_nonzero(&self.next)?))
331    }
332
333    fn allocation_generation(&self) -> Result<AllocationGeneration, BindingError> {
334        Ok(AllocationGeneration(next_nonzero(&self.next)?))
335    }
336}
337
338fn next_nonzero(counter: &AtomicU64) -> Result<NonZeroU64, BindingError> {
339    let value = counter
340        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
341            current.checked_add(1)
342        })
343        .map_err(|_| BindingError::IdentityExhausted)?;
344    NonZeroU64::new(value).ok_or(BindingError::IdentityExhausted)
345}
346
347#[derive(Debug)]
348struct ProviderContextEntry {
349    identity: ProviderContextIdentity,
350    device: DeviceKey,
351    _resource: Arc<dyn BindingResource>,
352}
353
354#[derive(Debug)]
355struct AuthorityEntry {
356    identity: AuthorityIdentity,
357    device: DeviceKey,
358    _resource: Arc<dyn BindingResource>,
359}
360
361#[derive(Clone, Copy, Debug)]
362struct AllocationRecord {
363    identity: AllocationIdentity,
364    ptr: usize,
365    bytes: usize,
366    align: usize,
367}
368
369#[derive(Debug)]
370struct MechanismState {
371    lifecycle: MechanismLifecycle,
372    loss_reason: Option<Arc<str>>,
373    allocations: HashMap<AllocationGeneration, AllocationRecord>,
374    /// Prepared releases that have left the live map but have not settled.
375    queued_releases: usize,
376    /// Ownership retained instead of released, keyed by the generation it was
377    /// prepared from so a settled request can never be recorded twice.
378    quarantined: HashMap<AllocationGeneration, QuarantinedAllocation>,
379}
380
381/// One registered allocator together with the resources its destructor needs.
382///
383/// Declaration order here is load-bearing rather than incidental. Rust drops
384/// struct fields in declaration order, so the allocator is destroyed first and
385/// its `Drop` still observes live provider-context and authority resources. A
386/// third-party allocator may release device state from `Drop`, and that work
387/// needs its provider context (a CUDA context, a loaded provider library) alive.
388/// The provider context is the deepest resource, so it is released last.
389///
390/// Keeping the three pins in one owner type means the ordering cannot be broken
391/// by unrelated field edits to [`MechanismEntry`].
392#[derive(Debug)]
393struct MechanismResources {
394    /// Destroyed first, while both pins below are still alive.
395    allocator: Arc<dyn DeviceAllocator>,
396    /// Outlives the allocator, so `Drop` can still settle accounting identity.
397    authority: Arc<AuthorityEntry>,
398    /// Outlives the allocator and the authority; released last.
399    context: Arc<ProviderContextEntry>,
400}
401
402#[derive(Debug)]
403pub(crate) struct MechanismEntry {
404    identity: MechanismIdentity,
405    device: DeviceKey,
406    coherence: MechanismCoherence,
407    state: Mutex<MechanismState>,
408    active_operations: AtomicUsize,
409    /// Declared last so the allocator and its pins are released only after the
410    /// entry's own identity/lifecycle state is gone.
411    resources: MechanismResources,
412}
413
414/// Whether a prepared release may still reach the allocator.
415///
416/// Read under the mechanism lock and acted on after that lock is dropped.
417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub(crate) enum ReleaseGate {
419    /// `Active` or `Retired`: the pinned allocator may perform the release.
420    Allowed,
421    /// The device or context was lost. The allocator must never be called.
422    DeviceLost,
423    /// Termination was confirmed; the device state provably no longer exists.
424    Terminated,
425    /// The mechanism lock was poisoned; fail safe without calling anything.
426    Poisoned,
427}
428
429impl MechanismEntry {
430    fn allocator(&self) -> &dyn DeviceAllocator {
431        self.resources.allocator.as_ref()
432    }
433
434    fn context_identity(&self) -> ProviderContextIdentity {
435        self.resources.context.identity
436    }
437
438    fn authority_identity(&self) -> AuthorityIdentity {
439        self.resources.authority.identity
440    }
441
442    fn lock_state(
443        &self,
444        operation: &'static str,
445    ) -> Result<MutexGuard<'_, MechanismState>, BindingError> {
446        self.state
447            .lock()
448            .map_err(|_| BindingError::LockPoisoned { operation })
449    }
450
451    fn inactive_error(&self, state: &MechanismState, operation: &'static str) -> BindingError {
452        match state.lifecycle {
453            MechanismLifecycle::DeviceLost => BindingError::DeviceLost {
454                device: self.device,
455                reason: state
456                    .loss_reason
457                    .clone()
458                    .unwrap_or_else(|| Arc::from("provider did not supply a reason")),
459            },
460            lifecycle => BindingError::InactiveMechanism {
461                mechanism: self.identity,
462                lifecycle,
463                operation,
464            },
465        }
466    }
467
468    fn begin_active(
469        self: &Arc<Self>,
470        operation: &'static str,
471    ) -> Result<MechanismOperation, BindingError> {
472        let state = self.lock_state(operation)?;
473        if state.lifecycle != MechanismLifecycle::Active {
474            return Err(self.inactive_error(&state, operation));
475        }
476        self.active_operations.fetch_add(1, Ordering::AcqRel);
477        drop(state);
478        Ok(MechanismOperation {
479            mechanism: Arc::clone(self),
480        })
481    }
482
483    /// Detach final ownership of `allocation` from the live record.
484    ///
485    /// The binding identity and the allocation generation are both matched, and
486    /// the live record is removed **exactly once** under this mechanism's lock,
487    /// so two racing final releases cannot both proceed and a stale handle over
488    /// a reused virtual address cannot match. No allocator call is made here.
489    fn begin_release(
490        self: &Arc<Self>,
491        expected_binding: BindingIdentity,
492        allocation: &BoundAllocation,
493    ) -> Result<MechanismOperation, BindingError> {
494        let operation = "preparing explicit release";
495        let mut state = self.lock_state(operation)?;
496        match state.lifecycle {
497            MechanismLifecycle::Active | MechanismLifecycle::Retired => {}
498            _ => return Err(self.inactive_error(&state, operation)),
499        }
500        validate_binding_identity(expected_binding, allocation.identity.binding)?;
501        let Some(record) = state.allocations.get(&allocation.identity.generation) else {
502            return Err(BindingError::StaleAllocation(allocation.identity));
503        };
504        if !allocation.matches_record(record) {
505            return Err(BindingError::StaleAllocation(allocation.identity));
506        }
507        state.allocations.remove(&allocation.identity.generation);
508        state.queued_releases += 1;
509        self.active_operations.fetch_add(1, Ordering::AcqRel);
510        drop(state);
511        Ok(MechanismOperation {
512            mechanism: Arc::clone(self),
513        })
514    }
515
516    /// Whether a prepared release may still call the allocator.
517    pub(crate) fn release_gate(&self) -> ReleaseGate {
518        let Ok(state) = self.state.lock() else {
519            return ReleaseGate::Poisoned;
520        };
521        match state.lifecycle {
522            MechanismLifecycle::Active | MechanismLifecycle::Retired => ReleaseGate::Allowed,
523            MechanismLifecycle::DeviceLost => ReleaseGate::DeviceLost,
524            MechanismLifecycle::Terminated => ReleaseGate::Terminated,
525        }
526    }
527
528    /// Record that a queued release completed. Never calls the allocator.
529    pub(crate) fn settle_release(&self, identity: AllocationIdentity) {
530        let Ok(mut state) = self.state.lock() else {
531            return;
532        };
533        state.queued_releases = state.queued_releases.saturating_sub(1);
534        debug_assert!(
535            !state.allocations.contains_key(&identity.generation),
536            "a settled release must not leave a live record behind"
537        );
538    }
539
540    /// Record retained ownership. Never calls the allocator and never waits.
541    pub(crate) fn settle_quarantine(&self, record: QuarantinedAllocation) {
542        let Ok(mut state) = self.state.lock() else {
543            return;
544        };
545        state.queued_releases = state.queued_releases.saturating_sub(1);
546        state.quarantined.insert(record.identity.generation, record);
547    }
548
549    pub(crate) fn allocator_arc(&self) -> Arc<dyn DeviceAllocator> {
550        Arc::clone(&self.resources.allocator)
551    }
552
553    fn quarantined(&self) -> Result<Vec<QuarantinedAllocation>, BindingError> {
554        let state = self.lock_state("listing quarantined ownership")?;
555        Ok(state.quarantined.values().copied().collect())
556    }
557
558    fn record_allocation(&self, record: AllocationRecord) -> Result<(), BindingError> {
559        let mut state = self.lock_state("recording allocation identity")?;
560        state.allocations.insert(record.identity.generation, record);
561        Ok(())
562    }
563
564    fn validate_allocation(
565        &self,
566        expected_binding: BindingIdentity,
567        allocation: &BoundAllocation,
568        operation: &'static str,
569    ) -> Result<(), BindingError> {
570        validate_binding_identity(expected_binding, allocation.identity.binding)?;
571        let state = self.lock_state(operation)?;
572        if state.lifecycle != MechanismLifecycle::Active {
573            return Err(self.inactive_error(&state, operation));
574        }
575        let Some(record) = state.allocations.get(&allocation.identity.generation) else {
576            return Err(BindingError::StaleAllocation(allocation.identity));
577        };
578        if !allocation.matches_record(record) {
579            return Err(BindingError::StaleAllocation(allocation.identity));
580        }
581        Ok(())
582    }
583
584    fn validate_view(
585        &self,
586        expected_binding: BindingIdentity,
587        view: &BoundMemoryView,
588        operation: &'static str,
589    ) -> Result<(), BindingError> {
590        validate_binding_identity(expected_binding, view.identity.binding)?;
591        let state = self.lock_state(operation)?;
592        if state.lifecycle != MechanismLifecycle::Active {
593            return Err(self.inactive_error(&state, operation));
594        }
595        let Some(record) = state.allocations.get(&view.identity.generation) else {
596            return Err(BindingError::StaleAllocation(view.identity));
597        };
598        if record.identity != view.identity
599            || record.ptr != view.allocation_ptr.as_ptr() as usize
600            || record.bytes != view.allocation_bytes
601            || record.align != view.align
602        {
603            return Err(BindingError::StaleAllocation(view.identity));
604        }
605        Ok(())
606    }
607
608    fn snapshot(&self) -> Result<MechanismSnapshot, BindingError> {
609        let state = self.lock_state("taking a mechanism snapshot")?;
610        Ok(MechanismSnapshot {
611            identity: self.identity,
612            device: self.device,
613            provider_context: self.context_identity(),
614            authority: self.authority_identity(),
615            coherence: self.coherence,
616            lifecycle: state.lifecycle,
617            live_allocations: state.allocations.len(),
618            active_operations: self.active_operations.load(Ordering::Acquire),
619            queued_releases: state.queued_releases,
620            quarantined_allocations: state.quarantined.len(),
621            quarantined_bytes: state
622                .quarantined
623                .values()
624                .map(|record| record.retained_bytes)
625                .sum(),
626        })
627    }
628}
629
630#[derive(Debug)]
631pub(crate) struct MechanismOperation {
632    mechanism: Arc<MechanismEntry>,
633}
634
635impl Drop for MechanismOperation {
636    fn drop(&mut self) {
637        self.mechanism
638            .active_operations
639            .fetch_sub(1, Ordering::AcqRel);
640    }
641}
642
643#[derive(Debug, Default)]
644struct RegistryState {
645    contexts: HashMap<ProviderContextIdentity, Arc<ProviderContextEntry>>,
646    authorities: HashMap<AuthorityIdentity, Arc<AuthorityEntry>>,
647    mechanisms: HashMap<MechanismIdentity, Arc<MechanismEntry>>,
648    selected: HashMap<DeviceKey, MechanismIdentity>,
649}
650
651#[derive(Debug)]
652struct RegistryInner {
653    identities: Arc<IdentitySource>,
654    state: Mutex<RegistryState>,
655    #[cfg(test)]
656    hooks: Mutex<Vec<RegistryHook>>,
657}
658
659/// What a test hook is keyed on.
660#[cfg(test)]
661#[derive(Clone, Copy, Debug, PartialEq, Eq)]
662enum HookSubject {
663    Mechanism(MechanismIdentity),
664    Device(DeviceKey),
665}
666
667/// A point inside a registry transition a test may pause at.
668///
669/// Selection and lifecycle transitions each span the registry lock and a
670/// mechanism lock, which may never be held together. Pausing between those
671/// phases is what makes the resulting races reproducible by construction rather
672/// than by timing.
673#[cfg(test)]
674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
675enum HookPhase {
676    /// In `select`, after the candidate validated `Active` and before its
677    /// selection is published.
678    SelectAfterValidation,
679    /// In `select`, after the selection is published and before the candidate is
680    /// re-checked and possibly withdrawn.
681    SelectAfterPublish,
682    /// In `retire`, between the mechanism-lock lifecycle phase and the
683    /// registry-lock selection phase.
684    RetireBetweenPhases,
685    /// In `invalidate_device`, between the mechanism-lock lifecycle phase and
686    /// the registry-lock selection phase.
687    InvalidateBetweenPhases,
688}
689
690#[cfg(test)]
691#[derive(Clone, Debug)]
692struct RegistryHook {
693    subject: HookSubject,
694    phase: HookPhase,
695    entered: Arc<std::sync::Barrier>,
696    resume: Arc<std::sync::Barrier>,
697}
698
699/// A narrow registry for provider/context pins and binding identity.
700///
701/// # Lock order
702///
703/// There are two lock classes: the registry lock protects registration and
704/// current selection; each mechanism lock protects only lifecycle, allocation
705/// identities, queued releases, and quarantined ownership. They are never held
706/// together. Allocator/capability callbacks, deferred-queue callbacks, waits,
707/// and device operations run with neither lock held.
708#[derive(Clone, Debug)]
709pub struct BindingRegistry {
710    inner: Arc<RegistryInner>,
711}
712
713impl BindingRegistry {
714    pub fn new() -> Result<Self, BindingError> {
715        Ok(Self {
716            inner: Arc::new(RegistryInner {
717                identities: Arc::new(IdentitySource::new()?),
718                state: Mutex::new(RegistryState::default()),
719                #[cfg(test)]
720                hooks: Mutex::new(Vec::new()),
721            }),
722        })
723    }
724
725    fn lock_state(
726        &self,
727        operation: &'static str,
728    ) -> Result<MutexGuard<'_, RegistryState>, BindingError> {
729        self.inner
730            .state
731            .lock()
732            .map_err(|_| BindingError::LockPoisoned { operation })
733    }
734
735    pub fn register_provider_context(
736        &self,
737        device: DeviceKey,
738        resource: Arc<dyn BindingResource>,
739    ) -> Result<RegisteredProviderContext, BindingError> {
740        let identity = ProviderContextIdentity(self.inner.identities.opaque()?);
741        let entry = Arc::new(ProviderContextEntry {
742            identity,
743            device,
744            _resource: resource,
745        });
746        self.lock_state("registering a provider context")?
747            .contexts
748            .insert(identity, entry);
749        Ok(RegisteredProviderContext { identity, device })
750    }
751
752    pub fn register_authority(
753        &self,
754        device: DeviceKey,
755        resource: Arc<dyn BindingResource>,
756    ) -> Result<RegisteredAuthority, BindingError> {
757        let identity = AuthorityIdentity(self.inner.identities.opaque()?);
758        let entry = Arc::new(AuthorityEntry {
759            identity,
760            device,
761            _resource: resource,
762        });
763        self.lock_state("registering an authority")?
764            .authorities
765            .insert(identity, entry);
766        Ok(RegisteredAuthority { identity, device })
767    }
768
769    pub fn register_allocator(
770        &self,
771        context: RegisteredProviderContext,
772        authority: RegisteredAuthority,
773        allocator: Arc<dyn DeviceAllocator>,
774    ) -> Result<RegisteredMechanism, BindingError> {
775        let allocator_device = allocator.device();
776        self.register_allocator_with_device(context, authority, allocator, allocator_device)
777    }
778
779    /// Register an allocator whose stable device identity was sampled before
780    /// entering an outer registration critical section.
781    ///
782    /// This is the process-manager adapter. Sampling a third-party allocator's
783    /// callback while the manager registration gate is held would permit
784    /// reentrant deadlock. The allocator contract requires `device()` to remain
785    /// stable for its lifetime; the caller supplies that already-sampled value.
786    #[doc(hidden)]
787    pub fn register_allocator_with_device(
788        &self,
789        context: RegisteredProviderContext,
790        authority: RegisteredAuthority,
791        allocator: Arc<dyn DeviceAllocator>,
792        allocator_device: DeviceKey,
793    ) -> Result<RegisteredMechanism, BindingError> {
794        self.register_mechanism(
795            context,
796            authority,
797            allocator,
798            allocator_device,
799            MechanismCoherence::SelfContained,
800        )
801    }
802
803    /// Register a transparent/composite wrapper as one trusted coherent bundle.
804    ///
805    /// # Safety
806    ///
807    /// The registrar must ensure ordinary allocation, optional capabilities, and
808    /// canonical release all reach one coherent device mechanism, authority, and
809    /// provider context. Rust cannot prove that a hostile split-inner wrapper
810    /// satisfies this raw-pointer contract.
811    pub unsafe fn register_trusted_composite(
812        &self,
813        context: RegisteredProviderContext,
814        authority: RegisteredAuthority,
815        allocator: Arc<dyn DeviceAllocator>,
816    ) -> Result<RegisteredMechanism, BindingError> {
817        let allocator_device = allocator.device();
818        self.register_mechanism(
819            context,
820            authority,
821            allocator,
822            allocator_device,
823            MechanismCoherence::TrustedComposite,
824        )
825    }
826
827    fn register_mechanism(
828        &self,
829        context: RegisteredProviderContext,
830        authority: RegisteredAuthority,
831        allocator: Arc<dyn DeviceAllocator>,
832        allocator_device: DeviceKey,
833        coherence: MechanismCoherence,
834    ) -> Result<RegisteredMechanism, BindingError> {
835        self.ensure_local(context.identity.0, "provider context")?;
836        self.ensure_local(authority.identity.0, "authority")?;
837        let (context_entry, authority_entry) = {
838            let state = self.lock_state("looking up mechanism resources")?;
839            let context_entry = state
840                .contexts
841                .get(&context.identity)
842                .cloned()
843                .ok_or(BindingError::UnregisteredProviderContext(context.identity))?;
844            let authority_entry = state
845                .authorities
846                .get(&authority.identity)
847                .cloned()
848                .ok_or(BindingError::UnregisteredAuthority(authority.identity))?;
849            (context_entry, authority_entry)
850        };
851        if context_entry.device != authority_entry.device {
852            return Err(BindingError::DeviceMismatch {
853                subject: "authority",
854                expected: context_entry.device,
855                actual: authority_entry.device,
856            });
857        }
858        if context_entry.device != allocator_device {
859            return Err(BindingError::DeviceMismatch {
860                subject: "allocator",
861                expected: context_entry.device,
862                actual: allocator_device,
863            });
864        }
865
866        let identity = MechanismIdentity(self.inner.identities.opaque()?);
867        let mut state = self.lock_state("registering a mechanism")?;
868        let entry = Arc::new(MechanismEntry {
869            identity,
870            device: context_entry.device,
871            coherence,
872            state: Mutex::new(MechanismState {
873                lifecycle: MechanismLifecycle::Active,
874                loss_reason: None,
875                allocations: HashMap::new(),
876                queued_releases: 0,
877                quarantined: HashMap::new(),
878            }),
879            active_operations: AtomicUsize::new(0),
880            resources: MechanismResources {
881                allocator,
882                authority: authority_entry,
883                context: context_entry,
884            },
885        });
886        state.mechanisms.insert(identity, entry);
887        state.selected.entry(context.device).or_insert(identity);
888        Ok(RegisteredMechanism {
889            identity,
890            device: context.device,
891            coherence,
892        })
893    }
894
895    /// Make `mechanism` the mechanism that later `bind(device)` calls use.
896    ///
897    /// A mechanism can be retired or lost between validation and publication, so
898    /// the candidate is re-checked after it is published. When that re-check
899    /// fails the selection is withdrawn, and the withdrawal never leaves a dead
900    /// or unregistered mechanism selected: it will not overwrite a newer
901    /// selection, restores the previous selection only while that is still
902    /// registered and `Active`, and otherwise clears the selection so a later
903    /// registration for the device can heal it.
904    pub fn select(&self, mechanism: RegisteredMechanism) -> Result<(), BindingError> {
905        self.ensure_local(mechanism.identity.0, "mechanism")?;
906        let entry = {
907            let state = self.lock_state("selecting a mechanism")?;
908            state
909                .mechanisms
910                .get(&mechanism.identity)
911                .cloned()
912                .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?
913        };
914        let snapshot = entry.snapshot()?;
915        if snapshot.lifecycle != MechanismLifecycle::Active {
916            return Err(BindingError::InactiveMechanism {
917                mechanism: mechanism.identity,
918                lifecycle: snapshot.lifecycle,
919                operation: "selecting a mechanism",
920            });
921        }
922        #[cfg(test)]
923        self.wait_at_hook(
924            HookSubject::Mechanism(mechanism.identity),
925            HookPhase::SelectAfterValidation,
926        );
927        let prior = self
928            .lock_state("publishing mechanism selection")?
929            .selected
930            .insert(mechanism.device, mechanism.identity);
931        #[cfg(test)]
932        self.wait_at_hook(
933            HookSubject::Mechanism(mechanism.identity),
934            HookPhase::SelectAfterPublish,
935        );
936        let published = entry.snapshot()?;
937        if published.lifecycle != MechanismLifecycle::Active {
938            self.withdraw_failed_selection(mechanism.device, mechanism.identity, prior)?;
939            return Err(BindingError::InactiveMechanism {
940                mechanism: mechanism.identity,
941                lifecycle: published.lifecycle,
942                operation: "selecting a mechanism",
943            });
944        }
945        Ok(())
946    }
947
948    /// Withdraw a published selection whose candidate turned out to be inactive.
949    ///
950    /// Three rules keep `selected` pointing only at a live registration:
951    ///
952    /// 1. the candidate is only replaced while it still owns `device`'s
953    ///    selection, so a newer selection published by another thread is never
954    ///    overwritten by a losing candidate;
955    /// 2. `prior` is restored only while it is still registered *and* `Active`,
956    ///    so a concurrently retired, lost, or removed prior is not resurrected;
957    /// 3. otherwise the selection is cleared, because an absent selection is the
958    ///    only state a later [`BindingRegistry::register_allocator`] can heal.
959    ///
960    /// The restored mechanism is re-checked after publication for the same
961    /// reason the candidate was, and that retry carries no further fallback, so
962    /// the loop runs at most twice.
963    ///
964    /// # Lock order
965    ///
966    /// The registry lock is released before every mechanism lifecycle snapshot,
967    /// so the two lock classes are still never held together and no allocator or
968    /// capability callback runs here.
969    fn withdraw_failed_selection(
970        &self,
971        device: DeviceKey,
972        candidate: MechanismIdentity,
973        prior: Option<MechanismIdentity>,
974    ) -> Result<(), BindingError> {
975        const OPERATION: &str = "withdrawing inactive mechanism selection";
976        let mut owner = candidate;
977        let mut replacement = prior;
978        loop {
979            let restorable = match replacement {
980                Some(identity) => {
981                    let entry = {
982                        let state = self.lock_state(OPERATION)?;
983                        if state.selected.get(&device) != Some(&owner) {
984                            return Ok(());
985                        }
986                        state.mechanisms.get(&identity).cloned()
987                    };
988                    match entry {
989                        Some(entry)
990                            if entry.snapshot()?.lifecycle == MechanismLifecycle::Active =>
991                        {
992                            Some(entry)
993                        }
994                        _ => None,
995                    }
996                }
997                None => None,
998            };
999
1000            let restored = {
1001                let mut state = self.lock_state(OPERATION)?;
1002                if state.selected.get(&device) != Some(&owner) {
1003                    return Ok(());
1004                }
1005                // Registration is re-confirmed under the lock that publishes it,
1006                // so a `remove` racing the lifecycle snapshot above cannot leave
1007                // an unregistered identity selected.
1008                match restorable {
1009                    Some(entry) if state.mechanisms.contains_key(&entry.identity) => {
1010                        state.selected.insert(device, entry.identity);
1011                        entry
1012                    }
1013                    _ => {
1014                        state.selected.remove(&device);
1015                        return Ok(());
1016                    }
1017                }
1018            };
1019
1020            if restored.snapshot()?.lifecycle == MechanismLifecycle::Active {
1021                return Ok(());
1022            }
1023            owner = restored.identity;
1024            replacement = None;
1025        }
1026    }
1027
1028    #[cfg(test)]
1029    fn install_hook(&self, hook: RegistryHook) {
1030        self.inner
1031            .hooks
1032            .lock()
1033            .expect("registry test hook lock")
1034            .push(hook);
1035    }
1036
1037    #[cfg(test)]
1038    fn wait_at_hook(&self, subject: HookSubject, phase: HookPhase) {
1039        let hook = self
1040            .inner
1041            .hooks
1042            .lock()
1043            .expect("registry test hook lock")
1044            .iter()
1045            .find(|hook| hook.subject == subject && hook.phase == phase)
1046            .cloned();
1047        // The hook lock is released before waiting so a paused caller never
1048        // blocks the test thread that is about to release it.
1049        if let Some(hook) = hook {
1050            hook.entered.wait();
1051            hook.resume.wait();
1052        }
1053    }
1054
1055    pub fn bind(&self, device: DeviceKey) -> Result<MemoryBinding, BindingError> {
1056        let entry = {
1057            let state = self.lock_state("looking up the selected mechanism")?;
1058            let identity = state
1059                .selected
1060                .get(&device)
1061                .copied()
1062                .ok_or(BindingError::NoSelectedMechanism(device))?;
1063            state
1064                .mechanisms
1065                .get(&identity)
1066                .cloned()
1067                .ok_or(BindingError::UnregisteredMechanism(identity))?
1068        };
1069        self.issue_binding(entry)
1070    }
1071
1072    pub fn bind_registered(
1073        &self,
1074        mechanism: RegisteredMechanism,
1075    ) -> Result<MemoryBinding, BindingError> {
1076        self.ensure_local(mechanism.identity.0, "mechanism")?;
1077        let entry = self
1078            .lock_state("looking up a registered mechanism")?
1079            .mechanisms
1080            .get(&mechanism.identity)
1081            .cloned()
1082            .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?;
1083        self.issue_binding(entry)
1084    }
1085
1086    fn issue_binding(&self, entry: Arc<MechanismEntry>) -> Result<MemoryBinding, BindingError> {
1087        let operation = entry.begin_active("issuing a binding")?;
1088        let identity = BindingIdentity {
1089            id: BindingId(self.inner.identities.opaque()?),
1090            generation: self.inner.identities.binding_generation()?,
1091            device: entry.device,
1092            mechanism: entry.identity,
1093            provider_context: entry.context_identity(),
1094            authority: entry.authority_identity(),
1095        };
1096        drop(operation);
1097        Ok(MemoryBinding {
1098            identity,
1099            identities: Arc::clone(&self.inner.identities),
1100            mechanism: entry,
1101        })
1102    }
1103
1104    /// Stop issuing new work through `mechanism`.
1105    ///
1106    /// Existing allocations keep the original allocator/context/authority pinned
1107    /// and may still use [`MemoryBinding::release`] explicitly.
1108    ///
1109    /// The lifecycle is made terminal *before* the selection is dropped. The two
1110    /// lock classes cannot be held together, so the reverse order leaves a window
1111    /// in which a concurrent `select` that already validated this mechanism
1112    /// publishes it after the clear and still observes `Active` at its own
1113    /// re-check, wedging the device on a retired selection. Retiring first means
1114    /// any such `select` must fail its re-check and withdraw itself.
1115    pub fn retire(&self, mechanism: RegisteredMechanism) -> Result<(), BindingError> {
1116        self.ensure_local(mechanism.identity.0, "mechanism")?;
1117        let entry = {
1118            let state = self.lock_state("retiring a mechanism")?;
1119            state
1120                .mechanisms
1121                .get(&mechanism.identity)
1122                .cloned()
1123                .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?
1124        };
1125        {
1126            let mut mechanism_state = entry.lock_state("retiring a mechanism")?;
1127            if mechanism_state.lifecycle == MechanismLifecycle::Active {
1128                mechanism_state.lifecycle = MechanismLifecycle::Retired;
1129            }
1130        }
1131        #[cfg(test)]
1132        self.wait_at_hook(
1133            HookSubject::Mechanism(mechanism.identity),
1134            HookPhase::RetireBetweenPhases,
1135        );
1136        self.drop_selection_of(mechanism.device, mechanism.identity, "retiring a mechanism")
1137    }
1138
1139    /// Drop `device`'s selection while it still names `mechanism`.
1140    ///
1141    /// Never touches a selection naming anything else, so a healthy selection
1142    /// published concurrently is left alone.
1143    fn drop_selection_of(
1144        &self,
1145        device: DeviceKey,
1146        mechanism: MechanismIdentity,
1147        operation: &'static str,
1148    ) -> Result<(), BindingError> {
1149        let mut state = self.lock_state(operation)?;
1150        if state.selected.get(&device) == Some(&mechanism) {
1151            state.selected.remove(&device);
1152        }
1153        Ok(())
1154    }
1155
1156    /// Invalidate every mechanism and binding for `device`.
1157    ///
1158    /// This method changes identity/lifetime state only. It does not invoke a
1159    /// device callback, free physical memory, release a lease, or refund quota.
1160    ///
1161    /// Like [`BindingRegistry::retire`], every affected mechanism is made
1162    /// terminal before the selection is dropped, so a `select` racing device loss
1163    /// cannot leave a lost mechanism selected. Only a selection naming a
1164    /// mechanism this call actually invalidated is dropped, so a mechanism
1165    /// registered after this call returns is never deselected by it. A
1166    /// registration that lands while this call is in flight may still end up
1167    /// unselected, because the slot it tried to claim was held by an identity
1168    /// this call then dropped; that fails closed, and the next registration or
1169    /// explicit [`BindingRegistry::select`] restores a selection.
1170    pub fn invalidate_device(
1171        &self,
1172        device: DeviceKey,
1173        reason: impl Into<Arc<str>>,
1174    ) -> Result<(), BindingError> {
1175        let reason = reason.into();
1176        let entries = {
1177            let state = self.lock_state("invalidating a device")?;
1178            state
1179                .mechanisms
1180                .values()
1181                .filter(|entry| entry.device == device)
1182                .cloned()
1183                .collect::<Vec<_>>()
1184        };
1185        for entry in &entries {
1186            let mut state = entry.lock_state("invalidating a device binding")?;
1187            if state.lifecycle != MechanismLifecycle::Terminated {
1188                state.lifecycle = MechanismLifecycle::DeviceLost;
1189                state.loss_reason = Some(Arc::clone(&reason));
1190            }
1191        }
1192        #[cfg(test)]
1193        self.wait_at_hook(
1194            HookSubject::Device(device),
1195            HookPhase::InvalidateBetweenPhases,
1196        );
1197        let mut state = self.lock_state("invalidating a device")?;
1198        let invalidated = state
1199            .selected
1200            .get(&device)
1201            .is_some_and(|selected| entries.iter().any(|entry| entry.identity == *selected));
1202        if invalidated {
1203            state.selected.remove(&device);
1204        }
1205        Ok(())
1206    }
1207
1208    /// Record externally observed provider-context/process termination.
1209    ///
1210    /// This is the device-loss teardown boundary. Allocation identities become
1211    /// terminal without calling the allocator. Accounting/delegated quota must be
1212    /// reconciled by the owning authority only after its own required process or
1213    /// context termination observation.
1214    pub fn confirm_context_terminated(
1215        &self,
1216        context: RegisteredProviderContext,
1217    ) -> Result<(), BindingError> {
1218        self.ensure_local(context.identity.0, "provider context")?;
1219        let entries = {
1220            let state = self.lock_state("looking up a terminated provider context")?;
1221            if !state.contexts.contains_key(&context.identity) {
1222                return Err(BindingError::UnregisteredProviderContext(context.identity));
1223            }
1224            state
1225                .mechanisms
1226                .values()
1227                .filter(|entry| entry.context_identity() == context.identity)
1228                .cloned()
1229                .collect::<Vec<_>>()
1230        };
1231        for entry in &entries {
1232            let state = entry.lock_state("checking provider context quiescence")?;
1233            if state.lifecycle != MechanismLifecycle::DeviceLost {
1234                return Err(entry.inactive_error(
1235                    &state,
1236                    "confirming termination before device-loss invalidation",
1237                ));
1238            }
1239            let active_operations = entry.active_operations.load(Ordering::Acquire);
1240            if active_operations != 0 {
1241                return Err(BindingError::ContextNotQuiescent {
1242                    context: context.identity,
1243                    active_operations,
1244                });
1245            }
1246        }
1247        for entry in entries {
1248            let mut state = entry.lock_state("confirming provider context termination")?;
1249            state.lifecycle = MechanismLifecycle::Terminated;
1250            state.allocations.clear();
1251            // Confirmed context/process termination is the one point where
1252            // quarantined device ownership provably no longer exists, so this is
1253            // where retained ownership is discharged. No allocator call is made.
1254            state.quarantined.clear();
1255        }
1256        Ok(())
1257    }
1258
1259    /// Ownership this mechanism deliberately retained instead of releasing.
1260    ///
1261    /// Taking this list never invokes a provider and never calls an allocator.
1262    pub fn quarantined(
1263        &self,
1264        mechanism: RegisteredMechanism,
1265    ) -> Result<Vec<QuarantinedAllocation>, BindingError> {
1266        self.ensure_local(mechanism.identity.0, "mechanism")?;
1267        let entry = self
1268            .lock_state("looking up quarantined ownership")?
1269            .mechanisms
1270            .get(&mechanism.identity)
1271            .cloned()
1272            .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?;
1273        entry.quarantined()
1274    }
1275
1276    /// Remove the registry's provider-context pin after all mechanism
1277    /// registrations using it have been removed. Existing binding handles keep
1278    /// their own pin until they retire.
1279    pub fn remove_provider_context(
1280        &self,
1281        context: RegisteredProviderContext,
1282    ) -> Result<(), BindingError> {
1283        self.ensure_local(context.identity.0, "provider context")?;
1284        let mut state = self.lock_state("removing a provider context")?;
1285        if !state.contexts.contains_key(&context.identity) {
1286            return Err(BindingError::UnregisteredProviderContext(context.identity));
1287        }
1288        if state
1289            .mechanisms
1290            .values()
1291            .any(|entry| entry.context_identity() == context.identity)
1292        {
1293            return Err(BindingError::ProviderContextInUse(context.identity));
1294        }
1295        state.contexts.remove(&context.identity);
1296        Ok(())
1297    }
1298
1299    /// Remove the registry's authority pin after all mechanism registrations
1300    /// using it have been removed. This does not refund charges or delegated
1301    /// quota; the authority owner performs accounting reconciliation separately.
1302    pub fn remove_authority(&self, authority: RegisteredAuthority) -> Result<(), BindingError> {
1303        self.ensure_local(authority.identity.0, "authority")?;
1304        let mut state = self.lock_state("removing an authority")?;
1305        if !state.authorities.contains_key(&authority.identity) {
1306            return Err(BindingError::UnregisteredAuthority(authority.identity));
1307        }
1308        if state
1309            .mechanisms
1310            .values()
1311            .any(|entry| entry.authority_identity() == authority.identity)
1312        {
1313            return Err(BindingError::AuthorityInUse(authority.identity));
1314        }
1315        state.authorities.remove(&authority.identity);
1316        Ok(())
1317    }
1318
1319    /// Remove a terminal/retired registration once no allocation metadata,
1320    /// queued release, quarantined ownership, or active callback remains.
1321    /// Existing binding/capability handles still pin the entry and resources,
1322    /// but remain inactive.
1323    ///
1324    /// Queued and quarantined ownership both block removal: a queued request
1325    /// still holds an active-operation pin, and quarantined ownership is
1326    /// reported separately so the caller learns *why* removal is unsafe rather
1327    /// than seeing a bare lifecycle complaint.
1328    pub fn remove(&self, mechanism: RegisteredMechanism) -> Result<(), BindingError> {
1329        self.ensure_local(mechanism.identity.0, "mechanism")?;
1330        let entry = {
1331            let state = self.lock_state("checking mechanism teardown")?;
1332            state
1333                .mechanisms
1334                .get(&mechanism.identity)
1335                .cloned()
1336                .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?
1337        };
1338        let snapshot = entry.snapshot()?;
1339        if snapshot.quarantined_allocations != 0 {
1340            return Err(BindingError::QuarantinedOwnership {
1341                mechanism: mechanism.identity,
1342                quarantined: snapshot.quarantined_allocations,
1343            });
1344        }
1345        if snapshot.lifecycle == MechanismLifecycle::Active
1346            || snapshot.live_allocations != 0
1347            || snapshot.queued_releases != 0
1348            || snapshot.active_operations != 0
1349        {
1350            return Err(BindingError::InactiveMechanism {
1351                mechanism: mechanism.identity,
1352                lifecycle: snapshot.lifecycle,
1353                operation: "removing a mechanism before it is quiescent",
1354            });
1355        }
1356        let mut state = self.lock_state("removing a mechanism")?;
1357        if state.selected.get(&mechanism.device) == Some(&mechanism.identity) {
1358            state.selected.remove(&mechanism.device);
1359        }
1360        state.mechanisms.remove(&mechanism.identity);
1361        Ok(())
1362    }
1363
1364    pub fn snapshot(
1365        &self,
1366        mechanism: RegisteredMechanism,
1367    ) -> Result<MechanismSnapshot, BindingError> {
1368        self.ensure_local(mechanism.identity.0, "mechanism")?;
1369        let entry = self
1370            .lock_state("looking up a mechanism snapshot")?
1371            .mechanisms
1372            .get(&mechanism.identity)
1373            .cloned()
1374            .ok_or(BindingError::UnregisteredMechanism(mechanism.identity))?;
1375        entry.snapshot()
1376    }
1377
1378    /// Snapshot every registered mechanism without exposing the registration
1379    /// map to a process manager.
1380    ///
1381    /// The registry lock is released before any per-mechanism lock is taken, so
1382    /// the two lock classes remain never-nested.
1383    pub fn snapshots(&self) -> Result<Vec<MechanismSnapshot>, BindingError> {
1384        let entries = self
1385            .lock_state("listing mechanism snapshots")?
1386            .mechanisms
1387            .values()
1388            .cloned()
1389            .collect::<Vec<_>>();
1390        entries.into_iter().map(|entry| entry.snapshot()).collect()
1391    }
1392
1393    fn ensure_local(
1394        &self,
1395        identity: OpaqueIdentity,
1396        kind: &'static str,
1397    ) -> Result<(), BindingError> {
1398        if identity.registry != self.inner.identities.registry {
1399            return Err(BindingError::ForeignRegistry { kind });
1400        }
1401        Ok(())
1402    }
1403}
1404
1405/// Registry handle for one provider context.
1406#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1407pub struct RegisteredProviderContext {
1408    identity: ProviderContextIdentity,
1409    device: DeviceKey,
1410}
1411
1412impl RegisteredProviderContext {
1413    pub const fn identity(self) -> ProviderContextIdentity {
1414        self.identity
1415    }
1416
1417    pub const fn device(self) -> DeviceKey {
1418        self.device
1419    }
1420}
1421
1422/// Registry handle for one accounting authority.
1423#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1424pub struct RegisteredAuthority {
1425    identity: AuthorityIdentity,
1426    device: DeviceKey,
1427}
1428
1429impl RegisteredAuthority {
1430    pub const fn identity(self) -> AuthorityIdentity {
1431        self.identity
1432    }
1433
1434    pub const fn device(self) -> DeviceKey {
1435        self.device
1436    }
1437}
1438
1439/// Registry handle for one allocator mechanism or trusted coherent bundle.
1440#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1441pub struct RegisteredMechanism {
1442    identity: MechanismIdentity,
1443    device: DeviceKey,
1444    coherence: MechanismCoherence,
1445}
1446
1447impl RegisteredMechanism {
1448    pub const fn identity(self) -> MechanismIdentity {
1449        self.identity
1450    }
1451
1452    pub const fn device(self) -> DeviceKey {
1453        self.device
1454    }
1455
1456    pub const fn coherence(self) -> MechanismCoherence {
1457        self.coherence
1458    }
1459}
1460
1461/// One binding to a registered device/mechanism/context/authority tuple.
1462///
1463/// Clones preserve the same binding identity and pin. A new registry lookup
1464/// receives a new binding id/generation even when it selects the same mechanism.
1465#[derive(Clone, Debug)]
1466pub struct MemoryBinding {
1467    identity: BindingIdentity,
1468    identities: Arc<IdentitySource>,
1469    mechanism: Arc<MechanismEntry>,
1470}
1471
1472impl MemoryBinding {
1473    pub const fn identity(&self) -> BindingIdentity {
1474        self.identity
1475    }
1476
1477    pub fn allocate(&self, bytes: usize, align: usize) -> Result<BoundAllocation, BindingError> {
1478        self.allocate_with(
1479            "allocating bound memory",
1480            |allocator| allocator.allocate(bytes, align),
1481            bytes,
1482            align,
1483        )
1484    }
1485
1486    fn allocate_with(
1487        &self,
1488        operation: &'static str,
1489        allocate: impl FnOnce(&dyn DeviceAllocator) -> Result<NonNull<u8>, MemoryError>,
1490        bytes: usize,
1491        align: usize,
1492    ) -> Result<BoundAllocation, BindingError> {
1493        let active = self.mechanism.begin_active(operation)?;
1494        let ptr = allocate(self.mechanism.allocator())?;
1495        let generation = match self.identities.allocation_generation() {
1496            Ok(generation) => generation,
1497            Err(error) => {
1498                // SAFETY: this is the exact allocation returned immediately
1499                // above; identity issuance failed before it escaped.
1500                unsafe { self.mechanism.allocator().deallocate(ptr, bytes, align) };
1501                return Err(error);
1502            }
1503        };
1504        let identity = AllocationIdentity {
1505            binding: self.identity,
1506            generation,
1507        };
1508        let allocation = BoundAllocation {
1509            binding: self.clone(),
1510            identity,
1511            ptr,
1512            bytes,
1513            align,
1514        };
1515        if let Err(error) = self.mechanism.record_allocation(allocation.record()) {
1516            // SAFETY: identity recording failed before the allocation escaped.
1517            unsafe { self.mechanism.allocator().deallocate(ptr, bytes, align) };
1518            return Err(error);
1519        }
1520        drop(active);
1521        Ok(allocation)
1522    }
1523
1524    /// Allocate and take **owning** responsibility for the result.
1525    ///
1526    /// The returned [`OwningAllocation`] has exactly one consuming release and a
1527    /// `Drop` that quarantines rather than frees, so a forgotten allocation is
1528    /// accounted for instead of silently double-freed or leaked without trace.
1529    pub fn allocate_owning(
1530        &self,
1531        bytes: usize,
1532        align: usize,
1533    ) -> Result<OwningAllocation, BindingError> {
1534        self.allocate(bytes, align).map(OwningAllocation::new)
1535    }
1536
1537    /// Issue an allocation generation for memory this binding's **own**
1538    /// mechanism produced through a specialized entry point this crate cannot
1539    /// express, and take owning responsibility for it.
1540    ///
1541    /// This is the narrow adoption seam for a provider-specific allocation call
1542    /// (the CUDA VMM arena's mapped-capacity allocation is the motivating case:
1543    /// it needs a governor capacity token that is deliberately not part of the
1544    /// portable [`VirtualBacking`](crate::VirtualBacking) capability). Adoption
1545    /// registers the address under a fresh generation *before* it escapes, so
1546    /// every later view, commit, and release is generation-validated exactly
1547    /// like a binding-issued allocation. Nothing else about the lifecycle is
1548    /// relaxed.
1549    ///
1550    /// # Safety
1551    ///
1552    /// The caller must guarantee all of:
1553    ///
1554    /// * `ptr` is one live allocation of exactly `bytes` at `align` produced by
1555    ///   **this binding's selected mechanism** (the same coherent allocator that
1556    ///   would serve [`allocate`](Self::allocate)), not by another allocator or
1557    ///   another device.
1558    /// * The allocation is not already recorded by this or any other binding,
1559    ///   and no other owner exists for it. Adoption is the single point at which
1560    ///   ownership enters the binding, and the returned owner is its sole owner.
1561    /// * The allocation may be released by that mechanism's canonical
1562    ///   [`DeviceAllocator::release`] with exactly this `ptr`/`bytes`/`align`.
1563    pub unsafe fn adopt_allocation(
1564        &self,
1565        ptr: NonNull<u8>,
1566        bytes: usize,
1567        align: usize,
1568    ) -> Result<OwningAllocation, BindingError> {
1569        let active = self
1570            .mechanism
1571            .begin_active("adopting a device allocation")?;
1572        let generation = self.identities.allocation_generation()?;
1573        let identity = AllocationIdentity {
1574            binding: self.identity,
1575            generation,
1576        };
1577        let allocation = BoundAllocation {
1578            binding: self.clone(),
1579            identity,
1580            ptr,
1581            bytes,
1582            align,
1583        };
1584        self.mechanism.record_allocation(allocation.record())?;
1585        drop(active);
1586        Ok(OwningAllocation::new(allocation))
1587    }
1588
1589    /// Detach final ownership of `allocation` without calling the allocator.
1590    ///
1591    /// This is the single preparation point for every release path. It matches
1592    /// the binding identity **and** the allocation generation and removes the
1593    /// live record exactly once under the per-mechanism lock, then returns an
1594    /// owned request that pins the allocator, authority, and provider context.
1595    ///
1596    /// # Lock order
1597    ///
1598    /// Only the mechanism lock is taken, and it is released before this method
1599    /// returns. Queue and allocator calls therefore always happen with no
1600    /// registry or mechanism lock held.
1601    pub fn prepare_release(
1602        &self,
1603        allocation: BoundAllocation,
1604    ) -> Result<PreparedAllocationRelease, ExplicitReleaseError> {
1605        let operation = match self.mechanism.begin_release(self.identity, &allocation) {
1606            Ok(operation) => operation,
1607            Err(error) => return Err(ExplicitReleaseError::unchanged(error, allocation)),
1608        };
1609        Ok(PreparedAllocationRelease::new(
1610            allocation.binding.clone(),
1611            allocation.identity,
1612            allocation.ptr,
1613            allocation.bytes,
1614            allocation.align,
1615            PreparedReleasePins {
1616                allocator: self.mechanism.allocator_arc(),
1617                authority: self.identity.authority,
1618                context: self.identity.provider_context,
1619                operation,
1620            },
1621        ))
1622    }
1623
1624    /// Explicitly release through this binding's pinned original allocator.
1625    ///
1626    /// This is the documented **migration adapter** for Phase-3 non-RAII
1627    /// [`BoundAllocation`] metadata. It is routed through the same structured
1628    /// prepared-release path as owning handles, so the generation is validated
1629    /// and the live record is retired before the allocator is invoked. It
1630    /// completes synchronously and never enqueues.
1631    ///
1632    /// On pre-mutation failure (identity mismatch, stale generation, device
1633    /// loss) the allocation is returned inside [`ExplicitReleaseError`] exactly
1634    /// as before, so metadata and lifetime pins are not silently discarded.
1635    ///
1636    /// # Limitations
1637    ///
1638    /// The adapter cannot report the structured success outcome, because its
1639    /// signature returns `()`. Callers that need the release accounting or that
1640    /// need to defer release past a stream fence should migrate to
1641    /// [`OwningAllocation`] or [`MemoryBinding::prepare_release`].
1642    ///
1643    /// When the allocator quarantines residual ownership the adapter reports
1644    /// [`BindingError::ReleaseQuarantined`] and hands back the now-dead
1645    /// metadata together with the structured outcome
1646    /// ([`ExplicitReleaseError::outcome`]). That metadata can never be released
1647    /// again: its record was already retired, so every later operation on it
1648    /// fails with [`BindingError::StaleAllocation`].
1649    pub fn release(&self, allocation: BoundAllocation) -> Result<(), ExplicitReleaseError> {
1650        let prepared = self.prepare_release(allocation)?;
1651        let stale = self.stale_metadata(&prepared);
1652        match prepared.execute() {
1653            AllocationReleaseOutcome::Complete { .. } => Ok(()),
1654            outcome @ (AllocationReleaseOutcome::Quarantined { .. }
1655            | AllocationReleaseOutcome::Failed { .. }) => {
1656                let residual = outcome.residual();
1657                Err(ExplicitReleaseError::quarantined(
1658                    BindingError::ReleaseQuarantined {
1659                        identity: stale.identity,
1660                        state: outcome.state(),
1661                        reason: residual.map_or(QuarantineReason::AllocatorRefused, |residual| {
1662                            residual.reason
1663                        }),
1664                        retained_bytes: residual.map_or(0, |residual| residual.retained_bytes),
1665                    },
1666                    stale,
1667                    outcome,
1668                ))
1669            }
1670        }
1671    }
1672
1673    /// Rebuild inert metadata for an allocation whose live record is already
1674    /// retired.
1675    ///
1676    /// Only the legacy adapter uses this, and only to keep its historical
1677    /// "the error hands the allocation back" shape. The rebuilt value can never
1678    /// be released again because its generation is no longer recorded.
1679    fn stale_metadata(&self, prepared: &PreparedAllocationRelease) -> BoundAllocation {
1680        BoundAllocation {
1681            binding: self.clone(),
1682            identity: prepared.identity(),
1683            ptr: prepared.as_ptr(),
1684            bytes: prepared.len(),
1685            align: prepared.alignment(),
1686        }
1687    }
1688
1689    /// Ownership this binding's mechanism retained instead of releasing.
1690    pub fn quarantined(&self) -> Result<Vec<QuarantinedAllocation>, BindingError> {
1691        self.mechanism.quarantined()
1692    }
1693
1694    /// Lifecycle and ownership counts for this binding's mechanism.
1695    pub fn mechanism_snapshot(&self) -> Result<MechanismSnapshot, BindingError> {
1696        self.mechanism.snapshot()
1697    }
1698
1699    pub(crate) fn mechanism(&self) -> &Arc<MechanismEntry> {
1700        &self.mechanism
1701    }
1702
1703    pub fn virtual_backing(&self) -> Result<Option<BoundVirtualBacking>, BindingError> {
1704        let operation = self
1705            .mechanism
1706            .begin_active("discovering virtual backing capability")?;
1707        let present = self.mechanism.allocator().as_virtual_backing().is_some();
1708        drop(operation);
1709        Ok(present.then(|| BoundVirtualBacking {
1710            binding: self.clone(),
1711        }))
1712    }
1713
1714    pub fn shared_mapping(&self) -> Result<Option<BoundSharedMapping>, BindingError> {
1715        let operation = self
1716            .mechanism
1717            .begin_active("discovering shared mapping capability")?;
1718        let present = self.mechanism.allocator().as_shared_mapping().is_some();
1719        drop(operation);
1720        Ok(present.then(|| BoundSharedMapping {
1721            binding: self.clone(),
1722        }))
1723    }
1724
1725    /// Validate a view, then invoke `operation` without a registry or mechanism
1726    /// lock held. This is the binding boundary for kernel/copy callbacks.
1727    pub fn with_view<R>(
1728        &self,
1729        view: &BoundMemoryView,
1730        operation: impl FnOnce(ValidatedMemoryView) -> R,
1731    ) -> Result<R, BindingError> {
1732        let active = self
1733            .mechanism
1734            .begin_active("validating a view for device use")?;
1735        self.mechanism
1736            .validate_view(self.identity, view, "validating a view for device use")?;
1737        let validated = ValidatedMemoryView {
1738            ptr: view.ptr,
1739            bytes: view.bytes,
1740        };
1741        let result = operation(validated);
1742        drop(active);
1743        Ok(result)
1744    }
1745}
1746
1747/// Non-RAII allocation metadata issued by one [`MemoryBinding`].
1748///
1749/// Dropping this value does not free memory. Call [`MemoryBinding::release`]
1750/// explicitly through the same binding.
1751#[derive(Debug)]
1752pub struct BoundAllocation {
1753    binding: MemoryBinding,
1754    identity: AllocationIdentity,
1755    ptr: NonNull<u8>,
1756    bytes: usize,
1757    align: usize,
1758}
1759
1760// SAFETY: this type is allocation metadata over a provider-defined device
1761// address. It exposes no safe dereference and pins a Send + Sync allocator and
1762// context. Moving or sharing the metadata does not access the pointed-to bytes.
1763unsafe impl Send for BoundAllocation {}
1764// SAFETY: shared access exposes only copied metadata and a non-dereferenced
1765// pointer; explicit release consumes the allocation.
1766unsafe impl Sync for BoundAllocation {}
1767
1768impl BoundAllocation {
1769    pub const fn identity(&self) -> AllocationIdentity {
1770        self.identity
1771    }
1772
1773    pub const fn binding(&self) -> &MemoryBinding {
1774        &self.binding
1775    }
1776
1777    pub const fn as_ptr(&self) -> NonNull<u8> {
1778        self.ptr
1779    }
1780
1781    pub const fn len(&self) -> usize {
1782        self.bytes
1783    }
1784
1785    pub const fn is_empty(&self) -> bool {
1786        self.bytes == 0
1787    }
1788
1789    pub const fn alignment(&self) -> usize {
1790        self.align
1791    }
1792
1793    pub fn view(&self, offset: usize, bytes: usize) -> Result<BoundMemoryView, BindingError> {
1794        let end = offset
1795            .checked_add(bytes)
1796            .ok_or(BindingError::ViewOutOfBounds {
1797                offset,
1798                end: usize::MAX,
1799                allocation_bytes: self.bytes,
1800            })?;
1801        if end > self.bytes {
1802            return Err(BindingError::ViewOutOfBounds {
1803                offset,
1804                end,
1805                allocation_bytes: self.bytes,
1806            });
1807        }
1808        self.binding.mechanism.validate_allocation(
1809            self.binding.identity,
1810            self,
1811            "creating a bound view",
1812        )?;
1813        Ok(BoundMemoryView {
1814            binding: self.binding.clone(),
1815            identity: self.identity,
1816            allocation_ptr: self.ptr,
1817            ptr: NonNull::new(self.ptr.as_ptr().wrapping_add(offset))
1818                .expect("offset within a live allocation cannot produce null"),
1819            allocation_bytes: self.bytes,
1820            bytes,
1821            align: self.align,
1822        })
1823    }
1824
1825    fn record(&self) -> AllocationRecord {
1826        AllocationRecord {
1827            identity: self.identity,
1828            ptr: self.ptr.as_ptr() as usize,
1829            bytes: self.bytes,
1830            align: self.align,
1831        }
1832    }
1833
1834    fn matches_record(&self, record: &AllocationRecord) -> bool {
1835        record.identity == self.identity
1836            && record.ptr == self.ptr.as_ptr() as usize
1837            && record.bytes == self.bytes
1838            && record.align == self.align
1839    }
1840}
1841
1842/// Cloneable metadata for a sub-range of one bound allocation.
1843#[derive(Clone, Debug)]
1844pub struct BoundMemoryView {
1845    binding: MemoryBinding,
1846    identity: AllocationIdentity,
1847    allocation_ptr: NonNull<u8>,
1848    ptr: NonNull<u8>,
1849    allocation_bytes: usize,
1850    bytes: usize,
1851    align: usize,
1852}
1853
1854// SAFETY: like BoundAllocation, a view is inert metadata over an opaque address;
1855// validated access still requires the caller to uphold device synchronization.
1856unsafe impl Send for BoundMemoryView {}
1857// SAFETY: shared references cannot mutate memory through this type.
1858unsafe impl Sync for BoundMemoryView {}
1859
1860impl BoundMemoryView {
1861    pub const fn binding(&self) -> &MemoryBinding {
1862        &self.binding
1863    }
1864
1865    pub const fn allocation_identity(&self) -> AllocationIdentity {
1866        self.identity
1867    }
1868
1869    pub const fn len(&self) -> usize {
1870        self.bytes
1871    }
1872
1873    pub const fn is_empty(&self) -> bool {
1874        self.bytes == 0
1875    }
1876}
1877
1878/// Pointer/extent exposed only after binding validation.
1879#[derive(Clone, Copy, Debug)]
1880pub struct ValidatedMemoryView {
1881    ptr: NonNull<u8>,
1882    bytes: usize,
1883}
1884
1885// SAFETY: validation produces a copied opaque device address, not a Rust
1886// reference. Dereferencing remains unsafe and provider synchronization remains
1887// the caller's responsibility.
1888unsafe impl Send for ValidatedMemoryView {}
1889// SAFETY: the value has no safe memory access or interior mutation.
1890unsafe impl Sync for ValidatedMemoryView {}
1891
1892impl ValidatedMemoryView {
1893    pub const fn as_ptr(self) -> NonNull<u8> {
1894        self.ptr
1895    }
1896
1897    pub const fn len(self) -> usize {
1898        self.bytes
1899    }
1900
1901    pub const fn is_empty(self) -> bool {
1902        self.bytes == 0
1903    }
1904}
1905
1906/// Owning responsibility for exactly one [`BoundAllocation`].
1907///
1908/// This is the Phase-4 owner. It is deliberately **not** `Clone` and **not**
1909/// `Copy`: ownership of a physical allocation cannot be duplicated. Aliases are
1910/// expressed as [`OwnedView`]s, which can never release anything.
1911///
1912/// # Release paths
1913///
1914/// * [`release_now`](Self::release_now) — synchronous, no queue. This is the
1915///   CPU/eager path: one mechanism-lock preparation plus one allocator call.
1916/// * [`release_deferred`](Self::release_deferred) — hand final ownership to a
1917///   provider/context-owned [`DeferredReleaseQueue`], for GPU allocations whose
1918///   release must wait for a stream fence.
1919/// * [`prepare_release`](Self::prepare_release) — take the owned request and
1920///   route it manually.
1921///
1922/// All three consume the owner, so there is exactly one final release.
1923///
1924/// # Drop
1925///
1926/// Dropping an owner without releasing it **quarantines** the allocation: the
1927/// live record is retired under the mechanism lock and residual ownership is
1928/// recorded. `Drop` never calls the allocator, never enqueues, and never waits.
1929/// Freeing from `Drop` is what makes stale-pointer double frees possible, so
1930/// this type refuses to do it.
1931///
1932/// # Outstanding views block physical release
1933///
1934/// While any [`OwnedView`] (or clone of one) is alive, release is refused with
1935/// [`BindingError::OutstandingViews`] and the owner is handed back untouched.
1936#[derive(Debug)]
1937pub struct OwningAllocation {
1938    /// `None` only between a consuming method taking the allocation and the
1939    /// shell being dropped, which is what keeps `Drop` from quarantining an
1940    /// allocation that was already handed on.
1941    allocation: Option<BoundAllocation>,
1942    views: Arc<AtomicUsize>,
1943}
1944
1945impl OwningAllocation {
1946    /// Take ownership of Phase-3 metadata.
1947    ///
1948    /// This is the migration entry point from [`BoundAllocation`] to owning
1949    /// semantics; the generation is still validated at release time.
1950    pub fn new(allocation: BoundAllocation) -> Self {
1951        Self {
1952            allocation: Some(allocation),
1953            views: Arc::new(AtomicUsize::new(0)),
1954        }
1955    }
1956
1957    fn allocation(&self) -> &BoundAllocation {
1958        self.allocation
1959            .as_ref()
1960            .expect("an owning allocation holds its allocation until it is consumed")
1961    }
1962
1963    pub fn identity(&self) -> AllocationIdentity {
1964        self.allocation().identity
1965    }
1966
1967    pub fn binding(&self) -> &MemoryBinding {
1968        &self.allocation().binding
1969    }
1970
1971    /// Borrow the allocation metadata for a bound capability call.
1972    ///
1973    /// [`BoundAllocation`] is not `Clone`, so a shared borrow can be handed to
1974    /// [`BoundVirtualBacking`] commit/decommit/query operations — every one of
1975    /// which re-validates the binding identity and the allocation generation —
1976    /// without giving up owning responsibility. There is no path from this
1977    /// borrow to a release: releasing still needs the owner by value.
1978    pub fn bound(&self) -> &BoundAllocation {
1979        self.allocation()
1980    }
1981
1982    /// The owned address. Never dereferenced by this crate.
1983    pub fn as_ptr(&self) -> NonNull<u8> {
1984        self.allocation().ptr
1985    }
1986
1987    pub fn len(&self) -> usize {
1988        self.allocation().bytes
1989    }
1990
1991    pub fn is_empty(&self) -> bool {
1992        self.allocation().bytes == 0
1993    }
1994
1995    pub fn alignment(&self) -> usize {
1996        self.allocation().align
1997    }
1998
1999    /// Always [`AllocationReleaseState::Live`]; any other state means the owner
2000    /// was already consumed.
2001    pub const fn state(&self) -> AllocationReleaseState {
2002        AllocationReleaseState::Live
2003    }
2004
2005    /// Borrow a sub-range. The returned view never releases anything and keeps
2006    /// this allocation from being physically released while it is alive.
2007    pub fn view(&self, offset: usize, bytes: usize) -> Result<OwnedView, BindingError> {
2008        let view = self.allocation().view(offset, bytes)?;
2009        self.views.fetch_add(1, Ordering::AcqRel);
2010        Ok(OwnedView {
2011            view,
2012            outstanding: Arc::clone(&self.views),
2013        })
2014    }
2015
2016    /// How many borrowed views and aliases are still alive.
2017    pub fn outstanding_views(&self) -> usize {
2018        self.views.load(Ordering::Acquire)
2019    }
2020
2021    /// Give up owning semantics and return to Phase-3 metadata.
2022    ///
2023    /// Documented migration adapter: the result must be released explicitly
2024    /// through [`MemoryBinding::release`], and dropping it releases nothing.
2025    pub fn into_bound(self) -> Result<BoundAllocation, OwningReleaseError> {
2026        self.take("disowning an allocation with outstanding views")
2027    }
2028
2029    /// Detach final ownership without calling the allocator.
2030    pub fn prepare_release(self) -> Result<PreparedAllocationRelease, OwningReleaseError> {
2031        let views = Arc::clone(&self.views);
2032        let allocation = self.take("preparing release with outstanding views")?;
2033        let binding = allocation.binding.clone();
2034        binding.prepare_release(allocation).map_err(|error| {
2035            let (error, allocation) = error.into_parts();
2036            OwningReleaseError {
2037                error,
2038                allocation: Box::new(Self {
2039                    allocation: Some(allocation),
2040                    views,
2041                }),
2042            }
2043        })
2044    }
2045
2046    /// Release immediately and synchronously through the pinned allocator.
2047    ///
2048    /// No queue is involved and no wait happens, so this is the low-overhead
2049    /// path for CPU/eager mechanisms.
2050    pub fn release_now(self) -> Result<AllocationReleaseOutcome, OwningReleaseError> {
2051        Ok(self.prepare_release()?.execute())
2052    }
2053
2054    /// Hand final ownership to a provider/context-owned queue.
2055    ///
2056    /// The queue is called after every registry and mechanism lock is dropped.
2057    /// If the queue refuses, the exact prepared request is quarantined rather
2058    /// than freed or lost, and the rejection is reported.
2059    pub fn release_deferred(
2060        self,
2061        queue: &dyn DeferredReleaseQueue,
2062    ) -> Result<DeferredReleaseDisposition, OwningReleaseError> {
2063        let prepared = self.prepare_release()?;
2064        let identity = prepared.identity();
2065        match queue.enqueue(prepared) {
2066            Ok(()) => Ok(DeferredReleaseDisposition::Queued { identity }),
2067            Err(error) => {
2068                let rejection = error.rejection();
2069                Ok(DeferredReleaseDisposition::Quarantined {
2070                    identity,
2071                    rejection,
2072                    outcome: error.quarantine(),
2073                })
2074            }
2075        }
2076    }
2077
2078    fn take(mut self, operation: &'static str) -> Result<BoundAllocation, OwningReleaseError> {
2079        let _ = operation;
2080        let views = Arc::clone(&self.views);
2081        let outstanding = views.load(Ordering::Acquire);
2082        let allocation = self
2083            .allocation
2084            .take()
2085            .expect("an owning allocation holds its allocation until it is consumed");
2086        if outstanding != 0 {
2087            return Err(OwningReleaseError {
2088                error: BindingError::OutstandingViews {
2089                    identity: allocation.identity,
2090                    views: outstanding,
2091                },
2092                allocation: Box::new(Self {
2093                    allocation: Some(allocation),
2094                    views,
2095                }),
2096            });
2097        }
2098        Ok(allocation)
2099    }
2100}
2101
2102impl Drop for OwningAllocation {
2103    /// Quarantine an owner that was dropped without an explicit release.
2104    ///
2105    /// This never frees. It retires the live record under the mechanism lock and
2106    /// records residual ownership, so the bytes stay visible to accounting and
2107    /// block unsafe mechanism removal. When the record can no longer be
2108    /// prepared — device loss, termination, or an already stale generation — the
2109    /// metadata is simply dropped: in the device-loss case the live record is
2110    /// still recorded at the mechanism and is discharged by confirmed context
2111    /// termination.
2112    fn drop(&mut self) {
2113        let Some(allocation) = self.allocation.take() else {
2114            return;
2115        };
2116        let binding = allocation.binding.clone();
2117        if let Ok(prepared) = binding.prepare_release(allocation) {
2118            let _ = prepared.quarantine(QuarantineReason::OwnerDropped);
2119        }
2120    }
2121}
2122
2123/// A borrowed, cloneable alias of one [`OwningAllocation`].
2124///
2125/// A view can never release anything: it has no release method and its `Drop`
2126/// only decrements the outstanding-view count. Clones are aliases, and every
2127/// alias independently keeps physical release blocked.
2128#[derive(Debug)]
2129pub struct OwnedView {
2130    view: BoundMemoryView,
2131    outstanding: Arc<AtomicUsize>,
2132}
2133
2134impl Clone for OwnedView {
2135    fn clone(&self) -> Self {
2136        self.outstanding.fetch_add(1, Ordering::AcqRel);
2137        Self {
2138            view: self.view.clone(),
2139            outstanding: Arc::clone(&self.outstanding),
2140        }
2141    }
2142}
2143
2144impl Drop for OwnedView {
2145    fn drop(&mut self) {
2146        self.outstanding.fetch_sub(1, Ordering::AcqRel);
2147    }
2148}
2149
2150impl OwnedView {
2151    pub const fn view(&self) -> &BoundMemoryView {
2152        &self.view
2153    }
2154
2155    pub const fn binding(&self) -> &MemoryBinding {
2156        self.view.binding()
2157    }
2158
2159    pub const fn allocation_identity(&self) -> AllocationIdentity {
2160        self.view.allocation_identity()
2161    }
2162
2163    pub const fn len(&self) -> usize {
2164        self.view.len()
2165    }
2166
2167    pub const fn is_empty(&self) -> bool {
2168        self.view.is_empty()
2169    }
2170}
2171
2172/// Owning-release failure that hands the exact owner back.
2173///
2174/// Every failure carried by this type is *pre-mutation*: nothing was released,
2175/// nothing was queued, and the returned [`OwningAllocation`] is still live and
2176/// still owns its allocation.
2177#[derive(Debug)]
2178pub struct OwningReleaseError {
2179    error: BindingError,
2180    /// Boxed so the common `Ok` path does not pay for an owner-sized `Err`.
2181    allocation: Box<OwningAllocation>,
2182}
2183
2184impl OwningReleaseError {
2185    pub const fn error(&self) -> &BindingError {
2186        &self.error
2187    }
2188
2189    pub const fn allocation(&self) -> &OwningAllocation {
2190        &self.allocation
2191    }
2192
2193    /// Nothing was mutated, so the owner is still [`AllocationReleaseState::Live`].
2194    pub const fn state(&self) -> AllocationReleaseState {
2195        AllocationReleaseState::Live
2196    }
2197
2198    pub fn into_parts(self) -> (BindingError, OwningAllocation) {
2199        (self.error, *self.allocation)
2200    }
2201}
2202
2203impl std::fmt::Display for OwningReleaseError {
2204    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2205        std::fmt::Display::fmt(&self.error, formatter)
2206    }
2207}
2208
2209impl std::error::Error for OwningReleaseError {
2210    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2211        Some(&self.error)
2212    }
2213}
2214
2215/// Explicit release failure that preserves allocation metadata and pins.
2216///
2217/// Two dispositions share this type:
2218///
2219/// * **Unchanged** — the failure happened before any device mutation, and
2220///   [`into_parts`](Self::into_parts) returns the exact live allocation.
2221/// * **Quarantined** — the release was prepared and the allocator did not
2222///   complete it. [`outcome`](Self::outcome) carries the structured accounting
2223///   and residual facts, and the returned metadata is provably dead because its
2224///   generation record was already retired.
2225#[derive(Debug)]
2226pub struct ExplicitReleaseError {
2227    error: BindingError,
2228    allocation: Box<BoundAllocation>,
2229    /// Boxed so a successful release does not pay for an outcome-sized `Err`.
2230    outcome: Option<Box<AllocationReleaseOutcome>>,
2231}
2232
2233impl ExplicitReleaseError {
2234    fn unchanged(error: BindingError, allocation: BoundAllocation) -> Self {
2235        Self {
2236            error,
2237            allocation: Box::new(allocation),
2238            outcome: None,
2239        }
2240    }
2241
2242    fn quarantined(
2243        error: BindingError,
2244        allocation: BoundAllocation,
2245        outcome: AllocationReleaseOutcome,
2246    ) -> Self {
2247        Self {
2248            error,
2249            allocation: Box::new(allocation),
2250            outcome: Some(Box::new(outcome)),
2251        }
2252    }
2253
2254    pub const fn error(&self) -> &BindingError {
2255        &self.error
2256    }
2257
2258    /// The structured outcome, when the allocator was actually invoked.
2259    ///
2260    /// `None` means nothing was mutated and the allocation is still live.
2261    pub fn outcome(&self) -> Option<&AllocationReleaseOutcome> {
2262        self.outcome.as_deref()
2263    }
2264
2265    /// Whether the allocation's ownership was retained after preparation.
2266    pub const fn is_quarantined(&self) -> bool {
2267        self.outcome.is_some()
2268    }
2269
2270    /// The lifecycle state the allocation was left in.
2271    pub fn state(&self) -> AllocationReleaseState {
2272        self.outcome.as_deref().map_or(
2273            AllocationReleaseState::Live,
2274            AllocationReleaseOutcome::state,
2275        )
2276    }
2277
2278    pub fn into_parts(self) -> (BindingError, BoundAllocation) {
2279        (self.error, *self.allocation)
2280    }
2281}
2282
2283impl std::fmt::Display for ExplicitReleaseError {
2284    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2285        std::fmt::Display::fmt(&self.error, formatter)
2286    }
2287}
2288
2289impl std::error::Error for ExplicitReleaseError {
2290    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2291        Some(&self.error)
2292    }
2293}
2294
2295/// Virtual-backing access pinned to one binding.
2296#[derive(Clone, Debug)]
2297pub struct BoundVirtualBacking {
2298    binding: MemoryBinding,
2299}
2300
2301impl BoundVirtualBacking {
2302    pub const fn binding_identity(&self) -> BindingIdentity {
2303        self.binding.identity
2304    }
2305
2306    pub fn allocate_committed(
2307        &self,
2308        bytes: usize,
2309        align: usize,
2310        committed_ranges: &[std::ops::Range<usize>],
2311    ) -> Result<BoundAllocation, BindingError> {
2312        self.binding.allocate_with(
2313            "allocating through bound virtual backing",
2314            |allocator| {
2315                allocator
2316                    .as_virtual_backing()
2317                    .expect("capability presence is stable for a registered allocator")
2318                    .allocate_committed(bytes, align, committed_ranges)
2319            },
2320            bytes,
2321            align,
2322        )
2323    }
2324
2325    pub fn commit_allocation_range(
2326        &self,
2327        allocation: &BoundAllocation,
2328        offset: usize,
2329        bytes: usize,
2330    ) -> Result<(), BindingError> {
2331        let active = self
2332            .binding
2333            .mechanism
2334            .begin_active("committing through bound virtual backing")?;
2335        self.binding.mechanism.validate_allocation(
2336            self.binding.identity,
2337            allocation,
2338            "validating allocation for virtual commit",
2339        )?;
2340        let capability = self
2341            .binding
2342            .mechanism
2343            .allocator()
2344            .as_virtual_backing()
2345            .expect("capability presence is stable for a registered allocator");
2346        capability.commit_allocation_range(
2347            allocation.ptr,
2348            allocation.bytes,
2349            allocation.align,
2350            offset,
2351            bytes,
2352        )?;
2353        drop(active);
2354        Ok(())
2355    }
2356
2357    pub fn commit_allocation_ranges(
2358        &self,
2359        ranges: &[(&BoundAllocation, usize, usize)],
2360    ) -> Result<(), BindingError> {
2361        let active = self
2362            .binding
2363            .mechanism
2364            .begin_active("committing ranges through bound virtual backing")?;
2365        let mut raw = Vec::with_capacity(ranges.len());
2366        for &(allocation, offset, bytes) in ranges {
2367            self.binding.mechanism.validate_allocation(
2368                self.binding.identity,
2369                allocation,
2370                "validating allocation ranges for virtual commit",
2371            )?;
2372            raw.push(AllocationCommitRange {
2373                ptr: allocation.ptr,
2374                allocation_bytes: allocation.bytes,
2375                align: allocation.align,
2376                offset,
2377                bytes,
2378            });
2379        }
2380        self.binding
2381            .mechanism
2382            .allocator()
2383            .as_virtual_backing()
2384            .expect("capability presence is stable for a registered allocator")
2385            .commit_allocation_ranges(&raw)?;
2386        drop(active);
2387        Ok(())
2388    }
2389
2390    pub fn mapped_bytes_for_allocation(
2391        &self,
2392        bytes: usize,
2393        align: usize,
2394    ) -> Result<u64, BindingError> {
2395        let active = self
2396            .binding
2397            .mechanism
2398            .begin_active("querying bound virtual backing")?;
2399        let mapped = self
2400            .binding
2401            .mechanism
2402            .allocator()
2403            .as_virtual_backing()
2404            .expect("capability presence is stable for a registered allocator")
2405            .mapped_bytes_for_allocation(bytes, align)?;
2406        drop(active);
2407        Ok(mapped)
2408    }
2409
2410    pub fn decommit_allocation_range(
2411        &self,
2412        allocation: &BoundAllocation,
2413        offset: usize,
2414        bytes: usize,
2415    ) -> Result<u64, BindingError> {
2416        let active = self
2417            .binding
2418            .mechanism
2419            .begin_active("decommitting through bound virtual backing")?;
2420        self.binding.mechanism.validate_allocation(
2421            self.binding.identity,
2422            allocation,
2423            "validating allocation for virtual decommit",
2424        )?;
2425        let unmapped = self
2426            .binding
2427            .mechanism
2428            .allocator()
2429            .as_virtual_backing()
2430            .expect("capability presence is stable for a registered allocator")
2431            .decommit_allocation_range(
2432                allocation.ptr,
2433                allocation.bytes,
2434                allocation.align,
2435                offset,
2436                bytes,
2437            )?;
2438        drop(active);
2439        Ok(unmapped)
2440    }
2441
2442    pub fn allocation_committed_bytes(
2443        &self,
2444        allocation: &BoundAllocation,
2445    ) -> Result<usize, BindingError> {
2446        let active = self
2447            .binding
2448            .mechanism
2449            .begin_active("querying bound allocation commitment")?;
2450        self.binding.mechanism.validate_allocation(
2451            self.binding.identity,
2452            allocation,
2453            "validating allocation commitment query",
2454        )?;
2455        let committed = self
2456            .binding
2457            .mechanism
2458            .allocator()
2459            .as_virtual_backing()
2460            .expect("capability presence is stable for a registered allocator")
2461            .allocation_committed_bytes(allocation.ptr, allocation.bytes, allocation.align);
2462        drop(active);
2463        Ok(committed)
2464    }
2465}
2466
2467/// Shared-mapping access pinned to one binding.
2468#[derive(Clone, Debug)]
2469pub struct BoundSharedMapping {
2470    binding: MemoryBinding,
2471}
2472
2473impl BoundSharedMapping {
2474    pub const fn binding_identity(&self) -> BindingIdentity {
2475        self.binding.identity
2476    }
2477
2478    pub fn create_shared_prefix(&self, bytes: usize) -> Result<BoundSharedPrefix, BindingError> {
2479        let active = self
2480            .binding
2481            .mechanism
2482            .begin_active("creating a bound shared prefix")?;
2483        let prefix = self
2484            .binding
2485            .mechanism
2486            .allocator()
2487            .as_shared_mapping()
2488            .expect("capability presence is stable for a registered allocator")
2489            .create_shared_prefix(bytes)?;
2490        drop(active);
2491        Ok(BoundSharedPrefix {
2492            prefix,
2493            binding: self.binding.clone(),
2494        })
2495    }
2496
2497    pub fn incremental_owned_bytes_for_shared_prefix(
2498        &self,
2499        prefix: &BoundSharedPrefix,
2500    ) -> Result<u64, BindingError> {
2501        let active = self
2502            .binding
2503            .mechanism
2504            .begin_active("querying a bound shared prefix")?;
2505        validate_binding_identity(self.binding.identity, prefix.binding.identity)?;
2506        let bytes = self
2507            .binding
2508            .mechanism
2509            .allocator()
2510            .as_shared_mapping()
2511            .expect("capability presence is stable for a registered allocator")
2512            .incremental_owned_bytes_for_shared_prefix(prefix.prefix.as_ref())?;
2513        drop(active);
2514        Ok(bytes)
2515    }
2516
2517    pub fn commit_shared_prefix(
2518        &self,
2519        prefix: &BoundSharedPrefix,
2520        allocation: &BoundAllocation,
2521        byte_offset: usize,
2522    ) -> Result<SharedPrefixCommitInfo, BindingError> {
2523        let active = self
2524            .binding
2525            .mechanism
2526            .begin_active("committing a bound shared prefix")?;
2527        validate_binding_identity(self.binding.identity, prefix.binding.identity)?;
2528        self.binding.mechanism.validate_allocation(
2529            self.binding.identity,
2530            allocation,
2531            "validating allocation for shared prefix commit",
2532        )?;
2533        let info = self
2534            .binding
2535            .mechanism
2536            .allocator()
2537            .as_shared_mapping()
2538            .expect("capability presence is stable for a registered allocator")
2539            .commit_shared_prefix(
2540                prefix.prefix.as_ref(),
2541                allocation.ptr,
2542                allocation.bytes,
2543                byte_offset,
2544            )?;
2545        drop(active);
2546        Ok(info)
2547    }
2548}
2549
2550/// Shared physical-prefix handle pinned to one binding.
2551///
2552/// Physical teardown remains driven by dropping this handle, not by
2553/// [`BindingRegistry`] lifecycle transitions. The prefix is dropped before its
2554/// binding pin so its provider context is still alive during teardown. Phase 4
2555/// owns stream ordering and partial-failure-safe physical release.
2556#[derive(Debug)]
2557pub struct BoundSharedPrefix {
2558    prefix: Box<dyn SharedDevicePrefix>,
2559    binding: MemoryBinding,
2560}
2561
2562impl BoundSharedPrefix {
2563    pub const fn binding_identity(&self) -> BindingIdentity {
2564        self.binding.identity
2565    }
2566
2567    pub fn device_ptr(&self) -> u64 {
2568        self.prefix.device_ptr()
2569    }
2570
2571    pub fn committed_physical_bytes(&self) -> u64 {
2572        self.prefix.committed_physical_bytes()
2573    }
2574
2575    pub fn mapped_bytes(&self) -> usize {
2576        self.prefix.mapped_bytes()
2577    }
2578
2579    pub fn requested_bytes(&self) -> usize {
2580        self.prefix.requested_bytes()
2581    }
2582}
2583
2584fn validate_binding_identity(
2585    expected: BindingIdentity,
2586    actual: BindingIdentity,
2587) -> Result<(), BindingError> {
2588    if expected != actual {
2589        return Err(BindingError::BindingMismatch {
2590            expected: expected.id,
2591            actual: actual.id,
2592        });
2593    }
2594    Ok(())
2595}
2596
2597#[cfg(test)]
2598mod tests {
2599    use std::sync::{Arc, Barrier};
2600    use std::thread;
2601
2602    use crate::{BindingResource, HostAllocator};
2603
2604    use super::*;
2605
2606    /// A real [`BindingRegistry`] with one registered context/authority pair.
2607    ///
2608    /// Every selection test below drives the production registry; only the
2609    /// pause points are test-owned, so the code under test is unchanged.
2610    struct SelectionFixture {
2611        registry: BindingRegistry,
2612        context: RegisteredProviderContext,
2613        authority: RegisteredAuthority,
2614    }
2615
2616    impl SelectionFixture {
2617        fn new() -> Self {
2618            let registry = BindingRegistry::new().expect("registry");
2619            let context = registry
2620                .register_provider_context(
2621                    DeviceKey::HOST,
2622                    Arc::new(()) as Arc<dyn BindingResource>,
2623                )
2624                .expect("context registration");
2625            let authority = registry
2626                .register_authority(DeviceKey::HOST, Arc::new(()) as Arc<dyn BindingResource>)
2627                .expect("authority registration");
2628            Self {
2629                registry,
2630                context,
2631                authority,
2632            }
2633        }
2634
2635        /// Register one more mechanism. The first registration for a device also
2636        /// becomes its initial selection.
2637        fn mechanism(&self) -> RegisteredMechanism {
2638            self.registry
2639                .register_allocator(
2640                    self.context,
2641                    self.authority,
2642                    Arc::new(HostAllocator) as Arc<dyn DeviceAllocator>,
2643                )
2644                .expect("mechanism registration")
2645        }
2646
2647        fn gate(&self, mechanism: RegisteredMechanism, phase: HookPhase) -> Gate {
2648            self.gate_subject(HookSubject::Mechanism(mechanism.identity), phase)
2649        }
2650
2651        fn gate_device(&self, phase: HookPhase) -> Gate {
2652            self.gate_subject(HookSubject::Device(DeviceKey::HOST), phase)
2653        }
2654
2655        fn gate_subject(&self, subject: HookSubject, phase: HookPhase) -> Gate {
2656            let entered = Arc::new(Barrier::new(2));
2657            let resume = Arc::new(Barrier::new(2));
2658            self.registry.install_hook(RegistryHook {
2659                subject,
2660                phase,
2661                entered: Arc::clone(&entered),
2662                resume: Arc::clone(&resume),
2663            });
2664            Gate { entered, resume }
2665        }
2666
2667        fn select_on_thread(
2668            &self,
2669            mechanism: RegisteredMechanism,
2670        ) -> thread::JoinHandle<Result<(), BindingError>> {
2671            let registry = self.registry.clone();
2672            thread::spawn(move || registry.select(mechanism))
2673        }
2674
2675        fn selected_mechanism(&self) -> Result<MechanismIdentity, BindingError> {
2676            self.registry
2677                .bind(DeviceKey::HOST)
2678                .map(|binding| binding.identity().mechanism())
2679        }
2680
2681        fn assert_nothing_selected(&self) {
2682            let error = self
2683                .selected_mechanism()
2684                .expect_err("withdrawal must leave no selection to heal from");
2685            assert!(
2686                matches!(error, BindingError::NoSelectedMechanism(device) if device == DeviceKey::HOST),
2687                "selection was left pointing at a dead or unregistered mechanism: {error:?}"
2688            );
2689        }
2690    }
2691
2692    /// One paused select, released only when the test says so.
2693    struct Gate {
2694        entered: Arc<Barrier>,
2695        resume: Arc<Barrier>,
2696    }
2697
2698    impl Gate {
2699        fn wait_entered(&self) {
2700            self.entered.wait();
2701        }
2702
2703        fn resume(&self) {
2704            self.resume.wait();
2705        }
2706    }
2707
2708    fn assert_select_failed(
2709        result: Result<(), BindingError>,
2710        expected: RegisteredMechanism,
2711        expected_lifecycle: MechanismLifecycle,
2712    ) {
2713        let error = result.expect_err("select must fail once its candidate is inactive");
2714        assert!(
2715            matches!(
2716                error,
2717                BindingError::InactiveMechanism {
2718                    mechanism,
2719                    lifecycle,
2720                    operation: "selecting a mechanism",
2721                } if mechanism == expected.identity && lifecycle == expected_lifecycle
2722            ),
2723            "unexpected select error: {error:?}"
2724        );
2725    }
2726
2727    /// Park a candidate after validation, retire it, then let it publish. The
2728    /// candidate is guaranteed to fail its post-publish re-check.
2729    fn publish_a_doomed_candidate(
2730        fixture: &SelectionFixture,
2731        candidate: RegisteredMechanism,
2732    ) -> (thread::JoinHandle<Result<(), BindingError>>, Gate) {
2733        let validated = fixture.gate(candidate, HookPhase::SelectAfterValidation);
2734        let published = fixture.gate(candidate, HookPhase::SelectAfterPublish);
2735        let selecting = fixture.select_on_thread(candidate);
2736
2737        validated.wait_entered();
2738        // The candidate is not selected yet, so retiring it does not clear the
2739        // selection; it only makes the pending publication stale.
2740        fixture
2741            .registry
2742            .retire(candidate)
2743            .expect("retire candidate");
2744        validated.resume();
2745
2746        // Returns with the candidate published and its prior recorded.
2747        published.wait_entered();
2748        (selecting, published)
2749    }
2750
2751    #[test]
2752    fn failed_select_restores_the_prior_healthy_selection() {
2753        let fixture = SelectionFixture::new();
2754        let prior = fixture.mechanism();
2755        let candidate = fixture.mechanism();
2756
2757        let (selecting, published) = publish_a_doomed_candidate(&fixture, candidate);
2758        published.resume();
2759
2760        assert_select_failed(
2761            selecting.join().expect("select thread"),
2762            candidate,
2763            MechanismLifecycle::Retired,
2764        );
2765        assert_eq!(
2766            fixture
2767                .selected_mechanism()
2768                .expect("healthy prior restored"),
2769            prior.identity
2770        );
2771    }
2772
2773    #[test]
2774    fn failed_select_clears_a_retired_prior_instead_of_restoring_it() {
2775        let fixture = SelectionFixture::new();
2776        let prior = fixture.mechanism();
2777        let candidate = fixture.mechanism();
2778
2779        let (selecting, published) = publish_a_doomed_candidate(&fixture, candidate);
2780        // The prior is no longer selected, so retiring it cannot clear the
2781        // selection itself; only the withdrawal can notice it went inactive.
2782        fixture.registry.retire(prior).expect("retire prior");
2783        published.resume();
2784
2785        assert_select_failed(
2786            selecting.join().expect("select thread"),
2787            candidate,
2788            MechanismLifecycle::Retired,
2789        );
2790        fixture.assert_nothing_selected();
2791    }
2792
2793    #[test]
2794    fn failed_select_clears_a_removed_prior_instead_of_restoring_it() {
2795        let fixture = SelectionFixture::new();
2796        let prior = fixture.mechanism();
2797        let candidate = fixture.mechanism();
2798
2799        let (selecting, published) = publish_a_doomed_candidate(&fixture, candidate);
2800        fixture.registry.retire(prior).expect("retire prior");
2801        fixture.registry.remove(prior).expect("remove prior");
2802        published.resume();
2803
2804        assert_select_failed(
2805            selecting.join().expect("select thread"),
2806            candidate,
2807            MechanismLifecycle::Retired,
2808        );
2809        fixture.assert_nothing_selected();
2810    }
2811
2812    #[test]
2813    fn failed_select_does_not_overwrite_a_newer_selection() {
2814        let fixture = SelectionFixture::new();
2815        let _prior = fixture.mechanism();
2816        let candidate = fixture.mechanism();
2817        let newer = fixture.mechanism();
2818
2819        let (selecting, published) = publish_a_doomed_candidate(&fixture, candidate);
2820        // A healthy selection lands while the losing candidate is parked.
2821        fixture.registry.select(newer).expect("newer selection");
2822        published.resume();
2823
2824        assert_select_failed(
2825            selecting.join().expect("select thread"),
2826            candidate,
2827            MechanismLifecycle::Retired,
2828        );
2829        assert_eq!(
2830            fixture
2831                .selected_mechanism()
2832                .expect("newer selection stands"),
2833            newer.identity
2834        );
2835    }
2836
2837    #[test]
2838    fn two_failed_selects_never_leave_a_dead_mechanism_selected() {
2839        let fixture = SelectionFixture::new();
2840        let prior = fixture.mechanism();
2841        let first = fixture.mechanism();
2842        let second = fixture.mechanism();
2843
2844        let first_published = fixture.gate(first, HookPhase::SelectAfterPublish);
2845        let second_validated = fixture.gate(second, HookPhase::SelectAfterValidation);
2846        let second_published = fixture.gate(second, HookPhase::SelectAfterPublish);
2847
2848        // The first candidate publishes over the healthy prior and parks.
2849        let selecting_first = fixture.select_on_thread(first);
2850        first_published.wait_entered();
2851
2852        // The second candidate validates while the first still owns selection,
2853        // so retiring it now does not clear the selection.
2854        let selecting_second = fixture.select_on_thread(second);
2855        second_validated.wait_entered();
2856        fixture.registry.retire(second).expect("retire second");
2857        second_validated.resume();
2858
2859        // The second candidate is now selected and recorded the first as its
2860        // prior. Retiring the first makes that recorded prior dead.
2861        second_published.wait_entered();
2862        fixture.registry.retire(first).expect("retire first");
2863
2864        // The first candidate withdraws while the second owns selection, so it
2865        // must leave the newer selection alone.
2866        first_published.resume();
2867        assert_select_failed(
2868            selecting_first.join().expect("first select thread"),
2869            first,
2870            MechanismLifecycle::Retired,
2871        );
2872
2873        // The second candidate withdraws last and must not resurrect the first.
2874        second_published.resume();
2875        assert_select_failed(
2876            selecting_second.join().expect("second select thread"),
2877            second,
2878            MechanismLifecycle::Retired,
2879        );
2880
2881        fixture.assert_nothing_selected();
2882        // The untouched healthy prior is still selectable.
2883        fixture.registry.select(prior).expect("reselect prior");
2884        assert_eq!(
2885            fixture.selected_mechanism().expect("prior reselected"),
2886            prior.identity
2887        );
2888    }
2889
2890    #[test]
2891    fn a_later_registration_heals_a_cleared_selection() {
2892        let fixture = SelectionFixture::new();
2893        let prior = fixture.mechanism();
2894        let candidate = fixture.mechanism();
2895
2896        let (selecting, published) = publish_a_doomed_candidate(&fixture, candidate);
2897        fixture.registry.retire(prior).expect("retire prior");
2898        fixture.registry.remove(prior).expect("remove prior");
2899        published.resume();
2900
2901        assert_select_failed(
2902            selecting.join().expect("select thread"),
2903            candidate,
2904            MechanismLifecycle::Retired,
2905        );
2906        fixture.assert_nothing_selected();
2907
2908        // A cleared selection is the only state a later registration can heal;
2909        // a stale identity left in the slot would make this registration a
2910        // no-op and wedge the device permanently.
2911        let healed = fixture.mechanism();
2912        assert_eq!(
2913            fixture
2914                .selected_mechanism()
2915                .expect("registration self-heal"),
2916            healed.identity
2917        );
2918    }
2919
2920    #[test]
2921    fn retire_racing_a_select_cannot_leave_the_retired_mechanism_selected() {
2922        let fixture = SelectionFixture::new();
2923        let prior = fixture.mechanism();
2924        let candidate = fixture.mechanism();
2925
2926        let validated = fixture.gate(candidate, HookPhase::SelectAfterValidation);
2927        let retiring_gate = fixture.gate(candidate, HookPhase::RetireBetweenPhases);
2928
2929        // The candidate validates `Active` and parks before publishing.
2930        let selecting = fixture.select_on_thread(candidate);
2931        validated.wait_entered();
2932
2933        // Retirement finishes its lifecycle phase and parks before its selection
2934        // phase. Clearing the selection first would leave exactly this window
2935        // open, because the candidate would still observe `Active` afterwards.
2936        let registry = fixture.registry.clone();
2937        let retiring = thread::spawn(move || registry.retire(candidate));
2938        retiring_gate.wait_entered();
2939
2940        // The candidate publishes straight into that window and must still fail.
2941        validated.resume();
2942        assert_select_failed(
2943            selecting.join().expect("select thread"),
2944            candidate,
2945            MechanismLifecycle::Retired,
2946        );
2947
2948        retiring_gate.resume();
2949        retiring
2950            .join()
2951            .expect("retire thread")
2952            .expect("retire must succeed");
2953
2954        assert_eq!(
2955            fixture
2956                .selected_mechanism()
2957                .expect("healthy prior restored"),
2958            prior.identity
2959        );
2960    }
2961
2962    #[test]
2963    fn device_loss_racing_a_select_cannot_leave_a_lost_mechanism_selected() {
2964        let fixture = SelectionFixture::new();
2965        let _prior = fixture.mechanism();
2966        let candidate = fixture.mechanism();
2967
2968        let validated = fixture.gate(candidate, HookPhase::SelectAfterValidation);
2969        let losing = fixture.gate_device(HookPhase::InvalidateBetweenPhases);
2970
2971        let selecting = fixture.select_on_thread(candidate);
2972        validated.wait_entered();
2973
2974        // Device loss marks every mechanism lost and parks before dropping the
2975        // selection, mirroring the retirement race.
2976        let registry = fixture.registry.clone();
2977        let invalidating =
2978            thread::spawn(move || registry.invalidate_device(DeviceKey::HOST, "select race"));
2979        losing.wait_entered();
2980
2981        validated.resume();
2982        assert_select_failed(
2983            selecting.join().expect("select thread"),
2984            candidate,
2985            MechanismLifecycle::DeviceLost,
2986        );
2987
2988        losing.resume();
2989        invalidating
2990            .join()
2991            .expect("invalidate thread")
2992            .expect("invalidate must succeed");
2993
2994        // Every mechanism on the device is lost, so there is no healthy prior to
2995        // fall back to and the selection must be empty rather than stale.
2996        fixture.assert_nothing_selected();
2997    }
2998
2999    #[test]
3000    fn retirement_and_device_loss_drop_the_current_selection() {
3001        let fixture = SelectionFixture::new();
3002        let retired = fixture.mechanism();
3003        assert_eq!(
3004            fixture
3005                .selected_mechanism()
3006                .expect("first registration selects"),
3007            retired.identity
3008        );
3009
3010        fixture.registry.retire(retired).expect("retire");
3011        fixture.assert_nothing_selected();
3012
3013        let lost = fixture.mechanism();
3014        assert_eq!(
3015            fixture
3016                .selected_mechanism()
3017                .expect("registration self-heal"),
3018            lost.identity
3019        );
3020
3021        fixture
3022            .registry
3023            .invalidate_device(DeviceKey::HOST, "device lost")
3024            .expect("invalidate");
3025        fixture.assert_nothing_selected();
3026    }
3027}