Skip to main content

detcore/tool_global/
native_exec.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Coordinator-owned native exec transactions. No Tool is decoded or run here.
10//!
11//! This inactive interface requires a trusted watched native launcher. A PID,
12//! ticket or socket credential alone is not proof that a kernel exec occurred.
13//! The launcher owns that unsafe contract, including the covered interpreter,
14//! protected bootstrap descriptors and successor-before-loader initialization.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::os::fd::OwnedFd;
19use std::sync::Arc;
20
21use detcore_model::liteinst_native_exec_state as state_frame;
22use serde::Deserialize;
23use serde::Serialize;
24use sha2::Digest as _;
25use sha2::Sha256;
26/// Neutral, admitted live-cut data outside opaque Tool bytes.
27pub use state_frame::CutBinding as NativeExecNeutralBinding;
28
29/// Existing full request wire type; native controls preserve its exact tags.
30pub use super::GlobalRequest;
31/// Existing full response wire type; neutral native responses carry no clock.
32pub use super::GlobalResponse;
33use crate::types::DetTid;
34use crate::types::FilesId;
35use crate::types::MmId;
36
37/// Maximum complete state frame, including both Tool and runtime state.
38pub const MAX_STATE_BYTES: usize = state_frame::MAX_BYTES;
39/// Finite retained history for the first, bounded native exec implementation.
40pub const MAX_TRANSITIONS: usize = 1024;
41const STATE_HEADER: usize = state_frame::HEADER_BYTES;
42
43/// A protected single-use token. It carries no guest-visible deterministic data.
44#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
45pub struct ExecTicket(pub [u8; 32]);
46
47impl fmt::Debug for ExecTicket {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.write_str("ExecTicket(<private>)")
50    }
51}
52
53/// A native control refusal, never a replacement for a returned kernel errno.
54#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
55pub enum ExecControlError {
56    /// No watched native authority was installed.
57    Unavailable,
58    /// The actual sender, watched birth or image incarnation disagrees.
59    Identity,
60    /// The existing Detcore exec preparation or one-thread domain disagrees.
61    Preparation,
62    /// A strict actual-source forwarding check failed.
63    Forwarding,
64    /// Payload bounds or a supported wire version disagree.
65    Bounds,
66    /// Actual bytes disagree with an independently supplied digest.
67    Digest,
68    /// The requested edge is not legal from the retained state.
69    Transition,
70    /// A physical death or terminal transport failure has been observed.
71    Dead,
72    /// The bounded retained history cannot accept another transaction.
73    Capacity,
74    /// Native runtime state or target/bootstrap qualification was refused.
75    Qualification,
76}
77
78/// Socket facts queried by the genuine protected guest owner at its live cut.
79/// Numeric slots are handoff data, not independent host proof of a child FD.
80#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
81pub struct NativeExecSocketBinding {
82    /// Current protected descriptor observed by the quiescent guest owner.
83    pub fd: i32,
84    /// Device identity of the independently held socket object.
85    pub device: u64,
86    /// Inode identity of the independently held socket object.
87    pub inode: u64,
88}
89impl NativeExecSocketBinding {
90    /// Socket identity without its image-specific descriptor number.
91    pub fn object(self) -> (u64, u64) {
92        (self.device, self.inode)
93    }
94}
95
96/// The same independently admitted socket roles in their current handoff slots.
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
98pub struct NativeExecSocketBindings {
99    /// Existing protected deterministic-log output socket.
100    pub detlog: NativeExecSocketBinding,
101    /// Existing protected native-exec observation socket.
102    pub observer: NativeExecSocketBinding,
103}
104impl NativeExecSocketBindings {
105    /// Structural checks only; the proof must independently check both objects,
106    /// and the qualified guest owner must query/recheck actual protected slots.
107    pub fn validate(self) -> Result<(), ExecControlError> {
108        if [self.detlog.fd, self.observer.fd]
109            .into_iter()
110            .any(|fd| fd <= 2 || (198..=202).contains(&fd))
111            || self.detlog.fd == self.observer.fd
112            || self.detlog.object() == self.observer.object()
113        {
114            return Err(ExecControlError::Identity);
115        }
116        Ok(())
117    }
118}
119
120/// Data submitted at the already-prepared, still-live Detcore injection cut.
121#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
122pub struct PrepareNativeExec {
123    /// Current protected socket slots, queried/rechecked by the genuine owner.
124    pub sockets: NativeExecSocketBindings,
125    /// Actual old live-table/cut receipt, checked independently before retention.
126    pub cut: NativeExecNeutralBinding,
127    /// Fresh incarnation produced by the existing same-leader exec operation.
128    pub prepared_mm: MmId,
129    /// Digest of the unchanged canonical Config.
130    pub config_digest: [u8; 32],
131    /// Independently held immutable target identity.
132    pub target_digest: [u8; 32],
133    /// Independently held bootstrap/interpreter object identity.
134    pub bootstrap_digest: [u8; 32],
135    /// Actual guest mask that the native operation must preserve.
136    pub resume_mask: u64,
137    /// Digest of the exact opaque typed Tool snapshot.
138    pub snapshot_digest: [u8; 32],
139    /// Bounded snapshot; the coordinator never deserializes its Tool.
140    pub snapshot: Vec<u8>,
141}
142
143/// Scheduler facts read under its mutex before a neutral native control edge.
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub struct NativeExecContext {
146    /// Authenticated deterministic main thread.
147    pub owner: DetTid,
148    /// The old incarnation retained by the ordinary PrepareExec path.
149    pub old_mm: MmId,
150    /// Exact new incarnation of that same leader.
151    pub prepared_mm: MmId,
152    /// Independently computed canonical Config digest.
153    pub config_digest: [u8; 32],
154}
155
156/// Real watcher and immutable-object authority, retained for a single birth.
157///
158/// # Safety
159/// Implementations must retain the genuine pidfd/watch registration, accept
160/// only the exact same living birth and main thread, and recheck terminal
161/// watcher failure. `matches_peer` must compare the actual accepted peer pidfd
162/// to that retained birth. Qualification must independently bind the actual
163/// captured resume mask and the one-main-thread, childless preparation cut,
164/// the immutable admitted namespace, and the held target/bootstrap files.
165/// The runtime proof must exactly decode all carried counters, observer
166/// sequence and forwarding boundary, refusing unsupported patch state.
167///
168/// The installed proof and its admission adapter must protect the ticket and
169/// provisioned objects, constrain Issue to the exact original native exec leaf,
170/// and prevent redemption from the old image. They must establish the covered
171/// fresh bootstrap and successor-before-loader transition, and accept Complete
172/// only from that genuine successor after its real post-exec and frontend
173/// proofs. A same-birth pidfd, token, seal or digest is not proof of kernel exec.
174/// Socket qualification must compare against independently held launch objects;
175/// actual numeric ownership requires the qualified guest owner's physical
176/// queries and final rechecks, never object equality alone.
177/// No request field is authority for any of these facts. These obligations
178/// belong to the trusted native launch chain, not to this opaque storage.
179pub unsafe trait NativeExecProof: fmt::Debug + Send + Sync {
180    /// Deterministic owner established by actual admission.
181    fn owner(&self) -> DetTid;
182    /// Physical main-thread identity from the same retained watched birth.
183    /// Missing proof refuses control RPCs. Implementations must never derive
184    /// this identity from request IDs or unretained numerical PID equality.
185    fn physical_ids(&self) -> Option<(i32, i32)> {
186        None
187    }
188    /// The retained birth remains alive with no watcher failure.
189    fn is_live(&self) -> bool;
190    /// Actual accepted peer pidfd names precisely that retained live birth.
191    fn matches_peer(&self, peer: &OwnedFd) -> bool;
192    /// Validate held target/bootstrap objects and immutable namespace, and
193    /// compare the requested mask with the independently captured actual cut.
194    fn qualify_target(&self, request: &PrepareNativeExec) -> bool;
195    /// Validate the real, versioned backend runtime state before sealing it.
196    fn qualify_runtime(&self, bytes: &[u8]) -> bool;
197    /// Revalidate the actual accepted successor's held mappings, namespace,
198    /// birth and binding immediately before Complete. Refuses by default.
199    /// Called under the registry mutex; must never acquire scheduler/pending
200    /// locks or recurse into the registry. This augments, and never replaces,
201    /// the unsafe genuine-kernel-exec/lifecycle/consumer obligations above.
202    fn qualify_completion(&self, _ticket: ExecTicket, _binding: NativeExecBinding) -> bool {
203        false
204    }
205}
206
207/// Retained transaction stage. Terminal records cannot be removed or replayed.
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum ExecStage {
210    /// Prepared Tool bytes; runtime state may still need sealing.
211    Prepared,
212    /// Provision succeeded and the final old-image issue was acknowledged.
213    Issued,
214    /// Consumed before sending successor ACK, Config or state bytes.
215    Redeemed,
216    /// Genuine post-exec and frontend completion were acknowledged.
217    Completed,
218    /// A pre-redemption failure cancelled the native transaction.
219    Cancelled,
220    /// Watched death invalidated the transaction.
221    Dead,
222    /// A terminal provision/transport/bootstrap failure invalidated it.
223    Failed,
224}
225
226impl ExecStage {
227    fn live(self) -> bool {
228        matches!(self, Self::Prepared | Self::Issued | Self::Redeemed)
229    }
230}
231
232/// Immutable target and state identities retained under the ticket.
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub struct NativeExecBinding {
235    /// Admitted old-image to bootstrap slots; successor may reserve them again.
236    pub sockets: NativeExecSocketBindings,
237    /// Independently admitted neutral cut, emitted before opaque Tool bytes.
238    pub cut: NativeExecNeutralBinding,
239    /// Independently validated scheduler and Config identity.
240    pub context: NativeExecContext,
241    /// Held immutable target identity.
242    pub target_digest: [u8; 32],
243    /// Held bootstrap/interpreter identity.
244    pub bootstrap_digest: [u8; 32],
245    /// Actual guest mask preserved across the operation.
246    pub resume_mask: u64,
247    /// Exact prepared Tool bytes retained by this transaction.
248    pub snapshot_digest: [u8; 32],
249}
250
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252enum ProvisionState {
253    Fresh,
254    Consumed,
255    Delivered,
256}
257
258#[derive(Debug)]
259struct Transition {
260    binding: NativeExecBinding,
261    stage: ExecStage,
262    marked_past_first_exec: bool,
263    provision: ProvisionState,
264    tool: Vec<u8>,
265    state: Vec<u8>,
266    state_digest: Option<[u8; 32]>,
267}
268
269/// Serial retained store for one watched native process. The coordinator must
270/// place this behind the same mutex for RPC, admission and watcher callbacks.
271/// No socket EOF calls `died`: the birth watcher supplies actual death.
272pub struct NativeExecRegistry {
273    proof: Arc<dyn NativeExecProof>,
274    nonce: [u8; 32],
275    next_ticket: u64,
276    current_mm: Option<MmId>,
277    current_files: FilesId,
278    initial_mm: Option<MmId>,
279    forwarding_receipt: Option<crate::detlog::CoordinatorForwardingReceipt>,
280    transitions: BTreeMap<ExecTicket, Transition>,
281    dead: bool,
282    terminal_failure: bool,
283}
284
285impl fmt::Debug for NativeExecRegistry {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        f.debug_struct("NativeExecRegistry")
288            .field("owner", &self.proof.owner())
289            .field("retained_transitions", &self.transitions.len())
290            .field("dead", &self.dead)
291            .field("terminal_failure", &self.terminal_failure)
292            .finish_non_exhaustive()
293    }
294}
295
296impl NativeExecRegistry {
297    /// Retain the launcher's proof and a private fresh CSPRNG nonce. The nonce
298    /// is control-plane secrecy, never a draw from any guest PRNG or clock.
299    pub fn new(proof: Arc<dyn NativeExecProof>, nonce: [u8; 32]) -> Result<Self, ExecControlError> {
300        if proof.owner().as_raw() <= 0 || !proof.is_live() || nonce == [0; 32] {
301            return Err(ExecControlError::Identity);
302        }
303        // Actual first-domain main initialization uses FileMetadata::new(pid),
304        // whose table is FilesId::initial(pid). This is a model incarnation
305        // anchor; the genuine guest receipt separately validates the live table.
306        let current_files = FilesId::initial(proof.owner());
307        Ok(Self {
308            proof,
309            nonce,
310            next_ticket: 1,
311            current_mm: None,
312            current_files,
313            initial_mm: None,
314            forwarding_receipt: None,
315            transitions: BTreeMap::new(),
316            dead: false,
317            terminal_failure: false,
318        })
319    }
320
321    fn check_owner(&mut self, owner: DetTid) -> Result<(), ExecControlError> {
322        if self.terminal_failure {
323            return Err(ExecControlError::Dead);
324        }
325        if self.dead || !self.proof.is_live() {
326            self.died();
327            return Err(ExecControlError::Dead);
328        }
329        if owner != self.proof.owner() {
330            return Err(ExecControlError::Identity);
331        }
332        Ok(())
333    }
334
335    // Called under sched and this registry's mutex before a control transition.
336    // Every cut retains and checks the same real forwarding source; no reset or
337    // synthetic count can establish a fresh boundary for a later native edge.
338    fn check_forwarding_cut(
339        &mut self,
340        owner: DetTid,
341        produced: u64,
342        require_empty: bool,
343    ) -> Result<(), ExecControlError> {
344        self.check_owner(owner)?;
345        let (pid, tid) = self
346            .proof
347            .physical_ids()
348            .ok_or(ExecControlError::Identity)?;
349        if pid <= 0 || pid != tid {
350            return Err(ExecControlError::Identity);
351        }
352        if require_empty && produced != 0 {
353            return Err(ExecControlError::Forwarding);
354        }
355        let receipt = crate::detlog::check_native_exec_forwarding_cut(
356            tid,
357            produced,
358            self.forwarding_receipt.as_ref(),
359        )
360        .map_err(|_| ExecControlError::Forwarding)?;
361        self.forwarding_receipt = Some(receipt);
362        Ok(())
363    }
364
365    /// Retain exact Tool bytes only after existing exec preparation is proved.
366    pub fn prepare(
367        &mut self,
368        context: NativeExecContext,
369        request: PrepareNativeExec,
370    ) -> Result<ExecTicket, ExecControlError> {
371        self.check_owner(context.owner)?;
372        request.sockets.validate()?;
373        let (physical_pid, physical_tid) = self
374            .proof
375            .physical_ids()
376            .ok_or(ExecControlError::Identity)?;
377        request
378            .cut
379            .validate()
380            .map_err(|_| ExecControlError::Identity)?;
381        if physical_pid <= 0
382            || physical_pid != physical_tid
383            || request.cut.physical_tid != physical_tid
384            || request.cut.dettid != context.owner
385            || request.cut.detpid != context.owner
386            || request.cut.old_mm != context.old_mm
387            || request.cut.old_files != self.current_files
388            || request.cut.prepared_mm != context.prepared_mm
389            || request.cut.config_digest != context.config_digest
390            || self.current_mm.is_some_and(|mm| mm != context.old_mm)
391            || context.old_mm.generation().checked_add(1) != Some(context.prepared_mm.generation())
392            || context.prepared_mm.creator() != context.owner
393            || request.prepared_mm != context.prepared_mm
394            || request.config_digest != context.config_digest
395        {
396            return Err(ExecControlError::Identity);
397        }
398        if self.transitions.values().any(|value| value.stage.live()) {
399            return Err(ExecControlError::Transition);
400        }
401        if self.transitions.len() >= MAX_TRANSITIONS {
402            return Err(ExecControlError::Capacity);
403        }
404        if request.snapshot.is_empty()
405            || request.snapshot.len() > MAX_STATE_BYTES - STATE_HEADER - 1
406        {
407            return Err(ExecControlError::Bounds);
408        }
409        if <[u8; 32]>::from(Sha256::digest(&request.snapshot)) != request.snapshot_digest {
410            return Err(ExecControlError::Digest);
411        }
412        if !self.proof.qualify_target(&request) {
413            return Err(ExecControlError::Qualification);
414        }
415        // The secret run nonce plus a checked, never-reused ordinal makes
416        // tickets distinct without touching deterministic execution state.
417        let mut hash = Sha256::new();
418        hash.update(b"Hermit native exec ticket v1\0");
419        hash.update(self.nonce);
420        hash.update(self.next_ticket.to_le_bytes());
421        let ticket = ExecTicket(hash.finalize().into());
422        self.next_ticket = self
423            .next_ticket
424            .checked_add(1)
425            .ok_or(ExecControlError::Capacity)?;
426        if self.transitions.contains_key(&ticket) {
427            return Err(ExecControlError::Transition);
428        }
429        self.current_mm = Some(context.old_mm);
430        self.initial_mm.get_or_insert(context.old_mm);
431        self.transitions.insert(
432            ticket,
433            Transition {
434                binding: NativeExecBinding {
435                    sockets: request.sockets,
436                    cut: request.cut,
437                    context,
438                    target_digest: request.target_digest,
439                    bootstrap_digest: request.bootstrap_digest,
440                    resume_mask: request.resume_mask,
441                    snapshot_digest: request.snapshot_digest,
442                },
443                stage: ExecStage::Prepared,
444                marked_past_first_exec: false,
445                provision: ProvisionState::Fresh,
446                tool: request.snapshot,
447                state: Vec::new(),
448                state_digest: None,
449            },
450        );
451        Ok(ticket)
452    }
453
454    fn transaction(
455        &mut self,
456        owner: DetTid,
457        ticket: ExecTicket,
458        mm: MmId,
459    ) -> Result<&mut Transition, ExecControlError> {
460        self.check_owner(owner)?;
461        let entry = self
462            .transitions
463            .get_mut(&ticket)
464            .ok_or(ExecControlError::Identity)?;
465        if entry.binding.context.owner != owner || entry.binding.context.prepared_mm != mm {
466            return Err(ExecControlError::Identity);
467        }
468        Ok(entry)
469    }
470
471    // Only the successful same-leader ordinary MarkPastFirstExecve handler
472    // calls this after its real scheduler reconnect. An alternate registration
473    // or a global startup boolean cannot establish this per-ticket receipt.
474    pub(super) fn mark_past_first_exec(
475        &mut self,
476        owner: DetTid,
477        mm: MmId,
478    ) -> Result<(), ExecControlError> {
479        if !self
480            .transitions
481            .values()
482            .any(|entry| entry.stage.live() && entry.binding.context.owner == owner)
483        {
484            return Ok(());
485        }
486        self.check_owner(owner)?;
487        let entry = self
488            .transitions
489            .values_mut()
490            .find(|entry| entry.stage.live() && entry.binding.context.owner == owner)
491            .expect("one live transaction was checked under this mutex");
492        if entry.stage != ExecStage::Redeemed || entry.binding.context.prepared_mm != mm {
493            return Err(ExecControlError::Transition);
494        }
495        entry.marked_past_first_exec = true;
496        Ok(())
497    }
498
499    /// Classify clean EOF for this exact accepted native connection incarnation.
500    /// `None` denotes the admitted initial stream; a successor tag names only
501    /// its own consumed ticket. Delayed EOF remains valid after the later
502    /// transition completes. Prepared/cancelled attempts and another image's
503    /// Issue cannot grant permission. Physical terminal closure is separately
504    /// owned by the real watch; this method never turns EOF into death.
505    pub fn permits_connection_close(
506        &mut self,
507        peer: &OwnedFd,
508        predecessor: Option<ExecTicket>,
509    ) -> Result<bool, ExecControlError> {
510        self.check_owner(self.proof.owner())?;
511        if !self.proof.matches_peer(peer) {
512            return Err(ExecControlError::Identity);
513        }
514        let mm = match predecessor {
515            None => match self.initial_mm {
516                Some(mm) => mm,
517                None => return Ok(false),
518            },
519            Some(ticket) => {
520                let entry = self
521                    .transitions
522                    .get(&ticket)
523                    .ok_or(ExecControlError::Identity)?;
524                if !matches!(entry.stage, ExecStage::Redeemed | ExecStage::Completed) {
525                    return Err(ExecControlError::Transition);
526                }
527                entry.binding.context.prepared_mm
528            }
529        };
530        Ok(self.transitions.values().any(|entry| {
531            entry.binding.context.old_mm == mm
532                && matches!(
533                    entry.stage,
534                    ExecStage::Issued | ExecStage::Redeemed | ExecStage::Completed
535                )
536        }))
537    }
538
539    /// Read the exact retained binding for provisioning or successor map proof.
540    /// This does not issue or redeem the transaction.
541    pub fn binding(
542        &mut self,
543        owner: DetTid,
544        ticket: ExecTicket,
545        mm: MmId,
546    ) -> Result<NativeExecBinding, ExecControlError> {
547        let entry = self.transaction(owner, ticket, mm)?;
548        if !entry.stage.live() {
549            return Err(ExecControlError::Transition);
550        }
551        Ok(entry.binding)
552    }
553
554    /// Read the retained Issued binding for the actual authenticated successor
555    /// peer without asking the packet to supply its owner or image incarnation.
556    /// This neither consumes nor authorizes delivery; redeem still performs all
557    /// independent Config/composite checks and consumes before sending bytes.
558    pub fn issued_binding(
559        &mut self,
560        peer: &OwnedFd,
561        ticket: ExecTicket,
562    ) -> Result<NativeExecBinding, ExecControlError> {
563        self.check_owner(self.proof.owner())?;
564        if !self.proof.matches_peer(peer) {
565            return Err(ExecControlError::Identity);
566        }
567        let entry = self
568            .transitions
569            .get(&ticket)
570            .ok_or(ExecControlError::Identity)?;
571        if entry.stage != ExecStage::Issued {
572            return Err(ExecControlError::Transition);
573        }
574        Ok(entry.binding)
575    }
576
577    /// Consume one Prepared+sealed provision attempt before constructing or
578    /// sending any packet or descriptor. The caller holds the very same mutex
579    /// as RPC/admission/watch callbacks through this affine lease. No scheduler
580    /// lock may be acquired while the lease is held.
581    pub fn begin_provision(
582        &mut self,
583        peer: &OwnedFd,
584        ticket: ExecTicket,
585    ) -> Result<NativeExecProvision<'_>, ExecControlError> {
586        self.check_owner(self.proof.owner())?;
587        if !self.proof.matches_peer(peer) {
588            return Err(ExecControlError::Identity);
589        }
590        let entry = self
591            .transitions
592            .get_mut(&ticket)
593            .ok_or(ExecControlError::Identity)?;
594        if entry.stage != ExecStage::Prepared || entry.provision != ProvisionState::Fresh {
595            return Err(ExecControlError::Transition);
596        }
597        let digest = entry.state_digest.ok_or(ExecControlError::Transition)?;
598        let binding = entry.binding;
599        entry.provision = ProvisionState::Consumed;
600        Ok(NativeExecProvision {
601            registry: self,
602            ticket,
603            binding,
604            digest,
605            completed: false,
606        })
607    }
608
609    /// Seal the actual runtime bytes exactly once before final input provision.
610    /// The complete digest covers version, both lengths, Tool and runtime bytes.
611    pub fn seal(
612        &mut self,
613        owner: DetTid,
614        ticket: ExecTicket,
615        mm: MmId,
616        runtime_digest: [u8; 32],
617        runtime: &[u8],
618    ) -> Result<[u8; 32], ExecControlError> {
619        self.check_owner(owner)?;
620        if runtime.is_empty() || runtime.len() > MAX_STATE_BYTES - STATE_HEADER - 1 {
621            return Err(ExecControlError::Bounds);
622        }
623        if <[u8; 32]>::from(Sha256::digest(runtime)) != runtime_digest {
624            return Err(ExecControlError::Digest);
625        }
626        if !self.proof.qualify_runtime(runtime) {
627            return Err(ExecControlError::Qualification);
628        }
629        let entry = self.transaction(owner, ticket, mm)?;
630        if entry.stage != ExecStage::Prepared || entry.state_digest.is_some() {
631            return Err(ExecControlError::Transition);
632        }
633        let length = STATE_HEADER
634            .checked_add(entry.tool.len())
635            .and_then(|n| n.checked_add(runtime.len()))
636            .ok_or(ExecControlError::Bounds)?;
637        if length > MAX_STATE_BYTES {
638            return Err(ExecControlError::Bounds);
639        }
640        let mut state = Vec::with_capacity(length);
641        let mut header = [0; STATE_HEADER];
642        state_frame::encode_header(
643            entry.binding.cut,
644            entry.tool.len(),
645            runtime.len(),
646            &mut header,
647        )
648        .map_err(|_| ExecControlError::Bounds)?;
649        state.extend_from_slice(&header);
650        state.extend_from_slice(&entry.tool);
651        state.extend_from_slice(runtime);
652        let digest = Sha256::digest(&state).into();
653        entry.tool = Vec::new();
654        entry.state = state;
655        entry.state_digest = Some(digest);
656        Ok(digest)
657    }
658
659    /// Last old-image edge. Sealed runtime and completed provision are required.
660    pub fn issue(
661        &mut self,
662        owner: DetTid,
663        ticket: ExecTicket,
664        mm: MmId,
665        digest: [u8; 32],
666    ) -> Result<(), ExecControlError> {
667        let entry = self.transaction(owner, ticket, mm)?;
668        if entry.stage != ExecStage::Prepared
669            || entry.state_digest != Some(digest)
670            || entry.provision != ProvisionState::Delivered
671        {
672            return Err(ExecControlError::Transition);
673        }
674        entry.stage = ExecStage::Issued;
675        Ok(())
676    }
677
678    /// Redeem against the actual accepted socket's pidfd. Consume first: failed
679    /// ACK/config/state delivery can never make this ticket redeemable again.
680    /// The unsafe launcher contract, not these credentials, proves native exec.
681    pub fn redeem(
682        &mut self,
683        peer: &OwnedFd,
684        ticket: ExecTicket,
685        config: [u8; 32],
686        digest: [u8; 32],
687    ) -> Result<Vec<u8>, ExecControlError> {
688        let owner = self.proof.owner();
689        self.check_owner(owner)?;
690        if !self.proof.matches_peer(peer) {
691            return Err(ExecControlError::Identity);
692        }
693        let entry = self
694            .transitions
695            .get_mut(&ticket)
696            .ok_or(ExecControlError::Identity)?;
697        if entry.stage != ExecStage::Issued {
698            return Err(ExecControlError::Transition);
699        }
700        if entry.binding.context.config_digest != config || entry.state_digest != Some(digest) {
701            return Err(ExecControlError::Digest);
702        }
703        entry.stage = ExecStage::Redeemed;
704        Ok(std::mem::take(&mut entry.state))
705    }
706
707    /// Returned exec errno or an earlier refusal cancels only pre-redemption.
708    /// Ordinary Detcore CancelExec still performs its own full rollback.
709    pub fn cancel(
710        &mut self,
711        owner: DetTid,
712        ticket: ExecTicket,
713        mm: MmId,
714    ) -> Result<(), ExecControlError> {
715        let entry = self.transaction(owner, ticket, mm)?;
716        if !matches!(entry.stage, ExecStage::Prepared | ExecStage::Issued) {
717            return Err(ExecControlError::Transition);
718        }
719        entry.stage = ExecStage::Cancelled;
720        entry.tool = Vec::new();
721        entry.state = Vec::new();
722        Ok(())
723    }
724
725    /// The trusted successor reports completed real lifecycle and frontend
726    /// verification, before the acknowledgment can release the guest loader.
727    pub fn complete(
728        &mut self,
729        owner: DetTid,
730        ticket: ExecTicket,
731        mm: MmId,
732    ) -> Result<(), ExecControlError> {
733        let entry = self.transaction(owner, ticket, mm)?;
734        if entry.stage != ExecStage::Redeemed {
735            return Err(ExecControlError::Transition);
736        }
737        let binding = entry.binding;
738        if !self.proof.qualify_completion(ticket, binding) {
739            self.failed(ticket)?;
740            return Err(ExecControlError::Qualification);
741        }
742        // Reborrow only after the proof; no mutable entry reference crosses it.
743        self.transitions
744            .get_mut(&ticket)
745            .expect("retained Redeemed record")
746            .stage = ExecStage::Completed;
747        self.current_mm = Some(mm);
748        self.current_files = binding.cut.old_files.for_exec(owner);
749        Ok(())
750    }
751
752    /// Permanently fail this birth's native launch chain, retaining the Failed
753    /// tombstone. Only explicit pre-redemption cancellation permits a retry.
754    pub fn failed(&mut self, ticket: ExecTicket) -> Result<(), ExecControlError> {
755        let entry = self
756            .transitions
757            .get_mut(&ticket)
758            .ok_or(ExecControlError::Identity)?;
759        if !entry.stage.live() {
760            return Err(ExecControlError::Transition);
761        }
762        entry.stage = ExecStage::Failed;
763        entry.tool = Vec::new();
764        entry.state = Vec::new();
765        self.terminal_failure = true;
766        Ok(())
767    }
768
769    /// A same-birth authenticated provision stream became ambiguous before a
770    /// usable ticket could be consumed. Preserve terminal records, fail all live
771    /// transactions and prohibit retries for this birth. Unauthenticated peers
772    /// must never call this method.
773    pub fn provision_transport_failed(&mut self) {
774        self.terminal_failure = true;
775        for entry in self
776            .transitions
777            .values_mut()
778            .filter(|entry| entry.stage.live())
779        {
780            entry.stage = ExecStage::Failed;
781            entry.tool = Vec::new();
782            entry.state = Vec::new();
783        }
784    }
785
786    /// Called only by actual birth-watch death/failure, never replacement EOF.
787    pub fn died(&mut self) {
788        self.dead = true;
789        for entry in self
790            .transitions
791            .values_mut()
792            .filter(|entry| entry.stage.live())
793        {
794            entry.stage = ExecStage::Dead;
795            entry.tool = Vec::new();
796            entry.state = Vec::new();
797        }
798    }
799
800    /// Observe the retained stage without mutating a transaction or its bytes.
801    pub fn stage(&self, ticket: ExecTicket) -> Option<ExecStage> {
802        self.transitions.get(&ticket).map(|entry| entry.stage)
803    }
804}
805
806/// Affine host-only provision access. Dropping incomplete access poisons the
807/// birth and retains its Failed tombstone. It is neither Clone nor Copy.
808#[must_use = "an incomplete provision poisons the native birth"]
809pub struct NativeExecProvision<'a> {
810    registry: &'a mut NativeExecRegistry,
811    ticket: ExecTicket,
812    binding: NativeExecBinding,
813    digest: [u8; 32],
814    completed: bool,
815}
816
817impl NativeExecProvision<'_> {
818    /// Exact independently qualified binding retained by this transaction.
819    pub fn binding(&self) -> NativeExecBinding {
820        self.binding
821    }
822    /// Exact complete Tool/runtime composite digest retained by Seal.
823    pub fn state_digest(&self) -> [u8; 32] {
824        self.digest
825    }
826
827    /// Record successful one-shot transfer before allowing Issue.
828    ///
829    /// # Safety
830    /// The trusted host provider must have sent exactly the final fully sealed
831    /// HLNEXEC1 input and held immutable role descriptors to this same accepted
832    /// peer, with one exact bounded SCM_RIGHTS send and no partial/error result.
833    /// This is host transport evidence only; guest FD validation, final cuts
834    /// and original native exec proof remain launch-chain obligations.
835    pub unsafe fn complete(mut self) -> Result<(), ExecControlError> {
836        self.registry.check_owner(self.binding.context.owner)?;
837        let entry = self.registry.transaction(
838            self.binding.context.owner,
839            self.ticket,
840            self.binding.context.prepared_mm,
841        )?;
842        if entry.stage != ExecStage::Prepared
843            || entry.provision != ProvisionState::Consumed
844            || entry.state_digest != Some(self.digest)
845        {
846            return Err(ExecControlError::Transition);
847        }
848        entry.provision = ProvisionState::Delivered;
849        self.completed = true;
850        Ok(())
851    }
852}
853
854impl Drop for NativeExecProvision<'_> {
855    fn drop(&mut self) {
856        if !self.completed {
857            // Do not relabel Dead/Cancelled tombstones. The exclusive lease
858            // excludes concurrent cancellation; failed completion may mark Dead.
859            let _ = self.registry.failed(self.ticket);
860            self.registry.terminal_failure = true;
861        }
862    }
863}
864
865/// Split an already-authenticated complete state frame with exact lengths.
866/// This checks its structural format; the caller must separately authenticate
867/// the expected digest and perform bounded exact decoding of both payloads.
868pub fn split_state(bytes: &[u8]) -> Result<(&[u8], &[u8]), ExecControlError> {
869    let (_, tool, runtime) = split_state_binding(bytes)?;
870    Ok((tool, runtime))
871}
872
873/// Decode the admitted neutral identity outside the opaque Tool bytes. The
874/// transport caller must first authenticate the entire expected frame digest.
875pub fn split_state_binding(
876    bytes: &[u8],
877) -> Result<(NativeExecNeutralBinding, &[u8], &[u8]), ExecControlError> {
878    state_frame::decode(bytes).map_err(|_| ExecControlError::Bounds)
879}
880
881/// Identify a native leaf through any number of envelopes. Malformed envelopes
882/// still reach the native refusal path before ordinary forwarding or clocks.
883pub(super) fn is_native_exec_control(mut request: &super::GlobalRequest) -> bool {
884    use super::GlobalRequest as Q;
885    loop {
886        match request {
887            Q::WithForwardedCount(_, inner) => request = inner,
888            Q::PrepareNativeExec(_)
889            | Q::SealNativeExecStats { .. }
890            | Q::IssueNativeExec { .. }
891            | Q::CancelNativeExec { .. }
892            | Q::CompleteNativeExec { .. } => return true,
893            _ => return false,
894        }
895    }
896}
897
898#[derive(Clone, Copy)]
899enum ControlCut {
900    Prepared,
901    EmptyPrepared,
902    Completed,
903}
904
905impl super::GlobalState {
906    /// The complete actual host configuration retained by this GlobalState,
907    /// including legitimate host-only fields omitted from the Tool wire.
908    /// Native admission uses this owner rather than a separate caller Config.
909    pub fn native_exec_host_config(&self) -> &crate::Config {
910        &self.cfg
911    }
912
913    /// Install the genuine watched authority once, before native controls.
914    /// Ordinary runs leave this absent. No request can create this capability.
915    pub fn install_native_exec_registry(
916        &self,
917        registry: NativeExecRegistry,
918    ) -> Result<(), ExecControlError> {
919        self.native_exec_registry
920            .set(std::sync::Mutex::new(registry))
921            .map_err(|_| ExecControlError::Transition)
922    }
923
924    /// The very same retained store for native admission and birth watchers.
925    /// Callbacks must drop this guard before acquiring the scheduler mutex.
926    pub fn native_exec_registry(&self) -> Option<&std::sync::Mutex<NativeExecRegistry>> {
927        self.native_exec_registry.get()
928    }
929
930    fn native_exec_transition<R>(
931        &self,
932        owner: DetTid,
933        request_mm: MmId,
934        produced: Option<u64>,
935        cut: ControlCut,
936        ticket: Option<ExecTicket>,
937        transition: impl FnOnce(
938            &mut NativeExecRegistry,
939            Option<NativeExecContext>,
940        ) -> Result<R, ExecControlError>,
941    ) -> Result<R, ExecControlError> {
942        let produced = produced.ok_or(ExecControlError::Forwarding)?;
943        let registry = self
944            .native_exec_registry
945            .get()
946            .ok_or(ExecControlError::Unavailable)?;
947        // This is a synchronous neutral check, not lock_rpc_scheduler's parked
948        // backend-failure continuation. Hold sched through the strict cut and
949        // registry transition. Lock order is sched -> pending -> registry.
950        let sched = self.sched.lock().unwrap();
951        if sched.backend_failed() {
952            return Err(ExecControlError::Dead);
953        }
954        if !self.cfg.sequentialize_threads
955            || sched.registered_process(owner) != Some(owner)
956            || !sched.thread_is_live(owner)
957            || sched.thread_is_logically_killed(owner)
958        {
959            return Err(ExecControlError::Identity);
960        }
961        let live_group: Vec<_> = sched.live_process_threads(owner).collect();
962        if live_group != [owner]
963            || !sched.thread_tree.unreaped_children_of(owner).is_empty()
964            || !sched.holds_serial_grant(owner)
965        {
966            return Err(ExecControlError::Preparation);
967        }
968        let pending = self.pending_exec_states.lock().unwrap();
969        let context = match cut {
970            ControlCut::Completed => {
971                if !self
972                    .past_first_execve
973                    .load(std::sync::atomic::Ordering::SeqCst)
974                    || pending.contains_key(&owner)
975                    || sched.thread_mm(owner) != Some(request_mm)
976                    || !sched.rpc_incarnation_matches(owner, request_mm)
977                {
978                    return Err(ExecControlError::Preparation);
979                }
980                None
981            }
982            ControlCut::Prepared | ControlCut::EmptyPrepared => {
983                let prepared = pending.get(&owner).ok_or(ExecControlError::Preparation)?;
984                // Native Detcore already stages its old-mm wakes in ThreadState.
985                // The DBT coordinator vector has a separate existing owner.
986                if !prepared.fd_blocking.robust_wakes.is_empty() {
987                    return Err(ExecControlError::Preparation);
988                }
989                if prepared.caller != owner || prepared.process != owner {
990                    return Err(ExecControlError::Identity);
991                }
992                if sched.thread_mm(owner) != Some(prepared.mm)
993                    || !sched.rpc_incarnation_matches(owner, prepared.mm)
994                    || request_mm != prepared.mm.for_exec(owner)
995                {
996                    return Err(ExecControlError::Identity);
997                }
998                Some(NativeExecContext {
999                    owner,
1000                    old_mm: prepared.mm,
1001                    prepared_mm: request_mm,
1002                    config_digest: crate::native_exec::native_exec_config_digest(&self.cfg)
1003                        .map_err(|_| ExecControlError::Digest)?,
1004                })
1005            }
1006        };
1007        let mut registry = registry.lock().unwrap();
1008        registry.check_owner(owner)?;
1009        if let Some(ticket) = ticket {
1010            if registry.forwarding_receipt.is_none() {
1011                return Err(ExecControlError::Forwarding);
1012            }
1013            let binding = registry.binding(owner, ticket, request_mm)?;
1014            if context.is_some_and(|context| binding.context != context) {
1015                return Err(ExecControlError::Identity);
1016            }
1017            if matches!(cut, ControlCut::Completed)
1018                && !registry
1019                    .transaction(owner, ticket, request_mm)?
1020                    .marked_past_first_exec
1021            {
1022                return Err(ExecControlError::Preparation);
1023            }
1024        }
1025        registry.check_forwarding_cut(owner, produced, matches!(cut, ControlCut::EmptyPrepared))?;
1026        transition(&mut registry, context)
1027    }
1028
1029    pub(super) fn dispatch_native_exec_control(
1030        &self,
1031        from: reverie::Tid,
1032        request: <Self as reverie::GlobalTool>::Request,
1033    ) -> <Self as reverie::GlobalTool>::Response {
1034        use super::GlobalRequest as Q;
1035        use super::GlobalResponse as R;
1036        let (_, mm, mut request) = request;
1037        let mut produced = None;
1038        let mut envelopes = 0usize;
1039        loop {
1040            match request {
1041                Q::WithForwardedCount(count, inner) => {
1042                    envelopes = envelopes.saturating_add(1);
1043                    produced = Some(count);
1044                    request = *inner;
1045                }
1046                leaf => {
1047                    request = leaf;
1048                    break;
1049                }
1050            }
1051        }
1052        if envelopes != 1 {
1053            produced = None;
1054        }
1055        let owner = DetTid::from_raw(from.as_raw());
1056        let response = match request {
1057            Q::PrepareNativeExec(request) => R::PrepareNativeExec(self.native_exec_transition(
1058                owner,
1059                mm,
1060                produced,
1061                ControlCut::Prepared,
1062                None,
1063                |registry, context| {
1064                    registry.prepare(
1065                        context.expect("prepared native cut supplies context"),
1066                        request,
1067                    )
1068                },
1069            )),
1070            Q::SealNativeExecStats {
1071                ticket,
1072                runtime_digest,
1073                runtime,
1074            } => R::SealNativeExecStats(self.native_exec_transition(
1075                owner,
1076                mm,
1077                produced,
1078                ControlCut::EmptyPrepared,
1079                Some(ticket),
1080                |registry, _| registry.seal(owner, ticket, mm, runtime_digest, &runtime),
1081            )),
1082            Q::IssueNativeExec {
1083                ticket,
1084                state_digest,
1085            } => R::IssueNativeExec(self.native_exec_transition(
1086                owner,
1087                mm,
1088                produced,
1089                ControlCut::EmptyPrepared,
1090                Some(ticket),
1091                |registry, _| registry.issue(owner, ticket, mm, state_digest),
1092            )),
1093            Q::CancelNativeExec { ticket } => R::CancelNativeExec(self.native_exec_transition(
1094                owner,
1095                mm,
1096                produced,
1097                ControlCut::Prepared,
1098                Some(ticket),
1099                |registry, _| registry.cancel(owner, ticket, mm),
1100            )),
1101            Q::CompleteNativeExec { ticket } => R::CompleteNativeExec(self.native_exec_transition(
1102                owner,
1103                mm,
1104                produced,
1105                ControlCut::Completed,
1106                Some(ticket),
1107                |registry, _| registry.complete(owner, ticket, mm),
1108            )),
1109            _ => unreachable!("only native leaves are dispatched on the neutral path"),
1110        };
1111        (None, response)
1112    }
1113}
1114
1115#[cfg(test)]
1116mod control_tests {
1117    use std::os::fd::AsRawFd;
1118    use std::os::fd::FromRawFd;
1119    use std::os::fd::OwnedFd;
1120    use std::sync::Arc;
1121    use std::sync::Mutex;
1122    use std::time::Duration;
1123
1124    use reverie::GlobalTool;
1125    use reverie::Tid;
1126    use sha2::Sha256;
1127
1128    use super::*;
1129    use crate::config::Config;
1130    use crate::ivar::Ivar;
1131    use crate::scheduler::DEFAULT_PRIORITY;
1132    use crate::scheduler::ThreadNextTurn;
1133    use crate::tool_global::GlobalRequest as Q;
1134    use crate::tool_global::GlobalResponse as R;
1135    use crate::tool_global::GlobalState;
1136    use crate::tool_global::PendingExecState;
1137    use crate::types::DetTime;
1138    use crate::types::LogicalTime;
1139
1140    // Protocol-only trusted proof mock. These tests prove coordinator guards,
1141    // actual socket counting and neutral dispatch; they do not prove watched
1142    // birth admission, immutable target maps, runtime qualification or exec.
1143    #[derive(Debug)]
1144    struct ProtocolProof {
1145        owner: DetTid,
1146        physical: Option<(i32, i32)>,
1147    }
1148
1149    // SAFETY: test-only model with no production installer. All qualification
1150    // answers are deliberately modelled; actual source records below are real.
1151    unsafe impl NativeExecProof for ProtocolProof {
1152        fn owner(&self) -> DetTid {
1153            self.owner
1154        }
1155        fn physical_ids(&self) -> Option<(i32, i32)> {
1156            self.physical
1157        }
1158        fn is_live(&self) -> bool {
1159            true
1160        }
1161        fn matches_peer(&self, _: &OwnedFd) -> bool {
1162            true
1163        }
1164        fn qualify_target(&self, request: &PrepareNativeExec) -> bool {
1165            request.target_digest == [2; 32]
1166                && request.bootstrap_digest == [3; 32]
1167                && request.sockets.detlog.object() == (1, 2)
1168                && request.sockets.observer.object() == (1, 3)
1169        }
1170        fn qualify_runtime(&self, bytes: &[u8]) -> bool {
1171            bytes == b"protocol-runtime"
1172        }
1173        fn qualify_completion(&self, _: ExecTicket, _: NativeExecBinding) -> bool {
1174            // Explicit registry model only; not watched native execution proof.
1175            true
1176        }
1177    }
1178
1179    fn registry(owner: DetTid, physical: Option<(i32, i32)>) -> NativeExecRegistry {
1180        NativeExecRegistry::new(Arc::new(ProtocolProof { owner, physical }), [7; 32]).unwrap()
1181    }
1182
1183    fn fixture(install: bool, physical: Option<(i32, i32)>) -> (GlobalState, DetTid, MmId, MmId) {
1184        let cfg = Config {
1185            sequentialize_threads: true,
1186            ..Config::default()
1187        };
1188        let state = GlobalState::initialize(&cfg, false);
1189        let owner = DetTid::from_raw(41);
1190        let old_mm = MmId::initial(owner).for_exec(owner);
1191        {
1192            let mut sched = state.sched.lock().unwrap();
1193            sched.thread_tree.add_child(owner, owner, true);
1194            sched.next_turns.insert(
1195                owner,
1196                ThreadNextTurn {
1197                    dettid: owner,
1198                    child_tid_addr: 0,
1199                    req: Ivar::new(),
1200                    resp: Ivar::new(),
1201                    protocol: Default::default(),
1202                },
1203            );
1204            sched.priorities.insert(owner, DEFAULT_PRIORITY);
1205            sched.record_thread_mm(owner, old_mm);
1206            sched.install_test_exec_incarnation(owner, old_mm);
1207            sched.set_running_for_test(owner);
1208            assert!(sched.prepare_exec_teardown(owner, owner, old_mm));
1209        }
1210        state.pending_exec_states.lock().unwrap().insert(
1211            owner,
1212            PendingExecState {
1213                caller: owner,
1214                process: owner,
1215                mm: old_mm,
1216                fd_blocking: Default::default(),
1217            },
1218        );
1219        let mut clock = DetTime::new(&cfg);
1220        clock.add_syscall_with_cost(1337);
1221        state.global_time.lock().unwrap().update_global_time(
1222            owner,
1223            clock.as_nanos(),
1224            clock.inherited_nanos(),
1225        );
1226        if install {
1227            state
1228                .install_native_exec_registry(registry(owner, physical))
1229                .unwrap();
1230        }
1231        (state, owner, old_mm, old_mm.for_exec(owner))
1232    }
1233
1234    fn prepare(state: &GlobalState, mm: MmId) -> Q {
1235        let bytes = b"protocol-tool".to_vec();
1236        let owner = DetTid::from_raw(41);
1237        let old_mm = state.pending_exec_states.lock().unwrap()[&owner].mm;
1238        let config_digest = crate::native_exec::native_exec_config_digest(&state.cfg).unwrap();
1239        let physical_tid = state
1240            .native_exec_registry()
1241            .and_then(|mutex| mutex.lock().unwrap().proof.physical_ids())
1242            .map_or(41, |ids| ids.1);
1243        Q::PrepareNativeExec(PrepareNativeExec {
1244            sockets: NativeExecSocketBindings {
1245                detlog: NativeExecSocketBinding {
1246                    fd: 1025,
1247                    device: 1,
1248                    inode: 2,
1249                },
1250                observer: NativeExecSocketBinding {
1251                    fd: 1026,
1252                    device: 1,
1253                    inode: 3,
1254                },
1255            },
1256            cut: NativeExecNeutralBinding {
1257                dettid: owner,
1258                detpid: owner,
1259                physical_tid,
1260                old_mm,
1261                old_files: FilesId::initial(owner),
1262                prepared_mm: mm,
1263                config_digest,
1264            },
1265            prepared_mm: mm,
1266            config_digest: crate::native_exec::native_exec_config_digest(&state.cfg).unwrap(),
1267            target_digest: [2; 32],
1268            bootstrap_digest: [3; 32],
1269            resume_mask: 0x42,
1270            snapshot_digest: Sha256::digest(&bytes).into(),
1271            snapshot: bytes,
1272        })
1273    }
1274
1275    fn counted(count: u64, request: Q) -> Q {
1276        Q::WithForwardedCount(count, Box::new(request))
1277    }
1278
1279    fn encoded<T: serde::Serialize>(value: &T) -> Vec<u8> {
1280        bincode::serde::encode_to_vec(value, bincode::config::legacy()).unwrap()
1281    }
1282
1283    async fn neutral(state: &GlobalState, owner: DetTid, mm: MmId, request: Q) -> R {
1284        // Preserve a real stored deadline hint. A native response cannot consume
1285        // it, recompute it or wrap the result with WithDeadline.
1286        let now = state.global_time.lock().unwrap().as_nanos();
1287        {
1288            let mut sched = state.sched.lock().unwrap();
1289            sched.register_alarm(
1290                owner,
1291                owner,
1292                now,
1293                LogicalTime::from_nanos(5000),
1294                LogicalTime::ZERO,
1295                nix::sys::signal::Signal::SIGALRM,
1296            );
1297            sched.note_arming_hint(owner, now);
1298        }
1299        let global_before = encoded(&*state.global_time.lock().unwrap());
1300        let sched_before = format!("{:?}", *state.sched.lock().unwrap());
1301        let pending_before = state.pending_exec_states.lock().unwrap().clone();
1302        let mut guest_time = DetTime::new(&state.cfg);
1303        guest_time.add_syscall_with_cost(987654);
1304        let (time, response) = state
1305            .receive_rpc(
1306                Tid::from_raw(owner.as_raw()),
1307                (guest_time.clone(), mm, request),
1308            )
1309            .await;
1310        assert_eq!(time, None);
1311        assert!(!matches!(response, R::WithDeadline(..)));
1312        assert_eq!(
1313            encoded(&*state.global_time.lock().unwrap()),
1314            global_before,
1315            "complete GlobalTime remains unchanged"
1316        );
1317        assert_eq!(
1318            format!("{:?}", *state.sched.lock().unwrap()),
1319            sched_before,
1320            "complete scheduler debug state, including PRNGs, grants and hint, is unchanged"
1321        );
1322        assert_eq!(*state.pending_exec_states.lock().unwrap(), pending_before);
1323        assert_eq!(
1324            state.sched.lock().unwrap().take_deadline_hint(owner),
1325            Some(Duration::from_nanos(5000))
1326        );
1327        response
1328    }
1329
1330    struct Source {
1331        send: OwnedFd,
1332        written: Arc<Mutex<Vec<Vec<u8>>>>,
1333    }
1334
1335    impl Source {
1336        fn new() -> Self {
1337            let mut fds = [-1; 2];
1338            // SAFETY: writable pair storage; success creates two owned sockets.
1339            assert_eq!(
1340                unsafe {
1341                    libc::socketpair(
1342                        libc::AF_UNIX,
1343                        libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
1344                        0,
1345                        fds.as_mut_ptr(),
1346                    )
1347                },
1348                0
1349            );
1350            let receive = unsafe { OwnedFd::from_raw_fd(fds[0]) };
1351            let send = unsafe { OwnedFd::from_raw_fd(fds[1]) };
1352            let written = Arc::new(Mutex::new(Vec::new()));
1353            let sink = Arc::clone(&written);
1354            crate::detlog::set_forwarded_source(
1355                receive,
1356                Box::new(move |bytes| sink.lock().unwrap().push(bytes.to_vec())),
1357            );
1358            Self { send, written }
1359        }
1360        fn send(&self, physical: i32, index: u64, bytes: &[u8]) {
1361            let record = crate::detlog::tagged_forwarded_message(physical, index, bytes);
1362            // SAFETY: owned socket and valid initialized message storage.
1363            assert_eq!(
1364                unsafe {
1365                    libc::send(
1366                        self.send.as_raw_fd(),
1367                        record.as_ptr().cast(),
1368                        record.len(),
1369                        libc::MSG_NOSIGNAL,
1370                    )
1371                },
1372                record.len() as isize
1373            );
1374        }
1375    }
1376
1377    // The forwarding source and determinism-loss latch are process-global.
1378    // A child libtest process avoids racing unrelated tests without clearing a
1379    // real retained loss or changing the completeness checks to admit this test.
1380    fn isolated(name: &str, body: impl FnOnce()) {
1381        const KEY: &str = "HERMIT_NATIVE_CONTROL_PROTOCOL_CHILD";
1382        if std::env::var(KEY).as_deref() == Ok(name) {
1383            body();
1384            return;
1385        }
1386        let full_name = format!("tool_global::native_exec::control_tests::{name}");
1387        let output = std::process::Command::new(std::env::current_exe().unwrap())
1388            .args(["--exact", &full_name, "--nocapture", "--test-threads=1"])
1389            .env(KEY, name)
1390            .output()
1391            .unwrap();
1392        assert!(
1393            output.status.success(),
1394            "{full_name}: {:?}\n{}\n{}",
1395            output.status,
1396            String::from_utf8_lossy(&output.stdout),
1397            String::from_utf8_lossy(&output.stderr)
1398        );
1399        let stdout = String::from_utf8_lossy(&output.stdout);
1400        assert!(
1401            stdout.contains("running 1 test") && stdout.contains("1 passed; 0 failed"),
1402            "the exact child test must actually execute: {stdout}"
1403        );
1404    }
1405
1406    #[test]
1407    fn native_exec_coordinator_robust_wakes_refuse_without_consuming_preparation() {
1408        isolated(
1409            "native_exec_coordinator_robust_wakes_refuse_without_consuming_preparation",
1410            || {
1411                tokio::runtime::Builder::new_current_thread()
1412                    .enable_all()
1413                    .build()
1414                    .unwrap()
1415                    .block_on(async {
1416                        // Real healthy forwarding source and actual process ID;
1417                        // authority remains the existing explicit registry model.
1418                        let _source = Source::new();
1419                        let physical = i32::try_from(std::process::id()).unwrap();
1420                        let (state, owner, old_mm, mm) = fixture(true, Some((physical, physical)));
1421                        let clean_request = prepare(&state, mm);
1422                        let clean_ticket =
1423                            match neutral(&state, owner, mm, counted(0, clean_request)).await {
1424                                R::PrepareNativeExec(Ok(ticket)) => ticket,
1425                                response => panic!("clean preparation must qualify: {response:?}"),
1426                            };
1427                        assert_eq!(
1428                            neutral(
1429                                &state,
1430                                owner,
1431                                mm,
1432                                counted(
1433                                    0,
1434                                    Q::CancelNativeExec {
1435                                        ticket: clean_ticket
1436                                    }
1437                                ),
1438                            )
1439                            .await,
1440                            R::CancelNativeExec(Ok(()))
1441                        );
1442                        state
1443                            .pending_exec_states
1444                            .lock()
1445                            .unwrap()
1446                            .get_mut(&owner)
1447                            .unwrap()
1448                            .fd_blocking
1449                            .robust_wakes
1450                            .push((owner, crate::types::FutexID::private(old_mm, 0x456000)));
1451                        let request = prepare(&state, mm);
1452                        assert_eq!(
1453                            neutral(&state, owner, mm, counted(0, request)).await,
1454                            R::PrepareNativeExec(Err(ExecControlError::Preparation))
1455                        );
1456                        assert_eq!(
1457                            state.pending_exec_states.lock().unwrap()[&owner]
1458                                .fd_blocking
1459                                .robust_wakes,
1460                            vec![(owner, crate::types::FutexID::private(old_mm, 0x456000))]
1461                        );
1462                    });
1463            },
1464        );
1465    }
1466
1467    /// A native exec is prepared only while its owner is its process's only
1468    /// live thread. A live thread of another process whose ID the thread
1469    /// tree still lists under the owner (the owner's former thread 43, now
1470    /// process 44's) is not the owner's, so preparation qualifies. A live
1471    /// thread of the owner's process that the tree's walk misses (46, whose
1472    /// creating thread's ID 45 now leads another process) is, so preparation
1473    /// is refused.
1474    #[test]
1475    fn native_exec_preparation_counts_only_the_owners_own_live_threads() {
1476        isolated(
1477            "native_exec_preparation_counts_only_the_owners_own_live_threads",
1478            || {
1479                tokio::runtime::Builder::new_current_thread()
1480                    .enable_all()
1481                    .build()
1482                    .unwrap()
1483                    .block_on(async {
1484                        let _source = Source::new();
1485                        let physical = i32::try_from(std::process::id()).unwrap();
1486                        let (state, owner, _, mm) = fixture(true, Some((physical, physical)));
1487                        let register = |thread: DetTid| {
1488                            let mut sched = state.sched.lock().unwrap();
1489                            sched.next_turns.insert(
1490                                thread,
1491                                ThreadNextTurn {
1492                                    dettid: thread,
1493                                    child_tid_addr: 0,
1494                                    req: Ivar::new(),
1495                                    resp: Ivar::new(),
1496                                    protocol: Default::default(),
1497                                },
1498                            );
1499                            sched.priorities.insert(thread, DEFAULT_PRIORITY);
1500                        };
1501                        let tid = DetTid::from_raw;
1502                        // Processes 44 and 45 are CLONE_PARENT siblings of the
1503                        // root owner, so neither is a child it must reap first.
1504                        {
1505                            let mut sched = state.sched.lock().unwrap();
1506                            let tree = &mut sched.thread_tree;
1507                            tree.add_child(owner, tid(43), false);
1508                            tree.add_child_with_wait_metadata(
1509                                owner,
1510                                tid(44),
1511                                true,
1512                                true,
1513                                libc::SIGCHLD,
1514                            );
1515                            tree.add_child(tid(44), tid(43), false);
1516                        }
1517                        register(tid(43));
1518                        let ticket =
1519                            match neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await
1520                            {
1521                                R::PrepareNativeExec(Ok(ticket)) => ticket,
1522                                response => panic!(
1523                                    "another process's reused thread must not refuse: {response:?}"
1524                                ),
1525                            };
1526                        assert_eq!(
1527                            neutral(
1528                                &state,
1529                                owner,
1530                                mm,
1531                                counted(0, Q::CancelNativeExec { ticket }),
1532                            )
1533                            .await,
1534                            R::CancelNativeExec(Ok(()))
1535                        );
1536                        {
1537                            let mut sched = state.sched.lock().unwrap();
1538                            let tree = &mut sched.thread_tree;
1539                            tree.add_child(owner, tid(45), false);
1540                            tree.add_child(tid(45), tid(46), false);
1541                            tree.add_child_with_wait_metadata(
1542                                owner,
1543                                tid(45),
1544                                true,
1545                                true,
1546                                libc::SIGCHLD,
1547                            );
1548                        }
1549                        register(tid(46));
1550                        assert_eq!(
1551                            neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await,
1552                            R::PrepareNativeExec(Err(ExecControlError::Preparation))
1553                        );
1554                    });
1555            },
1556        );
1557    }
1558
1559    #[test]
1560    fn native_exec_control_absent_and_envelope_refusals_are_neutral() {
1561        isolated(
1562            "native_exec_control_absent_and_envelope_refusals_are_neutral",
1563            || {
1564                tokio::runtime::Builder::new_current_thread()
1565                    .enable_all()
1566                    .build()
1567                    .unwrap()
1568                    .block_on(async {
1569                        let (state, owner, _, mm) = fixture(false, None);
1570                        let ticket = ExecTicket([8; 32]);
1571                        let requests = [
1572                            (
1573                                prepare(&state, mm),
1574                                R::PrepareNativeExec(Err(ExecControlError::Unavailable)),
1575                            ),
1576                            (
1577                                Q::SealNativeExecStats {
1578                                    ticket,
1579                                    runtime_digest: [0; 32],
1580                                    runtime: vec![1],
1581                                },
1582                                R::SealNativeExecStats(Err(ExecControlError::Unavailable)),
1583                            ),
1584                            (
1585                                Q::IssueNativeExec {
1586                                    ticket,
1587                                    state_digest: [0; 32],
1588                                },
1589                                R::IssueNativeExec(Err(ExecControlError::Unavailable)),
1590                            ),
1591                            (
1592                                Q::CancelNativeExec { ticket },
1593                                R::CancelNativeExec(Err(ExecControlError::Unavailable)),
1594                            ),
1595                            (
1596                                Q::CompleteNativeExec { ticket },
1597                                R::CompleteNativeExec(Err(ExecControlError::Unavailable)),
1598                            ),
1599                        ];
1600                        for (request, expected) in requests {
1601                            assert_eq!(
1602                                neutral(&state, owner, mm, counted(0, request)).await,
1603                                expected
1604                            );
1605                        }
1606                        assert_eq!(
1607                            neutral(&state, owner, mm, prepare(&state, mm)).await,
1608                            R::PrepareNativeExec(Err(ExecControlError::Forwarding))
1609                        );
1610                        assert_eq!(
1611                            neutral(
1612                                &state,
1613                                owner,
1614                                mm,
1615                                counted(0, counted(0, prepare(&state, mm)))
1616                            )
1617                            .await,
1618                            R::PrepareNativeExec(Err(ExecControlError::Forwarding))
1619                        );
1620                        // Direct dispatch has the same interception before ordinary drain.
1621                        let before = format!("{:?}", *state.sched.lock().unwrap());
1622                        let response = state
1623                            .dispatch_rpc(
1624                                Tid::from_raw(owner.as_raw()),
1625                                (DetTime::new(&state.cfg), mm, prepare(&state, mm)),
1626                            )
1627                            .await;
1628                        assert_eq!(
1629                            response,
1630                            (
1631                                None,
1632                                R::PrepareNativeExec(Err(ExecControlError::Forwarding))
1633                            )
1634                        );
1635                        assert_eq!(format!("{:?}", *state.sched.lock().unwrap()), before);
1636                    });
1637            },
1638        );
1639    }
1640
1641    #[test]
1642    fn native_exec_control_protocol_counts_real_source_and_completes_after_lifecycle() {
1643        isolated(
1644            "native_exec_control_protocol_counts_real_source_and_completes_after_lifecycle",
1645            || {
1646                tokio::runtime::Builder::new_current_thread()
1647                    .enable_all()
1648                    .build()
1649                    .unwrap()
1650                    .block_on(async {
1651                        // The model proof supplies this test process's actual physical
1652                        // main PID/TID, independent of deterministic owner 41.
1653                        let physical = unsafe { libc::getpid() };
1654                        let source = Source::new();
1655                        let (state, owner, old, mm) = fixture(true, Some((physical, physical)));
1656                        // Model the already-owned serial grant's genuine log
1657                        // order using the existing commit API. Counts still
1658                        // come only from the real socket records below.
1659                        crate::detlog::write_forwarded_for(physical, true);
1660                        assert_eq!(
1661                            state.install_native_exec_registry(registry(
1662                                owner,
1663                                Some((physical, physical))
1664                            )),
1665                            Err(ExecControlError::Transition)
1666                        );
1667                        source.send(physical, 1, b"before-cut\n");
1668                        let ticket =
1669                            match neutral(&state, owner, mm, counted(1, prepare(&state, mm))).await
1670                            {
1671                                R::PrepareNativeExec(Ok(ticket)) => ticket,
1672                                response => panic!("prepare: {response:?}"),
1673                            };
1674                        assert_eq!(
1675                            *source.written.lock().unwrap(),
1676                            vec![b"before-cut\n".to_vec()]
1677                        );
1678                        let runtime = b"protocol-runtime".to_vec();
1679                        let digest = match neutral(
1680                            &state,
1681                            owner,
1682                            mm,
1683                            counted(
1684                                0,
1685                                Q::SealNativeExecStats {
1686                                    ticket,
1687                                    runtime_digest: Sha256::digest(&runtime).into(),
1688                                    runtime,
1689                                },
1690                            ),
1691                        )
1692                        .await
1693                        {
1694                            R::SealNativeExecStats(Ok(digest)) => digest,
1695                            response => panic!("seal: {response:?}"),
1696                        };
1697                        // Protocol fixture models host transfer only. The
1698                        // real neutral RPC cut and all assertions remain intact.
1699                        {
1700                            let provision_peer: OwnedFd =
1701                                std::fs::File::open("/dev/null").unwrap().into();
1702                            let mut provision_registry =
1703                                state.native_exec_registry().unwrap().lock().unwrap();
1704                            // SAFETY: fixture qualification is model authority.
1705                            unsafe {
1706                                provision_registry
1707                                    .begin_provision(&provision_peer, ticket)
1708                                    .unwrap()
1709                                    .complete()
1710                            }
1711                            .unwrap();
1712                        }
1713                        assert_eq!(
1714                            neutral(
1715                                &state,
1716                                owner,
1717                                mm,
1718                                counted(
1719                                    0,
1720                                    Q::IssueNativeExec {
1721                                        ticket,
1722                                        state_digest: digest
1723                                    }
1724                                )
1725                            )
1726                            .await,
1727                            R::IssueNativeExec(Ok(()))
1728                        );
1729                        assert_eq!(
1730                            neutral(
1731                                &state,
1732                                owner,
1733                                mm,
1734                                counted(0, Q::CompleteNativeExec { ticket })
1735                            )
1736                            .await,
1737                            R::CompleteNativeExec(Err(ExecControlError::Preparation))
1738                        );
1739                        // Model redemption only: this does not assert any Linux exec or
1740                        // native birth/mapping/runtime qualification. The ordinary
1741                        // MarkPastFirstExecve RPC below supplies real lifecycle state.
1742                        let peer = std::fs::File::open("/dev/null").unwrap().into();
1743                        let bytes = state
1744                            .native_exec_registry()
1745                            .unwrap()
1746                            .lock()
1747                            .unwrap()
1748                            .redeem(
1749                                &peer,
1750                                ticket,
1751                                crate::native_exec::native_exec_config_digest(&state.cfg).unwrap(),
1752                                digest,
1753                            )
1754                            .unwrap();
1755                        assert_eq!(<[u8; 32]>::from(Sha256::digest(&bytes)), digest);
1756                        {
1757                            let mut registry =
1758                                state.native_exec_registry().unwrap().lock().unwrap();
1759                            registry
1760                                .mark_past_first_exec(DetTid::from_raw(99), mm)
1761                                .unwrap();
1762                            assert!(
1763                                !registry
1764                                    .transaction(owner, ticket, mm)
1765                                    .unwrap()
1766                                    .marked_past_first_exec
1767                            );
1768                            assert_eq!(
1769                                registry.mark_past_first_exec(owner, old),
1770                                Err(ExecControlError::Transition)
1771                            );
1772                            assert!(
1773                                !registry
1774                                    .transaction(owner, ticket, mm)
1775                                    .unwrap()
1776                                    .marked_past_first_exec
1777                            );
1778                        }
1779                        // Alternate registration and pending-map consumption
1780                        // install the right current mm but cannot replace the
1781                        // actual MarkPastFirstExecve receipt for this ticket.
1782                        let pending = state
1783                            .pending_exec_states
1784                            .lock()
1785                            .unwrap()
1786                            .remove(&owner)
1787                            .unwrap();
1788                        {
1789                            let mut sched = state.sched.lock().unwrap();
1790                            sched.reconnect_after_exec(crate::scheduler::ExecReconnect {
1791                                caller: owner,
1792                                new_leader: owner,
1793                                detpid: owner,
1794                                pre_exec_mm: old,
1795                                post_exec_mm: mm,
1796                                child_tid_addr: 0,
1797                                reconnect_priority: None,
1798                            });
1799                        }
1800                        state
1801                            .past_first_execve
1802                            .store(true, std::sync::atomic::Ordering::SeqCst);
1803                        assert_eq!(
1804                            neutral(
1805                                &state,
1806                                owner,
1807                                mm,
1808                                counted(0, Q::CompleteNativeExec { ticket })
1809                            )
1810                            .await,
1811                            R::CompleteNativeExec(Err(ExecControlError::Preparation))
1812                        );
1813                        assert!(
1814                            !state
1815                                .native_exec_registry()
1816                                .unwrap()
1817                                .lock()
1818                                .unwrap()
1819                                .transaction(owner, ticket, mm)
1820                                .unwrap()
1821                                .marked_past_first_exec
1822                        );
1823                        // Restore the old-image protocol fixture, then exercise
1824                        // the real ordinary Mark RPC rather than its registry hook.
1825                        state
1826                            .pending_exec_states
1827                            .lock()
1828                            .unwrap()
1829                            .insert(owner, pending);
1830                        {
1831                            let mut sched = state.sched.lock().unwrap();
1832                            sched.record_thread_mm(owner, old);
1833                            sched.install_test_exec_incarnation(owner, old);
1834                        }
1835                        state
1836                            .past_first_execve
1837                            .store(false, std::sync::atomic::Ordering::SeqCst);
1838                        let mut mark_time = DetTime::new(&state.cfg);
1839                        mark_time.add_syscall_with_cost(987654);
1840                        let mark = state
1841                            .receive_rpc(
1842                                Tid::from_raw(owner.as_raw()),
1843                                (
1844                                    mark_time,
1845                                    mm,
1846                                    Q::MarkPastFirstExecve(owner, None, None, None),
1847                                ),
1848                            )
1849                            .await;
1850                        let mark_response = match mark.1 {
1851                            R::WithDeadline(_, response) => *response,
1852                            response => response,
1853                        };
1854                        assert_eq!(mark_response, R::MarkPastFirstExecve(Default::default()));
1855                        assert_eq!(state.sched.lock().unwrap().thread_mm(owner), Some(mm));
1856                        assert!(state.pending_exec_states.lock().unwrap().is_empty());
1857                        assert!(
1858                            state
1859                                .past_first_execve
1860                                .load(std::sync::atomic::Ordering::SeqCst)
1861                        );
1862                        assert_ne!(mm, old);
1863                        assert_eq!(
1864                            neutral(
1865                                &state,
1866                                owner,
1867                                old,
1868                                counted(0, Q::CompleteNativeExec { ticket })
1869                            )
1870                            .await,
1871                            R::CompleteNativeExec(Err(ExecControlError::Preparation))
1872                        );
1873                        assert_eq!(
1874                            neutral(
1875                                &state,
1876                                owner,
1877                                mm,
1878                                counted(0, Q::CompleteNativeExec { ticket })
1879                            )
1880                            .await,
1881                            R::CompleteNativeExec(Ok(()))
1882                        );
1883                        assert_eq!(
1884                            state
1885                                .native_exec_registry()
1886                                .unwrap()
1887                                .lock()
1888                                .unwrap()
1889                                .stage(ticket),
1890                            Some(ExecStage::Completed)
1891                        );
1892                        assert_eq!(
1893                            neutral(
1894                                &state,
1895                                owner,
1896                                mm,
1897                                counted(0, Q::CompleteNativeExec { ticket })
1898                            )
1899                            .await,
1900                            R::CompleteNativeExec(Err(ExecControlError::Transition))
1901                        );
1902                        crate::detlog::clear_forwarded_source();
1903                    });
1904            },
1905        );
1906    }
1907
1908    #[test]
1909    fn native_exec_control_completed_cut_counts_real_post_exec_record() {
1910        isolated(
1911            "native_exec_control_completed_cut_counts_real_post_exec_record",
1912            || {
1913                tokio::runtime::Builder::new_current_thread()
1914                    .enable_all()
1915                    .build()
1916                    .unwrap()
1917                    .block_on(async {
1918                        // Existing protocol model, not a watched kernel exec.
1919                        // The source, interval, ordinary Mark and neutral host
1920                        // Complete dispatch below are actual production paths.
1921                        let physical = unsafe { libc::getpid() };
1922                        let source = Source::new();
1923                        let (state, owner, _, mm) = fixture(true, Some((physical, physical)));
1924                        crate::detlog::write_forwarded_for(physical, true);
1925                        let ticket =
1926                            match neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await
1927                            {
1928                                R::PrepareNativeExec(Ok(ticket)) => ticket,
1929                                response => panic!("prepare: {response:?}"),
1930                            };
1931                        let runtime = b"protocol-runtime".to_vec();
1932                        let digest = match neutral(
1933                            &state,
1934                            owner,
1935                            mm,
1936                            counted(
1937                                0,
1938                                Q::SealNativeExecStats {
1939                                    ticket,
1940                                    runtime_digest: Sha256::digest(&runtime).into(),
1941                                    runtime,
1942                                },
1943                            ),
1944                        )
1945                        .await
1946                        {
1947                            R::SealNativeExecStats(Ok(digest)) => digest,
1948                            response => panic!("seal: {response:?}"),
1949                        };
1950                        {
1951                            let peer = std::fs::File::open("/dev/null").unwrap().into();
1952                            let mut registry =
1953                                state.native_exec_registry().unwrap().lock().unwrap();
1954                            // SAFETY: existing test-only provider qualification model.
1955                            unsafe { registry.begin_provision(&peer, ticket).unwrap().complete() }
1956                                .unwrap();
1957                        }
1958                        assert_eq!(
1959                            neutral(
1960                                &state,
1961                                owner,
1962                                mm,
1963                                counted(
1964                                    0,
1965                                    Q::IssueNativeExec {
1966                                        ticket,
1967                                        state_digest: digest
1968                                    }
1969                                )
1970                            )
1971                            .await,
1972                            R::IssueNativeExec(Ok(()))
1973                        );
1974                        let peer = std::fs::File::open("/dev/null").unwrap().into();
1975                        state
1976                            .native_exec_registry()
1977                            .unwrap()
1978                            .lock()
1979                            .unwrap()
1980                            .redeem(
1981                                &peer,
1982                                ticket,
1983                                crate::native_exec::native_exec_config_digest(&state.cfg).unwrap(),
1984                                digest,
1985                            )
1986                            .unwrap();
1987                        // Match the existing protocol fixture's ordinary Mark
1988                        // clock, after its retained initial time of 1337 ns.
1989                        let mut mark_time = DetTime::new(&state.cfg);
1990                        mark_time.add_syscall_with_cost(987654);
1991                        let mark = state
1992                            .receive_rpc(
1993                                Tid::from_raw(owner.as_raw()),
1994                                (
1995                                    mark_time,
1996                                    mm,
1997                                    Q::MarkPastFirstExecve(owner, None, None, None),
1998                                ),
1999                            )
2000                            .await;
2001                        let response = match mark.1 {
2002                            R::WithDeadline(_, response) => *response,
2003                            response => response,
2004                        };
2005                        assert_eq!(response, R::MarkPastFirstExecve(Default::default()));
2006                        assert!(state.pending_exec_states.lock().unwrap().is_empty());
2007                        assert_eq!(
2008                            state
2009                                .native_exec_registry()
2010                                .unwrap()
2011                                .lock()
2012                                .unwrap()
2013                                .stage(ticket),
2014                            Some(ExecStage::Redeemed)
2015                        );
2016                        source.send(physical, 1, b"successor-post-exec\n");
2017                        assert!(source.written.lock().unwrap().is_empty());
2018                        // Unlike Seal/Issue, Completed owns the actual nonzero
2019                        // interval. neutral() also checks full scheduler/clock
2020                        // equality and retained deadline/pending-map invariants.
2021                        assert_eq!(
2022                            neutral(
2023                                &state,
2024                                owner,
2025                                mm,
2026                                counted(1, Q::CompleteNativeExec { ticket })
2027                            )
2028                            .await,
2029                            R::CompleteNativeExec(Ok(()))
2030                        );
2031                        assert_eq!(
2032                            *source.written.lock().unwrap(),
2033                            vec![b"successor-post-exec\n".to_vec()]
2034                        );
2035                        assert_eq!(
2036                            state
2037                                .native_exec_registry()
2038                                .unwrap()
2039                                .lock()
2040                                .unwrap()
2041                                .stage(ticket),
2042                            Some(ExecStage::Completed)
2043                        );
2044                        assert_eq!(
2045                            neutral(
2046                                &state,
2047                                owner,
2048                                mm,
2049                                counted(0, Q::CompleteNativeExec { ticket })
2050                            )
2051                            .await,
2052                            R::CompleteNativeExec(Err(ExecControlError::Transition))
2053                        );
2054                        assert_eq!(
2055                            *source.written.lock().unwrap(),
2056                            vec![b"successor-post-exec\n".to_vec()]
2057                        );
2058                        crate::detlog::clear_forwarded_source();
2059                    });
2060            },
2061        );
2062    }
2063
2064    #[test]
2065    fn native_exec_control_protocol_refuses_wrong_context_late_records_and_source_change() {
2066        isolated(
2067            "native_exec_control_protocol_refuses_wrong_context_late_records_and_source_change",
2068            || {
2069                tokio::runtime::Builder::new_current_thread()
2070                    .enable_all()
2071                    .build()
2072                    .unwrap()
2073                    .block_on(async {
2074                        let physical = unsafe { libc::getpid() };
2075                        let source = Source::new();
2076                        let (missing, owner, _, mm) = fixture(true, None);
2077                        assert_eq!(
2078                            neutral(&missing, owner, mm, counted(0, prepare(&missing, mm))).await,
2079                            R::PrepareNativeExec(Err(ExecControlError::Identity))
2080                        );
2081                        let (wrong_proof, owner, _, mm) = fixture(false, None);
2082                        wrong_proof
2083                            .install_native_exec_registry(registry(
2084                                DetTid::from_raw(99),
2085                                Some((physical, physical)),
2086                            ))
2087                            .unwrap();
2088                        assert_eq!(
2089                            neutral(
2090                                &wrong_proof,
2091                                owner,
2092                                mm,
2093                                counted(0, prepare(&wrong_proof, mm))
2094                            )
2095                            .await,
2096                            R::PrepareNativeExec(Err(ExecControlError::Identity))
2097                        );
2098                        let (state, owner, old, mm) = fixture(true, Some((physical, physical)));
2099                        assert_eq!(
2100                            neutral(&state, owner, old, counted(0, prepare(&state, mm))).await,
2101                            R::PrepareNativeExec(Err(ExecControlError::Identity))
2102                        );
2103                        let missing_preparation_request = prepare(&state, mm);
2104                        let prepared = state
2105                            .pending_exec_states
2106                            .lock()
2107                            .unwrap()
2108                            .remove(&owner)
2109                            .unwrap();
2110                        assert_eq!(
2111                            neutral(&state, owner, mm, counted(0, missing_preparation_request))
2112                                .await,
2113                            R::PrepareNativeExec(Err(ExecControlError::Preparation))
2114                        );
2115                        state
2116                            .pending_exec_states
2117                            .lock()
2118                            .unwrap()
2119                            .insert(owner, prepared.clone());
2120                        state
2121                            .pending_exec_states
2122                            .lock()
2123                            .unwrap()
2124                            .get_mut(&owner)
2125                            .unwrap()
2126                            .caller = DetTid::from_raw(99);
2127                        assert_eq!(
2128                            neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await,
2129                            R::PrepareNativeExec(Err(ExecControlError::Identity))
2130                        );
2131                        state
2132                            .pending_exec_states
2133                            .lock()
2134                            .unwrap()
2135                            .insert(owner, prepared);
2136                        let child = DetTid::from_raw(42);
2137                        state
2138                            .sched
2139                            .lock()
2140                            .unwrap()
2141                            .thread_tree
2142                            .add_child(owner, child, true);
2143                        assert_eq!(
2144                            neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await,
2145                            R::PrepareNativeExec(Err(ExecControlError::Preparation))
2146                        );
2147                        assert_eq!(
2148                            state
2149                                .sched
2150                                .lock()
2151                                .unwrap()
2152                                .thread_tree
2153                                .unreaped_children_of(owner),
2154                            vec![child],
2155                            "an exited/unregistered but unreaped child still refuses"
2156                        );
2157
2158                        let (siblings, owner, _, mm) = fixture(true, Some((physical, physical)));
2159                        {
2160                            let mut sched = siblings.sched.lock().unwrap();
2161                            sched.thread_tree.add_child(owner, child, false);
2162                            sched.next_turns.insert(
2163                                child,
2164                                ThreadNextTurn {
2165                                    dettid: child,
2166                                    child_tid_addr: 0,
2167                                    req: Ivar::new(),
2168                                    resp: Ivar::new(),
2169                                    protocol: Default::default(),
2170                                },
2171                            );
2172                            assert!(sched.holds_serial_grant(owner));
2173                            assert!(sched.thread_tree.unreaped_children_of(owner).is_empty());
2174                        }
2175                        assert_eq!(
2176                            neutral(&siblings, owner, mm, counted(0, prepare(&siblings, mm))).await,
2177                            R::PrepareNativeExec(Err(ExecControlError::Preparation))
2178                        );
2179
2180                        let (failed, owner, _, mm) = fixture(true, Some((physical, physical)));
2181                        failed.report_backend_failure(reverie::BackendFailure {
2182                            pid: reverie::Pid::from_raw(owner.as_raw()),
2183                            tid: Tid::from_raw(owner.as_raw()),
2184                            phase: "native control protocol test",
2185                        });
2186                        assert_eq!(
2187                            neutral(&failed, owner, mm, counted(0, prepare(&failed, mm))).await,
2188                            R::PrepareNativeExec(Err(ExecControlError::Dead))
2189                        );
2190
2191                        let (state, owner, _, mm) = fixture(true, Some((physical, physical)));
2192                        source.send(physical, 1, b"counted\n");
2193                        let ticket =
2194                            match neutral(&state, owner, mm, counted(1, prepare(&state, mm))).await
2195                            {
2196                                R::PrepareNativeExec(Ok(ticket)) => ticket,
2197                                response => panic!("prepare: {response:?}"),
2198                            };
2199                        let runtime = b"protocol-runtime".to_vec();
2200                        let digest = match neutral(
2201                            &state,
2202                            owner,
2203                            mm,
2204                            counted(
2205                                0,
2206                                Q::SealNativeExecStats {
2207                                    ticket,
2208                                    runtime_digest: Sha256::digest(&runtime).into(),
2209                                    runtime,
2210                                },
2211                            ),
2212                        )
2213                        .await
2214                        {
2215                            R::SealNativeExecStats(Ok(digest)) => digest,
2216                            response => panic!("seal: {response:?}"),
2217                        };
2218                        source.send(physical, 1, b"after-seal\n");
2219                        // A declared nonempty interval refuses before consuming it.
2220                        assert_eq!(
2221                            neutral(
2222                                &state,
2223                                owner,
2224                                mm,
2225                                counted(
2226                                    1,
2227                                    Q::IssueNativeExec {
2228                                        ticket,
2229                                        state_digest: digest
2230                                    }
2231                                )
2232                            )
2233                            .await,
2234                            R::IssueNativeExec(Err(ExecControlError::Forwarding))
2235                        );
2236                        assert_eq!(
2237                            state
2238                                .native_exec_registry()
2239                                .unwrap()
2240                                .lock()
2241                                .unwrap()
2242                                .stage(ticket),
2243                            Some(ExecStage::Prepared)
2244                        );
2245                        // Cancellation may account the actual retained nonempty interval.
2246                        assert_eq!(
2247                            neutral(
2248                                &state,
2249                                owner,
2250                                mm,
2251                                counted(1, Q::CancelNativeExec { ticket })
2252                            )
2253                            .await,
2254                            R::CancelNativeExec(Ok(()))
2255                        );
2256                        assert_eq!(
2257                            *source.written.lock().unwrap(),
2258                            vec![b"counted\n".to_vec(), b"after-seal\n".to_vec()]
2259                        );
2260                        assert!(
2261                            state
2262                                .pending_exec_states
2263                                .lock()
2264                                .unwrap()
2265                                .contains_key(&owner),
2266                            "native cancellation never replaces ordinary CancelExec"
2267                        );
2268                        let _replacement = Source::new();
2269                        assert_eq!(
2270                            neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await,
2271                            R::PrepareNativeExec(Err(ExecControlError::Forwarding))
2272                        );
2273                        assert_eq!(
2274                            state
2275                                .native_exec_registry()
2276                                .unwrap()
2277                                .lock()
2278                                .unwrap()
2279                                .stage(ticket),
2280                            Some(ExecStage::Cancelled)
2281                        );
2282                        crate::detlog::clear_forwarded_source();
2283                    });
2284            },
2285        );
2286    }
2287    #[test]
2288    fn native_exec_control_protocol_refuses_zero_claim_with_actual_late_record() {
2289        isolated(
2290            "native_exec_control_protocol_refuses_zero_claim_with_actual_late_record",
2291            || {
2292                tokio::runtime::Builder::new_current_thread()
2293                    .enable_all()
2294                    .build()
2295                    .unwrap()
2296                    .block_on(async {
2297                        let physical = unsafe { libc::getpid() };
2298                        let source = Source::new();
2299                        let (state, owner, _, mm) = fixture(true, Some((physical, physical)));
2300                        let ticket =
2301                            match neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await
2302                            {
2303                                R::PrepareNativeExec(Ok(ticket)) => ticket,
2304                                response => panic!("prepare: {response:?}"),
2305                            };
2306                        let runtime = b"protocol-runtime".to_vec();
2307                        let digest = match neutral(
2308                            &state,
2309                            owner,
2310                            mm,
2311                            counted(
2312                                0,
2313                                Q::SealNativeExecStats {
2314                                    ticket,
2315                                    runtime_digest: Sha256::digest(&runtime).into(),
2316                                    runtime,
2317                                },
2318                            ),
2319                        )
2320                        .await
2321                        {
2322                            R::SealNativeExecStats(Ok(digest)) => digest,
2323                            response => panic!("seal: {response:?}"),
2324                        };
2325                        source.send(physical, 1, b"actual-late-record\n");
2326                        assert_eq!(
2327                            neutral(
2328                                &state,
2329                                owner,
2330                                mm,
2331                                counted(
2332                                    0,
2333                                    Q::IssueNativeExec {
2334                                        ticket,
2335                                        state_digest: digest
2336                                    }
2337                                )
2338                            )
2339                            .await,
2340                            R::IssueNativeExec(Err(ExecControlError::Forwarding))
2341                        );
2342                        assert!(crate::detlog::determinism_loss().is_some());
2343                        assert_eq!(
2344                            state
2345                                .native_exec_registry()
2346                                .unwrap()
2347                                .lock()
2348                                .unwrap()
2349                                .stage(ticket),
2350                            Some(ExecStage::Prepared)
2351                        );
2352                        assert_eq!(
2353                            neutral(
2354                                &state,
2355                                owner,
2356                                mm,
2357                                counted(1, Q::CancelNativeExec { ticket })
2358                            )
2359                            .await,
2360                            R::CancelNativeExec(Err(ExecControlError::Forwarding)),
2361                            "a persistent forwarding loss cannot be waived by cancellation"
2362                        );
2363                        assert_eq!(
2364                            state
2365                                .native_exec_registry()
2366                                .unwrap()
2367                                .lock()
2368                                .unwrap()
2369                                .stage(ticket),
2370                            Some(ExecStage::Prepared)
2371                        );
2372                        assert!(source.written.lock().unwrap().iter().any(|record| {
2373                            record.starts_with(crate::detlog::FORWARDING_LOSS_NOTICE.as_bytes())
2374                        }));
2375                        crate::detlog::clear_forwarded_source();
2376                        assert!(
2377                            crate::detlog::determinism_loss().is_some(),
2378                            "source cleanup never clears the retained failure"
2379                        );
2380                    });
2381            },
2382        );
2383    }
2384
2385    #[test]
2386    fn native_exec_control_protocol_refuses_missing_actual_record() {
2387        isolated(
2388            "native_exec_control_protocol_refuses_missing_actual_record",
2389            || {
2390                tokio::runtime::Builder::new_current_thread()
2391                    .enable_all()
2392                    .build()
2393                    .unwrap()
2394                    .block_on(async {
2395                        let physical = unsafe { libc::getpid() };
2396                        let source = Source::new();
2397                        let (state, owner, _, mm) = fixture(true, Some((physical, physical)));
2398                        // The actual registered socket has zero records. Declaring one
2399                        // is a negative completeness case, never a fabricated success.
2400                        assert_eq!(
2401                            neutral(&state, owner, mm, counted(1, prepare(&state, mm))).await,
2402                            R::PrepareNativeExec(Err(ExecControlError::Forwarding))
2403                        );
2404                        assert!(
2405                            state
2406                                .native_exec_registry()
2407                                .unwrap()
2408                                .lock()
2409                                .unwrap()
2410                                .transitions
2411                                .is_empty()
2412                        );
2413                        assert!(crate::detlog::determinism_loss().is_some());
2414                        assert!(source.written.lock().unwrap().iter().any(|record| {
2415                            record.starts_with(crate::detlog::FORWARDING_LOSS_NOTICE.as_bytes())
2416                        }));
2417                        assert_eq!(
2418                            neutral(&state, owner, mm, counted(0, prepare(&state, mm))).await,
2419                            R::PrepareNativeExec(Err(ExecControlError::Forwarding))
2420                        );
2421                        crate::detlog::clear_forwarded_source();
2422                        assert!(crate::detlog::determinism_loss().is_some());
2423                    });
2424            },
2425        );
2426    }
2427}
2428
2429#[cfg(test)]
2430mod tests {
2431    use std::os::fd::AsRawFd;
2432    use std::os::fd::FromRawFd;
2433    use std::sync::atomic::AtomicBool;
2434    use std::sync::atomic::Ordering;
2435
2436    use super::*;
2437
2438    // This proof deliberately models qualification. These tests establish
2439    // registry transitions, not immutable mappings, a watcher or native exec.
2440    #[derive(Debug)]
2441    struct Proof {
2442        live: AtomicBool,
2443        pidfd: OwnedFd,
2444    }
2445
2446    fn same_file(left: &OwnedFd, right: &OwnedFd) -> bool {
2447        let mut a = std::mem::MaybeUninit::<libc::stat>::uninit();
2448        let mut b = std::mem::MaybeUninit::<libc::stat>::uninit();
2449        // SAFETY: valid owned descriptors and writable stat storage.
2450        unsafe {
2451            if libc::fstat(left.as_raw_fd(), a.as_mut_ptr()) != 0
2452                || libc::fstat(right.as_raw_fd(), b.as_mut_ptr()) != 0
2453            {
2454                return false;
2455            }
2456            let (a, b) = (a.assume_init(), b.assume_init());
2457            (a.st_dev, a.st_ino) == (b.st_dev, b.st_ino)
2458        }
2459    }
2460
2461    // SAFETY: test-only authority; no installer/guest execution uses it. The
2462    // peer check compares genuine held pidfds, while qualification is modelled.
2463    unsafe impl NativeExecProof for Proof {
2464        fn owner(&self) -> DetTid {
2465            DetTid::from_raw(41)
2466        }
2467        fn physical_ids(&self) -> Option<(i32, i32)> {
2468            // Explicit registry model, not a native process admission.
2469            Some((41, 41))
2470        }
2471        fn is_live(&self) -> bool {
2472            self.live.load(Ordering::SeqCst)
2473        }
2474        fn matches_peer(&self, peer: &OwnedFd) -> bool {
2475            same_file(&self.pidfd, peer)
2476        }
2477        fn qualify_target(&self, request: &PrepareNativeExec) -> bool {
2478            request.target_digest == [2; 32]
2479                && request.bootstrap_digest == [3; 32]
2480                && request.resume_mask == 0x42
2481                && request.sockets.detlog.object() == (1, 2)
2482                && request.sockets.observer.object() == (1, 3)
2483        }
2484        fn qualify_runtime(&self, bytes: &[u8]) -> bool {
2485            bytes == b"actual-runtime-model"
2486        }
2487        fn qualify_completion(&self, _: ExecTicket, _: NativeExecBinding) -> bool {
2488            // Explicit registry model only; not watched native execution proof.
2489            true
2490        }
2491    }
2492
2493    fn fixture() -> (
2494        NativeExecRegistry,
2495        Arc<Proof>,
2496        NativeExecContext,
2497        PrepareNativeExec,
2498    ) {
2499        // SAFETY: read-only process identity and a new owned pidfd on success.
2500        let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, libc::getpid(), 0) };
2501        assert!(raw >= 0, "pidfd_open: {}", std::io::Error::last_os_error());
2502        let proof = Arc::new(Proof {
2503            live: AtomicBool::new(true),
2504            pidfd: unsafe { OwnedFd::from_raw_fd(raw as i32) },
2505        });
2506        let owner = proof.owner();
2507        let old_mm = MmId::initial(owner).for_exec(owner);
2508        let context = NativeExecContext {
2509            owner,
2510            old_mm,
2511            prepared_mm: old_mm.for_exec(owner),
2512            config_digest: [1; 32],
2513        };
2514        let request = PrepareNativeExec {
2515            sockets: NativeExecSocketBindings {
2516                detlog: NativeExecSocketBinding {
2517                    fd: 1025,
2518                    device: 1,
2519                    inode: 2,
2520                },
2521                observer: NativeExecSocketBinding {
2522                    fd: 1026,
2523                    device: 1,
2524                    inode: 3,
2525                },
2526            },
2527            cut: NativeExecNeutralBinding {
2528                dettid: owner,
2529                detpid: owner,
2530                physical_tid: proof.physical_ids().unwrap().1,
2531                old_mm,
2532                old_files: FilesId::initial(owner),
2533                prepared_mm: context.prepared_mm,
2534                config_digest: context.config_digest,
2535            },
2536            prepared_mm: context.prepared_mm,
2537            config_digest: context.config_digest,
2538            target_digest: [2; 32],
2539            bootstrap_digest: [3; 32],
2540            resume_mask: 0x42,
2541            snapshot_digest: Sha256::digest(b"actual-tool-model").into(),
2542            snapshot: b"actual-tool-model".to_vec(),
2543        };
2544        (
2545            NativeExecRegistry::new(proof.clone(), [7; 32]).unwrap(),
2546            proof,
2547            context,
2548            request,
2549        )
2550    }
2551
2552    fn seal(
2553        registry: &mut NativeExecRegistry,
2554        context: NativeExecContext,
2555        ticket: ExecTicket,
2556    ) -> [u8; 32] {
2557        let runtime = b"actual-runtime-model";
2558        registry
2559            .seal(
2560                context.owner,
2561                ticket,
2562                context.prepared_mm,
2563                Sha256::digest(runtime).into(),
2564                runtime,
2565            )
2566            .unwrap()
2567    }
2568
2569    #[test]
2570    fn native_registry_retains_exact_binding_and_completes_once() {
2571        let (mut registry, proof, context, request) = fixture();
2572        let ticket = registry.prepare(context, request.clone()).unwrap();
2573        let binding = registry
2574            .binding(context.owner, ticket, context.prepared_mm)
2575            .unwrap();
2576        assert_eq!(binding.context, context);
2577        assert_eq!(binding.sockets, request.sockets);
2578        assert_eq!(binding.cut, request.cut);
2579        assert_eq!(binding.target_digest, request.target_digest);
2580        assert_eq!(binding.bootstrap_digest, request.bootstrap_digest);
2581        assert_eq!(binding.resume_mask, request.resume_mask);
2582        assert_eq!(binding.snapshot_digest, request.snapshot_digest);
2583        assert_eq!(
2584            registry.prepare(context, request.clone()),
2585            Err(ExecControlError::Transition)
2586        );
2587        assert_eq!(
2588            registry.issue(context.owner, ticket, context.prepared_mm, [0; 32]),
2589            Err(ExecControlError::Transition)
2590        );
2591        assert_eq!(
2592            registry.redeem(&proof.pidfd, ticket, context.config_digest, [0; 32]),
2593            Err(ExecControlError::Transition)
2594        );
2595        let digest = seal(&mut registry, context, ticket);
2596        assert_eq!(
2597            registry.seal(
2598                context.owner,
2599                ticket,
2600                context.prepared_mm,
2601                Sha256::digest(b"actual-runtime-model").into(),
2602                b"actual-runtime-model"
2603            ),
2604            Err(ExecControlError::Transition)
2605        );
2606        // Model transport completion setup; existing assertions unchanged.
2607        // SAFETY: fixture models transfer, not native execution.
2608        unsafe {
2609            registry
2610                .begin_provision(&proof.pidfd, ticket)
2611                .unwrap()
2612                .complete()
2613        }
2614        .unwrap();
2615        registry
2616            .issue(context.owner, ticket, context.prepared_mm, digest)
2617            .unwrap();
2618        assert_eq!(
2619            registry.issue(context.owner, ticket, context.prepared_mm, digest),
2620            Err(ExecControlError::Transition)
2621        );
2622        let state = registry
2623            .redeem(&proof.pidfd, ticket, context.config_digest, digest)
2624            .unwrap();
2625        assert_eq!(<[u8; 32]>::from(Sha256::digest(&state)), digest);
2626        assert_eq!(
2627            split_state(&state),
2628            Ok((
2629                request.snapshot.as_slice(),
2630                b"actual-runtime-model".as_slice()
2631            ))
2632        );
2633        assert_eq!(registry.stage(ticket), Some(ExecStage::Redeemed));
2634        assert_eq!(
2635            registry.redeem(&proof.pidfd, ticket, context.config_digest, digest),
2636            Err(ExecControlError::Transition)
2637        );
2638        assert_eq!(
2639            registry.cancel(context.owner, ticket, context.prepared_mm),
2640            Err(ExecControlError::Transition)
2641        );
2642        registry
2643            .complete(context.owner, ticket, context.prepared_mm)
2644            .unwrap();
2645        assert_eq!(
2646            registry.complete(context.owner, ticket, context.prepared_mm),
2647            Err(ExecControlError::Transition)
2648        );
2649        assert_eq!(registry.stage(ticket), Some(ExecStage::Completed));
2650        assert_eq!(
2651            registry.prepare(context, request.clone()),
2652            Err(ExecControlError::Identity)
2653        );
2654        let next_context = NativeExecContext {
2655            old_mm: context.prepared_mm,
2656            prepared_mm: context.prepared_mm.for_exec(context.owner),
2657            ..context
2658        };
2659        let next_request = PrepareNativeExec {
2660            cut: NativeExecNeutralBinding {
2661                old_mm: next_context.old_mm,
2662                old_files: request.cut.old_files.for_exec(context.owner),
2663                prepared_mm: next_context.prepared_mm,
2664                ..request.cut
2665            },
2666            prepared_mm: next_context.prepared_mm,
2667            ..request
2668        };
2669        let next = registry.prepare(next_context, next_request).unwrap();
2670        assert_ne!(next, ticket);
2671        assert_eq!(registry.stage(ticket), Some(ExecStage::Completed));
2672    }
2673
2674    #[test]
2675    fn model_socket_bindings_refuse_invalid_slots_aliases_and_unheld_objects() {
2676        let (mut registry, _, context, request) = fixture();
2677        let original = request.sockets;
2678        let mut structural = Vec::new();
2679        for fd in [-1, 0, 1, 2, 198, 199, 200, 201, 202] {
2680            structural.push(NativeExecSocketBindings {
2681                detlog: NativeExecSocketBinding {
2682                    fd,
2683                    ..original.detlog
2684                },
2685                ..original
2686            });
2687            structural.push(NativeExecSocketBindings {
2688                observer: NativeExecSocketBinding {
2689                    fd,
2690                    ..original.observer
2691                },
2692                ..original
2693            });
2694        }
2695        structural.push(NativeExecSocketBindings {
2696            observer: NativeExecSocketBinding {
2697                fd: original.detlog.fd,
2698                ..original.observer
2699            },
2700            ..original
2701        });
2702        structural.push(NativeExecSocketBindings {
2703            observer: NativeExecSocketBinding {
2704                device: original.detlog.device,
2705                inode: original.detlog.inode,
2706                ..original.observer
2707            },
2708            ..original
2709        });
2710        for sockets in structural {
2711            assert_eq!(
2712                registry.prepare(
2713                    context,
2714                    PrepareNativeExec {
2715                        sockets,
2716                        ..request.clone()
2717                    }
2718                ),
2719                Err(ExecControlError::Identity),
2720                "invalid socket shape {sockets:?}"
2721            );
2722            assert!(registry.transitions.is_empty());
2723        }
2724        for sockets in [
2725            NativeExecSocketBindings {
2726                detlog: NativeExecSocketBinding {
2727                    device: 9,
2728                    ..original.detlog
2729                },
2730                ..original
2731            },
2732            NativeExecSocketBindings {
2733                detlog: NativeExecSocketBinding {
2734                    inode: 9,
2735                    ..original.detlog
2736                },
2737                ..original
2738            },
2739            NativeExecSocketBindings {
2740                observer: NativeExecSocketBinding {
2741                    device: 9,
2742                    ..original.observer
2743                },
2744                ..original
2745            },
2746            NativeExecSocketBindings {
2747                observer: NativeExecSocketBinding {
2748                    inode: 9,
2749                    ..original.observer
2750                },
2751                ..original
2752            },
2753            NativeExecSocketBindings {
2754                detlog: original.observer,
2755                observer: original.detlog,
2756            },
2757        ] {
2758            assert_eq!(
2759                registry.prepare(
2760                    context,
2761                    PrepareNativeExec {
2762                        sockets,
2763                        ..request.clone()
2764                    }
2765                ),
2766                Err(ExecControlError::Qualification),
2767                "unheld socket objects {sockets:?}"
2768            );
2769            assert!(registry.transitions.is_empty());
2770        }
2771        let ticket = registry.prepare(context, request).unwrap();
2772        assert_eq!(
2773            registry
2774                .binding(context.owner, ticket, context.prepared_mm)
2775                .unwrap()
2776                .sockets,
2777            original
2778        );
2779    }
2780
2781    #[test]
2782    fn model_socket_bindings_are_per_transaction_and_cancelled_mapping_cannot_provision() {
2783        let (mut registry, proof, context, request) = fixture();
2784        let first = registry.prepare(context, request.clone()).unwrap();
2785        let digest = seal(&mut registry, context, first);
2786        // SAFETY: modeled transfer only, never native execution authority.
2787        unsafe {
2788            registry
2789                .begin_provision(&proof.pidfd, first)
2790                .unwrap()
2791                .complete()
2792        }
2793        .unwrap();
2794        registry
2795            .issue(context.owner, first, context.prepared_mm, digest)
2796            .unwrap();
2797        registry
2798            .redeem(&proof.pidfd, first, context.config_digest, digest)
2799            .unwrap();
2800        registry
2801            .complete(context.owner, first, context.prepared_mm)
2802            .unwrap();
2803
2804        let next = NativeExecContext {
2805            old_mm: context.prepared_mm,
2806            prepared_mm: context.prepared_mm.for_exec(context.owner),
2807            ..context
2808        };
2809        let current = NativeExecSocketBindings {
2810            detlog: NativeExecSocketBinding {
2811                fd: 3025,
2812                ..request.sockets.detlog
2813            },
2814            observer: NativeExecSocketBinding {
2815                fd: 3026,
2816                ..request.sockets.observer
2817            },
2818        };
2819        let next_request = PrepareNativeExec {
2820            sockets: current,
2821            cut: NativeExecNeutralBinding {
2822                old_mm: next.old_mm,
2823                old_files: request.cut.old_files.for_exec(context.owner),
2824                prepared_mm: next.prepared_mm,
2825                ..request.cut
2826            },
2827            prepared_mm: next.prepared_mm,
2828            ..request.clone()
2829        };
2830        let cancelled = registry.prepare(next, next_request.clone()).unwrap();
2831        assert_eq!(
2832            registry
2833                .binding(next.owner, cancelled, next.prepared_mm)
2834                .unwrap()
2835                .sockets,
2836            current
2837        );
2838        registry
2839            .cancel(next.owner, cancelled, next.prepared_mm)
2840            .unwrap();
2841        assert_eq!(
2842            registry.begin_provision(&proof.pidfd, cancelled).err(),
2843            Some(ExecControlError::Transition)
2844        );
2845        assert_eq!(registry.stage(cancelled), Some(ExecStage::Cancelled));
2846
2847        let fresh = NativeExecSocketBindings {
2848            detlog: NativeExecSocketBinding {
2849                fd: 4025,
2850                ..current.detlog
2851            },
2852            observer: NativeExecSocketBinding {
2853                fd: 4026,
2854                ..current.observer
2855            },
2856        };
2857        let third = registry
2858            .prepare(
2859                next,
2860                PrepareNativeExec {
2861                    sockets: fresh,
2862                    ..next_request
2863                },
2864            )
2865            .unwrap();
2866        assert_ne!(first, third);
2867        assert_ne!(cancelled, third);
2868        assert_eq!(
2869            registry
2870                .binding(next.owner, third, next.prepared_mm)
2871                .unwrap()
2872                .sockets,
2873            fresh
2874        );
2875        assert_eq!(
2876            registry.transitions[&first].binding.sockets,
2877            request.sockets
2878        );
2879        assert_eq!(registry.transitions[&cancelled].binding.sockets, current);
2880        assert_eq!(registry.stage(first), Some(ExecStage::Completed));
2881        assert_eq!(
2882            registry.begin_provision(&proof.pidfd, cancelled).err(),
2883            Some(ExecControlError::Transition)
2884        );
2885        assert_eq!(registry.stage(third), Some(ExecStage::Prepared));
2886        assert_eq!(
2887            registry.begin_provision(&proof.pidfd, first).err(),
2888            Some(ExecControlError::Transition)
2889        );
2890        let _digest = seal(&mut registry, next, third);
2891        let provision = registry.begin_provision(&proof.pidfd, third).unwrap();
2892        assert_eq!(provision.binding().sockets, fresh);
2893        // SAFETY: same modeled transport completion as the first transaction.
2894        unsafe { provision.complete() }.unwrap();
2895    }
2896
2897    #[test]
2898    fn model_neutral_cut_rejects_owner_memory_table_physical_and_config_mismatch() {
2899        let (mut registry, _, context, request) = fixture();
2900        let other = DetTid::from_raw(42);
2901        for cut in [
2902            NativeExecNeutralBinding {
2903                dettid: other,
2904                ..request.cut
2905            },
2906            NativeExecNeutralBinding {
2907                detpid: other,
2908                ..request.cut
2909            },
2910            NativeExecNeutralBinding {
2911                physical_tid: 42,
2912                ..request.cut
2913            },
2914            NativeExecNeutralBinding {
2915                old_mm: context.prepared_mm,
2916                ..request.cut
2917            },
2918            NativeExecNeutralBinding {
2919                prepared_mm: context.old_mm,
2920                ..request.cut
2921            },
2922            NativeExecNeutralBinding {
2923                old_files: request.cut.old_files.for_exec(context.owner),
2924                ..request.cut
2925            },
2926            NativeExecNeutralBinding {
2927                config_digest: [9; 32],
2928                ..request.cut
2929            },
2930        ] {
2931            assert_eq!(
2932                registry.prepare(
2933                    context,
2934                    PrepareNativeExec {
2935                        cut,
2936                        ..request.clone()
2937                    }
2938                ),
2939                Err(ExecControlError::Identity)
2940            );
2941        }
2942        assert_eq!(registry.current_files, FilesId::initial(context.owner));
2943        let first = registry.prepare(context, request.clone()).unwrap();
2944        registry
2945            .cancel(context.owner, first, context.prepared_mm)
2946            .unwrap();
2947        assert_eq!(registry.current_files, FilesId::initial(context.owner));
2948        let retry = registry.prepare(context, request).unwrap();
2949        assert_ne!(first, retry);
2950    }
2951
2952    #[test]
2953    fn native_registry_cancel_and_delivery_failure_never_reopen_a_ticket() {
2954        for issued in [false, true] {
2955            let (mut registry, proof, context, request) = fixture();
2956            let ticket = registry.prepare(context, request.clone()).unwrap();
2957            let digest = seal(&mut registry, context, ticket);
2958            if issued {
2959                // Model transport completion setup; existing assertions unchanged.
2960                // SAFETY: fixture models transfer, not native execution.
2961                unsafe {
2962                    registry
2963                        .begin_provision(&proof.pidfd, ticket)
2964                        .unwrap()
2965                        .complete()
2966                }
2967                .unwrap();
2968                registry
2969                    .issue(context.owner, ticket, context.prepared_mm, digest)
2970                    .unwrap();
2971            }
2972            registry
2973                .cancel(context.owner, ticket, context.prepared_mm)
2974                .unwrap();
2975            assert_eq!(registry.stage(ticket), Some(ExecStage::Cancelled));
2976            assert_eq!(
2977                registry.cancel(context.owner, ticket, context.prepared_mm),
2978                Err(ExecControlError::Transition)
2979            );
2980            assert_eq!(
2981                registry.redeem(&proof.pidfd, ticket, context.config_digest, digest),
2982                Err(ExecControlError::Transition)
2983            );
2984            assert_ne!(registry.prepare(context, request).unwrap(), ticket);
2985        }
2986        let (mut registry, proof, context, request) = fixture();
2987        let ticket = registry.prepare(context, request).unwrap();
2988        let digest = seal(&mut registry, context, ticket);
2989        // Model transport completion setup; existing assertions unchanged.
2990        // SAFETY: fixture models transfer, not native execution.
2991        unsafe {
2992            registry
2993                .begin_provision(&proof.pidfd, ticket)
2994                .unwrap()
2995                .complete()
2996        }
2997        .unwrap();
2998        registry
2999            .issue(context.owner, ticket, context.prepared_mm, digest)
3000            .unwrap();
3001        let _undelivered = registry
3002            .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3003            .unwrap();
3004        registry.failed(ticket).unwrap();
3005        assert_eq!(registry.stage(ticket), Some(ExecStage::Failed));
3006        assert_eq!(
3007            registry.redeem(&proof.pidfd, ticket, context.config_digest, digest),
3008            Err(ExecControlError::Dead)
3009        );
3010        assert_eq!(
3011            registry.complete(context.owner, ticket, context.prepared_mm),
3012            Err(ExecControlError::Dead)
3013        );
3014    }
3015
3016    #[test]
3017    fn native_registry_wrong_peer_owner_mm_and_digests_cannot_consume() {
3018        let (mut registry, proof, context, request) = fixture();
3019        let ticket = registry.prepare(context, request).unwrap();
3020        let digest = seal(&mut registry, context, ticket);
3021        let stranger = DetTid::from_raw(42);
3022        assert_eq!(
3023            registry.issue(stranger, ticket, context.prepared_mm, digest),
3024            Err(ExecControlError::Identity)
3025        );
3026        assert_eq!(
3027            registry.issue(context.owner, ticket, context.old_mm, digest),
3028            Err(ExecControlError::Identity)
3029        );
3030        // Model transport completion setup; existing assertions unchanged.
3031        // SAFETY: fixture models transfer, not native execution.
3032        unsafe {
3033            registry
3034                .begin_provision(&proof.pidfd, ticket)
3035                .unwrap()
3036                .complete()
3037        }
3038        .unwrap();
3039        registry
3040            .issue(context.owner, ticket, context.prepared_mm, digest)
3041            .unwrap();
3042        let wrong_peer: OwnedFd = std::fs::File::open("/dev/null").unwrap().into();
3043        assert_eq!(
3044            registry.redeem(&wrong_peer, ticket, context.config_digest, digest),
3045            Err(ExecControlError::Identity)
3046        );
3047        assert_eq!(
3048            registry.redeem(
3049                &proof.pidfd,
3050                ExecTicket([0; 32]),
3051                context.config_digest,
3052                digest
3053            ),
3054            Err(ExecControlError::Identity)
3055        );
3056        assert_eq!(
3057            registry.redeem(&proof.pidfd, ticket, [9; 32], digest),
3058            Err(ExecControlError::Digest)
3059        );
3060        assert_eq!(
3061            registry.redeem(&proof.pidfd, ticket, context.config_digest, [9; 32]),
3062            Err(ExecControlError::Digest)
3063        );
3064        assert_eq!(registry.stage(ticket), Some(ExecStage::Issued));
3065        assert!(
3066            !registry
3067                .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3068                .unwrap()
3069                .is_empty()
3070        );
3071    }
3072
3073    #[test]
3074    fn native_registry_terminal_failure_refuses_every_future_edge() {
3075        for edge in 0..4 {
3076            let (mut registry, proof, context, request) = fixture();
3077            let ticket = registry.prepare(context, request.clone()).unwrap();
3078            let digest = if edge > 0 {
3079                seal(&mut registry, context, ticket)
3080            } else {
3081                [0; 32]
3082            };
3083            if edge > 1 {
3084                // Model transport completion setup; existing assertions unchanged.
3085                // SAFETY: fixture models transfer, not native execution.
3086                unsafe {
3087                    registry
3088                        .begin_provision(&proof.pidfd, ticket)
3089                        .unwrap()
3090                        .complete()
3091                }
3092                .unwrap();
3093                registry
3094                    .issue(context.owner, ticket, context.prepared_mm, digest)
3095                    .unwrap();
3096            }
3097            if edge > 2 {
3098                registry
3099                    .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3100                    .unwrap();
3101            }
3102            registry.failed(ticket).unwrap();
3103            assert_eq!(registry.stage(ticket), Some(ExecStage::Failed));
3104            assert_eq!(
3105                registry.prepare(context, request.clone()),
3106                Err(ExecControlError::Dead)
3107            );
3108            let next_context = NativeExecContext {
3109                old_mm: context.prepared_mm,
3110                prepared_mm: context.prepared_mm.for_exec(context.owner),
3111                ..context
3112            };
3113            assert_eq!(
3114                registry.prepare(
3115                    next_context,
3116                    PrepareNativeExec {
3117                        prepared_mm: next_context.prepared_mm,
3118                        ..request
3119                    }
3120                ),
3121                Err(ExecControlError::Dead)
3122            );
3123            assert_eq!(
3124                registry.seal(
3125                    context.owner,
3126                    ticket,
3127                    context.prepared_mm,
3128                    Sha256::digest(b"actual-runtime-model").into(),
3129                    b"actual-runtime-model"
3130                ),
3131                Err(ExecControlError::Dead)
3132            );
3133            assert_eq!(
3134                registry.issue(context.owner, ticket, context.prepared_mm, digest),
3135                Err(ExecControlError::Dead)
3136            );
3137            assert_eq!(
3138                registry.redeem(&proof.pidfd, ticket, context.config_digest, digest),
3139                Err(ExecControlError::Dead)
3140            );
3141            assert_eq!(
3142                registry.complete(context.owner, ticket, context.prepared_mm),
3143                Err(ExecControlError::Dead)
3144            );
3145            assert_eq!(
3146                registry.cancel(context.owner, ticket, context.prepared_mm),
3147                Err(ExecControlError::Dead)
3148            );
3149            assert_eq!(registry.stage(ticket), Some(ExecStage::Failed));
3150        }
3151    }
3152
3153    #[test]
3154    fn native_registry_mask_must_match_the_independently_captured_cut() {
3155        let (mut registry, _, context, mut request) = fixture();
3156        request.resume_mask ^= 1;
3157        assert_eq!(
3158            registry.prepare(context, request),
3159            Err(ExecControlError::Qualification)
3160        );
3161        assert!(registry.transitions.is_empty());
3162        assert_eq!(registry.next_ticket, 1);
3163    }
3164
3165    #[test]
3166    fn native_registry_death_at_each_live_edge_preserves_tombstones() {
3167        for edge in 0..4 {
3168            let (mut registry, proof, context, request) = fixture();
3169            let ticket = registry.prepare(context, request.clone()).unwrap();
3170            let digest = if edge > 0 {
3171                seal(&mut registry, context, ticket)
3172            } else {
3173                [0; 32]
3174            };
3175            if edge > 1 {
3176                // Model transport completion setup; existing assertions unchanged.
3177                // SAFETY: fixture models transfer, not native execution.
3178                unsafe {
3179                    registry
3180                        .begin_provision(&proof.pidfd, ticket)
3181                        .unwrap()
3182                        .complete()
3183                }
3184                .unwrap();
3185                registry
3186                    .issue(context.owner, ticket, context.prepared_mm, digest)
3187                    .unwrap();
3188            }
3189            if edge > 2 {
3190                registry
3191                    .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3192                    .unwrap();
3193            }
3194            proof.live.store(false, Ordering::SeqCst);
3195            assert_eq!(
3196                registry.complete(context.owner, ticket, context.prepared_mm),
3197                Err(ExecControlError::Dead)
3198            );
3199            assert_eq!(registry.stage(ticket), Some(ExecStage::Dead));
3200            proof.live.store(true, Ordering::SeqCst);
3201            assert_eq!(
3202                registry.prepare(context, request),
3203                Err(ExecControlError::Dead)
3204            );
3205            assert_eq!(
3206                registry.redeem(&proof.pidfd, ticket, context.config_digest, digest),
3207                Err(ExecControlError::Dead)
3208            );
3209        }
3210    }
3211
3212    #[test]
3213    fn native_registry_bounds_and_qualification_leave_no_partial_preparation() {
3214        for invalid in 0..7 {
3215            let (mut registry, _, context, mut request) = fixture();
3216            match invalid {
3217                0 => request.prepared_mm = context.old_mm,
3218                1 => request.config_digest = [9; 32],
3219                2 => request.snapshot.clear(),
3220                3 => request.snapshot.resize(MAX_STATE_BYTES, 0),
3221                4 => request.snapshot_digest = [9; 32],
3222                5 => request.target_digest = [9; 32],
3223                6 => request.bootstrap_digest = [9; 32],
3224                _ => unreachable!(),
3225            }
3226            assert!(registry.prepare(context, request).is_err());
3227            assert!(registry.transitions.is_empty());
3228            assert_eq!(registry.next_ticket, 1);
3229        }
3230        let (mut registry, _, context, request) = fixture();
3231        let ticket = registry.prepare(context, request).unwrap();
3232        for runtime in [b"".as_slice(), b"unsupported-runtime".as_slice()] {
3233            assert!(
3234                registry
3235                    .seal(
3236                        context.owner,
3237                        ticket,
3238                        context.prepared_mm,
3239                        Sha256::digest(runtime).into(),
3240                        runtime
3241                    )
3242                    .is_err()
3243            );
3244            assert_eq!(registry.stage(ticket), Some(ExecStage::Prepared));
3245            assert!(registry.transitions[&ticket].state_digest.is_none());
3246        }
3247        assert_eq!(
3248            registry.seal(
3249                context.owner,
3250                ticket,
3251                context.prepared_mm,
3252                [9; 32],
3253                b"actual-runtime-model"
3254            ),
3255            Err(ExecControlError::Digest)
3256        );
3257        seal(&mut registry, context, ticket);
3258    }
3259
3260    #[test]
3261    fn native_registry_state_parser_rejects_all_truncations_and_wrong_lengths() {
3262        let (mut registry, proof, context, request) = fixture();
3263        let ticket = registry.prepare(context, request).unwrap();
3264        let digest = seal(&mut registry, context, ticket);
3265        // Model transport completion setup; existing assertions unchanged.
3266        // SAFETY: fixture models transfer, not native execution.
3267        unsafe {
3268            registry
3269                .begin_provision(&proof.pidfd, ticket)
3270                .unwrap()
3271                .complete()
3272        }
3273        .unwrap();
3274        registry
3275            .issue(context.owner, ticket, context.prepared_mm, digest)
3276            .unwrap();
3277        let bytes = registry
3278            .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3279            .unwrap();
3280        for n in 0..bytes.len() {
3281            assert!(split_state(&bytes[..n]).is_err(), "length {n}");
3282        }
3283        for field in [0, 8, 10, 14] {
3284            let mut bad = bytes.clone();
3285            bad[field] ^= 1;
3286            assert!(split_state(&bad).is_err());
3287        }
3288        let mut extra = bytes.clone();
3289        extra.push(0);
3290        assert!(split_state(&extra).is_err());
3291        extra.resize(MAX_STATE_BYTES + 1, 0);
3292        assert!(split_state(&extra).is_err());
3293        assert!(split_state(&bytes).is_ok());
3294    }
3295
3296    // These additional tests reuse the explicitly modelled qualification proof
3297    // above. They test provision state/actual pidfd identity, not native exec.
3298    #[test]
3299    fn native_provision_preseal_wrong_peer_and_never_provisioned_issue_refuse() {
3300        let (mut registry, proof, context, request) = fixture();
3301        let ticket = registry.prepare(context, request).unwrap();
3302        assert!(matches!(
3303            registry.begin_provision(&proof.pidfd, ticket),
3304            Err(ExecControlError::Transition)
3305        ));
3306        assert_eq!(
3307            registry.transitions[&ticket].provision,
3308            ProvisionState::Fresh
3309        );
3310        let digest = seal(&mut registry, context, ticket);
3311        let wrong: OwnedFd = std::fs::File::open("/dev/null").unwrap().into();
3312        assert!(matches!(
3313            registry.begin_provision(&wrong, ticket),
3314            Err(ExecControlError::Identity)
3315        ));
3316        assert_eq!(
3317            registry.transitions[&ticket].provision,
3318            ProvisionState::Fresh
3319        );
3320        assert_eq!(
3321            registry.issue(context.owner, ticket, context.prepared_mm, digest),
3322            Err(ExecControlError::Transition)
3323        );
3324        assert_eq!(registry.stage(ticket), Some(ExecStage::Prepared));
3325    }
3326
3327    #[test]
3328    fn native_provision_pending_failure_is_terminal_and_cannot_issue() {
3329        let (mut registry, proof, context, request) = fixture();
3330        let ticket = registry.prepare(context, request.clone()).unwrap();
3331        let digest = seal(&mut registry, context, ticket);
3332        {
3333            let lease = registry.begin_provision(&proof.pidfd, ticket).unwrap();
3334            assert_eq!(lease.binding().context, context);
3335            assert_eq!(lease.state_digest(), digest);
3336            assert_eq!(
3337                lease.registry.transitions[&ticket].provision,
3338                ProvisionState::Consumed
3339            );
3340            assert_eq!(
3341                lease
3342                    .registry
3343                    .issue(context.owner, ticket, context.prepared_mm, digest),
3344                Err(ExecControlError::Transition)
3345            );
3346            // Model send error: Drop performs terminal failure, with no retry.
3347        }
3348        assert_eq!(registry.stage(ticket), Some(ExecStage::Failed));
3349        assert!(matches!(
3350            registry.begin_provision(&proof.pidfd, ticket),
3351            Err(ExecControlError::Dead)
3352        ));
3353        assert_eq!(
3354            registry.issue(context.owner, ticket, context.prepared_mm, digest),
3355            Err(ExecControlError::Dead)
3356        );
3357        assert_eq!(
3358            registry.prepare(context, request),
3359            Err(ExecControlError::Dead)
3360        );
3361    }
3362
3363    #[test]
3364    fn native_provision_complete_once_then_issue_and_cancel_tombstone() {
3365        let (mut registry, proof, context, request) = fixture();
3366        let ticket = registry.prepare(context, request.clone()).unwrap();
3367        let digest = seal(&mut registry, context, ticket);
3368        // SAFETY: explicit transport model, not native execution authority.
3369        unsafe {
3370            registry
3371                .begin_provision(&proof.pidfd, ticket)
3372                .unwrap()
3373                .complete()
3374        }
3375        .unwrap();
3376        assert_eq!(
3377            registry.transitions[&ticket].provision,
3378            ProvisionState::Delivered
3379        );
3380        assert!(matches!(
3381            registry.begin_provision(&proof.pidfd, ticket),
3382            Err(ExecControlError::Transition)
3383        ));
3384        registry
3385            .issue(context.owner, ticket, context.prepared_mm, digest)
3386            .unwrap();
3387        registry
3388            .cancel(context.owner, ticket, context.prepared_mm)
3389            .unwrap();
3390        assert!(matches!(
3391            registry.begin_provision(&proof.pidfd, ticket),
3392            Err(ExecControlError::Transition)
3393        ));
3394        assert_eq!(registry.stage(ticket), Some(ExecStage::Cancelled));
3395        let next = registry.prepare(context, request).unwrap();
3396        assert_ne!(next, ticket);
3397        registry.provision_transport_failed();
3398        assert_eq!(registry.stage(ticket), Some(ExecStage::Cancelled));
3399        assert_eq!(registry.stage(next), Some(ExecStage::Failed));
3400    }
3401
3402    #[test]
3403    fn native_provision_death_before_and_during_completion_preserves_dead() {
3404        for consumed in [false, true] {
3405            let (mut registry, proof, context, request) = fixture();
3406            let ticket = registry.prepare(context, request).unwrap();
3407            let digest = seal(&mut registry, context, ticket);
3408            if consumed {
3409                let lease = registry.begin_provision(&proof.pidfd, ticket).unwrap();
3410                proof.live.store(false, Ordering::SeqCst);
3411                // SAFETY: modelled transfer; actual live proof must still refuse.
3412                assert_eq!(unsafe { lease.complete() }, Err(ExecControlError::Dead));
3413            } else {
3414                proof.live.store(false, Ordering::SeqCst);
3415                assert!(matches!(
3416                    registry.begin_provision(&proof.pidfd, ticket),
3417                    Err(ExecControlError::Dead)
3418                ));
3419            }
3420            assert_eq!(registry.stage(ticket), Some(ExecStage::Dead));
3421            assert_eq!(
3422                registry.issue(context.owner, ticket, context.prepared_mm, digest),
3423                Err(ExecControlError::Dead)
3424            );
3425        }
3426    }
3427    #[derive(Debug)]
3428    struct RefusingCompletion(Arc<Proof>);
3429    // SAFETY: explicit registry model only; completion is deliberately left at
3430    // its default refusal. No runtime installer or native launch uses this mock.
3431    unsafe impl NativeExecProof for RefusingCompletion {
3432        fn owner(&self) -> DetTid {
3433            self.0.owner()
3434        }
3435        fn physical_ids(&self) -> Option<(i32, i32)> {
3436            self.0.physical_ids()
3437        }
3438        fn is_live(&self) -> bool {
3439            self.0.is_live()
3440        }
3441        fn matches_peer(&self, peer: &OwnedFd) -> bool {
3442            self.0.matches_peer(peer)
3443        }
3444        fn qualify_target(&self, request: &PrepareNativeExec) -> bool {
3445            self.0.qualify_target(request)
3446        }
3447        fn qualify_runtime(&self, bytes: &[u8]) -> bool {
3448            self.0.qualify_runtime(bytes)
3449        }
3450    }
3451    #[test]
3452    fn model_default_completion_refusal_is_terminal_without_retry() {
3453        let (_, proof, context, request) = fixture();
3454        let mut registry =
3455            NativeExecRegistry::new(Arc::new(RefusingCompletion(proof.clone())), [9; 32]).unwrap();
3456        let ticket = registry.prepare(context, request.clone()).unwrap();
3457        let digest = seal(&mut registry, context, ticket);
3458        // SAFETY: explicit model transfer only.
3459        unsafe {
3460            registry
3461                .begin_provision(&proof.pidfd, ticket)
3462                .unwrap()
3463                .complete()
3464        }
3465        .unwrap();
3466        registry
3467            .issue(context.owner, ticket, context.prepared_mm, digest)
3468            .unwrap();
3469        registry
3470            .redeem(&proof.pidfd, ticket, context.config_digest, digest)
3471            .unwrap();
3472        assert_eq!(
3473            registry.complete(context.owner, ticket, context.prepared_mm),
3474            Err(ExecControlError::Qualification)
3475        );
3476        assert_eq!(registry.stage(ticket), Some(ExecStage::Failed));
3477        assert_eq!(
3478            registry.complete(context.owner, ticket, context.prepared_mm),
3479            Err(ExecControlError::Dead)
3480        );
3481        assert_eq!(
3482            registry.prepare(context, request),
3483            Err(ExecControlError::Dead)
3484        );
3485        assert_eq!(
3486            registry.cancel(context.owner, ticket, context.prepared_mm),
3487            Err(ExecControlError::Dead)
3488        );
3489    }
3490
3491    #[test]
3492    fn model_connection_close_is_incarnation_specific_and_survives_delayed_eof() {
3493        let (mut registry, proof, context, request) = fixture();
3494        assert_eq!(
3495            registry.permits_connection_close(&proof.pidfd, None),
3496            Ok(false)
3497        );
3498        let wrong: OwnedFd = std::fs::File::open("/dev/null").unwrap().into();
3499        assert_eq!(
3500            registry.permits_connection_close(&wrong, None),
3501            Err(ExecControlError::Identity)
3502        );
3503        let first = registry.prepare(context, request.clone()).unwrap();
3504        assert_eq!(
3505            registry.permits_connection_close(&proof.pidfd, None),
3506            Ok(false)
3507        );
3508        let digest = seal(&mut registry, context, first);
3509        // SAFETY: existing explicit registry model transfer, not native launch.
3510        unsafe {
3511            registry
3512                .begin_provision(&proof.pidfd, first)
3513                .unwrap()
3514                .complete()
3515        }
3516        .unwrap();
3517        registry
3518            .issue(context.owner, first, context.prepared_mm, digest)
3519            .unwrap();
3520        assert_eq!(
3521            registry.permits_connection_close(&proof.pidfd, None),
3522            Ok(true)
3523        );
3524        registry
3525            .redeem(&proof.pidfd, first, context.config_digest, digest)
3526            .unwrap();
3527        registry
3528            .complete(context.owner, first, context.prepared_mm)
3529            .unwrap();
3530        assert_eq!(
3531            registry.permits_connection_close(&proof.pidfd, None),
3532            Ok(true)
3533        );
3534        assert_eq!(
3535            registry.permits_connection_close(&proof.pidfd, Some(first)),
3536            Ok(false)
3537        );
3538        let next_context = NativeExecContext {
3539            old_mm: context.prepared_mm,
3540            prepared_mm: context.prepared_mm.for_exec(context.owner),
3541            ..context
3542        };
3543        let next_request = PrepareNativeExec {
3544            cut: NativeExecNeutralBinding {
3545                old_mm: next_context.old_mm,
3546                old_files: request.cut.old_files.for_exec(context.owner),
3547                prepared_mm: next_context.prepared_mm,
3548                ..request.cut
3549            },
3550            prepared_mm: next_context.prepared_mm,
3551            ..request
3552        };
3553        let second = registry.prepare(next_context, next_request).unwrap();
3554        assert_eq!(
3555            registry.permits_connection_close(&proof.pidfd, Some(first)),
3556            Ok(false)
3557        );
3558        let digest = seal(&mut registry, next_context, second);
3559        // SAFETY: existing explicit registry model transfer, not native launch.
3560        unsafe {
3561            registry
3562                .begin_provision(&proof.pidfd, second)
3563                .unwrap()
3564                .complete()
3565        }
3566        .unwrap();
3567        registry
3568            .issue(context.owner, second, next_context.prepared_mm, digest)
3569            .unwrap();
3570        registry
3571            .redeem(&proof.pidfd, second, context.config_digest, digest)
3572            .unwrap();
3573        registry
3574            .complete(context.owner, second, next_context.prepared_mm)
3575            .unwrap();
3576        assert_eq!(
3577            registry.permits_connection_close(&proof.pidfd, None),
3578            Ok(true)
3579        );
3580        assert_eq!(
3581            registry.permits_connection_close(&proof.pidfd, Some(first)),
3582            Ok(true)
3583        );
3584        assert_eq!(
3585            registry.permits_connection_close(&proof.pidfd, Some(second)),
3586            Ok(false)
3587        );
3588    }
3589
3590    #[test]
3591    fn model_cancelled_issue_cannot_admit_connection_close() {
3592        let (mut registry, proof, context, request) = fixture();
3593        let ticket = registry.prepare(context, request).unwrap();
3594        let digest = seal(&mut registry, context, ticket);
3595        // SAFETY: existing explicit registry model transfer, not native launch.
3596        unsafe {
3597            registry
3598                .begin_provision(&proof.pidfd, ticket)
3599                .unwrap()
3600                .complete()
3601        }
3602        .unwrap();
3603        registry
3604            .issue(context.owner, ticket, context.prepared_mm, digest)
3605            .unwrap();
3606        registry
3607            .cancel(context.owner, ticket, context.prepared_mm)
3608            .unwrap();
3609        assert_eq!(
3610            registry.permits_connection_close(&proof.pidfd, None),
3611            Ok(false)
3612        );
3613        assert_eq!(
3614            registry.permits_connection_close(&proof.pidfd, Some(ticket)),
3615            Err(ExecControlError::Transition)
3616        );
3617    }
3618}