Skip to main content

hermit/
tracer_thread.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//! The thread that runs a library launch's ptrace session.
8//!
9//! The public launch functions run the tracer, Detcore's tools and their
10//! runtime on this thread rather than on the caller's, so that a launch needs
11//! no particular stack from its caller. A debug build of Detcore keeps every
12//! syscall arm's temporaries in one frame, and the session needed about 2 MiB,
13//! which is the whole of Rust's default for a spawned thread or a test
14//! (<https://github.com/rrnewton/hermit/issues/4389>).
15//!
16//! Each calling thread gets one tracer thread, kept for that caller's
17//! lifetime, so that an operation retained at a cleanup checkpoint stays with
18//! the thread that owns it and the caller can resume it there. Ptrace binds a
19//! tracee to the thread that attached it, so a whole session (spawn, waits,
20//! teardown) stays on one tracer thread. The tracer thread exits with its
21//! caller; a retained operation then cannot recover, as one whose original
22//! thread exited could not before.
23//!
24//! Two consequences for callers. A tracing subscriber the caller installed for
25//! its own thread only (`tracing::subscriber::set_default`) does not see the
26//! session's events, which happen on the tracer thread; a global subscriber
27//! does. And each calling thread that has launched keeps one idle tracer
28//! thread, with 8 MiB of reserved stack, until the caller exits. A child forked
29//! from the caller gets a tracer thread of its own at its first launch.
30
31use std::cell::Cell;
32use std::cell::RefCell;
33use std::panic;
34use std::panic::AssertUnwindSafe;
35use std::sync::atomic::AtomicBool;
36use std::sync::atomic::Ordering;
37use std::sync::mpsc;
38use std::thread::ThreadId;
39
40/// The tracer thread's stack: the 8 MiB a Linux main thread usually has
41/// (the default `ulimit -s`), on which the CLI runs the same session.
42pub(crate) const TRACER_STACK_BYTES: usize = 8 << 20;
43
44type Job = Box<dyn FnOnce() + Send>;
45
46// Set by [`launch_on_calling_thread`] for the whole process.
47static ON_CALLER: AtomicBool = AtomicBool::new(false);
48
49/// Makes every later launch in this process run its ptrace session on the
50/// calling thread, as launches did before the tracer thread, and leaves the
51/// caller's stack to the caller. For a process that must not gain threads: the
52/// `hermit` CLI runs its launch inside a container child made with a raw
53/// `clone`, whose entry point ends with a thread exit, so a tracer thread
54/// still waiting for work would keep that child alive. The CLI launches from
55/// its main thread or from the child's own stack, which have room.
56pub fn launch_on_calling_thread() {
57    ON_CALLER.store(true, Ordering::Relaxed);
58}
59
60/// The calling process's identity, read once for a whole call, or `None`
61/// when the launch runs on the calling thread instead: on a tracer thread,
62/// after [`launch_on_calling_thread`], or without a readable PID namespace.
63/// In the last case (`/proc` hidden or not mounted) a process cannot tell its
64/// tracer handle from one a forked child inherited, so it keeps no handle,
65/// and the launch refuses on the calling thread (`Identity`), as it did before
66/// the tracer thread. A kernel without PID namespaces is not that case: its
67/// one namespace is read as such (`PidNamespace::Only`), and the PID alone
68/// tells a forked child from its parent. One read per call, so that `/proc`
69/// vanishing between two reads cannot make a call both keep and discard its
70/// handle.
71fn tracer_identity() -> Option<ProcessIdentity> {
72    if ON_TRACER.get() || ON_CALLER.load(Ordering::Relaxed) {
73        return None;
74    }
75    let identity = ProcessIdentity::current();
76    identity.namespace.is_some().then_some(identity)
77}
78
79struct TracerThread {
80    jobs: mpsc::Sender<Job>,
81    id: ThreadId,
82    // The process the tracer thread runs in. A child forked from the caller
83    // inherits this handle but not the thread, so a handle from another
84    // process is stale there.
85    process: ProcessIdentity,
86}
87
88/// A process's number together with its PID namespace, which the number alone
89/// does not name: a container init that forks into a new PID namespace makes
90/// a child that is PID 1 as well.
91#[derive(Clone, Copy, PartialEq, Eq)]
92struct ProcessIdentity {
93    pid: u32,
94    /// `None` when `/proc` cannot say.
95    namespace: Option<crate::ptrace_completion::PidNamespace>,
96}
97
98impl ProcessIdentity {
99    fn current() -> Self {
100        Self {
101            pid: std::process::id(),
102            namespace: crate::ptrace_completion::open_pid_namespace()
103                .ok()
104                .map(|(namespace, _file)| namespace),
105        }
106    }
107}
108
109thread_local! {
110    // The calling thread's tracer thread. Dropping it with the caller closes
111    // the channel, and the tracer thread returns.
112    static TRACER: RefCell<Option<TracerThread>> = const { RefCell::new(None) };
113    static ON_TRACER: Cell<bool> = const { Cell::new(false) };
114}
115
116fn spawn(process: ProcessIdentity) -> std::io::Result<TracerThread> {
117    let (jobs, receive) = mpsc::channel::<Job>();
118    let handle = std::thread::Builder::new()
119        .name("hermit-tracer".to_owned())
120        .stack_size(TRACER_STACK_BYTES)
121        .spawn(move || {
122            ON_TRACER.set(true);
123            for job in receive {
124                job();
125            }
126        })?;
127    Ok(TracerThread {
128        jobs,
129        id: handle.thread().id(),
130        process,
131    })
132}
133
134/// The calling thread's tracer thread in this process, if it has one. A
135/// handle inherited across `fork` names a thread the child does not have,
136/// even when the child has the same PID in a new PID namespace: it is
137/// forgotten (dropping it could take a lock another thread of the parent held
138/// at the fork) and the caller gets a fresh tracer thread.
139fn current(tracer: &mut Option<TracerThread>, process: ProcessIdentity) -> Option<&TracerThread> {
140    if tracer
141        .as_ref()
142        .is_some_and(|tracer| tracer.process != process)
143    {
144        std::mem::forget(tracer.take());
145    }
146    tracer.as_ref()
147}
148
149/// Runs `job` on the calling thread's tracer thread, started on first use,
150/// and returns its result. A panic in `job` resumes in the caller. It runs
151/// `job` on the calling thread instead when [`tracer_identity`] says so (on a
152/// tracer thread, after [`launch_on_calling_thread`], or without a readable
153/// PID namespace) and during the calling thread's teardown.
154pub(crate) fn execute<R: Send + 'static>(
155    job: impl FnOnce() -> R + Send + 'static,
156) -> std::io::Result<R> {
157    match tracer_identity() {
158        Some(identity) => execute_with(identity, job),
159        None => Ok(job()),
160    }
161}
162
163/// [`execute`] for a caller that already read its identity.
164fn execute_with<R: Send + 'static>(
165    identity: ProcessIdentity,
166    job: impl FnOnce() -> R + Send + 'static,
167) -> std::io::Result<R> {
168    if TRACER.try_with(|_| ()).is_err() {
169        // During the calling thread's teardown its storage is gone: run in
170        // place, where the launch refuses as it always did
171        // (`StorageUnavailable`).
172        return Ok(job());
173    }
174    let (send, receive) = mpsc::channel();
175    let job: Job = Box::new(move || {
176        let _ = send.send(panic::catch_unwind(AssertUnwindSafe(job)));
177    });
178    let sent = TRACER.try_with(|tracer| {
179        let mut tracer = tracer.borrow_mut();
180        if current(&mut tracer, identity).is_none() {
181            *tracer = Some(spawn(identity)?);
182        }
183        tracer
184            .as_ref()
185            .expect("the tracer thread was just set")
186            .jobs
187            .send(job)
188            .map_err(|_| std::io::Error::other("Hermit's tracer thread has exited"))
189    });
190    sent.map_err(|_| std::io::Error::other("Hermit's tracer thread storage is gone"))??;
191    match receive.recv() {
192        Ok(Ok(value)) => Ok(value),
193        Ok(Err(payload)) => panic::resume_unwind(payload),
194        Err(_) => Err(std::io::Error::other(
195            "Hermit's tracer thread exited without finishing the launch",
196        )),
197    }
198}
199
200/// Runs `job` on the calling thread's tracer thread, as [`execute`] does, if
201/// `owner` is that thread, which is where that owner's operations live (it
202/// reads the identity once and uses [`execute_with`]). `None` otherwise,
203/// including whenever [`tracer_identity`] says the launch runs in place.
204pub(crate) fn execute_on<R: Send + 'static>(
205    owner: ThreadId,
206    job: impl FnOnce() -> R + Send + 'static,
207) -> Option<std::io::Result<R>> {
208    let identity = tracer_identity()?;
209    // A tracer thread of another process (a handle a forked child inherited)
210    // owns nothing here: the caller's identity check refuses instead.
211    let mine = TRACER
212        .try_with(|tracer| current(&mut tracer.borrow_mut(), identity).map(|tracer| tracer.id))
213        .ok()
214        .flatten();
215    (mine == Some(owner)).then(|| execute_with(identity, job))
216}
217
218/// A [`reverie::process::Command`] on its way to the tracer thread.
219pub(crate) struct SendCommand(reverie::process::Command);
220
221// SAFETY: `Command`'s only part that is not `Send` is
222// `reverie_process::util::CStringArray`, whose raw pointers point into the heap
223// buffers of the `CString`s it owns. Moving it to another thread moves those
224// owners with it, and nothing is shared with the sending thread. Every other
225// field is `Send` (its `pre_exec` callbacks are `Send + Sync`).
226unsafe impl Send for SendCommand {}
227
228impl SendCommand {
229    pub(crate) fn new(command: reverie::process::Command) -> Self {
230        Self(command)
231    }
232
233    /// Called inside the closure, so that the closure captures the whole
234    /// wrapper rather than the command field alone.
235    pub(crate) fn into_inner(self) -> reverie::process::Command {
236        self.0
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    /// One tracer thread serves its caller: every job runs on the same named
245    /// thread, a job's panic resumes in the caller and leaves that thread
246    /// serving, and `execute_on` runs only for that thread.
247    #[test]
248    fn one_tracer_thread_serves_its_caller() {
249        let first = execute(|| std::thread::current().id()).unwrap();
250        assert_ne!(first, std::thread::current().id());
251        assert_eq!(
252            execute(|| std::thread::current().name().map(str::to_owned)).unwrap(),
253            Some("hermit-tracer".to_owned())
254        );
255        let panicked = std::panic::catch_unwind(|| execute(|| panic!("in the job")));
256        let payload = panicked.expect_err("the panic resumes in the caller");
257        assert_eq!(payload.downcast_ref::<&str>(), Some(&"in the job"));
258        assert_eq!(execute(|| std::thread::current().id()).unwrap(), first);
259        assert!(execute_on(std::thread::current().id(), || ()).is_none());
260        assert_eq!(
261            execute_on(first, || std::thread::current().id()).map(Result::unwrap),
262            Some(first)
263        );
264    }
265}