#![cfg(target_os = "linux")]
#[cfg(feature = "anchored")]
use soft_canonicalize::anchored_canonicalize;
use soft_canonicalize::soft_canonicalize;
use std::path::PathBuf;
use std::process;
#[test]
fn test_proc_self_root_canonicalization_preserves_prefix() {
let proc_self_root = PathBuf::from("/proc/self/root");
assert!(
proc_self_root.exists(),
"/proc/self/root should exist on Linux"
);
let canonicalized =
soft_canonicalize(&proc_self_root).expect("soft_canonicalize should succeed");
println!("Input: {:?}", proc_self_root);
println!("Canonicalized: {:?}", canonicalized);
assert_eq!(
canonicalized,
PathBuf::from("/proc/self/root"),
"soft_canonicalize should preserve /proc/self/root prefix"
);
}
#[test]
fn test_proc_pid_root_canonicalization_preserves_prefix() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
assert!(proc_pid_root.exists(), "/proc/{}/root should exist", pid);
let canonicalized =
soft_canonicalize(&proc_pid_root).expect("soft_canonicalize should succeed");
println!("Input: {:?}", proc_pid_root);
println!("Canonicalized: {:?}", canonicalized);
assert_eq!(
canonicalized, proc_pid_root,
"soft_canonicalize should preserve /proc/PID/root prefix"
);
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_canonicalize_proc_root_boundary_preserved() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
let escape_attempt = "../../../etc/passwd";
let result = anchored_canonicalize(&proc_pid_root, escape_attempt);
println!("Anchor: {:?}", proc_pid_root);
println!("Candidate: {:?}", escape_attempt);
match &result {
Ok(path) => {
println!("Result: {:?}", path);
let preserves_namespace = path.starts_with(&proc_pid_root);
println!("Preserves namespace prefix: {}", preserves_namespace);
assert!(
preserves_namespace,
"anchored_canonicalize should preserve /proc/PID/root prefix"
);
assert!(
path.starts_with(&proc_pid_root),
"Result should stay within namespace boundary"
);
}
Err(e) => {
println!("Error: {:?}", e);
}
}
}
#[test]
fn test_soft_canonicalize_differs_from_std_for_proc_root() {
let proc_self_root = PathBuf::from("/proc/self/root");
let std_result =
std::fs::canonicalize(&proc_self_root).expect("std::fs::canonicalize should succeed");
let our_result = soft_canonicalize(&proc_self_root).expect("soft_canonicalize should succeed");
println!("std::fs::canonicalize: {:?}", std_result);
println!("soft_canonicalize: {:?}", our_result);
assert_eq!(std_result, PathBuf::from("/"));
assert_eq!(our_result, PathBuf::from("/proc/self/root"));
assert_ne!(
std_result, our_result,
"soft_canonicalize should differ from std::fs::canonicalize for /proc magic symlinks"
);
}
#[test]
fn test_file_access_through_proc_root_works() {
let proc_self_root = PathBuf::from("/proc/self/root");
let through_proc = proc_self_root.join("etc/os-release");
let direct = PathBuf::from("/etc/os-release");
if through_proc.exists() && direct.exists() {
let through_content = std::fs::read_to_string(&through_proc).ok();
let direct_content = std::fs::read_to_string(&direct).ok();
println!("Through /proc/self/root: {:?}", through_proc);
println!("Direct access: {:?}", direct);
println!(
"Contents match: {}",
through_content == direct_content
);
assert_eq!(
through_content, direct_content,
"Traversing through /proc/self/root accesses the same namespace"
);
} else {
println!("Skipping file comparison - /etc/os-release not available");
}
}
#[test]
fn test_canonicalize_subpath_under_proc_root() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
let subpath = proc_pid_root.join("etc/passwd");
if subpath.exists() {
let canonicalized = soft_canonicalize(&subpath).expect("should succeed");
println!("Input: {:?}", subpath);
println!("Canonicalized: {:?}", canonicalized);
let has_proc_prefix = canonicalized
.to_string_lossy()
.contains(&format!("/proc/{}/root", pid));
println!("Has /proc/PID/root prefix: {}", has_proc_prefix);
assert!(
has_proc_prefix,
"/proc/PID/root prefix should be preserved during canonicalization"
);
assert!(
canonicalized.starts_with(&proc_pid_root),
"Path should stay within namespace boundary"
);
} else {
println!("Skipping - /etc/passwd not accessible through /proc/PID/root");
}
}
#[test]
fn test_summary_namespace_canonicalization_fixed() {
println!("\n");
println!("{}", "=".repeat(70));
println!("NAMESPACE CANONICALIZATION FIX VERIFICATION");
println!("{}", "=".repeat(70));
println!();
println!("The Original Problem:");
println!(" /proc/PID/root is a 'magic symlink' that:");
println!(" - readlink() returns '/'");
println!(" - But traversing THROUGH it crosses into the process's namespace");
println!();
println!("Old (Broken) Behavior:");
println!(" soft_canonicalize('/proc/PID/root') '/'");
println!(" soft_canonicalize('/proc/PID/root/etc/passwd') '/etc/passwd'");
println!();
println!("New (Fixed) Behavior:");
println!(" soft_canonicalize('/proc/PID/root') '/proc/PID/root'");
println!(" soft_canonicalize('/proc/PID/root/etc/passwd') '/proc/PID/root/etc/passwd'");
println!();
println!("Security Benefit:");
println!(" Using /proc/PID/root as a PathBoundary anchor now works correctly!");
println!(" Container tooling can safely use soft_canonicalize for namespace paths.");
println!();
println!("{}", "=".repeat(70));
println!();
let proc_self_root = PathBuf::from("/proc/self/root");
let result = soft_canonicalize(&proc_self_root).expect("should succeed");
assert_eq!(result, proc_self_root, "Fix verification failed!");
println!(" Fix verified: /proc/self/root is preserved correctly");
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_subdirectory_inside_proc_root() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
let subdir_anchor = proc_pid_root.join("tmp");
if !subdir_anchor.exists() {
println!("Skipping: {} doesn't exist", subdir_anchor.display());
return;
}
let result = anchored_canonicalize(&subdir_anchor, "subdir/file.txt");
println!("Anchor: {:?}", subdir_anchor);
println!("Candidate: subdir/file.txt");
println!("Result: {:?}", result);
match result {
Ok(path) => {
assert!(
path.starts_with(&proc_pid_root),
"Result should preserve /proc/PID/root prefix: {:?}",
path
);
let path_str = path.to_string_lossy();
assert!(
path_str.contains("/tmp/") || path_str.ends_with("/tmp"),
"Result should be within /tmp anchor: {:?}",
path
);
assert!(
path_str.contains("file.txt"),
"Result should contain requested filename: {:?}",
path
);
}
Err(e) => {
println!("Error (may be acceptable): {}", e);
}
}
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_subdirectory_escape_attempt_clamped() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
let subdir_anchor = proc_pid_root.join("tmp");
if !subdir_anchor.exists() {
println!("Skipping: {} doesn't exist", subdir_anchor.display());
return;
}
let escape_attempt = "../../../etc/passwd";
let result = anchored_canonicalize(&subdir_anchor, escape_attempt);
println!("Anchor: {:?}", subdir_anchor);
println!("Escape attempt: {:?}", escape_attempt);
println!("Result: {:?}", result);
match result {
Ok(path) => {
assert!(
path.starts_with(&proc_pid_root),
"SECURITY: Result must preserve /proc/PID/root prefix, got: {:?}",
path
);
let canonical_anchor =
soft_canonicalize(&subdir_anchor).expect("anchor should canonicalize");
assert!(
path.starts_with(&canonical_anchor),
"Escape should be clamped to anchor {:?}, got: {:?}",
canonical_anchor,
path
);
let path_str = path.to_string_lossy();
assert!(
path_str.contains("etc/passwd") || path_str.ends_with("etc/passwd"),
"Clamped path should contain etc/passwd: {:?}",
path
);
println!("✓ Escape attempt correctly clamped to subdirectory anchor");
}
Err(e) => {
println!("Blocked with error (acceptable): {}", e);
}
}
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_deep_subdirectory_inside_proc_root() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
let deep_anchor = proc_pid_root.join("usr").join("share");
if !deep_anchor.exists() {
println!("Skipping: {} doesn't exist", deep_anchor.display());
return;
}
let escape_to_proc_root = "../../etc/passwd";
let result = anchored_canonicalize(&deep_anchor, escape_to_proc_root);
println!("Deep anchor: {:?}", deep_anchor);
println!("Escape attempt: {:?}", escape_to_proc_root);
println!("Result: {:?}", result);
match result {
Ok(path) => {
assert!(
path.starts_with(&proc_pid_root),
"SECURITY: Must preserve /proc/PID/root prefix: {:?}",
path
);
let canonical_anchor =
soft_canonicalize(&deep_anchor).expect("anchor should canonicalize");
assert!(
path.starts_with(&canonical_anchor),
"Should be clamped to deep anchor {:?}, got: {:?}",
canonical_anchor,
path
);
println!("✓ Deep subdirectory anchor correctly enforced");
}
Err(e) => {
println!("Blocked with error (acceptable): {}", e);
}
}
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_nonexisting_subdirectory_inside_proc_root() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
if !proc_pid_root.exists() {
println!("Skipping: /proc/{}/root doesn't exist", pid);
return;
}
let nonexisting_anchor = proc_pid_root
.join("var")
.join("fictional_app_12345")
.join("data");
let result = anchored_canonicalize(&nonexisting_anchor, "config/settings.json");
println!("Non-existing anchor: {:?}", nonexisting_anchor);
println!("Candidate: config/settings.json");
println!("Result: {:?}", result);
match result {
Ok(path) => {
assert!(
path.starts_with(&proc_pid_root),
"CRITICAL: Must preserve /proc/PID/root even for non-existing anchors: {:?}",
path
);
let path_str = path.to_string_lossy();
assert!(
path_str.contains("fictional_app_12345"),
"Should preserve non-existing anchor components: {:?}",
path
);
assert!(
path_str.contains("settings.json"),
"Should contain requested filename: {:?}",
path
);
println!("✓ Non-existing subdirectory anchor preserves /proc prefix");
}
Err(e) => {
println!("Error: {}", e);
}
}
}
#[test]
#[cfg(feature = "anchored")]
fn test_anchored_escape_from_nonexisting_subdirectory() {
let pid = process::id();
let proc_pid_root = PathBuf::from(format!("/proc/{}/root", pid));
if !proc_pid_root.exists() {
println!("Skipping: /proc/{}/root doesn't exist", pid);
return;
}
let nonexisting_anchor = proc_pid_root.join("opt").join("fake_service");
let escape_attempt = "../../../../../../../../etc/shadow";
let result = anchored_canonicalize(&nonexisting_anchor, escape_attempt);
println!("Non-existing anchor: {:?}", nonexisting_anchor);
println!("Escape attempt: {:?}", escape_attempt);
println!("Result: {:?}", result);
match result {
Ok(path) => {
assert!(
path.starts_with(&proc_pid_root),
"SECURITY VIOLATION: Escaped /proc/PID/root boundary! Got: {:?}",
path
);
assert_ne!(
path,
PathBuf::from("/etc/shadow"),
"SECURITY VIOLATION: Resolved to host path!"
);
println!("✓ Aggressive escape clamped to /proc boundary");
}
Err(e) => {
println!("Blocked with error (good): {}", e);
}
}
}