detect-container 1.0.0

Detect whether the current process is running inside a container (Docker, Podman, etc.).
Documentation
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
#![warn(missing_debug_implementations)]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[cfg(target_os = "linux")]
mod linux {
    use std::fs;
    use std::os::unix::fs::MetadataExt;

    /// Hardcoded inode number of the root PID namespace since Linux 3.8.
    /// Defined in the kernel as `PROC_PID_INIT_INO`.
    pub(crate) const PROC_PID_INIT_INO: u64 = 0xEFFF_FFFC;

    /// Substrings in a cgroup line that indicate a container runtime.
    pub(crate) const CGROUP_MARKERS: &[&str] = &[
        "docker",
        "containerd",
        "kubepods",
        "lxc",
        "podman",
        "garden",
    ];

    /// Result of inspecting `/proc/self/ns/pid`.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub(crate) enum NsInode {
        /// Inode equals `PROC_PID_INIT_INO`; we are in the host's root
        /// PID namespace.
        Root,
        /// Inode differs from `PROC_PID_INIT_INO`; we are in a child
        /// PID namespace.
        NonRoot,
        /// `/proc/self/ns/pid` could not be read (no procfs, kernel
        /// without namespace support, etc.).
        Unknown,
    }

    pub(crate) fn read_pid_ns_inode() -> NsInode {
        match fs::metadata("/proc/self/ns/pid") {
            Ok(m) if m.ino() == PROC_PID_INIT_INO => NsInode::Root,
            Ok(_) => NsInode::NonRoot,
            Err(_) => NsInode::Unknown,
        }
    }

    pub(crate) fn is_pid_one() -> bool {
        std::process::id() == 1
    }

    /// Detect WSL1 by inspecting `/proc/sys/kernel/osrelease`. This is
    /// the [official WSL detection method](
    /// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364).
    ///
    /// WSL2 is excluded: it runs a real Linux kernel inside a
    /// lightweight Hyper-V VM, not a container, so its `osrelease`
    /// string (which contains `WSL2`) must not be treated as a
    /// container signal. Only WSL1 (which translates Linux syscalls
    /// onto the NT kernel and is more container-like in practice)
    /// matches here.
    pub(crate) fn is_wsl() -> bool {
        match fs::read_to_string("/proc/sys/kernel/osrelease") {
            Ok(s) => (s.contains("Microsoft") || s.contains("WSL")) && !s.contains("WSL2"),
            Err(_) => false,
        }
    }

    pub(crate) fn cgroup_indicates_container() -> bool {
        for path in ["/proc/1/cgroup", "/proc/self/cgroup"] {
            if let Ok(s) = fs::read_to_string(path) {
                if CGROUP_MARKERS.iter().any(|m| s.contains(m)) {
                    return true;
                }
            }
        }
        false
    }

    pub(crate) fn detect() -> bool {
        let ns = read_pid_ns_inode();

        // 1. Root PID namespace -> definitively not in a container.
        if matches!(ns, NsInode::Root) {
            return false;
        }

        // 2. PID 1 outside the host root namespace -> container init.
        //    (See the crate-level "Important assumption" warning.)
        if is_pid_one() {
            return true;
        }

        // 3. WSL.
        if is_wsl() {
            return true;
        }

        // 4. cgroup v1 references a known container runtime.
        if cgroup_indicates_container() {
            return true;
        }

        // 5. Fallback: any non-root PID namespace counts as a container.
        matches!(ns, NsInode::NonRoot)
    }
}

/// Returns `true` if the current process appears to be running inside a
/// container.
///
/// See the [crate-level documentation](crate) for the algorithm and
/// the supported platforms, **and** for the important assumption that
/// this crate is not used by code that may legitimately run as PID 1
/// on the host kernel.
///
/// The result is computed once and cached for the remainder of the
/// process; subsequent calls return the cached value.
///
/// # Examples
///
/// ```
/// let in_container = detect_container::is_container();
/// println!("in container: {in_container}");
/// ```
#[must_use]
#[inline]
pub fn is_container() -> bool {
    #[cfg(target_os = "linux")]
    {
        use std::sync::atomic::{AtomicU8, Ordering};

        // 0 = not yet computed, 1 = false, 2 = true.
        // `detect` is pure and idempotent, so a benign race in which
        // multiple threads compute the same value is fine and avoids
        // the overhead of `Once`/`LazyLock`.
        static CACHED: AtomicU8 = AtomicU8::new(0);

        match CACHED.load(Ordering::Relaxed) {
            1 => false,
            2 => true,
            _ => {
                let value = linux::detect();
                CACHED.store(if value { 2 } else { 1 }, Ordering::Relaxed);
                value
            }
        }
    }
    #[cfg(not(target_os = "linux"))]
    {
        false
    }
}

