Skip to content

Commit 8dfc788

Browse files
committed
fix(platform): guard WSL detection against Docker-on-WSL2 false positives
Prevent containers running on WSL2 from being incorrectly identified as WSL by adding is_container() guard that checks /.dockerenv, DOCKER_CONTAINER env var, OCI standard 'container' env var, and /proc/1/cgroup for docker paths. - New is_container() function with four detection methods in priority order - Guard added to is_wsl() and is_wsl2() functions before platform checks - Test-only is_container_with_probe() for comprehensive mock testing - 12 new tests verifying container detection and WSL/container interaction - All acceptance criteria met
1 parent ed136c4 commit 8dfc788

1 file changed

Lines changed: 224 additions & 3 deletions

File tree

src/platform/detection.rs

Lines changed: 224 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,13 @@ pub fn detect_capabilities(_platform: &Platform) -> PlatformCapabilities {
165165

166166
/// Returns `true` when running inside Windows Subsystem for Linux (WSL1 or WSL2).
167167
///
168-
/// Three-tier detection: environment variable, WSLInterop file, /proc/version.
168+
/// Returns `false` immediately if a container is detected. Otherwise uses
169+
/// three-tier detection: environment variable, WSLInterop file, /proc/version.
169170
fn is_wsl() -> bool {
171+
if is_container() {
172+
return false;
173+
}
174+
170175
if std::env::var("WSL_DISTRO_NAME").is_ok() {
171176
return true;
172177
}
@@ -182,12 +187,39 @@ fn is_wsl() -> bool {
182187

183188
/// Returns `true` only when running inside WSL2 (not WSL1).
184189
///
185-
/// Detection: `/proc/sys/fs/binfmt_misc/WSLInterop` file exists in WSL2 but
186-
/// is absent in WSL1.
190+
/// Returns `false` immediately if a container is detected. Otherwise checks
191+
/// whether `/proc/sys/fs/binfmt_misc/WSLInterop` exists (present in WSL2,
192+
/// absent in WSL1).
187193
fn is_wsl2() -> bool {
194+
if is_container() {
195+
return false;
196+
}
197+
188198
std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists()
189199
}
190200

201+
/// Returns `true` when running inside a container (Docker, Podman, etc.).
202+
///
203+
/// Checks four indicators in priority order: /.dockerenv file, DOCKER_CONTAINER
204+
/// env var, container env var (OCI standard), and /proc/1/cgroup contents.
205+
fn is_container() -> bool {
206+
if std::path::Path::new("/.dockerenv").exists() {
207+
return true;
208+
}
209+
210+
if std::env::var("DOCKER_CONTAINER").is_ok() {
211+
return true;
212+
}
213+
214+
if std::env::var("container").is_ok() {
215+
return true;
216+
}
217+
218+
std::fs::read_to_string("/proc/1/cgroup")
219+
.map(|c| c.contains("docker"))
220+
.unwrap_or(false)
221+
}
222+
191223
/// Parse the `ID=` field from `/etc/os-release` into a `LinuxDistro`.
192224
fn detect_linux_distro() -> LinuxDistro {
193225
let content = match std::fs::read_to_string("/etc/os-release") {
@@ -299,8 +331,32 @@ trait OsProbe {
299331
fn command_output(&self, cmd: &str, args: &[&str]) -> Option<String>;
300332
}
301333

334+
#[cfg(test)]
335+
fn is_container_with_probe(probe: &dyn OsProbe) -> bool {
336+
if probe.path_exists("/.dockerenv") {
337+
return true;
338+
}
339+
340+
if probe.env_var("DOCKER_CONTAINER").is_some() {
341+
return true;
342+
}
343+
344+
if probe.env_var("container").is_some() {
345+
return true;
346+
}
347+
348+
probe
349+
.read_file("/proc/1/cgroup")
350+
.map(|c| c.contains("docker"))
351+
.unwrap_or(false)
352+
}
353+
302354
#[cfg(test)]
303355
fn is_wsl_with_probe(probe: &dyn OsProbe) -> bool {
356+
if is_container_with_probe(probe) {
357+
return false;
358+
}
359+
304360
if probe.env_var("WSL_DISTRO_NAME").is_some() {
305361
return true;
306362
}
@@ -317,6 +373,10 @@ fn is_wsl_with_probe(probe: &dyn OsProbe) -> bool {
317373

318374
#[cfg(test)]
319375
fn is_wsl2_with_probe(probe: &dyn OsProbe) -> bool {
376+
if is_container_with_probe(probe) {
377+
return false;
378+
}
379+
320380
probe.path_exists("/proc/sys/fs/binfmt_misc/WSLInterop")
321381
}
322382

@@ -600,4 +660,165 @@ mod tests {
600660
let probe = MockProbe::new();
601661
assert_eq!(detect_shell_with_probe(&probe), "unknown");
602662
}
663+
664+
// -----------------------------------------------------------------------
665+
// Container detection tests (tests 25-36)
666+
// -----------------------------------------------------------------------
667+
668+
#[test]
669+
fn test_container_detected_from_dockerenv() {
670+
let mut probe = MockProbe::new();
671+
probe.paths.insert("/.dockerenv".into(), true);
672+
assert!(is_container_with_probe(&probe));
673+
}
674+
675+
#[test]
676+
fn test_container_detected_from_docker_container_env() {
677+
let mut probe = MockProbe::new();
678+
probe
679+
.env_vars
680+
.insert("DOCKER_CONTAINER".into(), "true".into());
681+
assert!(is_container_with_probe(&probe));
682+
}
683+
684+
#[test]
685+
fn test_container_detected_from_container_env() {
686+
let mut probe = MockProbe::new();
687+
probe
688+
.env_vars
689+
.insert("container".into(), "podman".into());
690+
assert!(is_container_with_probe(&probe));
691+
}
692+
693+
#[test]
694+
fn test_container_detected_from_cgroup() {
695+
let mut probe = MockProbe::new();
696+
probe.files.insert(
697+
"/proc/1/cgroup".into(),
698+
"12:memory:/docker/abc123def456\n11:cpuset:/docker/abc123def456\n".into(),
699+
);
700+
assert!(is_container_with_probe(&probe));
701+
}
702+
703+
#[test]
704+
fn test_not_container_when_no_indicators() {
705+
let probe = MockProbe::new();
706+
assert!(!is_container_with_probe(&probe));
707+
}
708+
709+
#[test]
710+
fn test_wsl_false_when_dockerenv_present() {
711+
let mut probe = MockProbe::new();
712+
// WSL indicators
713+
probe
714+
.paths
715+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
716+
probe.files.insert(
717+
"/proc/version".into(),
718+
"Linux version 6.6.87.2-microsoft-standard-WSL2".into(),
719+
);
720+
// Container indicator
721+
probe.paths.insert("/.dockerenv".into(), true);
722+
723+
assert!(!is_wsl_with_probe(&probe));
724+
}
725+
726+
#[test]
727+
fn test_wsl_false_when_container_env_set() {
728+
let mut probe = MockProbe::new();
729+
// WSL indicators -- all three tiers
730+
probe
731+
.env_vars
732+
.insert("WSL_DISTRO_NAME".into(), "Ubuntu".into());
733+
probe
734+
.paths
735+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
736+
probe.files.insert(
737+
"/proc/version".into(),
738+
"Linux version 6.6.87.2-microsoft-standard-WSL2".into(),
739+
);
740+
// Container indicator
741+
probe
742+
.env_vars
743+
.insert("container".into(), "podman".into());
744+
745+
assert!(!is_wsl_with_probe(&probe));
746+
}
747+
748+
#[test]
749+
fn test_wsl_false_when_docker_container_env_set() {
750+
let mut probe = MockProbe::new();
751+
// WSL indicators
752+
probe
753+
.paths
754+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
755+
probe.files.insert(
756+
"/proc/version".into(),
757+
"Linux version 6.6.87.2-microsoft-standard-WSL2".into(),
758+
);
759+
// Container indicator
760+
probe
761+
.env_vars
762+
.insert("DOCKER_CONTAINER".into(), "1".into());
763+
764+
assert!(!is_wsl_with_probe(&probe));
765+
}
766+
767+
#[test]
768+
fn test_wsl_true_genuine_wsl2_no_container() {
769+
let mut probe = MockProbe::new();
770+
probe
771+
.env_vars
772+
.insert("WSL_DISTRO_NAME".into(), "Ubuntu".into());
773+
probe
774+
.paths
775+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
776+
probe.files.insert(
777+
"/proc/version".into(),
778+
"Linux version 6.6.87.2-microsoft-standard-WSL2".into(),
779+
);
780+
781+
assert!(is_wsl_with_probe(&probe));
782+
}
783+
784+
#[test]
785+
fn test_wsl2_false_when_container_detected() {
786+
let mut probe = MockProbe::new();
787+
probe
788+
.paths
789+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
790+
probe.paths.insert("/.dockerenv".into(), true);
791+
792+
assert!(!is_wsl2_with_probe(&probe));
793+
}
794+
795+
#[test]
796+
fn test_wsl2_true_genuine_wsl2_no_container() {
797+
let mut probe = MockProbe::new();
798+
probe
799+
.paths
800+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
801+
802+
assert!(is_wsl2_with_probe(&probe));
803+
}
804+
805+
#[test]
806+
fn test_wsl_false_when_cgroup_contains_docker() {
807+
let mut probe = MockProbe::new();
808+
// WSL indicators
809+
probe
810+
.paths
811+
.insert("/proc/sys/fs/binfmt_misc/WSLInterop".into(), true);
812+
probe.files.insert(
813+
"/proc/version".into(),
814+
"Linux version 6.6.87.2-microsoft-standard-WSL2".into(),
815+
);
816+
// Only the cgroup indicator
817+
probe.files.insert(
818+
"/proc/1/cgroup".into(),
819+
"0::/docker/abc123\n".into(),
820+
);
821+
822+
assert!(!is_wsl_with_probe(&probe));
823+
}
603824
}

0 commit comments

Comments
 (0)