Skip to main content

detcore/
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//! Inactive, backend-neutral validation of a prepared single-thread exec cut.
10//!
11//! These APIs inspect actual Detcore data. They do not authorize kernel exec,
12//! observe a physical task, install a backend, or run a lifecycle callback.
13//! The caller must own the quiescent dispatch and separately prove the admitted
14//! one-main-thread/childless domain, physical identity and registrations.
15//! In particular, probing an Arc's lock does not establish that no other task
16//! owns it. No ordinary exec path calls this module.
17//!
18//! The transport consumer must bound the payload before allocation/decode,
19//! authenticate its exact bytes and Config, and reject trailing bytes. It must
20//! retain the same live cut through its final physical exec edge. Deserializing
21//! serde-skipped capabilities cannot reconstruct them; the live producer must
22//! validate them before preparing the replacement table.
23
24use std::fmt;
25use std::io;
26use std::sync::Arc;
27use std::sync::Mutex;
28use std::sync::MutexGuard;
29
30use serde::Deserialize;
31use serde::Serialize;
32use sha2::Digest as _;
33use sha2::Sha256;
34
35use crate::Config;
36use crate::Detcore;
37use crate::FileMetadata;
38use crate::ThreadState;
39/// The concrete empty subtool supported by this first typed snapshot domain.
40pub use crate::record_or_replay::NoopTool;
41use crate::types::DetPid;
42use crate::types::DetTid;
43use crate::types::FilesId;
44use crate::types::MmId;
45
46/// Version of the seven-field Detcore prepared-exec payload.
47pub const DETCORE_EXEC_SNAPSHOT_VERSION: u16 = 2;
48
49/// Maximum encoded snapshot size enforced by the concrete transport consumer.
50pub const MAX_SNAPSHOT_BYTES: usize = 1024 * 1024;
51
52/// A data validation refusal, not a backend errno or admission capability.
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum NativeExecCutError {
55    /// The supplied binding or a Tool/state/header identity disagrees.
56    Identity(&'static str),
57    /// The immutable configuration is outside the named snapshot domain.
58    Configuration(&'static str),
59    /// Actual embedded Config bytes disagree with the admitted digest.
60    ConfigDigest,
61    /// Existing Config serialization failed.
62    ConfigEncoding,
63    /// A skipped runtime capability or live operation remains.
64    LiveCapability(&'static str),
65    /// A supposedly quiescent model is locked or poisoned.
66    LockUnavailable(&'static str),
67    /// The replacement state was not prepared as the existing exec path does.
68    PreparedState(&'static str),
69    /// A table slot or its within-table alias identity is inconsistent.
70    Descriptor(&'static str),
71    /// The decoded payload version is unsupported.
72    Version,
73}
74
75impl fmt::Display for NativeExecCutError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        let (category, field) = match *self {
78            Self::Identity(field) => ("identity mismatch", field),
79            Self::Configuration(field) => ("unsupported configuration", field),
80            Self::ConfigDigest => ("Config digest mismatch", "Config"),
81            Self::ConfigEncoding => ("Config encoding failed", "Config"),
82            Self::LiveCapability(field) => ("live runtime capability", field),
83            Self::LockUnavailable(field) => ("model locked or poisoned", field),
84            Self::PreparedState(field) => ("unprepared replacement state", field),
85            Self::Descriptor(field) => ("descriptor mismatch", field),
86            Self::Version => ("unsupported snapshot version", "version"),
87        };
88        write!(f, "native exec cut {category}: {field}")
89    }
90}
91
92impl std::error::Error for NativeExecCutError {}
93
94/// Bind the genuine initial physical leader before its first thread-start.
95/// This does not initialize deterministic process identity or run a callback.
96///
97/// # Safety
98/// The caller owns the actual authenticated InitialImage lifecycle and passes
99/// PID/TID read from that same current kernel task through its qualified gate.
100/// No guest work has run; this is the unique freshly constructed initial state.
101pub unsafe fn bind_native_initial_physical_identity(
102    thread: &mut ThreadState<()>,
103    physical_pid: i32,
104    physical_tid: i32,
105) -> Result<(), NativeExecCutError> {
106    if physical_tid <= 0
107        || physical_pid != physical_tid
108        || thread.dettid.as_raw() != physical_tid
109        || thread.physical_tid.is_some()
110        || thread.detpid.is_some()
111        || thread.thread_start_entered
112        || thread.mm_id != MmId::initial(thread.dettid)
113        || try_native_exec_lock(&thread.file_metadata, "initial file_metadata")?
114            .native_exec_files_id()
115            != FilesId::initial(thread.dettid)
116    {
117        return Err(NativeExecCutError::Identity("initial physical state"));
118    }
119    if thread.signal_task_identity.is_some() {
120        return Err(NativeExecCutError::LiveCapability("signal_task_identity"));
121    }
122    thread.physical_tid = Some(physical_tid);
123    Ok(())
124}
125
126/// Rebind from the actual fresh Guest answer before native post-exec.
127/// The first domain has no admitted signal-control capability; equal None is
128/// supported data, never a fabricated task-lifetime identity.
129pub fn bind_native_fresh_signal_identity(
130    thread: &mut ThreadState<()>,
131    mode: reverie::BackendSignalControlMode,
132    actual: Option<reverie::SignalTaskIdentity>,
133) -> Result<(), NativeExecCutError> {
134    if mode != reverie::BackendSignalControlMode::Unchanged
135        || thread.signal_task_identity.is_some()
136        || actual.is_some()
137    {
138        return Err(NativeExecCutError::LiveCapability("signal_task_identity"));
139    }
140    thread.signal_task_identity = actual;
141    Ok(())
142}
143
144/// Independently supplied identities of the existing prepared transaction.
145///
146/// This is data, not proof of watched birth, physical state, or childlessness.
147/// The old mm/table identities must come from the live owned transaction,
148/// never from the payload being validated.
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct NativeExecCutBinding {
151    /// Deterministic main thread identity.
152    pub dettid: DetTid,
153    /// Continuing deterministic process identity; equal to dettid here.
154    pub detpid: DetPid,
155    /// Actual continuing physical main-thread ID proved by the backend.
156    pub physical_tid: i32,
157    /// Live old-image memory incarnation.
158    pub old_mm: MmId,
159    /// Live old-image descriptor-table incarnation.
160    pub old_files: FilesId,
161    /// Independently admitted SHA-256 of the unchanged legacy wire Config.
162    pub config_digest: [u8; 32],
163}
164
165impl NativeExecCutBinding {
166    fn validate(&self) -> Result<(), NativeExecCutError> {
167        if self.dettid != self.detpid || self.detpid.as_raw() <= 0 {
168            return Err(NativeExecCutError::Identity("main-thread IDs"));
169        }
170        if self.physical_tid <= 0 {
171            return Err(NativeExecCutError::Identity("physical_tid"));
172        }
173        if self.old_mm.generation() == u64::MAX || self.old_files.generation() == u64::MAX {
174            return Err(NativeExecCutError::Identity("incarnation overflow"));
175        }
176        Ok(())
177    }
178
179    /// Replacement mm produced by the existing same-leader exec transition.
180    pub fn prepared_mm(&self) -> MmId {
181        self.old_mm.for_exec(self.detpid)
182    }
183
184    /// Replacement descriptor-table identity produced by the existing path.
185    pub fn prepared_files(&self) -> FilesId {
186        self.old_files.for_exec(self.dettid)
187    }
188}
189
190/// Dispatch-owned evidence that the actual old table was inspected.
191///
192/// This deliberately implements neither Serialize/Deserialize nor Clone.
193/// Retaining the old table also lets the prepared cut recheck its skipped
194/// capabilities, including descriptions whose last slot was CLOEXEC.
195/// It is not a global admission or physical-exec authority.
196#[derive(Debug)]
197pub struct NativeExecPreparationReceipt {
198    binding: NativeExecCutBinding,
199    old_table: Arc<Mutex<FileMetadata>>,
200}
201
202/// A borrowed encoding of the actual already-prepared Tool and main state.
203///
204/// Field order and representation match the owned version. No new state is
205/// constructed and no PRNG, accounting or clock observation is performed.
206/// The receipt borrow keeps its old-table ownership alive during encoding.
207/// The caller must still keep all interior model data quiescent.
208#[derive(Debug, Serialize)]
209pub struct DetcoreExecSnapshotRefV2<'a> {
210    version: u16,
211    dettid: DetTid,
212    detpid: DetPid,
213    prepared_mm: MmId,
214    config_digest: [u8; 32],
215    tool: &'a Detcore<NoopTool>,
216    thread: &'a ThreadState<()>,
217    #[serde(skip)]
218    _receipt: &'a NativeExecPreparationReceipt,
219}
220
221/// Owned data decoded only after exact bounded transport authentication.
222///
223/// Deserialization restores the existing within-table OFD aliases. It cannot
224/// prove that skipped producer capabilities were absent before serialization.
225/// Use `into_validated_parts` before attaching this data to a replacement host.
226#[derive(Debug, Serialize, Deserialize)]
227pub struct DetcoreExecSnapshotV2 {
228    version: u16,
229    dettid: DetTid,
230    detpid: DetPid,
231    prepared_mm: MmId,
232    config_digest: [u8; 32],
233    tool: Detcore<NoopTool>,
234    thread: ThreadState<()>,
235}
236
237impl DetcoreExecSnapshotV2 {
238    /// Check the decoded header and actual state against independent admission.
239    ///
240    /// The expected Config is independently obtained from the admitted run.
241    /// This leaves every field and capability untouched. Backend incarnation
242    /// rebinding (including signal_task_identity) remains the host's explicit
243    /// responsibility before any restored lifecycle use.
244    pub fn validate(
245        &self,
246        binding: &NativeExecCutBinding,
247        expected_config: &Config,
248    ) -> Result<(), NativeExecCutError> {
249        binding.validate()?;
250        if self.version != DETCORE_EXEC_SNAPSHOT_VERSION {
251            return Err(NativeExecCutError::Version);
252        }
253        if self.dettid != binding.dettid || self.detpid != binding.detpid {
254            return Err(NativeExecCutError::Identity("snapshot IDs"));
255        }
256        if self.prepared_mm != binding.prepared_mm() {
257            return Err(NativeExecCutError::Identity("snapshot prepared_mm"));
258        }
259        // The admitted host Config can retain host-only output paths. Its
260        // omitted semantic fields must nevertheless be checked before wire
261        // equality loses them.
262        validate_native_exec_host_configuration(expected_config)?;
263        if self.config_digest != binding.config_digest
264            || native_exec_config_digest(expected_config)? != binding.config_digest
265        {
266            return Err(NativeExecCutError::ConfigDigest);
267        }
268        validate_tool_and_thread(&self.tool, &self.thread, binding)?;
269        self.thread.validate_prepared_native_exec_cut(binding)
270    }
271
272    /// Consume this payload only after all neutral validation succeeds.
273    pub fn into_validated_parts(
274        self,
275        binding: &NativeExecCutBinding,
276        expected_config: &Config,
277    ) -> Result<(Detcore<NoopTool>, ThreadState<()>), NativeExecCutError> {
278        self.validate(binding, expected_config)?;
279        Ok((self.tool, self.thread))
280    }
281}
282
283/// Validate actual old state before `FileMetadata::for_exec` drops capabilities.
284///
285/// This is an explicit inactive API; it is not an added ordinary syscall hook.
286pub fn validate_old_native_exec_cut(
287    tool: &Detcore<NoopTool>,
288    thread: &ThreadState<()>,
289    binding: &NativeExecCutBinding,
290) -> Result<NativeExecPreparationReceipt, NativeExecCutError> {
291    binding.validate()?;
292    validate_tool_and_thread(tool, thread, binding)?;
293    if thread.mm_id != binding.old_mm {
294        return Err(NativeExecCutError::Identity("old mm"));
295    }
296    {
297        let table = try_native_exec_lock(&thread.file_metadata, "old file_metadata")?;
298        table.validate_native_exec_cut(binding.old_files, false)?;
299    }
300    thread.validate_old_native_exec_robust_state()?;
301    Ok(NativeExecPreparationReceipt {
302        binding: *binding,
303        old_table: Arc::clone(&thread.file_metadata),
304    })
305}
306
307/// Borrow the actual prepared state, bound to its live old-table receipt.
308///
309/// Both old and prepared tables are checked without comparing their total Arc
310/// counts: the existing rollback future legitimately owns old-table aliases.
311pub fn capture_prepared_native_exec<'a>(
312    tool: &'a Detcore<NoopTool>,
313    thread: &'a ThreadState<()>,
314    binding: &NativeExecCutBinding,
315    receipt: &'a NativeExecPreparationReceipt,
316) -> Result<DetcoreExecSnapshotRefV2<'a>, NativeExecCutError> {
317    binding.validate()?;
318    if receipt.binding != *binding {
319        return Err(NativeExecCutError::Identity("preparation receipt"));
320    }
321    {
322        let old = try_native_exec_lock(&receipt.old_table, "old file_metadata")?;
323        old.validate_native_exec_cut(binding.old_files, false)?;
324    }
325    if Arc::ptr_eq(&receipt.old_table, &thread.file_metadata) {
326        return Err(NativeExecCutError::PreparedState(
327            "replacement table ownership",
328        ));
329    }
330    validate_tool_and_thread(tool, thread, binding)?;
331    thread.validate_prepared_native_exec_cut(binding)?;
332    Ok(DetcoreExecSnapshotRefV2 {
333        version: DETCORE_EXEC_SNAPSHOT_VERSION,
334        dettid: binding.dettid,
335        detpid: binding.detpid,
336        prepared_mm: binding.prepared_mm(),
337        config_digest: binding.config_digest,
338        tool,
339        thread,
340        _receipt: receipt,
341    })
342}
343
344/// Compare the final actual prepared state directly against already sealed
345/// bytes, using the unchanged legacy serializer without a temporary encoding.
346/// The caller retains the same exclusive dispatch and old-table receipt.
347/// This checks the complete encoding, including exact length, and never resets
348/// accounting, advances a PRNG, or constructs a replacement model.
349pub fn check_prepared_native_exec_bytes(
350    tool: &Detcore<NoopTool>,
351    thread: &ThreadState<()>,
352    binding: &NativeExecCutBinding,
353    receipt: &NativeExecPreparationReceipt,
354    expected: &[u8],
355) -> Result<(), NativeExecCutError> {
356    if expected.is_empty() || expected.len() > MAX_SNAPSHOT_BYTES {
357        return Err(NativeExecCutError::PreparedState("sealed snapshot bounds"));
358    }
359    let live = capture_prepared_native_exec(tool, thread, binding, receipt)?;
360    compare_encoding(&live, expected)
361}
362
363fn compare_encoding<T: Serialize + ?Sized>(
364    live: &T,
365    expected: &[u8],
366) -> Result<(), NativeExecCutError> {
367    struct CompareWriter<'a> {
368        expected: &'a [u8],
369        position: usize,
370        failed: bool,
371    }
372    impl io::Write for CompareWriter<'_> {
373        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
374            let end = self.position.checked_add(bytes.len());
375            if self.failed
376                || end.is_none_or(|end| self.expected.get(self.position..end) != Some(bytes))
377            {
378                self.failed = true;
379                return Err(io::ErrorKind::InvalidData.into());
380            }
381            self.position = end.unwrap();
382            Ok(bytes.len())
383        }
384        fn flush(&mut self) -> io::Result<()> {
385            Ok(())
386        }
387    }
388    let mut writer = CompareWriter {
389        expected,
390        position: 0,
391        failed: false,
392    };
393    let result =
394        bincode::serde::encode_into_std_write(live, &mut writer, bincode::config::legacy());
395    if result.is_err() || writer.failed || writer.position != expected.len() {
396        return Err(NativeExecCutError::PreparedState("sealed snapshot changed"));
397    }
398    Ok(())
399}
400
401fn validate_tool_and_thread(
402    tool: &Detcore<NoopTool>,
403    thread: &ThreadState<()>,
404    binding: &NativeExecCutBinding,
405) -> Result<(), NativeExecCutError> {
406    if tool.detpid != binding.detpid {
407        return Err(NativeExecCutError::Identity("Tool detpid"));
408    }
409    validate_native_exec_guest_configuration(&tool.cfg)?;
410    if native_exec_config_digest(&tool.cfg)? != binding.config_digest {
411        return Err(NativeExecCutError::ConfigDigest);
412    }
413    thread.validate_native_exec_cut(binding, &tool.cfg)
414}
415
416/// SHA-256 of the actual unchanged Config in the existing legacy bincode form.
417///
418/// This is a neutral identity operation, not transport framing or admission.
419/// It streams into the existing hash dependency instead of cloning Config.
420pub fn native_exec_config_digest(config: &Config) -> Result<[u8; 32], NativeExecCutError> {
421    struct HashWriter(Sha256);
422    impl io::Write for HashWriter {
423        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
424            self.0.update(bytes);
425            Ok(bytes.len())
426        }
427
428        fn flush(&mut self) -> io::Result<()> {
429            Ok(())
430        }
431    }
432    let mut writer = HashWriter(Sha256::new());
433    bincode::serde::encode_into_std_write(config, &mut writer, bincode::config::legacy())
434        .map_err(|_| NativeExecCutError::ConfigEncoding)?;
435    Ok(writer.0.finalize().into())
436}
437
438/// Check the actual host Config before granting native InitialImage or exec authority.
439///
440/// `host_input_log` and `fatal_core_capture` belong to the continuing host
441/// GlobalState. This policy preserves them without copying them to the guest.
442/// Their presence does not establish that the caller retained that owner; the
443/// launcher must keep it alive through exec and the final physical drain.
444/// Neither this check nor a Config digest establishes runtime admission.
445pub fn validate_native_exec_host_configuration(config: &Config) -> Result<(), NativeExecCutError> {
446    validate_native_exec_common_configuration(config)
447}
448
449/// Check the live guest Config before snapshotting or using a restored Tool.
450///
451/// Serde omits these fields, so a wire digest cannot establish their absence.
452/// The continuing host owns output paths; the guest must not carry a discarded
453/// copy. This function inspects the Config without clearing or rebinding it.
454pub fn validate_native_exec_guest_configuration(config: &Config) -> Result<(), NativeExecCutError> {
455    validate_native_exec_common_configuration(config)?;
456    if config.host_input_log.is_some() {
457        return Err(NativeExecCutError::Configuration("host_input_log"));
458    }
459    if config.fatal_core_capture.is_some() {
460        return Err(NativeExecCutError::Configuration("fatal_core_capture"));
461    }
462    Ok(())
463}
464
465fn validate_native_exec_common_configuration(config: &Config) -> Result<(), NativeExecCutError> {
466    // The guest branches on presence, including Some(empty). Do not silently
467    // turn a resolved program into None by passing it through serde.
468    if config.happens_before.is_some() {
469        return Err(NativeExecCutError::Configuration("happens_before"));
470    }
471    // This is an actual image address, never a transferable Config constant.
472    if config.untraced_code_range.is_some() {
473        return Err(NativeExecCutError::Configuration("untraced_code_range"));
474    }
475    if config.use_rcb_time() || config.max_timeslice.is_some() || config.target_timeslice.is_some()
476    {
477        return Err(NativeExecCutError::Configuration("RCB/timeslices"));
478    }
479    if config.imprecise_timers
480        || config.chaos
481        || config.chaos_target_races
482        || config.chaos_per_thread_slowdown
483    {
484        return Err(NativeExecCutError::Configuration("chaos/imprecise timers"));
485    }
486    if config.record_preemptions
487        || config.record_preemptions_to.is_some()
488        || config.replay_preemptions_from.is_some()
489        || config.replay_schedule_from.is_some()
490        || config.preemption_stacktrace
491        || config.preemption_stacktrace_log_file.is_some()
492        || !config.stacktrace_event.is_empty()
493        || config.stacktrace_signal.is_some()
494    {
495        return Err(NativeExecCutError::Configuration(
496            "preemption/replay/stacktrace",
497        ));
498    }
499    if config.recordreplay_modes || config.replaying || config.replay_data.is_some() {
500        return Err(NativeExecCutError::Configuration("record/replay"));
501    }
502    Ok(())
503}
504
505pub(crate) fn try_native_exec_lock<'a, T>(
506    lock: &'a Mutex<T>,
507    field: &'static str,
508) -> Result<MutexGuard<'a, T>, NativeExecCutError> {
509    lock.try_lock()
510        .map_err(|_| NativeExecCutError::LockUnavailable(field))
511}
512
513#[cfg(test)]
514pub(crate) mod tests {
515    use std::time::Duration;
516
517    use nix::fcntl::OFlag;
518    use rand::RngExt as _;
519
520    use super::*;
521    use crate::fd::DetFd;
522    use crate::fd::FdType;
523    use crate::memory::MemoryMetadata;
524    use crate::tool_local::ResourceLimit;
525    use crate::tool_local::RobustListWake;
526    use crate::types::FutexID;
527    use crate::types::LogicalTime;
528    use crate::types::OpenFileId;
529    use crate::types::RcbTimeMultiplier;
530    use crate::types::SharedMemoryObjectId;
531
532    pub(crate) fn fixture() -> (Detcore<NoopTool>, ThreadState<()>, NativeExecCutBinding) {
533        let owner = DetTid::from_raw(42);
534        let cfg = Config {
535            sequentialize_threads: true,
536            no_rcb_time: true,
537            max_timeslice: None,
538            target_timeslice: None,
539            rng_seed: Some(17),
540            sched_seed: Some(31),
541            clock_multiplier: Some(1.25),
542            ..Config::default()
543        };
544        let tool = Detcore {
545            detpid: owner,
546            cfg,
547            record_or_replay: NoopTool,
548        };
549        let mut thread = ThreadState::new(owner, &tool.cfg, ());
550        thread.detpid = Some(owner);
551        thread.physical_tid = Some(4242);
552        thread.thread_start_entered = true;
553        thread.past_global_first_execve = true;
554        let mut files = FileMetadata::new(owner);
555        for _ in 0..3 {
556            thread.mm_id = thread.mm_id.for_exec(owner);
557            files = files.for_exec(owner);
558        }
559        let old_files = files.files_id;
560        thread.file_metadata = Arc::new(Mutex::new(files));
561        let binding = NativeExecCutBinding {
562            dettid: owner,
563            detpid: owner,
564            physical_tid: 4242,
565            old_mm: thread.mm_id,
566            old_files,
567            config_digest: native_exec_config_digest(&tool.cfg).unwrap(),
568        };
569        (tool, thread, binding)
570    }
571
572    pub(crate) fn prepare(thread: &mut ThreadState<()>, binding: &NativeExecCutBinding) {
573        let files = thread
574            .file_metadata
575            .lock()
576            .unwrap()
577            .for_exec(binding.dettid);
578        thread.file_metadata = Arc::new(Mutex::new(files));
579        thread.mm_id = binding.prepared_mm();
580        thread.memory_metadata = Arc::new(Mutex::new(MemoryMetadata::new()));
581        thread.take_robust_list_for_exec();
582    }
583
584    // JSON Value cannot represent the full-width u128 PCG state. Compare all
585    // serialized fields in the actual wire format, without narrowing values.
586    // Cross-decode byte comparisons below use fixtures with empty HashMaps;
587    // nonempty descriptor maps are compared by entries in the alias test.
588    pub(crate) fn state_bytes<T: Serialize + ?Sized>(state: &T) -> Vec<u8> {
589        bincode::serde::encode_to_vec(state, bincode::config::legacy()).unwrap()
590    }
591
592    fn encoded(
593        tool: &Detcore<NoopTool>,
594        thread: &ThreadState<()>,
595        binding: &NativeExecCutBinding,
596        receipt: &NativeExecPreparationReceipt,
597    ) -> Vec<u8> {
598        bincode::serde::encode_to_vec(
599            capture_prepared_native_exec(tool, thread, binding, receipt).unwrap(),
600            bincode::config::legacy(),
601        )
602        .unwrap()
603    }
604
605    pub(crate) fn decoded(bytes: &[u8]) -> DetcoreExecSnapshotV2 {
606        let (state, consumed) = bincode::serde::decode_from_slice(
607            bytes,
608            bincode::config::legacy().with_limit::<MAX_SNAPSHOT_BYTES>(),
609        )
610        .unwrap();
611        assert_eq!(consumed, bytes.len());
612        state
613    }
614
615    pub(crate) fn thread_for_test(snapshot: &mut DetcoreExecSnapshotV2) -> &mut ThreadState<()> {
616        &mut snapshot.thread
617    }
618
619    #[test]
620    fn native_exec_v2_refuses_v1_before_returning_owned_state() {
621        let (tool, mut thread, binding) = fixture();
622        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
623        prepare(&mut thread, &binding);
624        let mut payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
625        assert_eq!(payload.version, 2);
626        payload.version = 1;
627        assert_eq!(
628            payload.validate(&binding, &tool.cfg),
629            Err(NativeExecCutError::Version)
630        );
631        assert_eq!(
632            payload
633                .into_validated_parts(&binding, &tool.cfg)
634                .unwrap_err(),
635            NativeExecCutError::Version
636        );
637    }
638
639    #[test]
640    fn native_exec_rejects_a_replay_change_point_before_snapshot() {
641        let (tool, mut thread, binding) = fixture();
642        assert_eq!(thread.last_replay_change_point, None);
643        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
644        thread.last_replay_change_point = Some(LogicalTime::from_nanos(1));
645        assert_eq!(
646            validate_old_native_exec_cut(&tool, &thread, &binding).unwrap_err(),
647            NativeExecCutError::LiveCapability("RCB/replay/chaos state")
648        );
649        thread.last_replay_change_point = None;
650        prepare(&mut thread, &binding);
651        capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap();
652        thread.last_replay_change_point = Some(LogicalTime::from_nanos(1));
653        assert_eq!(
654            capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap_err(),
655            NativeExecCutError::LiveCapability("RCB/replay/chaos state")
656        );
657    }
658
659    #[test]
660    fn native_exec_final_exact_comparison_keeps_state_and_refuses_every_byte_change() {
661        let (tool, mut thread, binding) = fixture();
662        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
663        prepare(&mut thread, &binding);
664        let bytes = encoded(&tool, &thread, &binding, &receipt);
665        let before = state_bytes(&thread);
666        check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes).unwrap();
667        assert_eq!(state_bytes(&thread), before);
668        for index in 0..bytes.len() {
669            let mut bad = bytes.clone();
670            bad[index] ^= 1;
671            assert!(
672                check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bad).is_err(),
673                "byte {index}"
674            );
675        }
676        for end in 0..bytes.len() {
677            assert!(
678                check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes[..end])
679                    .is_err(),
680                "truncation {end}"
681            );
682        }
683        let mut extra = bytes.clone();
684        extra.push(0);
685        assert!(
686            check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &extra).is_err()
687        );
688        assert_eq!(state_bytes(&thread), before);
689        let _ = thread.prng.random::<u64>();
690        assert!(
691            check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &bytes).is_err()
692        );
693        let changed = encoded(&tool, &thread, &binding, &receipt);
694        check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &changed).unwrap();
695        thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
696        assert!(
697            check_prepared_native_exec_bytes(&tool, &thread, &binding, &receipt, &changed).is_err()
698        );
699    }
700
701    #[test]
702    fn native_exec_final_comparison_latches_ignored_serializer_errors() {
703        struct Ignores;
704        impl Serialize for Ignores {
705            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
706                use serde::ser::SerializeTuple;
707                let mut tuple = serializer.serialize_tuple(2)?;
708                let _ = tuple.serialize_element(&99u64);
709                let _ = tuple.serialize_element(&17u64);
710                tuple.end()
711            }
712        }
713        assert!(compare_encoding(&Ignores, &17u64.to_le_bytes()).is_err());
714        let valid = state_bytes(&(99u64, 17u64));
715        compare_encoding(&Ignores, &valid).unwrap();
716    }
717
718    #[test]
719    fn native_exec_roundtrip_continues_full_noninitial_clock_accounting_and_prngs() {
720        let (tool, mut thread, binding) = fixture();
721        for _ in 0..13 {
722            let _ = thread.prng.random::<u64>();
723        }
724        for _ in 0..19 {
725            let _ = thread.chaos_prng.random::<u64>();
726        }
727        thread.pedigree = thread.pedigree.fork().1.fork().0;
728        thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
729        thread.stats.count_syscall(reverie::syscalls::Sysno::getpid);
730        thread.stats.count_signal();
731        thread.stats.timeslice_stats.record(173);
732        thread.timer_slack_ns = 137;
733        thread.default_timer_slack_ns = 91;
734        thread.resource_limits.lock().unwrap().set(
735            libc::RLIMIT_NOFILE,
736            ResourceLimit {
737                current: 97,
738                maximum: 211,
739            },
740        );
741        thread.thread_logical_time.add_syscall_with_cost(137);
742        thread
743            .thread_logical_time
744            .add_rcbs_with_multiplier(7, RcbTimeMultiplier::from_f64(1.5));
745        thread.thread_logical_time.add_cpuid();
746        thread.thread_logical_time = thread.thread_logical_time.clone_for_child();
747        thread.thread_cpu_start_user_time = thread.thread_logical_time.user_cpu_time();
748        thread.thread_cpu_start_system_time = thread.thread_logical_time.system_cpu_time();
749        thread.account_process_cpu_time();
750        thread.thread_logical_time.add_syscall_with_cost(19); // deliberately unaccounted
751        thread.thread_logical_time.add_rdtsc();
752        thread.committed_clock_value = 7;
753        let cut_time = thread.thread_logical_time.as_nanos() + Duration::from_nanos(43);
754        thread.thread_logical_time.advance_to(cut_time);
755        thread.observe_guest_clock(cut_time + Duration::from_nanos(23));
756        thread.end_of_timeslice = Some(cut_time + Duration::from_nanos(101));
757        thread.last_grant_deadline = Some(Duration::from_nanos(211));
758        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
759        prepare(&mut thread, &binding);
760        let before = state_bytes(&thread);
761        let bytes = encoded(&tool, &thread, &binding, &receipt);
762        assert_eq!(
763            encoded(&tool, &thread, &binding, &receipt),
764            bytes,
765            "validation must not advance or normalize the live state"
766        );
767        let payload = decoded(&bytes);
768        payload.validate(&binding, &tool.cfg).unwrap();
769        assert_eq!(state_bytes(&payload.thread), before);
770        assert_ne!(
771            payload.thread.thread_logical_time.inherited_nanos(),
772            LogicalTime::ZERO
773        );
774        assert_eq!(payload.thread.thread_logical_time.as_nanos(), cut_time);
775        assert_eq!(
776            payload.thread.observe_guest_clock(LogicalTime::ZERO),
777            cut_time + Duration::from_nanos(23)
778        );
779        // Decode a second independent oracle: Arc::clone would share its
780        // accounting/floor models and conceal a reset or double charge.
781        let mut oracle = decoded(&bytes).thread;
782        let (_, mut restored) = payload.into_validated_parts(&binding, &tool.cfg).unwrap();
783        for cost in [1, 7, 103, 29, 2, 71, 11, 43] {
784            assert_eq!(restored.prng.random::<u64>(), oracle.prng.random::<u64>());
785            assert_eq!(
786                restored.chaos_prng.random::<u64>(),
787                oracle.chaos_prng.random::<u64>()
788            );
789            let previous = restored.thread_logical_time.as_nanos();
790            restored.thread_logical_time.add_syscall_with_cost(cost);
791            oracle.thread_logical_time.add_syscall_with_cost(cost);
792            restored.thread_logical_time.add_cpuid();
793            oracle.thread_logical_time.add_cpuid();
794            assert!(restored.thread_logical_time.as_nanos() > previous);
795            assert_eq!(
796                state_bytes(&restored.thread_logical_time),
797                state_bytes(&oracle.thread_logical_time)
798            );
799            let now = restored.thread_logical_time.as_nanos();
800            assert_eq!(
801                restored.observe_guest_clock(now),
802                oracle.observe_guest_clock(now)
803            );
804            restored.account_process_cpu_time();
805            oracle.account_process_cpu_time();
806            let accounted = state_bytes(&*restored.process_cpu_time.lock().unwrap());
807            restored.account_process_cpu_time();
808            assert_eq!(
809                state_bytes(&*restored.process_cpu_time.lock().unwrap()),
810                accounted
811            );
812            assert_eq!(
813                accounted,
814                state_bytes(&*oracle.process_cpu_time.lock().unwrap())
815            );
816            assert_eq!(state_bytes(&restored), state_bytes(&oracle));
817        }
818    }
819
820    #[test]
821    fn native_exec_prepared_aliases_cloexec_and_old_receipt_are_preserved() {
822        let (tool, mut thread, binding) = fixture();
823        thread.add_fd(30, OFlag::O_RDWR, FdType::Rng, None).unwrap();
824        thread.dup_fd(30, 31, OFlag::empty()).unwrap();
825        thread.dup_fd(30, 32, OFlag::O_CLOEXEC).unwrap();
826        thread
827            .add_fd(33, OFlag::O_CLOEXEC, FdType::Rng, None)
828            .unwrap();
829        thread
830            .add_fd(34, OFlag::empty(), FdType::Rng, None)
831            .unwrap();
832        thread
833            .with_detfd(31, |fd| fd.advance_random_device_offset(37))
834            .unwrap();
835        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
836        prepare(&mut thread, &binding);
837        // The receipt and actual rollback table retain additional Arc owners.
838        assert!(
839            receipt
840                .old_table
841                .lock()
842                .unwrap()
843                .file_handles
844                .contains_key(&33)
845        );
846        let old_prepared = serde_json::to_value(&*thread.file_metadata.lock().unwrap()).unwrap();
847        let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
848        payload.validate(&binding, &tool.cfg).unwrap();
849        let table = payload.thread.file_metadata.lock().unwrap();
850        assert_eq!(serde_json::to_value(&*table).unwrap(), old_prepared);
851        assert!(!table.file_handles.contains_key(&32));
852        assert!(!table.file_handles.contains_key(&33));
853        let first = table.file_handles[&30]
854            .validate_native_exec_runtime_state()
855            .unwrap();
856        let alias = table.file_handles[&31]
857            .validate_native_exec_runtime_state()
858            .unwrap();
859        let distinct = table.file_handles[&34]
860            .validate_native_exec_runtime_state()
861            .unwrap();
862        assert_eq!(first, alias);
863        assert_ne!(first.0, distinct.0);
864        assert_ne!(first.1, distinct.1);
865        table.file_handles[&31].advance_random_device_offset(11);
866        assert_eq!(table.file_handles[&30].random_device_offset(), 48);
867        assert_eq!(table.file_handles[&34].random_device_offset(), 0);
868        drop(table);
869        for (slot, kind) in [(40, FdType::Rng), (41, FdType::Socket), (42, FdType::Rng)] {
870            thread.add_fd(slot, OFlag::empty(), kind, None).unwrap();
871            payload
872                .thread
873                .add_fd(slot, OFlag::empty(), kind, None)
874                .unwrap();
875            assert_eq!(
876                thread.with_detfd(slot, |fd| fd.open_file_id()).unwrap(),
877                payload
878                    .thread
879                    .with_detfd(slot, |fd| fd.open_file_id())
880                    .unwrap(),
881                "both independent OFD allocators must continue their saved sequence"
882            );
883        }
884        // A held lease on a description absent from the prepared table must
885        // still be refused by the live old-table receipt.
886        let lock = receipt.old_table.lock().unwrap().file_handles[&33].directory_lock();
887        let lease = lock.try_lock().unwrap();
888        assert!(matches!(
889            capture_prepared_native_exec(&tool, &thread, &binding, &receipt),
890            Err(NativeExecCutError::LiveCapability("directory lease"))
891        ));
892        drop(lease);
893        capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap();
894    }
895
896    #[test]
897    fn native_exec_live_alias_conflict_is_refused_before_interning() {
898        let (tool, thread, binding) = fixture();
899        let id = OpenFileId::new(binding.dettid, 7);
900        let mut table = thread.file_metadata.lock().unwrap();
901        table
902            .file_handles
903            .insert(3, DetFd::new(3, OFlag::empty(), FdType::Rng, id));
904        table
905            .file_handles
906            .insert(4, DetFd::new(4, OFlag::empty(), FdType::Rng, id));
907        drop(table);
908        assert!(matches!(
909            validate_old_native_exec_cut(&tool, &thread, &binding),
910            Err(NativeExecCutError::Descriptor("OFD alias identity"))
911        ));
912    }
913
914    #[test]
915    fn native_exec_staged_robust_wakes_keep_old_keys_and_are_consumed_once() {
916        let (tool, mut thread, binding) = fixture();
917        thread.record_robust_list_head(Some(0x123000));
918        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
919        prepare(&mut thread, &binding);
920        let shared = SharedMemoryObjectId::File {
921            device: 7,
922            inode: 11,
923        };
924        thread.stage_robust_list_wakes_for_exec(vec![(
925            binding.dettid,
926            vec![
927                RobustListWake {
928                    futex: FutexID::private(binding.old_mm, 0x456000),
929                },
930                RobustListWake {
931                    futex: FutexID::shared(shared, 64),
932                },
933                RobustListWake {
934                    futex: FutexID::private(binding.old_mm, 0x456008),
935                },
936            ],
937        )]);
938        let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
939        payload.validate(&binding, &tool.cfg).unwrap();
940        assert_eq!(state_bytes(&payload.thread), state_bytes(&thread));
941        let actual = payload.thread.take_robust_list_wakes_for_exec();
942        assert_eq!(actual, thread.take_robust_list_wakes_for_exec());
943        assert_eq!(actual.len(), 3);
944        assert!(payload.thread.take_robust_list_wakes_for_exec().is_empty());
945        assert_ne!(
946            FutexID::private(binding.old_mm, 0x456000),
947            FutexID::private(binding.prepared_mm(), 0x456000)
948        );
949    }
950
951    #[test]
952    fn native_exec_binding_config_and_prepared_identity_mismatches_refuse() {
953        let (tool, mut thread, binding) = fixture();
954        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
955        prepare(&mut thread, &binding);
956        let bytes = encoded(&tool, &thread, &binding, &receipt);
957        let mut changed_config = tool.cfg.clone();
958        changed_config.rng_seed = Some(23);
959        assert_eq!(
960            decoded(&bytes).validate(&binding, &changed_config),
961            Err(NativeExecCutError::ConfigDigest)
962        );
963        for field in 0..7 {
964            let mut payload = decoded(&bytes);
965            match field {
966                0 => payload.version += 1,
967                1 => payload.dettid = DetTid::from_raw(43),
968                2 => payload.detpid = DetTid::from_raw(43),
969                3 => payload.prepared_mm = binding.old_mm,
970                4 => payload.config_digest[0] ^= 1,
971                5 => payload.tool.detpid = DetTid::from_raw(43),
972                6 => payload.thread.physical_tid = Some(4243),
973                _ => unreachable!(),
974            }
975            assert!(
976                payload.validate(&binding, &tool.cfg).is_err(),
977                "field {field}"
978            );
979        }
980        let mut other_binding = binding;
981        other_binding.physical_tid += 1;
982        assert!(capture_prepared_native_exec(&tool, &thread, &other_binding, &receipt).is_err());
983        thread.mm_id = binding.old_mm;
984        assert!(capture_prepared_native_exec(&tool, &thread, &binding, &receipt).is_err());
985    }
986
987    #[test]
988    fn native_exec_guest_config_refuses_skipped_values_with_equal_wire_bytes() {
989        for field in [
990            "happens_before",
991            "host_input_log",
992            "fatal_core_capture",
993            "untraced_code_range",
994        ] {
995            let (mut tool, mut thread, binding) = fixture();
996            let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
997            prepare(&mut thread, &binding);
998            let mut payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
999            let expected_config = tool.cfg.clone();
1000            let wire = state_bytes(&tool.cfg);
1001            set_skipped_config_field(&mut tool.cfg, field);
1002            set_skipped_config_field(&mut payload.tool.cfg, field);
1003            assert_eq!(state_bytes(&tool.cfg), wire, "field {field}");
1004            assert_eq!(state_bytes(&payload.tool.cfg), wire, "field {field}");
1005            assert_eq!(
1006                native_exec_config_digest(&tool.cfg).unwrap(),
1007                binding.config_digest,
1008                "the wire digest alone cannot establish this policy"
1009            );
1010            assert_eq!(
1011                capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap_err(),
1012                NativeExecCutError::Configuration(field),
1013                "producer must refuse {field} before encoding loses it"
1014            );
1015            assert_eq!(
1016                payload.validate(&binding, &expected_config),
1017                Err(NativeExecCutError::Configuration(field)),
1018                "consumer must refuse live skipped value {field}"
1019            );
1020        }
1021    }
1022
1023    #[test]
1024    fn native_exec_expected_config_cannot_hide_unsupported_host_state() {
1025        for field in ["happens_before", "untraced_code_range"] {
1026            let (tool, mut thread, binding) = fixture();
1027            let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
1028            prepare(&mut thread, &binding);
1029            let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
1030            let mut expected_config = tool.cfg.clone();
1031            set_skipped_config_field(&mut expected_config, field);
1032            assert_eq!(state_bytes(&expected_config), state_bytes(&tool.cfg));
1033            assert_eq!(
1034                payload.validate(&binding, &expected_config),
1035                Err(NativeExecCutError::Configuration(field)),
1036                "independent admission must not lose {field} through its digest"
1037            );
1038        }
1039    }
1040
1041    #[test]
1042    fn native_exec_expected_config_keeps_host_only_verification_outputs() {
1043        let (mut tool, mut thread, mut binding) = fixture();
1044        tool.cfg.record_host_inputs = true;
1045        binding.config_digest = native_exec_config_digest(&tool.cfg).unwrap();
1046        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
1047        prepare(&mut thread, &binding);
1048        let payload = decoded(&encoded(&tool, &thread, &binding, &receipt));
1049        let mut expected_config = tool.cfg.clone();
1050        set_skipped_config_field(&mut expected_config, "host_input_log");
1051        set_skipped_config_field(&mut expected_config, "fatal_core_capture");
1052        let host_input_log = expected_config.host_input_log.clone();
1053        let fatal_core_capture = expected_config.fatal_core_capture.clone();
1054        assert_eq!(state_bytes(&expected_config), state_bytes(&tool.cfg));
1055        payload.validate(&binding, &expected_config).unwrap();
1056        assert_eq!(expected_config.host_input_log, host_input_log);
1057        assert_eq!(expected_config.fatal_core_capture, fatal_core_capture);
1058        assert!(payload.tool.cfg.host_input_log.is_none());
1059        assert!(payload.tool.cfg.fatal_core_capture.is_none());
1060        assert!(payload.tool.cfg.record_host_inputs);
1061    }
1062
1063    #[test]
1064    fn native_exec_empty_happens_before_is_still_an_unsupported_skipped_value() {
1065        let (mut tool, thread, binding) = fixture();
1066        tool.cfg.happens_before = Some(
1067            detcore_model::happens_before::HappensBeforeSpec::from_dsl("")
1068                .unwrap()
1069                .normalize()
1070                .unwrap(),
1071        );
1072        assert_eq!(
1073            native_exec_config_digest(&tool.cfg).unwrap(),
1074            binding.config_digest
1075        );
1076        assert_eq!(
1077            validate_old_native_exec_cut(&tool, &thread, &binding).unwrap_err(),
1078            NativeExecCutError::Configuration("happens_before")
1079        );
1080    }
1081
1082    fn set_skipped_config_field(config: &mut Config, field: &str) {
1083        match field {
1084            "happens_before" => {
1085                config.happens_before = Some(
1086                    detcore_model::happens_before::HappensBeforeSpec::from_dsl("42:sc=1 < 42:sc=2")
1087                        .unwrap()
1088                        .normalize()
1089                        .unwrap(),
1090                );
1091            }
1092            "host_input_log" => config.host_input_log = Some("native-inputs.jsonl".into()),
1093            "fatal_core_capture" => {
1094                config.fatal_core_capture = Some(detcore_model::config::FatalCoreCapture {
1095                    dir: "native-core-output".into(),
1096                    file_prefix: "native-exec-test-".into(),
1097                    max_core_bytes: 1024,
1098                    max_total_bytes: 4096,
1099                    time_limit: Duration::from_secs(1),
1100                });
1101            }
1102            "untraced_code_range" => config.untraced_code_range = Some((0x1000, 0x2000)),
1103            _ => panic!("unknown skipped Config field {field}"),
1104        }
1105    }
1106
1107    #[test]
1108    fn native_exec_unsupported_configuration_remains_a_refusal() {
1109        for field in 0..10 {
1110            let (mut tool, thread, mut binding) = fixture();
1111            match field {
1112                0 => {
1113                    tool.cfg.no_rcb_time = false;
1114                    tool.cfg.max_timeslice = std::num::NonZeroU64::new(10);
1115                }
1116                1 => tool.cfg.max_timeslice = std::num::NonZeroU64::new(1),
1117                2 => tool.cfg.target_timeslice = std::num::NonZeroU64::new(1),
1118                3 => tool.cfg.chaos = true,
1119                4 => tool.cfg.imprecise_timers = true,
1120                5 => tool.cfg.record_preemptions = true,
1121                6 => tool.cfg.preemption_stacktrace = true,
1122                7 => tool.cfg.recordreplay_modes = true,
1123                8 => tool.cfg.replaying = true,
1124                9 => tool.cfg.replay_data = Some("unopened".into()),
1125                _ => unreachable!(),
1126            }
1127            binding.config_digest = native_exec_config_digest(&tool.cfg).unwrap();
1128            assert!(
1129                matches!(
1130                    validate_old_native_exec_cut(&tool, &thread, &binding),
1131                    Err(NativeExecCutError::Configuration(_))
1132                ),
1133                "field {field}"
1134            );
1135        }
1136    }
1137
1138    #[test]
1139    fn native_initial_binding_keeps_lifecycle_unstarted_and_is_one_use() {
1140        // Model mechanics only: the production unsafe caller supplies the real
1141        // InitialImage permit and raw same-task IDs, neither simulated here.
1142        let owner = DetTid::from_raw(41);
1143        let mut thread = ThreadState::new(owner, &Config::default(), ());
1144        unsafe { bind_native_initial_physical_identity(&mut thread, 41, 41) }.unwrap();
1145        assert_eq!(thread.physical_tid, Some(41));
1146        assert_eq!(thread.detpid, None);
1147        assert!(!thread.thread_start_entered);
1148        assert_eq!(thread.mm_id, MmId::initial(owner));
1149        assert_eq!(
1150            thread.file_metadata.lock().unwrap().files_id,
1151            FilesId::initial(owner)
1152        );
1153        assert!(unsafe { bind_native_initial_physical_identity(&mut thread, 41, 41) }.is_err());
1154        for field in 0..6 {
1155            let mut thread = ThreadState::new(owner, &Config::default(), ());
1156            let (pid, tid) = match field {
1157                0 => (42, 41),
1158                1 => (42, 42),
1159                2 => (0, 0),
1160                3 => {
1161                    thread.detpid = Some(owner);
1162                    (41, 41)
1163                }
1164                4 => {
1165                    thread.thread_start_entered = true;
1166                    (41, 41)
1167                }
1168                5 => {
1169                    thread.mm_id = thread.mm_id.for_exec(owner);
1170                    (41, 41)
1171                }
1172                _ => unreachable!(),
1173            };
1174            assert!(
1175                unsafe { bind_native_initial_physical_identity(&mut thread, pid, tid) }.is_err(),
1176                "field {field}"
1177            );
1178            assert_eq!(thread.physical_tid, None);
1179        }
1180    }
1181
1182    #[test]
1183    fn native_fresh_signal_binding_refuses_carried_or_actual_capability() {
1184        let owner = DetTid::from_raw(41);
1185        let identity = reverie::SignalTaskIdentity {
1186            process: reverie::SignalProcessId {
1187                tgid: reverie::Pid::from_raw(41),
1188                generation: 0,
1189            },
1190            tid: reverie::Pid::from_raw(41),
1191            task_generation: 0,
1192        };
1193        let mut thread = ThreadState::new(owner, &Config::default(), ());
1194        bind_native_fresh_signal_identity(
1195            &mut thread,
1196            reverie::BackendSignalControlMode::Unchanged,
1197            None,
1198        )
1199        .unwrap();
1200        assert_eq!(thread.signal_task_identity, None);
1201        assert!(
1202            bind_native_fresh_signal_identity(
1203                &mut thread,
1204                reverie::BackendSignalControlMode::Unchanged,
1205                Some(identity)
1206            )
1207            .is_err()
1208        );
1209        assert_eq!(thread.signal_task_identity, None);
1210        thread.signal_task_identity = Some(identity);
1211        assert!(
1212            bind_native_fresh_signal_identity(
1213                &mut thread,
1214                reverie::BackendSignalControlMode::Unchanged,
1215                None
1216            )
1217            .is_err()
1218        );
1219        assert_eq!(thread.signal_task_identity, Some(identity));
1220        thread.signal_task_identity = None;
1221        assert!(
1222            bind_native_fresh_signal_identity(
1223                &mut thread,
1224                reverie::BackendSignalControlMode::ToolControlled,
1225                None
1226            )
1227            .is_err()
1228        );
1229    }
1230}