/// Diagnostic helpers that run every detection check without
/// short-circuiting.
///
/// This module is gated behind the `diagnostics` Cargo feature and is
/// **not** part of the crate's stable API surface. It exists so that
/// tests, debugging tools, and reports can show which individual
/// signals fired in a given environment.
#[cfg(feature = "diagnostics")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
pub mod diagnostics {
    /// The outcome of a single detection check.
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct CheckResult {
        /// Short, stable identifier (e.g. `"pid_namespace"`,
        /// `"pid_one"`, `"wsl"`, `"cgroup"`).
        pub name: &'static str,
        /// Human-readable description of what the check inspects.
        pub description: &'static str,
        /// `true` if this check, in isolation, indicates that the
        /// process is inside a container.
        pub matched: bool,
    }

    /// A full diagnostic report: every check, plus the final answer.
    #[derive(Debug, Clone)]
    pub struct Report {
        /// Per-check results in evaluation order.
        pub checks: Vec<CheckResult>,
        /// Final answer — equal to [`crate::is_container`] for the
        /// current process. This may differ from
        /// [`Report::any_matched`] because the algorithm has an
        /// early-return-`false` when in the root PID namespace.
        pub is_container: bool,
    }

    impl Report {
        /// `true` if any individual check matched. Note: this is **not**
        /// always equal to [`Report::is_container`], because the
        /// algorithm short-circuits to `false` when the process is in
        /// the host's root PID namespace.
        #[must_use]
        pub fn any_matched(&self) -> bool {
            self.checks.iter().any(|c| c.matched)
        }
    }

    /// Run every detection check without short-circuiting and return
    /// the outcome of each one, plus the final container-detection
    /// result.
    ///
    /// On non-Linux targets the returned report contains a single
    /// "platform" entry indicating that container detection is not
    /// supported.
    #[must_use]
    pub fn report() -> Report {
        #[cfg(target_os = "linux")]
        {
            use crate::linux::{self, NsInode};

            let ns = linux::read_pid_ns_inode();
            let pid_one = linux::is_pid_one();
            let wsl = linux::is_wsl();
            let cgroup = linux::cgroup_indicates_container();

            let checks = vec![
                CheckResult {
                    name: "pid_namespace",
                    description: match ns {
                        NsInode::Root => {
                            "/proc/self/ns/pid inode == PROC_PID_INIT_INO (root PID namespace)"
                        }
                        NsInode::NonRoot => {
                            "/proc/self/ns/pid inode != PROC_PID_INIT_INO (child PID namespace)"
                        }
                        NsInode::Unknown => {
                            "/proc/self/ns/pid could not be read (inode unknown)"
                        }
                    },
                    matched: matches!(ns, NsInode::NonRoot),
                },
                CheckResult {
                    name: "pid_one",
                    description: "getpid() == 1 (assumed to mean container init; never use this crate from a real init system)",
                    matched: pid_one,
                },
                CheckResult {
                    name: "wsl",
                    description: "/proc/sys/kernel/osrelease contains \"Microsoft\" or \"WSL\" but not \"WSL2\" (WSL2 is a VM, not a container)",
                    matched: wsl,
                },
                CheckResult {
                    name: "cgroup",
                    description: "/proc/1/cgroup or /proc/self/cgroup mentions a known runtime",
                    matched: cgroup,
                },
            ];

            Report {
                checks,
                is_container: crate::is_container(),
            }
        }
        #[cfg(not(target_os = "linux"))]
        {
            Report {
                checks: vec![CheckResult {
                    name: "platform",
                    description: "container detection is only implemented on Linux",
                    matched: false,
                }],
                is_container: crate::is_container(),
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn callable_and_cached() {
        // The function must be callable and must return the same value
        // on subsequent invocations.
        let first = is_container();
        let second = is_container();
        assert_eq!(first, second);
    }

    #[cfg(not(target_os = "linux"))]
    #[test]
    fn always_false_off_linux() {
        assert!(!is_container());
    }

    #[cfg(feature = "diagnostics")]
    #[test]
    fn report_agrees_with_is_container() {
        let report = diagnostics::report();
        assert_eq!(report.is_container, is_container());
    }

    #[cfg(all(feature = "diagnostics", target_os = "linux"))]
    #[test]
    fn report_runs_every_check() {
        // pid_namespace + pid_one + wsl + cgroup = 4 checks.
        let report = diagnostics::report();
        assert_eq!(report.checks.len(), 4);
    }
}