Skip to main content

detect_container/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(rust_2018_idioms)]
5#![warn(missing_debug_implementations)]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[cfg(target_os = "linux")]
9mod linux {
10    use std::fs;
11    use std::os::unix::fs::MetadataExt;
12
13    /// Hardcoded inode number of the root PID namespace since Linux 3.8.
14    /// Defined in the kernel as `PROC_PID_INIT_INO`.
15    pub(crate) const PROC_PID_INIT_INO: u64 = 0xEFFF_FFFC;
16
17    /// Substrings in a cgroup line that indicate a container runtime.
18    pub(crate) const CGROUP_MARKERS: &[&str] = &[
19        "docker",
20        "containerd",
21        "kubepods",
22        "lxc",
23        "podman",
24        "garden",
25    ];
26
27    /// Result of inspecting `/proc/self/ns/pid`.
28    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29    pub(crate) enum NsInode {
30        /// Inode equals `PROC_PID_INIT_INO`; we are in the host's root
31        /// PID namespace.
32        Root,
33        /// Inode differs from `PROC_PID_INIT_INO`; we are in a child
34        /// PID namespace.
35        NonRoot,
36        /// `/proc/self/ns/pid` could not be read (no procfs, kernel
37        /// without namespace support, etc.).
38        Unknown,
39    }
40
41    pub(crate) fn read_pid_ns_inode() -> NsInode {
42        match fs::metadata("/proc/self/ns/pid") {
43            Ok(m) if m.ino() == PROC_PID_INIT_INO => NsInode::Root,
44            Ok(_) => NsInode::NonRoot,
45            Err(_) => NsInode::Unknown,
46        }
47    }
48
49    pub(crate) fn is_pid_one() -> bool {
50        std::process::id() == 1
51    }
52
53    /// Detect WSL1 by inspecting `/proc/sys/kernel/osrelease`. This is
54    /// the [official WSL detection method](
55    /// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364).
56    ///
57    /// WSL2 is excluded: it runs a real Linux kernel inside a
58    /// lightweight Hyper-V VM, not a container, so its `osrelease`
59    /// string (which contains `WSL2`) must not be treated as a
60    /// container signal. Only WSL1 (which translates Linux syscalls
61    /// onto the NT kernel and is more container-like in practice)
62    /// matches here.
63    pub(crate) fn is_wsl() -> bool {
64        match fs::read_to_string("/proc/sys/kernel/osrelease") {
65            Ok(s) => (s.contains("Microsoft") || s.contains("WSL")) && !s.contains("WSL2"),
66            Err(_) => false,
67        }
68    }
69
70    pub(crate) fn cgroup_indicates_container() -> bool {
71        for path in ["/proc/1/cgroup", "/proc/self/cgroup"] {
72            if let Ok(s) = fs::read_to_string(path) {
73                if CGROUP_MARKERS.iter().any(|m| s.contains(m)) {
74                    return true;
75                }
76            }
77        }
78        false
79    }
80
81    pub(crate) fn detect() -> bool {
82        let ns = read_pid_ns_inode();
83
84        // 1. Root PID namespace -> definitively not in a container.
85        if matches!(ns, NsInode::Root) {
86            return false;
87        }
88
89        // 2. PID 1 outside the host root namespace -> container init.
90        //    (See the crate-level "Important assumption" warning.)
91        if is_pid_one() {
92            return true;
93        }
94
95        // 3. WSL.
96        if is_wsl() {
97            return true;
98        }
99
100        // 4. cgroup v1 references a known container runtime.
101        if cgroup_indicates_container() {
102            return true;
103        }
104
105        // 5. Fallback: any non-root PID namespace counts as a container.
106        matches!(ns, NsInode::NonRoot)
107    }
108}
109
110/// Returns `true` if the current process appears to be running inside a
111/// container.
112///
113/// See the [crate-level documentation](crate) for the algorithm and
114/// the supported platforms, **and** for the important assumption that
115/// this crate is not used by code that may legitimately run as PID 1
116/// on the host kernel.
117///
118/// The result is computed once and cached for the remainder of the
119/// process; subsequent calls return the cached value.
120///
121/// # Examples
122///
123/// ```
124/// let in_container = detect_container::is_container();
125/// println!("in container: {in_container}");
126/// ```
127#[must_use]
128#[inline]
129pub fn is_container() -> bool {
130    #[cfg(target_os = "linux")]
131    {
132        use std::sync::atomic::{AtomicU8, Ordering};
133
134        // 0 = not yet computed, 1 = false, 2 = true.
135        // `detect` is pure and idempotent, so a benign race in which
136        // multiple threads compute the same value is fine and avoids
137        // the overhead of `Once`/`LazyLock`.
138        static CACHED: AtomicU8 = AtomicU8::new(0);
139
140        match CACHED.load(Ordering::Relaxed) {
141            1 => false,
142            2 => true,
143            _ => {
144                let value = linux::detect();
145                CACHED.store(if value { 2 } else { 1 }, Ordering::Relaxed);
146                value
147            }
148        }
149    }
150    #[cfg(not(target_os = "linux"))]
151    {
152        false
153    }
154}
155
156/// Diagnostic helpers that run every detection check without
157/// short-circuiting.
158///
159/// This module is gated behind the `diagnostics` Cargo feature and is
160/// **not** part of the crate's stable API surface. It exists so that
161/// tests, debugging tools, and reports can show which individual
162/// signals fired in a given environment.
163#[cfg(feature = "diagnostics")]
164#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
165pub mod diagnostics {
166    /// The outcome of a single detection check.
167    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
168    pub struct CheckResult {
169        /// Short, stable identifier (e.g. `"pid_namespace"`,
170        /// `"pid_one"`, `"wsl"`, `"cgroup"`).
171        pub name: &'static str,
172        /// Human-readable description of what the check inspects.
173        pub description: &'static str,
174        /// `true` if this check, in isolation, indicates that the
175        /// process is inside a container.
176        pub matched: bool,
177    }
178
179    /// A full diagnostic report: every check, plus the final answer.
180    #[derive(Debug, Clone)]
181    pub struct Report {
182        /// Per-check results in evaluation order.
183        pub checks: Vec<CheckResult>,
184        /// Final answer — equal to [`crate::is_container`] for the
185        /// current process. This may differ from
186        /// [`Report::any_matched`] because the algorithm has an
187        /// early-return-`false` when in the root PID namespace.
188        pub is_container: bool,
189    }
190
191    impl Report {
192        /// `true` if any individual check matched. Note: this is **not**
193        /// always equal to [`Report::is_container`], because the
194        /// algorithm short-circuits to `false` when the process is in
195        /// the host's root PID namespace.
196        #[must_use]
197        pub fn any_matched(&self) -> bool {
198            self.checks.iter().any(|c| c.matched)
199        }
200    }
201
202    /// Run every detection check without short-circuiting and return
203    /// the outcome of each one, plus the final container-detection
204    /// result.
205    ///
206    /// On non-Linux targets the returned report contains a single
207    /// "platform" entry indicating that container detection is not
208    /// supported.
209    #[must_use]
210    pub fn report() -> Report {
211        #[cfg(target_os = "linux")]
212        {
213            use crate::linux::{self, NsInode};
214
215            let ns = linux::read_pid_ns_inode();
216            let pid_one = linux::is_pid_one();
217            let wsl = linux::is_wsl();
218            let cgroup = linux::cgroup_indicates_container();
219
220            let checks = vec![
221                CheckResult {
222                    name: "pid_namespace",
223                    description: match ns {
224                        NsInode::Root => {
225                            "/proc/self/ns/pid inode == PROC_PID_INIT_INO (root PID namespace)"
226                        }
227                        NsInode::NonRoot => {
228                            "/proc/self/ns/pid inode != PROC_PID_INIT_INO (child PID namespace)"
229                        }
230                        NsInode::Unknown => {
231                            "/proc/self/ns/pid could not be read (inode unknown)"
232                        }
233                    },
234                    matched: matches!(ns, NsInode::NonRoot),
235                },
236                CheckResult {
237                    name: "pid_one",
238                    description: "getpid() == 1 (assumed to mean container init; never use this crate from a real init system)",
239                    matched: pid_one,
240                },
241                CheckResult {
242                    name: "wsl",
243                    description: "/proc/sys/kernel/osrelease contains \"Microsoft\" or \"WSL\" but not \"WSL2\" (WSL2 is a VM, not a container)",
244                    matched: wsl,
245                },
246                CheckResult {
247                    name: "cgroup",
248                    description: "/proc/1/cgroup or /proc/self/cgroup mentions a known runtime",
249                    matched: cgroup,
250                },
251            ];
252
253            Report {
254                checks,
255                is_container: crate::is_container(),
256            }
257        }
258        #[cfg(not(target_os = "linux"))]
259        {
260            Report {
261                checks: vec![CheckResult {
262                    name: "platform",
263                    description: "container detection is only implemented on Linux",
264                    matched: false,
265                }],
266                is_container: crate::is_container(),
267            }
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn callable_and_cached() {
278        // The function must be callable and must return the same value
279        // on subsequent invocations.
280        let first = is_container();
281        let second = is_container();
282        assert_eq!(first, second);
283    }
284
285    #[cfg(not(target_os = "linux"))]
286    #[test]
287    fn always_false_off_linux() {
288        assert!(!is_container());
289    }
290
291    #[cfg(feature = "diagnostics")]
292    #[test]
293    fn report_agrees_with_is_container() {
294        let report = diagnostics::report();
295        assert_eq!(report.is_container, is_container());
296    }
297
298    #[cfg(all(feature = "diagnostics", target_os = "linux"))]
299    #[test]
300    fn report_runs_every_check() {
301        // pid_namespace + pid_one + wsl + cgroup = 4 checks.
302        let report = diagnostics::report();
303        assert_eq!(report.checks.len(), 4);
304    }
305}