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}