Skip to main content

hermit/
ptrace_completion.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 * This source code is licensed under the BSD-style license found in LICENSE.
5 */
6
7//! Same-thread ownership of a complete ordinary-ptrace operation.
8//!
9//! A checkpoint retains the original runtime, future and every guard in that
10//! future. It is not a completed cleanup. Deliberate retention on thread exit
11//! leaves admission closed; dropping a diagnostic does not abandon the owner.
12
13use std::any::Any;
14use std::any::TypeId;
15use std::cell::Cell;
16use std::cell::RefCell;
17use std::collections::BTreeMap;
18use std::ffi::CStr;
19use std::fmt;
20use std::fs::File;
21use std::future::Future;
22use std::future::poll_fn;
23use std::mem::ManuallyDrop;
24use std::os::fd::AsRawFd;
25use std::os::fd::FromRawFd;
26use std::os::fd::OwnedFd;
27use std::os::unix::fs::MetadataExt;
28use std::pin::Pin;
29use std::rc::Rc;
30use std::sync::Mutex;
31use std::sync::atomic::AtomicU64;
32use std::sync::atomic::Ordering;
33use std::task::Context;
34use std::task::Poll;
35use std::thread::ThreadId;
36use std::time::Duration;
37use std::time::Instant;
38
39use reverie_ptrace::CapturedPrefix;
40use reverie_ptrace::PtraceCallbackDiagnostic;
41use reverie_ptrace::PtraceRunFailure;
42use reverie_ptrace::PtraceTerminationHandle;
43use reverie_ptrace::ToolRunOutcome;
44use serde::Deserialize;
45use serde::Serialize;
46
47use crate::Error;
48use crate::FailureKind;
49use crate::GuestTimedOut;
50use crate::SchedulerEndedRun;
51
52const ATTEMPT_BUDGET: Duration = Duration::from_secs(2);
53static NEXT_KEY: AtomicU64 = AtomicU64::new(1);
54// Admission and unresolved-owner publication use this SAME lock. Operations
55// admitted before publication can finish; no later operation starts effects.
56static UNRESOLVED: Mutex<usize> = Mutex::new(0);
57
58type ErasedFuture = Pin<Box<dyn Future<Output = Result<Box<dyn Any>, Error>>>>;
59
60// Keep the pinned operation as a field, rather than an async wrapper's local,
61// when its poll unwinds. The poisoned entry is never polled again. This cannot
62// undo destructors already run by arbitrary code inside the operation's panic.
63struct EraseResult<F>(Pin<Box<F>>);
64impl<T: 'static, F: Future<Output = Result<T, Error>>> Future for EraseResult<F> {
65    type Output = Result<Box<dyn Any>, Error>;
66    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
67        self.get_mut()
68            .0
69            .as_mut()
70            .poll(cx)
71            .map(|result| result.map(|value| Box::new(value) as Box<dyn Any>))
72    }
73}
74
75thread_local! {
76    // TLS destruction must not drop a LocalSet or its backing directories.
77    // This deliberately retains resources if their original thread exits.
78    static OWNERS: RefCell<ManuallyDrop<BTreeMap<u64, Rc<Entry>>>> =
79        const { RefCell::new(ManuallyDrop::new(BTreeMap::new())) };
80}
81
82/// The phase whose original owner remains retained.
83#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
84pub enum HermitCleanupStage {
85    /// Spawn/startup has not yet returned a termination handle.
86    Startup,
87    /// The original ordinary-ptrace task, hooks or output drain are unfinished.
88    PtraceCleanup,
89    /// Physical ptrace completion returned failed global state; its natural
90    /// scheduler join or partial-recording flush remains unfinished.
91    GlobalStateCleanup,
92    /// An untouched tracer refused the ordinary completion API.
93    UnsupportedBackend,
94    /// Polling panicked. The retained operation must never be polled again.
95    Poisoned,
96}
97
98/// A completed failed ptrace run, with its original nonclone primary cause.
99/// Secondary backend/cleanup errors never determine its failure classification.
100pub struct HermitPtraceFailure {
101    failure: PtraceRunFailure,
102    cleanup: detcore::BackendFailureCleanup,
103    callbacks: Vec<PtraceCallbackDiagnostic>,
104    timeout_during_cleanup: bool,
105}
106impl HermitPtraceFailure {
107    /// The original typed failure, origin, secondary errors and captured bytes.
108    pub fn failure(&self) -> &PtraceRunFailure {
109        &self.failure
110    }
111    /// Actual failed-global-state cleanup outcomes, separate from the primary.
112    pub fn cleanup(&self) -> &detcore::BackendFailureCleanup {
113        &self.cleanup
114    }
115    /// Raw callback observations preserved by the backend.
116    pub fn callback_diagnostics(&self) -> &[PtraceCallbackDiagnostic] {
117        &self.callbacks
118    }
119    /// Whether the caller's deadline also expired during failed cleanup.
120    pub fn timeout_during_cleanup(&self) -> bool {
121        self.timeout_during_cleanup
122    }
123    pub(crate) fn kind(&self) -> FailureKind {
124        crate::error::classify_ptrace_primary(self.failure.primary())
125    }
126}
127impl fmt::Debug for HermitPtraceFailure {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.debug_struct("HermitPtraceFailure")
130            .field("failure", &self.failure)
131            .field("scheduler", &self.cleanup.scheduler)
132            .field("preemption_recording", &self.cleanup.preemption_recording)
133            .field("callbacks", &self.callbacks)
134            .field("timeout_during_cleanup", &self.timeout_during_cleanup)
135            .finish()
136    }
137}
138impl fmt::Display for HermitPtraceFailure {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        self.failure.fmt(f)
141    }
142}
143impl std::error::Error for HermitPtraceFailure {
144    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
145        Some(self.failure.primary())
146    }
147}
148
149#[derive(Clone, Debug)]
150struct Snapshot {
151    kind: FailureKind,
152    primary: String,
153    origin: Option<String>,
154    stdout: Option<Vec<u8>>,
155    stderr: Option<Vec<u8>>,
156    callbacks: Vec<PtraceCallbackDiagnostic>,
157}
158impl Snapshot {
159    fn startup() -> Self {
160        Self {
161            kind: FailureKind::Error,
162            primary: "ordinary operation is not yet complete".into(),
163            origin: None,
164            stdout: None,
165            stderr: None,
166            callbacks: Vec::new(),
167        }
168    }
169    fn failure(failure: &PtraceRunFailure, callbacks: Vec<PtraceCallbackDiagnostic>) -> Self {
170        Self {
171            kind: crate::error::classify_ptrace_primary(failure.primary()),
172            primary: failure.primary().to_string(),
173            origin: Some(format!("{:?}", failure.origin())),
174            stdout: failure.captured_prefix().map(|p| p.stdout().to_vec()),
175            stderr: failure.captured_prefix().map(|p| p.stderr().to_vec()),
176            callbacks,
177        }
178    }
179}
180
181/// Sendable diagnostic identifying an operation retained on its original thread.
182///
183/// This owns no runtime or tracee capability. `resume::<T>()` must use the exact
184/// successful return type of the original public call. Dropping this value does
185/// not release the owner or reopen ordinary admission. There is no library alarm
186/// or automatic process exit after this value is returned.
187///
188/// While any operation is retained, later ordinary-ptrace calls in this process
189/// refuse admission. Already admitted calls can complete. A poisoned owner or an
190/// owner whose original thread exited cannot recover; its resources are retained
191/// and admission stays closed for the process lifetime. In-process callers must
192/// explicitly supervise this condition rather than assume a failed call cleaned
193/// up or that dropping this diagnostic released the resources.
194#[derive(Clone, Debug)]
195pub struct HermitCleanupUnconfirmed {
196    key: u64,
197    origin: OriginNumbers,
198    stage: HermitCleanupStage,
199    snapshot: Snapshot,
200}
201impl HermitCleanupUnconfirmed {
202    /// Opaque recovery key; meaningful only with the original process/thread.
203    pub fn recovery_key(&self) -> u64 {
204        self.key
205    }
206    /// The unresolved phase, not a guest exit status.
207    pub fn stage(&self) -> HermitCleanupStage {
208        self.stage
209    }
210    /// Classification of the first received failure, never a secondary cause.
211    pub fn primary_kind(&self) -> FailureKind {
212        self.snapshot.kind
213    }
214    /// A diagnostic copy. The nonclone typed cause remains with the owner.
215    pub fn primary_message(&self) -> &str {
216        &self.snapshot.primary
217    }
218    /// The original backend callback/operation, when one has been received.
219    pub fn origin(&self) -> Option<&str> {
220        self.snapshot.origin.as_deref()
221    }
222    /// Captured stdout at this checkpoint; absent for noncapturing operations.
223    pub fn stdout_prefix(&self) -> Option<&[u8]> {
224        self.snapshot.stdout.as_deref()
225    }
226    /// Captured stderr at this checkpoint; absent for noncapturing operations.
227    pub fn stderr_prefix(&self) -> Option<&[u8]> {
228        self.snapshot.stderr.as_deref()
229    }
230    /// Backend callback observations received before this checkpoint.
231    pub fn callback_diagnostics(&self) -> &[PtraceCallbackDiagnostic] {
232        &self.snapshot.callbacks
233    }
234    /// Continue the SAME operation for one cooperative two-second attempt.
235    ///
236    /// A further checkpoint is returned as another typed
237    /// `HermitCleanupUnconfirmed` error. Wrong identity/type, nested runtimes,
238    /// reentry and poisoned owners refuse without polling or discarding it.
239    /// Synchronous kernel calls and user code cannot be preempted by this budget.
240    /// If the original deadline expired before startup produced a termination
241    /// handle, recovery continues that same startup and can therefore begin its
242    /// effects after the deadline. As soon as the handle becomes available the
243    /// already-expired timeout is published; no normal guest completion is
244    /// claimed from this recovery. Do not call resume expecting cancellation of
245    /// an operation that has not yet established its owned termination handle.
246    ///
247    /// An operation a public launch function started lives on the calling
248    /// thread's tracer thread (`crate::tracer_thread`); resuming it from that
249    /// caller continues it there.
250    pub fn resume<T: Send + 'static>(&self) -> Result<T, Error> {
251        reject_nested_runtime()?;
252        let this = self.clone();
253        if let Some(resumed) =
254            crate::tracer_thread::execute_on(self.origin.thread, move || this.resume_here::<T>())
255        {
256            return resumed?;
257        }
258        self.resume_here()
259    }
260
261    fn resume_here<T: 'static>(&self) -> Result<T, Error> {
262        self.origin.check()?;
263        let entry = OWNERS
264            .try_with(|owners| owners.borrow().get(&self.key).cloned())
265            .map_err(|_| RecoveryRefusal::StorageUnavailable)?
266            .ok_or(RecoveryRefusal::UnknownKey)?;
267        if entry.result_type != TypeId::of::<T>() {
268            return Err(RecoveryRefusal::WrongResultType.into());
269        }
270        drive(entry, true)
271    }
272}
273impl fmt::Display for HermitCleanupUnconfirmed {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(
276            f,
277            "Hermit cleanup remains unconfirmed ({:?}, recovery {}): {}",
278            self.stage, self.key, self.snapshot.primary
279        )
280    }
281}
282impl std::error::Error for HermitCleanupUnconfirmed {}
283
284/// A refusal that leaves any existing operation owner untouched.
285#[derive(Debug)]
286pub enum RecoveryRefusal {
287    /// Public synchronous entry cannot nest a Tokio runtime.
288    NestedRuntime,
289    /// Recovery must run on the original thread and process/PID namespace.
290    WrongIdentity,
291    /// Current process identity could not be established.
292    Identity(std::io::Error),
293    /// The ORIGINAL process pidfd is ready or returned unexpected poll flags.
294    /// This refuses recovery; it is not proof of guest cleanup.
295    OriginalProcessUnavailable { ready: i32, events: i16 },
296    /// Thread-local storage is unavailable during thread destruction.
297    StorageUnavailable,
298    /// The key was already completed or never belonged to this thread.
299    UnknownKey,
300    /// Recovery used a different successful return type.
301    WrongResultType,
302    /// This operation is already being polled.
303    Reentrant,
304    /// A previous poll panicked; it must not be resumed.
305    Poisoned,
306    /// An unresolved owner prevents admitting another ordinary operation.
307    AdmissionClosed,
308    /// The admission mutex was poisoned; admission is conservatively refused.
309    AdmissionPoisoned,
310    /// The monotonic recovery key space is exhausted.
311    KeyExhausted,
312}
313impl fmt::Display for RecoveryRefusal {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "Hermit owner recovery refused: {self:?}")
316    }
317}
318impl std::error::Error for RecoveryRefusal {}
319
320#[derive(Clone, Copy, Debug, Eq, PartialEq)]
321struct OriginNumbers {
322    thread: ThreadId,
323    pid: u32,
324    namespace: PidNamespace,
325}
326impl OriginNumbers {
327    fn current() -> Result<(Self, Option<File>), RecoveryRefusal> {
328        let (namespace, file) = open_pid_namespace().map_err(RecoveryRefusal::Identity)?;
329        Ok((
330            Self {
331                thread: std::thread::current().id(),
332                pid: std::process::id(),
333                namespace,
334            },
335            file,
336        ))
337    }
338    fn check(&self) -> Result<(), RecoveryRefusal> {
339        let (current, _namespace) = Self::current()?;
340        if *self == current {
341            Ok(())
342        } else {
343            Err(RecoveryRefusal::WrongIdentity)
344        }
345    }
346}
347
348/// The PID namespace a process is in.
349#[derive(Clone, Copy, Debug, Eq, PartialEq)]
350pub(crate) enum PidNamespace {
351    /// The device and inode of its `/proc/<pid>/ns/pid` file.
352    Inode(u64, u64),
353    /// The only one: a kernel built without PID namespaces (`CONFIG_PID_NS`
354    /// unset, as Android's kernels are) has one, which every process shares,
355    /// and its procfs lists no `ns/pid` entry.
356    Only,
357}
358
359/// The calling process's PID namespace, with its `ns/pid` file opened, which
360/// a caller holds while it records the namespace's identity so that no later
361/// namespace can take that identity (none where the kernel has only the one).
362/// An error when `/proc` cannot say.
363///
364/// Both lookups happen in the caller's own `ns` directory, held open and
365/// checked to be procfs, so neither a filesystem mounted over it nor a mount
366/// change between the lookups passes as a missing entry. A missing `pid`
367/// entry means the kernel has no PID namespaces only when the `mnt` entry,
368/// which every kernel lists, is there.
369pub(crate) fn open_pid_namespace() -> std::io::Result<(PidNamespace, Option<File>)> {
370    // A path handle (`O_PATH`), which needs only search permission: reading
371    // the directory needs the right to inspect its process, which a thread
372    // whose thread group leader has exited no longer has over the leader.
373    // SAFETY: the path is a C string, and a returned descriptor is owned here.
374    let fd = unsafe {
375        libc::open(
376            c"/proc/self/ns".as_ptr(),
377            libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
378        )
379    };
380    if fd < 0 {
381        return Err(std::io::Error::last_os_error());
382    }
383    // SAFETY: `fd` was just opened and nothing else owns it.
384    let directory = unsafe { File::from_raw_fd(fd) };
385    match open_entry(&directory, c"pid") {
386        Ok(file) => {
387            let meta = file.metadata()?;
388            Ok((PidNamespace::Inode(meta.dev(), meta.ino()), Some(file)))
389        }
390        Err(error)
391            if error.kind() == std::io::ErrorKind::NotFound
392                && is_procfs(&directory)
393                && has_entry(&directory, c"mnt") =>
394        {
395            Ok((PidNamespace::Only, None))
396        }
397        Err(error) => Err(error),
398    }
399}
400
401/// Opens the entry `name` of `directory`.
402fn open_entry(directory: &File, name: &CStr) -> std::io::Result<File> {
403    // SAFETY: `name` is a C string, and a returned descriptor is owned here.
404    let fd = unsafe {
405        libc::openat(
406            directory.as_raw_fd(),
407            name.as_ptr(),
408            libc::O_RDONLY | libc::O_CLOEXEC,
409        )
410    };
411    if fd < 0 {
412        return Err(std::io::Error::last_os_error());
413    }
414    // SAFETY: `fd` was just opened and nothing else owns it.
415    Ok(unsafe { File::from_raw_fd(fd) })
416}
417
418/// Whether `directory` is on procfs.
419fn is_procfs(directory: &File) -> bool {
420    // SAFETY: an all-zero `statfs` is a valid value, and `fstatfs` writes only
421    // the struct it is given.
422    let mut fs: libc::statfs = unsafe { std::mem::zeroed() };
423    let found = unsafe { libc::fstatfs(directory.as_raw_fd(), &mut fs) } == 0;
424    found && fs.f_type == libc::PROC_SUPER_MAGIC
425}
426
427/// Whether `directory` has an entry `name`.
428fn has_entry(directory: &File, name: &CStr) -> bool {
429    // SAFETY: an all-zero `stat` is a valid value, `name` is a C string, and
430    // `fstatat` writes only the struct it is given.
431    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
432    unsafe {
433        libc::fstatat(
434            directory.as_raw_fd(),
435            name.as_ptr(),
436            &mut stat,
437            libc::AT_SYMLINK_NOFOLLOW,
438        ) == 0
439    }
440}
441
442// A numeric PID, Rust ThreadId and namespace can all be copied across fork.
443// After the origin dies, even its PID can be reused. This descriptor keeps the
444// original kernel process identity; it is never reopened on recovery. Flags 0
445// deliberately track the process, including a surviving nonleader owner thread.
446struct OriginalProcess(OwnedFd);
447impl OriginalProcess {
448    fn capture(pid: u32) -> Result<Self, RecoveryRefusal> {
449        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0) };
450        if fd < 0 {
451            return Err(RecoveryRefusal::Identity(std::io::Error::last_os_error()));
452        }
453        Ok(Self(unsafe { OwnedFd::from_raw_fd(fd as i32) }))
454    }
455    fn check(&self) -> Result<(), RecoveryRefusal> {
456        Self::check_fd(self.0.as_raw_fd())
457    }
458    fn check_fd(fd: i32) -> Result<(), RecoveryRefusal> {
459        // poll deliberately ignores negative descriptors, so reject first.
460        if fd < 0 {
461            return Err(RecoveryRefusal::Identity(
462                std::io::Error::from_raw_os_error(libc::EBADF),
463            ));
464        }
465        let mut descriptor = libc::pollfd {
466            fd,
467            events: libc::POLLIN,
468            revents: 0,
469        };
470        let ready = unsafe { libc::poll(&mut descriptor, 1, 0) };
471        if ready < 0 {
472            return Err(RecoveryRefusal::Identity(std::io::Error::last_os_error()));
473        }
474        if ready != 0 || descriptor.revents != 0 {
475            return Err(RecoveryRefusal::OriginalProcessUnavailable {
476                ready,
477                events: descriptor.revents,
478            });
479        }
480        Ok(())
481    }
482}
483
484/// The result a run that ended with a scheduler terminal outcome reports in
485/// place of the guest status it never had: the outcome's status
486/// ([`detcore::TerminalOutcome::exit_status`]), with the output the guest
487/// wrote before it. `None` when the run kept no output to report.
488pub(crate) trait TerminalOutcomeResult: Sized {
489    fn terminal_outcome(
490        outcome: detcore::TerminalOutcome,
491        captured: Option<&CapturedPrefix>,
492    ) -> Option<Self>;
493}
494impl TerminalOutcomeResult for reverie::ExitStatus {
495    fn terminal_outcome(
496        outcome: detcore::TerminalOutcome,
497        _: Option<&CapturedPrefix>,
498    ) -> Option<Self> {
499        Some(reverie::ExitStatus::Exited(outcome.exit_status()))
500    }
501}
502impl TerminalOutcomeResult for reverie::process::Output {
503    fn terminal_outcome(
504        outcome: detcore::TerminalOutcome,
505        captured: Option<&CapturedPrefix>,
506    ) -> Option<Self> {
507        let captured = captured?;
508        Some(reverie::process::Output {
509            status: reverie::ExitStatus::Exited(outcome.exit_status()),
510            stdout: captured.stdout().to_vec(),
511            stderr: captured.stderr().to_vec(),
512        })
513    }
514}
515
516pub(crate) struct Control {
517    termination: RefCell<Option<PtraceTerminationHandle>>,
518    timeout: Option<(Instant, Duration)>,
519    expired: Cell<bool>,
520    received_failure: Cell<bool>,
521    /// The guest processes still running when the timeout expired, read once
522    /// at expiry (`timeout_survivors`), before the termination kills them.
523    survivors: RefCell<Option<String>>,
524    /// The run's root guest process, once the tracer has spawned it: a
525    /// survivor whose parent is Hermit but which is not the root was orphaned
526    /// and re-parented to Hermit, as it is when Hermit is the container's init.
527    root: Cell<Option<i32>>,
528    cancellation_deadline: Cell<Option<Instant>>,
529    global_cleanup_deadline: Cell<Option<Instant>>,
530    stage: Cell<HermitCleanupStage>,
531    snapshot: RefCell<Snapshot>,
532    checkpoint: Cell<bool>,
533    epoch: Cell<u64>,
534}
535impl Control {
536    fn new(timeout: Option<Duration>) -> Self {
537        Self {
538            termination: RefCell::new(None),
539            timeout: timeout.map(|d| (Instant::now() + d, d)),
540            expired: Cell::new(false),
541            received_failure: Cell::new(false),
542            survivors: RefCell::new(None),
543            root: Cell::new(None),
544            cancellation_deadline: Cell::new(None),
545            global_cleanup_deadline: Cell::new(None),
546            stage: Cell::new(HermitCleanupStage::Startup),
547            snapshot: RefCell::new(Snapshot::startup()),
548            checkpoint: Cell::new(false),
549            epoch: Cell::new(0),
550        }
551    }
552    fn register(&self, handle: Option<PtraceTerminationHandle>) {
553        *self.termination.borrow_mut() = handle;
554        self.stage.set(HermitCleanupStage::PtraceCleanup);
555        if self.expired.get() {
556            self.publish_timeout();
557        }
558    }
559    /// Let the scheduler hand this run's terminal outcome (a guest deadlock,
560    /// a virtual timeout) to the run instead of exiting the process, while the
561    /// returned guard lives: the unwind publishes the termination that tears
562    /// the parked guest down, as `--timeout` does, and reports whether it
563    /// became the run's cause. Only a run with a termination handle installs
564    /// it. The scheduler task runs on this thread (the run's current-thread
565    /// runtime), where the guard installs it.
566    fn unwind_terminal_outcomes(&self) -> Option<detcore::TerminalOutcomeUnwindGuard> {
567        let handle = self.termination.borrow().clone()?;
568        Some(detcore::TerminalOutcomeUnwindGuard::install(Box::new(
569            move |outcome| {
570                handle.terminate(reverie::Error::Tool(Error::new(SchedulerEndedRun::new(
571                    outcome, None,
572                ))))
573            },
574        )))
575    }
576    fn publish_timeout(&self) {
577        if let (Some(handle), Some((_, limit))) = (self.termination.borrow().as_ref(), self.timeout)
578        {
579            // Reverie preserves an earlier Tool failure as primary.
580            handle.terminate(reverie::Error::Tool(Error::new(self.timed_out(limit))));
581        }
582    }
583    /// Mark the run timed out if `now` is at or past its deadline. The caller
584    /// passes the same `now` it then compares deadlines against, so a deadline
585    /// can never be seen as passed while the timeout is still unexpired.
586    fn expire_if_due(&self, now: Instant) {
587        if !self.expired.get() && self.timeout.is_some_and(|(at, _)| now >= at) {
588            self.expired.set(true);
589            *self.survivors.borrow_mut() =
590                crate::timeout_survivors::traced_processes_now(self.root.get());
591            if !self.received_failure.get() {
592                let mut snapshot = self.snapshot.borrow_mut();
593                snapshot.kind = FailureKind::RunTimeout;
594                snapshot.primary = self.timed_out(self.timeout.unwrap().1).to_string();
595            }
596            self.publish_timeout();
597            self.cancellation_deadline
598                .set(Some(Instant::now() + ATTEMPT_BUDGET));
599        }
600    }
601    /// The timeout failure, naming the guest processes read at expiry.
602    fn timed_out(&self, limit: Duration) -> GuestTimedOut {
603        GuestTimedOut {
604            limit,
605            survivors: self.survivors.borrow().clone(),
606        }
607    }
608    async fn yield_checkpoint(&self) {
609        let epoch = self.epoch.get();
610        self.checkpoint.set(true);
611        poll_fn(|_| {
612            if self.epoch.get() != epoch {
613                Poll::Ready(())
614            } else {
615                Poll::Pending
616            }
617        })
618        .await;
619    }
620    fn global_failure(&self, failure: &PtraceRunFailure, callbacks: Vec<PtraceCallbackDiagnostic>) {
621        self.termination.borrow_mut().take();
622        self.received_failure.set(true);
623        *self.snapshot.borrow_mut() = Snapshot::failure(failure, callbacks);
624        self.stage.set(HermitCleanupStage::GlobalStateCleanup);
625        self.global_cleanup_deadline
626            .set(Some(Instant::now() + ATTEMPT_BUDGET));
627    }
628}
629
630struct Driver {
631    runtime: tokio::runtime::Runtime,
632    future: ErasedFuture,
633    control: Rc<Control>,
634}
635struct Entry {
636    key: u64,
637    origin: OriginNumbers,
638    _namespace: Option<File>,
639    original_process: OriginalProcess,
640    result_type: TypeId,
641    driver: RefCell<ManuallyDrop<Driver>>,
642    in_flight: Cell<bool>,
643    poisoned: Cell<bool>,
644    quarantined: Cell<bool>,
645}
646impl Entry {
647    fn check_origin(&self) -> Result<(), RecoveryRefusal> {
648        self.origin.check()?;
649        self.original_process.check()
650    }
651}
652
653struct PollGuard<'a>(&'a Cell<bool>);
654impl Drop for PollGuard<'_> {
655    fn drop(&mut self) {
656        self.0.set(false);
657    }
658}
659fn reject_nested_runtime() -> Result<(), RecoveryRefusal> {
660    if tokio::runtime::Handle::try_current().is_ok() {
661        Err(RecoveryRefusal::NestedRuntime)
662    } else {
663        Ok(())
664    }
665}
666
667/// [`run`] on the calling thread's tracer thread (`crate::tracer_thread`), with
668/// its explicit stack, so that a library launch needs no particular stack from
669/// its caller. A nested runtime on the calling thread is refused here, before
670/// anything moves to the tracer thread.
671pub(crate) fn run_on_tracer_thread<T, F, Make>(
672    timeout: Option<Duration>,
673    make: Make,
674) -> Result<T, Error>
675where
676    T: Send + 'static,
677    F: Future<Output = Result<T, Error>> + 'static,
678    Make: FnOnce(Rc<Control>) -> F + Send + 'static,
679{
680    reject_nested_runtime()?;
681    crate::tracer_thread::execute(move || run(timeout, make))?
682}
683
684/// Installs the whole operation before its first poll. `make` must only construct
685/// a future; every ordinary effect and all owned guards belong inside that future.
686pub(crate) fn run<T, F, Make>(timeout: Option<Duration>, make: Make) -> Result<T, Error>
687where
688    T: 'static,
689    F: Future<Output = Result<T, Error>> + 'static,
690    Make: FnOnce(Rc<Control>) -> F,
691{
692    reject_nested_runtime()?;
693    let (origin, namespace) = OriginNumbers::current()?;
694    let original_process = OriginalProcess::capture(origin.pid)?;
695    let key = NEXT_KEY
696        .try_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
697        .map_err(|_| RecoveryRefusal::KeyExhausted)?;
698    let control = Rc::new(Control::new(timeout));
699    let runtime = tokio::runtime::Builder::new_current_thread()
700        .enable_all()
701        .build()?;
702    let admission = UNRESOLVED
703        .lock()
704        .map_err(|_| RecoveryRefusal::AdmissionPoisoned)?;
705    if *admission != 0 {
706        return Err(RecoveryRefusal::AdmissionClosed.into());
707    }
708    // This is the admission linearization, before make or any first poll.
709    drop(admission);
710    let future = make(control.clone());
711    let future: ErasedFuture = Box::pin(EraseResult(Box::pin(future)));
712    let entry = Rc::new(Entry {
713        key,
714        origin,
715        _namespace: namespace,
716        original_process,
717        result_type: TypeId::of::<T>(),
718        driver: RefCell::new(ManuallyDrop::new(Driver {
719            runtime,
720            future,
721            control,
722        })),
723        in_flight: Cell::new(false),
724        poisoned: Cell::new(false),
725        quarantined: Cell::new(false),
726    });
727    OWNERS
728        .try_with(|owners| {
729            owners.borrow_mut().insert(key, entry.clone());
730        })
731        .map_err(|_| RecoveryRefusal::StorageUnavailable)?;
732    drive(entry, false)
733}
734
735fn drive<T: 'static>(entry: Rc<Entry>, recovery: bool) -> Result<T, Error> {
736    entry.check_origin()?;
737    if entry.poisoned.get() {
738        return Err(RecoveryRefusal::Poisoned.into());
739    }
740    if entry.in_flight.replace(true) {
741        return Err(RecoveryRefusal::Reentrant.into());
742    }
743    let _poll_guard = PollGuard(&entry.in_flight);
744    let attempt_deadline = recovery.then(|| Instant::now() + ATTEMPT_BUDGET);
745    let polled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
746        let mut stored = entry.driver.borrow_mut();
747        let Driver {
748            runtime,
749            future,
750            control,
751        } = &mut **stored;
752        if recovery {
753            control.checkpoint.set(false);
754            control.epoch.set(control.epoch.get().wrapping_add(1));
755        }
756        runtime.block_on(async {
757            let mut timer: Option<Pin<Box<tokio::time::Sleep>>> = None;
758            poll_fn(|cx| {
759                control.expire_if_due(Instant::now());
760                if !recovery
761                    && control.expired.get()
762                    && control.stage.get() == HermitCleanupStage::Startup
763                {
764                    return Poll::Ready(None);
765                }
766                let polled = future.as_mut().poll(cx);
767                // A synchronous poll can cross the absolute deadline. Observe
768                // that fact on its return without claiming preemption. This one
769                // reading of the clock also decides the deadline check below:
770                // with a second reading, the timeout could pass between the two,
771                // and the check would end the poll with the timeout unexpired,
772                // reporting HermitCleanupUnconfirmed with the startup snapshot
773                // instead of a run timeout (dh-4s1xh).
774                let now = Instant::now();
775                control.expire_if_due(now);
776                if let Poll::Ready(result) = polled {
777                    let result = match (result, control.timeout) {
778                        (Ok(_), Some((_, limit))) if control.expired.get() => {
779                            Err(Error::new(control.timed_out(limit)))
780                        }
781                        (result, _) => result,
782                    };
783                    return Poll::Ready(Some(result));
784                }
785                if control.checkpoint.get() {
786                    return Poll::Ready(None);
787                }
788                let deadline = if recovery {
789                    attempt_deadline
790                } else {
791                    [
792                        control
793                            .timeout
794                            .filter(|_| !control.expired.get())
795                            .map(|(at, _)| at),
796                        control.cancellation_deadline.get(),
797                        control.global_cleanup_deadline.get(),
798                    ]
799                    .into_iter()
800                    .flatten()
801                    .min()
802                };
803                if let Some(deadline) = deadline {
804                    if now >= deadline {
805                        return Poll::Ready(None);
806                    }
807                    let at = tokio::time::Instant::from_std(deadline);
808                    match timer.as_mut() {
809                        Some(timer) => timer.as_mut().reset(at),
810                        None => timer = Some(Box::pin(tokio::time::sleep_until(at))),
811                    }
812                    if timer.as_mut().unwrap().as_mut().poll(cx).is_ready() {
813                        cx.waker().wake_by_ref();
814                    }
815                }
816                Poll::Pending
817            })
818            .await
819        })
820    }));
821    match polled {
822        Ok(Some(result)) => {
823            OWNERS.with(|owners| {
824                owners.borrow_mut().remove(&entry.key);
825            });
826            if entry.quarantined.replace(false) {
827                let mut unresolved = UNRESOLVED
828                    .lock()
829                    .unwrap_or_else(|poison| poison.into_inner());
830                *unresolved -= 1;
831            }
832            // Runtime teardown happens only after the whole operation returned,
833            // outside block_on. No unresolved future is dropped here.
834            let driver = unsafe { ManuallyDrop::take(&mut *entry.driver.borrow_mut()) };
835            drop(driver);
836            result?
837                .downcast::<T>()
838                .map(|value| *value)
839                .map_err(|_| RecoveryRefusal::WrongResultType.into())
840        }
841        outcome => {
842            if !entry.quarantined.replace(true) {
843                let mut unresolved = UNRESOLVED
844                    .lock()
845                    .unwrap_or_else(|poison| poison.into_inner());
846                *unresolved = unresolved.saturating_add(1);
847            }
848            if outcome.is_err() {
849                entry.poisoned.set(true);
850            }
851            let stored = entry.driver.borrow();
852            let control = &stored.control;
853            if entry.poisoned.get() {
854                control.stage.set(HermitCleanupStage::Poisoned);
855                if !control.received_failure.get() && !control.expired.get() {
856                    let mut snapshot = control.snapshot.borrow_mut();
857                    snapshot.kind = FailureKind::Panic;
858                    snapshot.primary =
859                        "ordinary operation panicked while polling; owner is poisoned".into();
860                }
861            }
862            Err(Error::new(HermitCleanupUnconfirmed {
863                key: entry.key,
864                origin: entry.origin,
865                stage: control.stage.get(),
866                snapshot: control.snapshot.borrow().clone(),
867            }))
868        }
869    }
870}
871
872/// Wait for the run. A terminal outcome the scheduler hands this run
873/// (`detcore::TerminalOutcomeUnwindGuard`: a guest deadlock, a virtual
874/// timeout) is torn down like a `--timeout` and completes with the outcome's
875/// status; `GlobalState::terminal_outcome` then says the status is that, not
876/// the guest's.
877pub(crate) async fn wait(
878    tracer: reverie_ptrace::Tracer<detcore::GlobalState>,
879    control: Rc<Control>,
880) -> Result<(reverie::ExitStatus, detcore::GlobalState), Error> {
881    control.root.set(Some(tracer.guest_pid().as_raw()));
882    control.register(tracer.termination_handle());
883    let _unwind = control.unwind_terminal_outcomes();
884    consume(tracer.wait_completion().await, control).await
885}
886/// [`wait`], with the guest's output; a terminal outcome keeps the output the
887/// guest wrote before it.
888pub(crate) async fn wait_with_output(
889    tracer: reverie_ptrace::Tracer<detcore::GlobalState>,
890    control: Rc<Control>,
891) -> Result<(reverie::process::Output, detcore::GlobalState), Error> {
892    control.root.set(Some(tracer.guest_pid().as_raw()));
893    control.register(tracer.termination_handle());
894    let _unwind = control.unwind_terminal_outcomes();
895    consume(tracer.wait_with_output_completion().await, control).await
896}
897/// The outcome of a failed run that is a clean terminal-outcome teardown: the
898/// unwind's cause is the `primary`, so no earlier failure of the run is
899/// hidden, and the teardown recorded no failure of its own (`secondaries`: a
900/// capture drain or cleanup error), so the captured output is the whole
901/// output. Any other teardown stays a failed run, which
902/// `classify_ptrace_primary` reports as Hermit's.
903pub(crate) fn clean_terminal_outcome(
904    primary: &reverie::Error,
905    secondaries: usize,
906) -> Option<detcore::TerminalOutcome> {
907    match primary {
908        reverie::Error::Tool(inner) if secondaries == 0 => inner
909            .downcast_ref::<SchedulerEndedRun>()
910            .map(|ended| ended.outcome),
911        _ => None,
912    }
913}
914/// What a failed run completes with instead: the terminal outcome's result
915/// when its teardown was clean ([`clean_terminal_outcome`]) and kept what the
916/// caller asked for. `None` leaves it a failed run, whose primary
917/// `classify_ptrace_primary` reports as Hermit's failure (125): a terminal
918/// outcome whose teardown recorded `secondaries` failures of its own among
919/// them.
920pub(crate) fn completed_terminal_outcome<R: TerminalOutcomeResult>(
921    primary: &reverie::Error,
922    secondaries: usize,
923    captured: Option<&CapturedPrefix>,
924) -> Option<R> {
925    let outcome = clean_terminal_outcome(primary, secondaries)?;
926    R::terminal_outcome(outcome, captured)
927}
928async fn consume<R: TerminalOutcomeResult>(
929    mut outcome: ToolRunOutcome<detcore::GlobalState, R>,
930    control: Rc<Control>,
931) -> Result<(R, detcore::GlobalState), Error> {
932    loop {
933        match outcome {
934            ToolRunOutcome::Complete(completion) => {
935                let callbacks = completion.callback_diagnostics().to_vec();
936                control.termination.borrow_mut().take();
937                return match completion.result {
938                    Ok(result) => Ok((result, completion.global_state)),
939                    Err(failure)
940                        if let Some(result) = completed_terminal_outcome::<R>(
941                            failure.primary(),
942                            failure.secondary().len(),
943                            failure.captured_prefix(),
944                        ) =>
945                    {
946                        // The terminal outcome, not a failed run: the caller
947                        // finishes the run (summary, recording) as for an exit.
948                        Ok((result, completion.global_state))
949                    }
950                    Err(failure) => {
951                        control.global_failure(&failure, callbacks.clone());
952                        let cleanup = completion
953                            .global_state
954                            .clean_up_after_backend_failure()
955                            .await;
956                        Err(HermitPtraceFailure {
957                            failure,
958                            cleanup,
959                            callbacks,
960                            timeout_during_cleanup: control.expired.get(),
961                        }
962                        .into())
963                    }
964                };
965            }
966            ToolRunOutcome::CleanupPending(pending) => {
967                control.received_failure.set(true);
968                *control.snapshot.borrow_mut() =
969                    Snapshot::failure(pending.failure(), pending.callback_diagnostics());
970                control.stage.set(HermitCleanupStage::PtraceCleanup);
971                control.yield_checkpoint().await;
972                outcome = pending.resume_cleanup().await;
973            }
974            ToolRunOutcome::UnsupportedBackend(tracer) => {
975                control.stage.set(HermitCleanupStage::UnsupportedBackend);
976                control.snapshot.borrow_mut().primary =
977                    "configured tracer does not support ordinary owned completion".into();
978                // The untouched tracer remains in this async frame forever;
979                // repeated recovery refuses at a checkpoint, never legacy wait.
980                loop {
981                    std::hint::black_box(&tracer);
982                    control.yield_checkpoint().await;
983                }
984            }
985        }
986    }
987}
988
989#[cfg(test)]
990#[path = "ptrace_completion_tests.rs"]
991mod tests;