#[cfg(target_os = "linux")]
mod dotdot_bypass {
use proc_canonicalize::canonicalize;
use std::path::{Path, PathBuf};
#[test]
fn dotdot_in_pid_prefix_preserves_root_namespace() {
let pid = std::process::id();
let path = format!("/proc/{pid}/../{pid}/root");
let result = canonicalize(path).expect("canonicalize should succeed");
assert_ne!(
result,
Path::new("/"),
"namespace boundary lost: /proc/{pid}/../{pid}/root resolved to host '/'"
);
assert_eq!(
result,
PathBuf::from(format!("/proc/{pid}/root")),
"namespace prefix must be preserved through `..` normalization"
);
}
#[test]
fn dotdot_in_pid_prefix_with_subpath_preserves_root_namespace() {
let pid = std::process::id();
let path = format!("/proc/{pid}/../{pid}/root/etc");
let result = canonicalize(path).expect("canonicalize should succeed");
assert!(
result.starts_with(format!("/proc/{pid}/root")),
"namespace prefix lost for subpath: got {:?}",
result
);
}
#[test]
fn dotdot_in_pid_prefix_preserves_cwd_namespace() {
let pid = std::process::id();
let path = format!("/proc/{pid}/../{pid}/cwd");
let result = canonicalize(path).expect("canonicalize should succeed");
let host_cwd = std::env::current_dir().expect("get cwd");
assert_ne!(
result, host_cwd,
"cwd namespace boundary lost: resolved to actual host cwd"
);
assert_eq!(
result,
PathBuf::from(format!("/proc/{pid}/cwd")),
"cwd namespace prefix must be preserved through `..` normalization"
);
}
#[test]
fn dotdot_in_task_prefix_preserves_namespace() {
let pid = std::process::id();
let task_dir = Path::new("/proc/self/task");
let entry = std::fs::read_dir(task_dir)
.expect("read /proc/self/task")
.next()
.expect("at least one tid")
.expect("entry ok");
let tid = entry.file_name();
let tid_str = tid.to_string_lossy();
let path = format!("/proc/{pid}/task/{tid_str}/../{tid_str}/root");
let result = canonicalize(path).expect("canonicalize should succeed");
assert_ne!(
result,
Path::new("/"),
"task-level namespace boundary lost: resolved to host '/'"
);
assert_eq!(
result,
PathBuf::from(format!("/proc/{pid}/task/{tid_str}/root")),
"task-level namespace prefix must be preserved through `..` normalization"
);
}
#[test]
fn dotdot_in_self_prefix_preserves_namespace() {
let path = "/proc/self/../self/root";
let result = canonicalize(path).expect("canonicalize should succeed");
assert_ne!(
result,
Path::new("/"),
"namespace boundary lost via self/.. /self pattern"
);
let s = result.to_string_lossy();
assert!(
s.starts_with("/proc/") && s.ends_with("/root"),
"expected a /proc/.../root path, got {:?}",
result
);
}
#[test]
fn indirect_symlink_through_dotdot_preserves_namespace() {
use std::os::unix::fs::symlink;
let pid = std::process::id();
let temp = tempfile::tempdir().expect("tempdir");
let procdir = temp.path().join("procdir");
symlink(format!("/proc/{pid}"), &procdir).expect("create procdir symlink");
let attack = procdir.join("cwd/../root");
let result = canonicalize(attack).expect("canonicalize should succeed");
let host_cwd_parent = std::env::current_dir()
.expect("cwd")
.parent()
.map(Path::to_path_buf);
if let Some(parent) = host_cwd_parent {
assert_ne!(
result.join(""), parent.join("root"),
"namespace boundary lost: resolved to host parent-of-cwd path"
);
}
assert_ne!(
result,
Path::new("/"),
"namespace boundary lost: resolved to host '/'"
);
assert!(
result.starts_with(format!("/proc/{pid}/root"))
|| result.starts_with(format!("/proc/{pid}/cwd")),
"expected namespace prefix preserved, got {:?}",
result
);
}
}