use crate::{PathBoundary, StrictPathError};
use std::fs;
use std::path::{Path, PathBuf};
pub(super) fn get_proc_self_root() -> PathBuf {
PathBuf::from("/proc/self/root")
}
pub(super) fn get_self_pid() -> u32 {
std::process::id()
}
#[test]
fn blackbox_proc_self_root_prefix_preserved() {
let proc_self_root = get_proc_self_root();
match PathBoundary::<()>::try_new(&proc_self_root) {
Ok(container_dir) => {
let boundary_path = container_dir.interop_path();
let boundary_str = boundary_path.to_string_lossy();
assert!(
boundary_str.starts_with("/proc/self/root"),
"SECURITY FAILURE: /proc/self/root was resolved to {} instead of preserving the namespace prefix",
boundary_str
);
assert_ne!(
boundary_str, "/",
"SECURITY FAILURE: /proc/self/root was incorrectly resolved to /"
);
}
Err(e) => {
eprintln!("Skipping test: /proc/self/root not accessible: {e:?}");
}
}
}
#[test]
fn blackbox_container_escape_via_traversal_rejected() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let attack_patterns = [
"../etc/shadow",
"../../etc/passwd",
"../../../var/log/auth.log",
"../../../../root/.ssh/id_rsa",
"../../../../../etc/sudoers",
];
for attack_input in attack_patterns {
let result = container_dir.strict_join(attack_input);
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"SECURITY FAILURE: Attack pattern '{}' escaped to: {}",
attack_input,
path_str
);
}
Err(e) => {
panic!(
"Unexpected error for attack pattern '{}': {:?}",
attack_input, e
);
}
}
}
}
}
#[test]
fn blackbox_host_etc_passwd_access_blocked() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
match container_dir.strict_join("etc/passwd") {
Ok(passwd_path) => {
let path_str = passwd_path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"SECURITY FAILURE: etc/passwd resolved to host path: {}",
path_str
);
assert_ne!(
path_str, "/etc/passwd",
"SECURITY FAILURE: Boundary bypass allowed access to /etc/passwd"
);
}
Err(e) => {
eprintln!("Expected: etc/passwd access resulted in: {:?}", e);
}
}
}
}
#[test]
fn blackbox_absolute_path_escape_rejected() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let absolute_escape_attempts = ["/etc/shadow", "/root/.bashrc", "/var/log/syslog", "/home"];
for attack_input in absolute_escape_attempts {
let result = container_dir.strict_join(attack_input);
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"SECURITY FAILURE: Absolute path '{}' escaped to: {}",
attack_input,
path_str
);
}
Err(e) => {
panic!("Unexpected error for '{}': {:?}", attack_input, e);
}
}
}
}
}
#[test]
fn whitebox_all_proc_root_variants_preserved() {
let magic_patterns = [
"/proc/self/root",
"/proc/thread-self/root",
];
let pid = get_self_pid();
let pid_pattern = format!("/proc/{}/root", pid);
let mut all_patterns: Vec<&str> = magic_patterns.to_vec();
let pid_pattern_ref: &str = &pid_pattern;
all_patterns.push(pid_pattern_ref);
for pattern in all_patterns {
if !Path::new(pattern).exists() {
continue;
}
match PathBoundary::<()>::try_new(pattern) {
Ok(container_dir) => {
let boundary_path = container_dir.interop_path();
let boundary_str = boundary_path.to_string_lossy();
assert!(
boundary_str.starts_with("/proc/"),
"Boundary for '{}' doesn't start with /proc/: {}",
pattern,
boundary_str
);
assert!(
boundary_str.contains("/root"),
"Boundary for '{}' doesn't contain /root: {}",
pattern,
boundary_str
);
assert_ne!(
boundary_str, "/",
"SECURITY FAILURE: '{}' was resolved to /",
pattern
);
}
Err(e) => {
eprintln!("Pattern '{}' not accessible: {:?}", pattern, e);
}
}
}
}
#[test]
fn whitebox_proc_cwd_patterns_preserved() {
let cwd_patterns = ["/proc/self/cwd", "/proc/thread-self/cwd"];
let pid = get_self_pid();
let pid_cwd = format!("/proc/{}/cwd", pid);
for pattern in cwd_patterns
.iter()
.chain(std::iter::once(&pid_cwd.as_str()))
{
let pattern = *pattern;
if !Path::new(pattern).exists() {
continue;
}
match PathBoundary::<()>::try_new(pattern) {
Ok(container_dir) => {
let boundary_path = container_dir.interop_path();
let boundary_str = boundary_path.to_string_lossy();
assert!(
boundary_str.starts_with("/proc/") || boundary_str == pattern,
"Boundary for '{}' unexpectedly resolved: {}",
pattern,
boundary_str
);
}
Err(e) => {
eprintln!("Pattern '{}' not accessible: {:?}", pattern, e);
}
}
}
}
#[test]
fn whitebox_symlink_inside_proc_namespace() {
let temp = tempfile::tempdir().unwrap();
let container_root = temp.path();
let etc_dir = container_root.join("etc");
let var_dir = container_root.join("var");
fs::create_dir_all(&etc_dir).unwrap();
fs::create_dir_all(&var_dir).unwrap();
fs::write(etc_dir.join("passwd"), "root:x:0:0::/root:/bin/bash").unwrap();
let log_link = var_dir.join("log");
std::os::unix::fs::symlink(&etc_dir, &log_link).unwrap();
let container_dir: PathBoundary = PathBoundary::try_new(container_root).unwrap();
match container_dir.strict_join("var/log/passwd") {
Ok(path) => {
assert!(
path.strictpath_starts_with(container_dir.interop_path()),
"Symlink escape: {} is outside boundary",
path.strictpath_to_string_lossy()
);
}
Err(e) => {
eprintln!("Symlink test: {:?}", e);
}
}
}
#[test]
fn whitebox_nested_proc_paths_preserve_context() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let nested_paths = [
"usr/local/bin",
"home/user/.config",
"var/lib/dpkg/status",
"etc/apt/sources.list.d",
];
for nested_input in nested_paths {
match container_dir.strict_join(nested_input) {
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"Nested path '{}' lost namespace context: {}",
nested_input,
path_str
);
}
Err(_) => {
}
}
}
}
}
#[test]
fn cve_resistance_runc_style_proc_self_escape() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let attack_patterns = [
"../proc/self/exe", "../../proc/self/fd/0", "../../../proc/1/root", "../../../../proc/1/cwd", ];
for attack_input in attack_patterns {
let result = container_dir.strict_join(attack_input);
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"CVE-2019-5736 style escape succeeded: '{}' -> '{}'",
attack_input,
path_str
);
}
Err(e) => {
eprintln!("Pattern '{}': {:?}", attack_input, e);
}
}
}
}
}
#[test]
fn cve_resistance_container_runtime_escape_patterns() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let escape_patterns = [
"../../../host/etc/passwd",
"../../../var/run/docker.sock",
"../../../var/lib/kubelet/pods",
"../proc",
"../../../proc/1/ns/mnt",
];
for attack_input in escape_patterns {
let result = container_dir.strict_join(attack_input);
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Err(_) => {
}
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"Container escape pattern '{}' succeeded: {}",
attack_input,
path_str
);
}
}
}
}
}
#[test]
fn cve_resistance_namespace_confusion_attacks() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let confusion_patterns = [
"proc/self/root", "./proc/self/root", "proc/../etc/passwd", "./../../../proc/1/root", ];
for attack_input in confusion_patterns {
let result = container_dir.strict_join(attack_input);
match result {
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"Namespace confusion attack '{}' escaped: {}",
attack_input,
path_str
);
}
Err(_) => {
}
}
}
}
}
#[test]
fn pathboundary_proc_root_maintains_isolation() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let boundary_path_str = container_dir.interop_path().to_string_lossy().to_string();
assert!(
boundary_path_str.starts_with("/proc/self/root"),
"PathBoundary lost namespace context: {}",
boundary_path_str
);
if let Ok(etc_path) = container_dir.strict_join("etc") {
assert!(
etc_path.strictpath_starts_with(container_dir.interop_path()),
"Path escaped PathBoundary"
);
}
}
}
#[test]
fn pathboundary_strict_join_rejects_traversal() {
let proc_self_root = get_proc_self_root();
if let Ok(container_dir) = PathBoundary::<()>::try_new(&proc_self_root) {
let result = container_dir.strict_join("../../../etc/passwd");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"Traversal escaped boundary: {}",
path_str
);
}
Err(_) => {
}
}
}
}