#[cfg(target_os = "linux")]
use crate::PathBoundary;
#[cfg(all(target_os = "linux", feature = "virtual-path"))]
use crate::VirtualRoot;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
#[test]
fn issue_44_indirect_symlink_to_proc_root() {
let temp = tempfile::tempdir().unwrap();
let link_path = temp.path().join("container_root");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &link_path).unwrap();
match PathBoundary::<()>::try_new(&link_path) {
Ok(proc_dir) => {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(
boundary_str, "/",
"ISSUE #44 NOT FIXED: Indirect symlink to /proc/self/root resolved to /"
);
assert!(
boundary_str.starts_with("/proc/self/root"),
"ISSUE #44 NOT FIXED: Expected /proc/self/root prefix, got: {boundary_str}"
);
println!("Issue #44 FIXED: Resolved to {boundary_str}");
}
Err(e) => {
panic!("Issue #44 test failed unexpectedly: {:?}", e);
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn issue_44_indirect_symlink_with_suffix() {
let temp = tempfile::tempdir().unwrap();
let link_path = temp.path().join("container");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &link_path).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link_path) {
match proc_dir.strict_join("etc/passwd") {
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"Path through indirect symlink escaped: {}",
path_str
);
assert_ne!(
path_str, "/etc/passwd",
"SECURITY BUG: Accessed host /etc/passwd through indirect symlink"
);
}
Err(_) => {
}
}
}
}
#[cfg(all(target_os = "linux", feature = "virtual-path"))]
#[test]
fn issue_44_virtualroot_indirect_symlink() {
let temp = tempfile::tempdir().unwrap();
let link_path = temp.path().join("vroot_link");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &link_path).unwrap();
match VirtualRoot::<()>::try_new(&link_path) {
Ok(vroot) => {
let vroot_str = vroot.interop_path().to_string_lossy();
assert_ne!(
vroot_str, "/",
"ISSUE #44 NOT FIXED: VirtualRoot through indirect symlink resolved to /"
);
assert!(
vroot_str.starts_with("/proc/self/root"),
"ISSUE #44 NOT FIXED: VirtualRoot lost prefix: {}",
vroot_str
);
if let Ok(vpath) = vroot.virtual_join("etc/passwd") {
let system_path = vpath.as_unvirtual().strictpath_to_string_lossy();
assert!(
system_path.starts_with("/proc/self/root"),
"VirtualPath escaped via indirect symlink: {}",
system_path
);
}
}
Err(e) => {
panic!("VirtualRoot indirect symlink test failed: {:?}", e);
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_chained_symlinks_to_proc_root() {
let temp = tempfile::tempdir().unwrap();
let link2 = temp.path().join("link2");
let link1 = temp.path().join("link1");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &link2).unwrap();
std::os::unix::fs::symlink(&link2, &link1).unwrap();
match PathBoundary::<()>::try_new(&link1) {
Ok(proc_dir) => {
let boundary_str = proc_dir.interop_path().to_string_lossy();
println!("Resolved chained boundary: {}", boundary_str);
assert_ne!(boundary_str, "/", "Chained symlink resolved to /");
assert!(
boundary_str.starts_with("/proc/self/root"),
"Chained symlink lost prefix: {}",
boundary_str
);
}
Err(e) => eprintln!("Chained symlink test failed: {:?}", e),
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_triple_chained_symlinks_to_proc() {
let temp = tempfile::tempdir().unwrap();
let link3 = temp.path().join("link3");
let link2 = temp.path().join("link2");
let link1 = temp.path().join("link1");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &link3).unwrap();
std::os::unix::fs::symlink(&link3, &link2).unwrap();
std::os::unix::fs::symlink(&link2, &link1).unwrap();
match PathBoundary::<()>::try_new(&link1) {
Ok(proc_dir) => {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "Triple-chained symlink resolved to /");
assert!(
boundary_str.starts_with("/proc/self/root"),
"Triple-chained symlink lost prefix: {}",
boundary_str
);
}
Err(e) => eprintln!("Triple-chained symlink test: {:?}", e),
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_proc_self_cwd_preservation() {
let target = PathBuf::from("/proc/self/cwd");
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&target) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "/proc/self/cwd resolved to /");
assert!(
boundary_str.starts_with("/proc/self/cwd"),
"/proc/self/cwd lost prefix: {}",
boundary_str
);
}
let temp = tempfile::tempdir().unwrap();
let link = temp.path().join("link_to_cwd");
std::os::unix::fs::symlink(&target, &link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "Symlink to /proc/self/cwd resolved to /");
assert!(
boundary_str.starts_with("/proc/self/cwd"),
"Symlink to /proc/self/cwd lost prefix: {}",
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_proc_thread_self_root_preservation() {
let target = PathBuf::from("/proc/thread-self/root");
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&target) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "/proc/thread-self/root resolved to /");
assert!(
boundary_str.starts_with("/proc/thread-self/root"),
"/proc/thread-self/root lost prefix: {}",
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_indirect_symlink_to_proc_thread_self_root() {
let temp = tempfile::tempdir().unwrap();
let link = temp.path().join("thread_root_link");
let target = PathBuf::from("/proc/thread-self/root");
std::os::unix::fs::symlink(&target, &link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(
boundary_str, "/",
"Indirect symlink to /proc/thread-self/root resolved to /"
);
assert!(
boundary_str.starts_with("/proc/thread-self/root"),
"Indirect symlink lost /proc/thread-self/root prefix: {}",
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_proc_pid_root_with_actual_pid() {
let pid = std::process::id();
let target = PathBuf::from(format!("/proc/{}/root", pid));
if !target.exists() {
eprintln!("Skipping: /proc/{}/root does not exist", pid);
return;
}
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&target) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "/proc/{}/root resolved to /", pid);
assert!(
boundary_str.contains("/proc/") && boundary_str.contains("/root"),
"/proc/{}/root lost prefix: {}",
pid,
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_indirect_symlink_to_proc_pid_root() {
let pid = std::process::id();
let target = PathBuf::from(format!("/proc/{}/root", pid));
if !target.exists() {
eprintln!("Skipping: /proc/{}/root does not exist", pid);
return;
}
let temp = tempfile::tempdir().unwrap();
let link = temp.path().join("pid_root_link");
std::os::unix::fs::symlink(&target, &link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(
boundary_str, "/",
"Indirect symlink to /proc/{}/root resolved to /",
pid
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_symlink_deep_into_proc() {
let temp = tempfile::tempdir().unwrap();
let link = temp.path().join("link_deep");
let target = PathBuf::from("/proc/self/root/etc");
if !target.exists() {
eprintln!("Skipping deep link test: /proc/self/root/etc does not exist");
return;
}
std::os::unix::fs::symlink(&target, &link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/etc", "Deep link resolved to host /etc");
assert!(
boundary_str.starts_with("/proc/self/root"),
"Deep link lost /proc prefix: {}",
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_symlink_loop_handling() {
let temp = tempfile::tempdir().unwrap();
let link1 = temp.path().join("loop1");
let link2 = temp.path().join("loop2");
std::os::unix::fs::symlink(&link2, &link1).unwrap();
std::os::unix::fs::symlink(&link1, &link2).unwrap();
let result = PathBoundary::<()>::try_new(&link1);
assert!(result.is_err(), "Symlink loop should fail");
}
#[cfg(target_os = "linux")]
#[test]
fn test_mixed_chain_to_proc() {
let temp = tempfile::tempdir().unwrap();
let dir = temp.path().join("subdir");
std::fs::create_dir(&dir).unwrap();
let inner_link = dir.join("inner");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &inner_link).unwrap();
let outer_link = temp.path().join("outer");
std::os::unix::fs::symlink(&inner_link, &outer_link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&outer_link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(boundary_str, "/", "Mixed chain resolved to /");
assert!(
boundary_str.starts_with("/proc/self/root"),
"Mixed chain lost prefix: {}",
boundary_str
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn security_container_boundary_bypass_prevented() {
let temp = tempfile::tempdir().unwrap();
let container_root_link = temp.path().join("container_root");
let target = PathBuf::from("/proc/self/root");
std::os::unix::fs::symlink(&target, &container_root_link).unwrap();
match PathBoundary::<()>::try_new(&container_root_link) {
Ok(proc_dir) => {
let boundary_str = proc_dir.interop_path().to_string_lossy();
if boundary_str == "/" {
panic!(
"SECURITY VULNERABILITY: Container boundary resolved to /, \
this would allow accessing any file on the host!"
);
}
match proc_dir.strict_join("etc/shadow") {
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"SECURITY BUG: Accessed host path: {}",
path_str
);
}
Err(_) => {
}
}
match proc_dir.strict_join("../../../etc/shadow") {
Ok(path) => {
let path_str = path.strictpath_to_string_lossy();
assert!(
path_str.starts_with("/proc/self/root"),
"SECURITY BUG: Traversal escaped to: {}",
path_str
);
}
Err(_) => {
}
}
}
Err(e) => {
panic!("Unexpected boundary creation failure: {:?}", e);
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn security_host_root_never_accessible() {
let temp = tempfile::tempdir().unwrap();
let patterns = [
("/proc/self/root", "direct_proc"),
("/proc/self/root", "indirect_single"),
];
for (target, name) in patterns {
let link = temp.path().join(name);
std::os::unix::fs::symlink(target, &link).unwrap();
if let Ok(proc_dir) = PathBoundary::<()>::try_new(&link) {
let boundary_str = proc_dir.interop_path().to_string_lossy();
assert_ne!(
boundary_str, "/",
"Pattern '{}' (-> {}) resolved to host root /",
name, target
);
}
}
}