Skip to content

Commit 2d9e532

Browse files
fix(sandbox): skip fork-exec socket ambiguity test on SELinux-enforcing hosts (#1449)
Exec'ing /bin/sleep (SELinux label bin_t) from a user_home_t test binary causes /proc/<pid>/exe readlink to return ENOENT on SELinux-enforcing hosts due to the cross-domain boundary. Skip the test at runtime when getenforce reports Enforcing. Also adds a ChildGuard drop guard for safe child cleanup on panic and increases the exec-detection deadline from 2s to 5s. Signed-off-by: Derek Carr <decarr@redhat.com>
1 parent e3f009f commit 2d9e532

1 file changed

Lines changed: 33 additions & 16 deletions

File tree

crates/openshell-sandbox/src/proxy.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6515,18 +6515,43 @@ network_policies:
65156515

65166516
#[cfg(target_os = "linux")]
65176517
#[test]
6518+
// TODO: exec'ing /bin/sleep (SELinux label bin_t) from a user_home_t test
6519+
// binary causes /proc/<pid>/exe readlink to return ENOENT on
6520+
// SELinux-enforcing hosts. Fix by building a test-sleep-helper binary in
6521+
// the same crate so it inherits the user_home_t label.
65186522
fn resolve_process_identity_denies_fork_exec_shared_socket_ambiguity() {
65196523
use crate::identity::BinaryIdentityCache;
65206524
use std::ffi::CString;
65216525
use std::net::{TcpListener, TcpStream};
65226526
use std::os::fd::AsRawFd;
65236527
use std::time::{Duration, Instant};
65246528

6529+
struct ChildGuard(libc::pid_t);
6530+
impl Drop for ChildGuard {
6531+
fn drop(&mut self) {
6532+
#[allow(unsafe_code)]
6533+
unsafe {
6534+
libc::kill(self.0, libc::SIGKILL);
6535+
libc::waitpid(self.0, std::ptr::null_mut(), 0);
6536+
}
6537+
}
6538+
}
6539+
65256540
if !std::path::Path::new("/bin/sleep").exists() {
65266541
eprintln!("skipping: /bin/sleep not available");
65276542
return;
65286543
}
65296544

6545+
if std::process::Command::new("getenforce")
6546+
.output()
6547+
.is_ok_and(|o| String::from_utf8_lossy(&o.stdout).trim() == "Enforcing")
6548+
{
6549+
eprintln!(
6550+
"skipping: SELinux is enforcing — cross-label /proc/<pid>/exe readlink fails"
6551+
);
6552+
return;
6553+
}
6554+
65306555
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
65316556
let listener_port = listener.local_addr().unwrap().port();
65326557
let stream = TcpStream::connect(("127.0.0.1", listener_port)).expect("connect");
@@ -6567,7 +6592,10 @@ network_policies:
65676592
}
65686593
}
65696594

6570-
let deadline = Instant::now() + Duration::from_secs(2);
6595+
let _guard = ChildGuard(child_pid);
6596+
let entrypoint_pid = std::process::id();
6597+
6598+
let deadline = Instant::now() + Duration::from_secs(5);
65716599
loop {
65726600
if let Ok(link) = std::fs::read_link(format!("/proc/{child_pid}/exe"))
65736601
&& link.to_string_lossy().contains("sleep")
@@ -6576,38 +6604,27 @@ network_policies:
65766604
}
65776605
assert!(
65786606
Instant::now() < deadline,
6579-
"child pid {child_pid} did not exec into sleep within 2s"
6607+
"child pid {child_pid} did not exec into sleep within 5s"
65806608
);
65816609
std::thread::sleep(Duration::from_millis(20));
65826610
}
65836611

65846612
let cache = BinaryIdentityCache::new();
65856613

6586-
// Resolve with a brief retry loop — under heavy CI load the child's
6587-
// procfs entry can momentarily fail to resolve even though the loop
6588-
// above just verified `/proc/<pid>/exe` pointed at `sleep`. Retry a
6589-
// few times before declaring failure so the test is not flaky.
6590-
let mut result = resolve_process_identity(std::process::id(), peer_port, &cache);
6614+
let mut result = resolve_process_identity(entrypoint_pid, peer_port, &cache);
65916615
for _ in 0..5 {
65926616
match &result {
65936617
Err(err)
65946618
if err.reason.contains("No such file or directory")
65956619
|| err.reason.contains("os error 2") =>
65966620
{
65976621
std::thread::sleep(Duration::from_millis(50));
6598-
result = resolve_process_identity(std::process::id(), peer_port, &cache);
6622+
result = resolve_process_identity(entrypoint_pid, peer_port, &cache);
65996623
}
66006624
_ => break,
66016625
}
66026626
}
66036627

6604-
// libc/syscall FFI requires unsafe
6605-
#[allow(unsafe_code)]
6606-
unsafe {
6607-
libc::kill(child_pid, libc::SIGKILL);
6608-
libc::waitpid(child_pid, std::ptr::null_mut(), 0);
6609-
}
6610-
66116628
match result {
66126629
Ok(identity) => panic!(
66136630
"resolve_process_identity unexpectedly succeeded for shared socket owned by PID {}",
@@ -6620,7 +6637,7 @@ network_policies:
66206637
err.reason
66216638
);
66226639
assert!(
6623-
err.reason.contains(&std::process::id().to_string()),
6640+
err.reason.contains(&entrypoint_pid.to_string()),
66246641
"error should include parent PID; got: {}",
66256642
err.reason
66266643
);

0 commit comments

Comments
 (0)