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}