Skip to main content

running_process_platform_internal/platform/
process.rs

1//! Process spawning, containment, inspection, termination, and stdio.
2
3pub use crate::{
4    apply_process_priority, assign_child_to_windows_job, cancel_capture_reader,
5    canonical_environment_pairs, capture_reader_done, compat_shell_command, configure_exact_trace,
6    configure_process_command, configure_process_command_for_bounded_owner_death,
7    configure_sync_contained_command, configure_sync_daemon_command,
8    configure_sync_daemon_command_with_inheritance, configure_trampoline_command,
9    current_executable_build_id, exact_trace_capability, exit_code, exit_signal,
10    monitor_console_windows, parent_has_console, prepare_capture_reader, send_interrupt,
11    set_process_name, shell_command, soft_terminate_process_group, spawn_sync, spawn_sync_daemon,
12    spawn_sync_daemon_with_inheritance, start_attached_descendant_monitor,
13    start_descendant_monitor, start_exact_trace, sync_child_native_handle, trampoline_exit_code,
14    unix_mark_extra_fds_close_on_exec, CaptureCancellation, PlatformCaptureReaders,
15    PlatformStdChild, TracedChild, WindowsJobHandle,
16};
17
18#[cfg(feature = "async-process")]
19pub use crate::{
20    PlatformChild, PlatformEmergencySignal, PlatformLifecycle, PlatformOutput, PlatformStdin,
21    SpawnSpec, StreamMode,
22};
23
24#[cfg(feature = "process-inspection")]
25pub use crate::{kill_tree, process_snapshot, process_snapshot_for_pid};
26
27/// Host-neutral command options selected by the caller before spawning.
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub struct ProcessCommandConfig {
30    pub creation_flags: Option<u32>,
31    pub create_process_group: bool,
32    pub nice: Option<i32>,
33    pub address_space_limit_bytes: Option<u64>,
34}
35
36/// Opaque descriptor that a daemon spawn deliberately preserves through exec.
37///
38/// Normal daemon spawns retain the close-extra-descriptors default. The IPC
39/// listener handoff creates this value only after preparing its listener, and
40/// the Unix spawn boundary reopens exactly this descriptor after applying the
41/// default close-on-exec sweep.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct DaemonExecInheritance {
44    descriptor: i32,
45}
46
47impl DaemonExecInheritance {
48    // The token is constructed and consumed only by Unix IPC backends. Keep
49    // its representation host-neutral here so platform selection stays in
50    // those backend modules rather than leaking into the shared facade.
51    #[allow(dead_code)]
52    pub(crate) fn preserving_descriptor(descriptor: i32) -> Self {
53        Self { descriptor }
54    }
55
56    #[allow(dead_code)]
57    pub(crate) fn descriptor(self) -> i32 {
58        self.descriptor
59    }
60}
61
62/// Availability of an invasive, lossless launched-tree trace backend.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ExactTraceCapability {
65    pub available: bool,
66    pub backend: &'static str,
67    pub reason: &'static str,
68    pub non_invasive_backend: &'static str,
69    pub non_invasive_grade: NonInvasiveObservationGrade,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub enum NonInvasiveObservationGrade {
74    KernelNotification,
75    KernelHintReconciled,
76    SnapshotInferred,
77}
78
79/// A raw, bounded spawning-thread capture collected while a tracee is stopped.
80#[derive(Clone, Debug, Default, Eq, PartialEq)]
81pub struct TraceOriginArtifact {
82    pub origin_pid: u32,
83    pub thread_id: u32,
84    pub architecture: String,
85    pub register_format: String,
86    pub executable: Option<std::path::PathBuf>,
87    pub registers: Vec<u8>,
88    pub stack_pointer: Option<u64>,
89    pub instruction_pointer: Option<u64>,
90    pub stack: Vec<u8>,
91    pub truncated: bool,
92    pub module_map: Vec<u8>,
93    pub module_map_truncated: bool,
94}
95
96/// Native launched-tree event produced by an exact trace backend.
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct ExactTraceEvent {
99    pub sequence: u64,
100    pub pid: u32,
101    pub parent_pid: Option<u32>,
102    pub parent_start_key: Option<u64>,
103    pub start_key: Option<u64>,
104    pub timestamp: std::time::SystemTime,
105    pub kind: ExactTraceEventKind,
106    pub executable: Option<std::path::PathBuf>,
107    pub argv: Option<Vec<std::ffi::OsString>>,
108    pub origin: Option<TraceOriginArtifact>,
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub enum ExactTraceEventKind {
113    Spawn,
114    Exec,
115    Exit {
116        exit_code: Option<i32>,
117        signal: Option<i32>,
118        raw_status: i64,
119    },
120    Loss {
121        reason: String,
122    },
123}
124
125/// A descendant lifecycle fact reported by the host monitor.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum DescendantEvent {
128    Started {
129        pid: u32,
130        /// Immediate parent of the new descendant, when the discovery
131        /// mechanism knows it: the Linux `children`-file walk and the
132        /// macOS process-snapshot inversion both do; the Windows job
133        /// IOCP notification is PID-only, so it reports `None` rather
134        /// than paying a racy toolhelp scan per event.
135        parent_pid: Option<u32>,
136    },
137    Exited(u32),
138    /// The platform backend has completed its final reconciliation and no
139    /// further descendant events can arrive.
140    Completed,
141}
142
143/// Shared cancellation handle for a host-native descendant monitor.
144pub struct DescendantMonitorStop {
145    stopped: std::sync::atomic::AtomicBool,
146    mutex: std::sync::Mutex<()>,
147    wake: std::sync::Condvar,
148}
149
150impl DescendantMonitorStop {
151    /// Create an untriggered monitor cancellation handle.
152    pub fn new() -> Self {
153        Self {
154            stopped: std::sync::atomic::AtomicBool::new(false),
155            mutex: std::sync::Mutex::new(()),
156            wake: std::sync::Condvar::new(),
157        }
158    }
159
160    /// Report whether monitoring was cancelled.
161    pub fn is_stopped(&self) -> bool {
162        self.stopped.load(std::sync::atomic::Ordering::Acquire)
163    }
164
165    /// Cancel monitoring and wake a sleeping monitor immediately.
166    pub fn stop(&self) {
167        let _guard = self.mutex.lock().unwrap_or_else(|error| error.into_inner());
168        if !self.stopped.swap(true, std::sync::atomic::Ordering::AcqRel) {
169            self.wake.notify_all();
170        }
171    }
172
173    /// Wait until cancelled or `timeout` expires, returning whether cancelled.
174    pub fn wait_timeout(&self, timeout: std::time::Duration) -> bool {
175        if self.is_stopped() {
176            return true;
177        }
178        let guard = self.mutex.lock().unwrap_or_else(|error| error.into_inner());
179        if self.is_stopped() {
180            return true;
181        }
182        let (_guard, _wait_result) = self
183            .wake
184            .wait_timeout(guard, timeout)
185            .unwrap_or_else(|error| error.into_inner());
186        self.is_stopped()
187    }
188}
189
190impl Default for DescendantMonitorStop {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196/// Identifies one captured child output stream.
197#[derive(Clone, Copy)]
198pub enum CaptureStream {
199    Stdout,
200    Stderr,
201}
202
203/// Metadata about one visible window observed by console-popup monitoring.
204#[derive(Debug, Clone)]
205pub struct ConsoleWindowInfo {
206    pub pid: u32,
207    pub title: String,
208    pub hwnd: u64,
209}
210
211/// A platform-owned identity record used when observing a process tree.
212/// The timestamp fields are opaque host-native creation-time components and
213/// must only be compared for equality.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct ProcessSnapshot {
216    pub pid: u32,
217    pub parent_pid: u32,
218    pub start_time_a: u64,
219    pub start_time_b: u64,
220}
221
222/// Environment base selected by the shared caller for a synchronous spawn.
223///
224/// Explicit `Command::env` additions and removals remain on the command and
225/// are applied after this base by the selected platform implementation.
226#[derive(Clone, Debug, Eq, PartialEq)]
227pub enum SyncEnvironment {
228    /// Start with the spawning process's ambient environment.
229    Inherit,
230    /// Start with this complete, caller-assembled base environment.
231    Explicit(Vec<(std::ffi::OsString, std::ffi::OsString)>),
232}
233
234/// Caller-supplied stdio bindings for a contained synchronous child.
235///
236/// Each stream is independently configured. `drain_timeout` bounds how long
237/// wrapper-owned pipe ends remain open after the child exits; `None` leaves
238/// pipe closure entirely to the caller. `show_console` only affects Windows.
239pub struct SpawnStdio<'a> {
240    /// Child standard input source.
241    pub stdin: StdioSource<'a>,
242    /// Child standard output destination.
243    pub stdout: StdioSource<'a>,
244    /// Child standard error destination.
245    pub stderr: StdioSource<'a>,
246    /// Maximum post-exit pipe drain interval.
247    pub drain_timeout: Option<std::time::Duration>,
248    /// Whether a Windows child may inherit or allocate a visible console.
249    pub show_console: bool,
250}
251
252impl Default for SpawnStdio<'_> {
253    fn default() -> Self {
254        Self {
255            stdin: StdioSource::Null,
256            stdout: StdioSource::Parent,
257            stderr: StdioSource::Parent,
258            drain_timeout: Some(std::time::Duration::from_secs(2)),
259            show_console: false,
260        }
261    }
262}
263
264/// Caller-supplied output bindings for a detached synchronous child.
265///
266/// Detached children may write only to the platform null device or to a
267/// caller-owned file. Parent stdio and anonymous pipes are intentionally not
268/// available because either can retain or depend on the launching process.
269pub struct DaemonStdio<'a> {
270    /// Child standard output destination.
271    pub stdout: DaemonStdioSource<'a>,
272    /// Child standard error destination.
273    pub stderr: DaemonStdioSource<'a>,
274}
275
276impl Default for DaemonStdio<'_> {
277    fn default() -> Self {
278        Self {
279            stdout: DaemonStdioSource::Null,
280            stderr: DaemonStdioSource::Null,
281        }
282    }
283}
284
285/// Output destination accepted by the detached-child path.
286pub enum DaemonStdioSource<'a> {
287    /// Route output to the platform null device.
288    Null,
289    /// Duplicate a caller-owned file into the child.
290    File(&'a std::fs::File),
291}
292
293/// Standard-stream source or destination for a contained child.
294pub enum StdioSource<'a> {
295    /// Route the stream to the platform null device.
296    Null,
297    /// Inherit the matching stream from the parent process.
298    Parent,
299    /// Duplicate a caller-owned file into the child.
300    File(&'a std::fs::File),
301    /// Create and return an anonymous parent/child pipe pair.
302    Pipe,
303}
304
305/// Handle for a detached child that is not terminated when dropped.
306pub struct DaemonChild {
307    pub(crate) pid: u32,
308    pub(crate) inner: Box<dyn DaemonChildControl>,
309}
310
311pub(crate) trait DaemonChildControl:
312    Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe
313{
314    fn kill(&mut self) -> std::io::Result<()>;
315    fn wait(&mut self) -> std::io::Result<i32>;
316    fn try_wait(&mut self) -> std::io::Result<Option<i32>>;
317}
318
319impl DaemonChild {
320    /// Return the operating-system process identifier.
321    pub fn id(&self) -> u32 {
322        self.pid
323    }
324
325    /// Terminate the child process.
326    pub fn kill(&mut self) -> std::io::Result<()> {
327        self.inner.kill()
328    }
329
330    /// Wait for the child and return its numeric exit code.
331    pub fn wait(&mut self) -> std::io::Result<i32> {
332        self.inner.wait()
333    }
334
335    /// Return the exit code if the child has finished without blocking.
336    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
337        self.inner.try_wait()
338    }
339}
340
341/// Handle and optional parent pipe ends for a contained child.
342///
343/// Dropping this value shuts down the contained process group.
344pub struct SpawnedChild {
345    pub(crate) kill_on_drop: bool,
346    /// Writable parent end when standard input was configured as a pipe.
347    pub stdin: Option<std::process::ChildStdin>,
348    /// Readable parent end when standard output was configured as a pipe.
349    pub stdout: Option<std::process::ChildStdout>,
350    /// Readable parent end when standard error was configured as a pipe.
351    pub stderr: Option<std::process::ChildStderr>,
352    pub(crate) pid: u32,
353    pub(crate) inner: Box<dyn SpawnedChildControl>,
354}
355
356/// Native control operations for a contained child.
357///
358/// This trait is exposed so facade crates can preserve the contained-child
359/// type identity.  Constructing an implementation remains the responsibility
360/// of the platform substrate.
361pub trait SpawnedChildControl:
362    Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe
363{
364    fn kill(&mut self) -> std::io::Result<()>;
365    fn wait(&mut self) -> std::io::Result<i32>;
366    fn try_wait(&mut self) -> std::io::Result<Option<i32>>;
367    fn shutdown(&mut self);
368    #[cfg(feature = "independent-spawn")]
369    fn retain_exit_identity(&mut self) {}
370    #[cfg(feature = "independent-spawn")]
371    fn detach(&mut self) -> std::io::Result<()> {
372        Ok(())
373    }
374    #[cfg(feature = "independent-spawn")]
375    fn kill_tree(&mut self) -> std::io::Result<()> {
376        self.kill()
377    }
378}
379
380impl SpawnedChild {
381    /// Transfer caller-owned pipes and lifecycle control into this handle.
382    ///
383    /// This constructor does not launch a process or establish containment.
384    /// The caller must supply the matching PID, pipes, and control for an
385    /// already-contained child. Dropping the returned handle invokes
386    /// [`SpawnedChildControl::shutdown`]; the supplied control owns the actual
387    /// termination and reaping policy.
388    pub fn from_parts(
389        pid: u32,
390        stdin: Option<std::process::ChildStdin>,
391        stdout: Option<std::process::ChildStdout>,
392        stderr: Option<std::process::ChildStderr>,
393        inner: Box<dyn SpawnedChildControl>,
394    ) -> Self {
395        Self {
396            kill_on_drop: true,
397            stdin,
398            stdout,
399            stderr,
400            pid,
401            inner,
402        }
403    }
404
405    #[cfg(feature = "independent-spawn")]
406    pub(crate) fn retain_exit_identity(&mut self) {
407        self.inner.retain_exit_identity();
408    }
409    #[cfg(feature = "independent-spawn")]
410    pub(crate) fn commit_detached(&mut self) -> std::io::Result<()> {
411        self.inner.detach()?;
412        self.kill_on_drop = false;
413        Ok(())
414    }
415    #[cfg(feature = "independent-spawn")]
416    pub(crate) fn kill_tree(&mut self) -> std::io::Result<()> {
417        self.inner.kill_tree()
418    }
419    /// Return the operating-system process identifier.
420    pub fn id(&self) -> u32 {
421        self.pid
422    }
423
424    /// Forcibly terminate the child on a best-effort basis.
425    pub fn kill(&mut self) -> std::io::Result<()> {
426        self.inner.kill()
427    }
428
429    /// Wait for the child and return its numeric exit code.
430    pub fn wait(&mut self) -> std::io::Result<i32> {
431        self.inner.wait()
432    }
433
434    /// Return the exit code if the child has finished without blocking.
435    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
436        self.inner.try_wait()
437    }
438}
439
440impl Drop for SpawnedChild {
441    fn drop(&mut self) {
442        if self.kill_on_drop {
443            self.inner.shutdown();
444        }
445    }
446}
447
448#[derive(Clone, Copy)]
449pub enum ObserverScope {
450    SystemWide,
451    LaunchedProcessTree,
452}
453#[derive(Clone, Copy)]
454pub enum ObserverCategory {
455    File,
456    Network,
457    Process,
458}
459#[derive(Clone, Copy)]
460pub enum ObserverSupport {
461    Supported,
462    Partial,
463    Unavailable,
464}
465#[derive(Clone, Copy)]
466pub struct ObserverBackend {
467    pub support: ObserverSupport,
468    pub backend: &'static str,
469    pub reason: &'static str,
470}
471pub use crate::{
472    process_observer_backend as observer_backend, process_read_argv as read_process_argv,
473    process_read_cmdline as read_process_cmdline,
474    process_read_file_handles as read_process_file_handles,
475};
476
477/// Platform-neutral Unix signal selectors used by the compatibility facade.
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum UnixSignalKind {
480    Interrupt,
481    Terminate,
482    Kill,
483}
484
485pub use crate::{
486    unix_set_priority, unix_signal_process, unix_signal_process_group, unix_signal_raw,
487};
488
489/// What this host installed so a child outlives its owner no longer than it
490/// should.
491///
492/// The variants name the *guarantee*, not the call that produced it. A caller
493/// deciding whether to spawn a supervisor cares that the kernel will not do
494/// the reaping for it; whether the kernel would have used a parent-death
495/// signal or a job object is not a distinction it can act on.
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub enum OwnerDeathCleanup {
498    /// The kernel signals this process when its owner exits.
499    OwnerDeathSignal,
500    /// This process belongs to a container the kernel destroys with its owner.
501    KillOnOwnerHandleClose,
502    /// This process was already in such a container, installed by someone else.
503    AlreadyContained,
504    /// The host offers no kernel mechanism; a supervisor must do the reaping.
505    SupervisorRequired,
506    /// The host offers nothing and no supervisor contract is defined here.
507    Unsupported,
508}
509
510/// Which step of installing owner-death containment failed.
511///
512/// The caller's operator-facing messages distinguish these, and rightly: not
513/// being allowed to *build* a container is a different situation from
514/// building one and not being allowed to *join* it. Collapsing both into one
515/// error would make the two indistinguishable in a log, so the stage travels
516/// with the error rather than being inferred from the host.
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub enum OwnerDeathCleanupStage {
519    /// Asking the kernel to signal this process when its owner exits.
520    RequestSignal,
521    /// Creating the container that the kernel destroys with its owner.
522    CreateContainer,
523    /// Placing this process inside that container.
524    JoinContainer,
525}
526
527/// A failure to install owner-death containment, and the step it failed at.
528#[derive(Debug)]
529pub struct OwnerDeathCleanupError {
530    /// The step that failed.
531    pub stage: OwnerDeathCleanupStage,
532    /// What the host reported.
533    pub source: std::io::Error,
534}
535
536impl std::fmt::Display for OwnerDeathCleanupError {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        write!(f, "{:?}: {}", self.stage, self.source)
539    }
540}
541
542impl std::error::Error for OwnerDeathCleanupError {
543    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
544        Some(&self.source)
545    }
546}
547
548pub use crate::{
549    process_install_owner_death_cleanup as install_owner_death_cleanup,
550    process_owner_death_cleanup_target as owner_death_cleanup_target,
551};
552
553/// Why a host could not answer a question about a process.
554///
555/// The three named cases are the ones a caller can act on: a PID that could
556/// never name a process, a process that is not there, and a question this
557/// host does not answer. Everything else is the host's own report, kept
558/// whole rather than flattened into one of the three.
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
560pub enum ProcessInspectErrorKind {
561    /// The PID is outside the range this host issues.
562    InvalidPid,
563    /// No process on this host currently has that PID.
564    NotFound,
565    /// This host has no such primitive.
566    Unsupported,
567    /// The host was asked and refused, or failed.
568    Host,
569}
570
571/// A failure to inspect or signal a process, and what kind of failure it was.
572#[derive(Debug)]
573pub struct ProcessInspectError {
574    /// Which of the four situations this is.
575    pub kind: ProcessInspectErrorKind,
576    /// What the host reported.
577    pub source: std::io::Error,
578}
579
580impl ProcessInspectError {
581    /// Build an error of `kind` carrying the host's last reported error.
582    pub fn last_os_error(kind: ProcessInspectErrorKind) -> Self {
583        Self {
584            kind,
585            source: std::io::Error::last_os_error(),
586        }
587    }
588
589    /// Build an error of `kind` with a message this crate composed itself.
590    pub fn stated(kind: ProcessInspectErrorKind, message: &str) -> Self {
591        Self {
592            kind,
593            source: std::io::Error::other(message.to_string()),
594        }
595    }
596}
597
598impl std::fmt::Display for ProcessInspectError {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        write!(f, "{:?}: {}", self.kind, self.source)
601    }
602}
603
604impl std::error::Error for ProcessInspectError {
605    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
606        Some(&self.source)
607    }
608}
609
610pub use crate::{
611    process_executable_path as executable_path, process_fault_code_name as fault_code_name,
612    process_force_kill as force_kill, process_same_executable_path as same_executable_path,
613    process_signal_terminate as signal_terminate, ProcessLiveness,
614};
615
616/// A standing request from the host that this process shut down.
617///
618/// Hosts deliver this differently -- a POSIX signal, a Windows console
619/// control event injected on a thread of the OS's choosing -- but both arrive
620/// in a context where almost nothing is safe to do. A handler may not
621/// allocate, log, take a lock, or join a thread. So neither host runs the
622/// caller's code: each sets one flag, and the caller reads it whenever it is
623/// somewhere it can act.
624///
625/// That is why this is a poll rather than a callback. A callback would invite
626/// exactly the work the delivery context forbids.
627pub struct ShutdownRequest {
628    flag: &'static std::sync::atomic::AtomicBool,
629}
630
631impl ShutdownRequest {
632    /// Build a handle watching a flag the caller already owns.
633    ///
634    /// The host implementations use this to hand out a view of their own
635    /// static. It is public because a caller that already has a shutdown flag
636    /// -- one set by a supervisor protocol, or by a test -- can present it
637    /// through the same type rather than the loop it feeds needing two shapes
638    /// of "should I stop".
639    ///
640    /// `'static` is not incidental: a handler set by the OS outlives any
641    /// scope, so the flag it writes has to as well.
642    pub fn watching(flag: &'static std::sync::atomic::AtomicBool) -> Self {
643        Self { flag }
644    }
645
646    /// Whether the host has asked this process to shut down.
647    ///
648    /// Latching, not edge-triggered: once true it stays true, so a caller that
649    /// checks between two pieces of work cannot miss a request delivered while
650    /// it was busy.
651    pub fn requested(&self) -> bool {
652        self.flag.load(std::sync::atomic::Ordering::Relaxed)
653    }
654}
655
656impl std::fmt::Debug for ShutdownRequest {
657    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        f.debug_struct("ShutdownRequest")
659            .field("requested", &self.requested())
660            .finish()
661    }
662}
663
664pub use crate::process_install_shutdown_request_handler as install_shutdown_request_handler;
665
666/// Whether this host can replace the running image with another program.
667///
668/// Unix can: `execve` keeps the process -- its PID, its open descriptors,
669/// its place in the process tree -- and swaps the program underneath.
670/// Windows has no equivalent; the nearest thing is starting a successor and
671/// exiting, which is a *different* process with a different PID and does not
672/// keep anything a parent or supervisor was holding onto.
673///
674/// Callers that can accept a successor should ask this and fall back. Callers
675/// that genuinely need the same process to continue have no fallback, and
676/// should treat `false` as unsupported rather than approximating it.
677pub use crate::{
678    process_can_replace_current_image as can_replace_current_image,
679    process_replace_current_image as replace_current_image,
680};
681
682/// Object format of an image the host loader has mapped into this process.
683///
684/// The facade reports which format the loader hands out; reading the image's
685/// headers and sections is the caller's job. No object-file parsing lives
686/// here (#974): that is format code, not host selection.
687#[derive(Clone, Copy, Debug, Eq, PartialEq)]
688#[non_exhaustive]
689pub enum LoadedImageFormat {
690    /// A PE image whose headers are mapped at [`LoadedImage::header_address`].
691    Pe,
692    /// An ELF object described by [`LoadedImage::mapped_ranges`] and
693    /// [`LoadedImage::elf_load_bias`].
694    Elf,
695    /// A Mach-O image whose `mach_header` is at
696    /// [`LoadedImage::header_address`].
697    MachO,
698}
699
700/// One native image mapped into the current process, as the loader reports it.
701///
702/// These are raw host facts in the loader's own order. A field a host does not
703/// report is zero, empty or `None` rather than guessed.
704#[derive(Clone, Debug)]
705pub struct LoadedImage {
706    /// Object format, which selects how the caller reads the mapped headers.
707    pub format: LoadedImageFormat,
708    /// Address of the mapped image header (PE image base, Mach-O
709    /// `mach_header`). Zero for ELF, which reports mapped ranges instead.
710    pub header_address: u64,
711    /// Mapped image size the loader reports (PE `SizeOfImage`); zero elsewhere.
712    pub image_size: u64,
713    /// dyld's virtual-memory slide for a Mach-O image; zero elsewhere.
714    pub slide: i64,
715    /// Path of the image on disk, when the loader reports one.
716    pub path: Option<String>,
717    /// File-backed mappings of this image, sorted by start address (ELF).
718    pub mapped_ranges: Vec<std::ops::Range<u64>>,
719    /// The subset of `mapped_ranges` whose protection permits execution (ELF).
720    pub executable_ranges: Vec<std::ops::Range<u64>>,
721    /// `dl_iterate_phdr` load bias of the loaded object overlapping
722    /// `mapped_ranges`, when that object carries a GNU build id (ELF).
723    pub elf_load_bias: Option<u64>,
724    /// GNU build id read from that loaded object's mapped `PT_NOTE` (ELF).
725    pub build_id: Option<Vec<u8>>,
726    /// Identity of the file backing the mapping, re-checked on reopen. Read
727    /// only by hosts whose loader reports one.
728    #[allow(dead_code)]
729    pub(crate) backing_file: Option<LoadedImageBackingFile>,
730}
731
732/// Device and inode recorded for a file-backed mapping.
733///
734/// Only a host whose loader reports one constructs it; elsewhere the field
735/// stays `None`.
736#[derive(Clone, Debug, Eq, PartialEq)]
737#[allow(dead_code)]
738pub(crate) struct LoadedImageBackingFile {
739    pub(crate) device_major: u64,
740    pub(crate) device_minor: u64,
741    pub(crate) inode: String,
742}
743
744/// Enumerate the native images mapped into this process, and reopen the file
745/// behind one of them.
746///
747/// `open_loaded_image_file` returns `None` rather than a different file: where
748/// the host recorded which file backs the mapping (device and inode on Linux),
749/// a path that now names something else is refused, because reading it would
750/// pair the loaded image with another build's metadata.
751pub use crate::{
752    process_loaded_images as loaded_images,
753    process_open_loaded_image_file as open_loaded_image_file,
754};
755
756#[cfg(test)]
757mod loaded_image_tests {
758    /// A function whose address must fall inside an image the loader reports.
759    #[inline(never)]
760    fn landmark() -> u64 {
761        std::hint::black_box(0x0974_0004_u64)
762    }
763
764    #[test]
765    fn the_image_holding_this_code_is_reported_with_the_hosts_format() {
766        let address = landmark as fn() -> u64 as usize as u64;
767        assert_eq!(landmark(), 0x0974_0004);
768        let images = super::loaded_images().expect("enumerate loaded images");
769        assert!(!images.is_empty(), "the loader reported no images");
770        let expected = match std::env::consts::OS {
771            "windows" => super::LoadedImageFormat::Pe,
772            "macos" => super::LoadedImageFormat::MachO,
773            _ => super::LoadedImageFormat::Elf,
774        };
775        assert!(images.iter().all(|image| image.format == expected));
776        // PE and ELF report the covered range; dyld reports only the header,
777        // so a Mach-O owner is the nearest header at or below the address.
778        let owner = images
779            .iter()
780            .find(|image| {
781                image
782                    .mapped_ranges
783                    .iter()
784                    .any(|range| range.contains(&address))
785                    || (image.header_address..image.header_address.saturating_add(image.image_size))
786                        .contains(&address)
787            })
788            .or_else(|| {
789                images
790                    .iter()
791                    .filter(|image| image.format == super::LoadedImageFormat::MachO)
792                    .filter(|image| image.header_address <= address)
793                    .max_by_key(|image| image.header_address)
794            });
795        let owner = owner.unwrap_or_else(|| panic!("no loaded image covers {address:#x}"));
796        assert!(owner.path.is_some(), "owning image reported no path");
797        assert!(
798            super::open_loaded_image_file(owner).is_some(),
799            "the file behind the owning image could not be reopened: {:?}",
800            owner.path
801        );
802    }
803}
804
805#[cfg(test)]
806mod exit_signal_tests {
807    /// A process that exits on its own carries no terminating signal, on
808    /// every host.
809    #[test]
810    fn a_normal_exit_reports_no_signal() {
811        let status = std::process::ExitStatus::default();
812        assert!(status.success());
813        assert_eq!(super::exit_signal(&status), None);
814    }
815}