use syscalls::Errno;
use super::Pid;
pub(super) const CHILD_STACK_SIZE: usize = 8 * 1024 * 1024;
const GUARD_PAGES: usize = 4;
pub(super) struct ChildStack {
mapping: *mut libc::c_void,
mapping_len: usize,
guard_len: usize,
}
pub(super) fn child_stack() -> Result<ChildStack, Errno> {
let page_size = Errno::result(unsafe { libc::sysconf(libc::_SC_PAGESIZE) })? as usize;
let guard_len = page_size * GUARD_PAGES;
let mapping_len = guard_len + CHILD_STACK_SIZE;
let mapping = unsafe {
libc::mmap(
std::ptr::null_mut(),
mapping_len,
libc::PROT_NONE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_STACK,
-1,
0,
)
};
if mapping == libc::MAP_FAILED {
return Err(Errno::last());
}
let stack = ChildStack {
mapping,
mapping_len,
guard_len,
};
Errno::result(unsafe {
libc::mprotect(
stack.bottom().cast(),
CHILD_STACK_SIZE,
libc::PROT_READ | libc::PROT_WRITE,
)
})?;
Ok(stack)
}
impl ChildStack {
fn bottom(&self) -> *mut u8 {
unsafe { self.mapping.cast::<u8>().add(self.guard_len) }
}
fn top(&mut self) -> *mut libc::c_void {
let top = unsafe { self.bottom().add(CHILD_STACK_SIZE) };
unsafe { top.sub(top as usize % 16) }.cast()
}
}
impl Drop for ChildStack {
fn drop(&mut self) {
unsafe { libc::munmap(self.mapping, self.mapping_len) };
}
}
pub fn clone<F>(cb: F, flags: libc::c_int) -> Result<Pid, Errno>
where
F: FnMut() -> i32,
{
let mut stack = child_stack()?;
clone_with_stack(cb, flags, &mut stack)
}
pub(super) fn clone_with_stack<F>(
cb: F,
flags: libc::c_int,
stack: &mut ChildStack,
) -> Result<Pid, Errno>
where
F: FnMut() -> i32,
{
if flags & libc::CLONE_VM != 0 {
return Err(Errno::EINVAL);
}
extern "C" fn callback<G: FnMut() -> i32>(data: *mut libc::c_void) -> libc::c_int {
super::launch_window::reset_after_raw_clone();
let cb = unsafe { &mut *data.cast::<G>() };
cb() as libc::c_int
}
let mut cb = cb;
let res = unsafe {
libc::clone(
callback::<F>,
stack.top(),
flags,
(&mut cb as *mut F).cast::<libc::c_void>(),
)
};
Errno::result(res).map(Pid::from_raw)
}
pub(super) struct OwnedClone {
pub(super) pid: Pid,
pub(super) pidfd: Option<std::os::fd::OwnedFd>,
}
pub(super) fn clone_with_stack_owned<F>(
cb: F,
namespaces: super::Namespace,
stack: &mut ChildStack,
) -> Result<OwnedClone, Errno>
where
F: FnMut() -> i32,
{
use std::os::fd::FromRawFd;
if namespaces.bits() & !super::Namespace::all().bits() != 0 {
return Err(Errno::EINVAL);
}
#[cfg(test)]
if let OwnedCloneTestFault::Probe(error) = OWNED_CLONE_FAULT.with(|f| f.get()) {
return Err(error);
}
drop(super::fd::Fd::pidfd_open(unsafe { libc::getpid() }, 0)?);
#[cfg(test)]
if OWNED_CLONE_FAULT.with(|f| f.get()) == OwnedCloneTestFault::ExhaustAtClone {
let limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
Errno::result(unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) })?;
}
type CloneCb<'a> = Box<dyn FnMut() -> i32 + 'a>;
extern "C" fn callback(data: *mut CloneCb) -> libc::c_int {
super::launch_window::reset_after_raw_clone();
let cb: &mut CloneCb = unsafe { &mut *data };
(*cb)() as libc::c_int
}
let mut cb: CloneCb = Box::new(cb);
let mut pidfd = -1;
let result = unsafe {
libc::clone(
core::mem::transmute::<
extern "C" fn(*mut CloneCb) -> i32,
extern "C" fn(*mut libc::c_void) -> libc::c_int,
>(callback),
stack.top(),
namespaces.bits() | libc::SIGCHLD | libc::CLONE_PIDFD,
(&mut cb as *mut CloneCb).cast::<libc::c_void>(),
&mut pidfd as *mut libc::c_int,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_int>(),
)
};
let pid = Pid::from_raw(Errno::result(result)?);
let pidfd = if pidfd >= 0 {
Some(unsafe { std::os::fd::OwnedFd::from_raw_fd(pidfd) })
} else {
None
};
#[cfg(test)]
let pidfd = if OWNED_CLONE_FAULT.with(|f| f.get()) == OwnedCloneTestFault::MissingPidfd {
drop(pidfd);
None
} else {
pidfd
};
Ok(OwnedClone { pid, pidfd })
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum OwnedCloneTestFault {
None,
Probe(Errno),
ExhaustAtClone,
MissingPidfd,
}
#[cfg(test)]
thread_local! {
pub(super) static OWNED_CLONE_FAULT: std::cell::Cell<OwnedCloneTestFault> = const { std::cell::Cell::new(OwnedCloneTestFault::None) };
}
#[cfg(test)]
mod tests {
use std::io::Read;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use nix::sys::signal::Signal;
use nix::sys::wait::WaitStatus;
use nix::sys::wait::waitpid;
use super::*;
use crate::fd::pipe;
fn wait(pid: Pid) -> WaitStatus {
loop {
match waitpid(nix::unistd::Pid::from(pid), None) {
Err(nix::errno::Errno::EINTR) => continue,
result => return result.unwrap(),
}
}
}
#[test]
fn default_child_stack_keeps_the_container_run_minimum() {
if crate::test_runs_in_own_process() {
return;
}
let mut stack = child_stack().unwrap();
let size = stack.top() as usize - stack.bottom() as usize;
assert!(
size >= 2 * 1024 * 1024,
"the cloned child runs container setup before exec and needs at least 2 MiB"
);
assert_eq!(size, CHILD_STACK_SIZE);
assert_eq!(stack.top() as usize % 16, 0);
assert!(stack.guard_len >= unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize);
}
#[test]
fn default_child_stack_is_a_main_thread_stack() {
if crate::test_runs_in_own_process() {
return;
}
let mut stack = child_stack().unwrap();
let size = stack.top() as usize - stack.bottom() as usize;
assert!(
size >= 8 * 1024 * 1024,
"the in-container tracer's main thread runs on the child stack and needs the \
8 MiB a main thread gets; 2 MiB was measured to overflow"
);
assert_eq!(stack.mapping_len, stack.guard_len + CHILD_STACK_SIZE);
}
#[test]
fn child_keeps_its_stack_after_parent_unmaps() {
if crate::test_runs_in_own_process() {
return;
}
let mut stack = child_stack().unwrap();
let bottom = stack.bottom() as usize;
let top = stack.top() as usize;
let (reader, mut writer) = pipe().unwrap();
let reader_fd = reader.as_raw_fd();
let writer_fd = writer.as_raw_fd();
let pid = clone_with_stack(
|| {
unsafe {
libc::alarm(5);
libc::close(writer_fd);
let mut byte = 0_u8;
if libc::read(reader_fd, (&mut byte as *mut u8).cast(), 1) != 1 {
return 1;
}
let address = &byte as *const u8 as usize;
if (bottom..top).contains(&address) && byte == 42 {
42
} else {
2
}
}
},
libc::SIGCHLD,
&mut stack,
)
.unwrap();
drop(stack);
drop(reader);
writer.write_all(&[42]).unwrap();
drop(writer);
assert_eq!(wait(pid), WaitStatus::Exited(pid.into(), 42));
}
#[test]
fn stack_is_unmapped_after_clone_failure() {
if crate::test_runs_in_own_process() {
return;
}
let pid = unsafe { libc::fork() };
assert!(pid >= 0);
if pid == 0 {
let check = || -> Result<(), Errno> {
let mut stack = child_stack()?;
let mapping = stack.mapping;
let len = stack.mapping_len;
let page_size = stack.guard_len / GUARD_PAGES;
if clone_with_stack(|| 99, libc::SIGCHLD | libc::CLONE_SIGHAND, &mut stack)
!= Err(Errno::EINVAL)
{
return Err(Errno::EINVAL);
}
drop(stack);
for offset in (0..len).step_by(page_size) {
let mut resident = 0;
let result = unsafe {
libc::mincore(
mapping.cast::<u8>().wrapping_add(offset).cast(),
1,
&mut resident,
)
};
if result != -1 || Errno::last() != Errno::ENOMEM {
return Err(Errno::EINVAL);
}
}
Ok(())
};
unsafe {
libc::alarm(5);
libc::_exit(match std::panic::catch_unwind(check) {
Ok(Ok(())) => 0,
Ok(Err(_)) => 1,
Err(_) => 2,
});
}
}
assert_eq!(
wait(Pid::from_raw(pid)),
WaitStatus::Exited(nix::unistd::Pid::from_raw(pid), 0)
);
}
#[test]
fn shared_address_space_is_rejected() {
if crate::test_runs_in_own_process() {
return;
}
assert_eq!(
clone(|| 99, libc::SIGCHLD | libc::CLONE_VM),
Err(Errno::EINVAL)
);
}
static FAULT_FD: AtomicI32 = AtomicI32::new(-1);
extern "C" fn report_fault(_: libc::c_int, info: *mut libc::siginfo_t, _: *mut libc::c_void) {
unsafe {
let report = [(*info).si_addr() as usize, (*info).si_code as usize];
let size = std::mem::size_of_val(&report);
if libc::write(
FAULT_FD.load(Ordering::Relaxed),
report.as_ptr().cast(),
size,
) != size as isize
{
libc::_exit(100);
}
}
}
#[inline(never)]
fn recurse_into_guard(bottom: usize) -> u8 {
let mut frame = [0_u8; 1024];
std::hint::black_box(&mut frame);
if frame.as_ptr() as usize >= bottom {
frame[0] = recurse_into_guard(std::hint::black_box(bottom));
}
unsafe { std::ptr::read_volatile(frame.as_ptr()) }
}
#[test]
fn stack_overflow_faults_in_guard() {
if crate::test_runs_in_own_process() {
return;
}
let mut stack = child_stack().unwrap();
let guard_start = stack.mapping as usize;
let bottom = stack.bottom() as usize;
let mut signal_stack = vec![0_u8; 64 * 1024];
let alternate = libc::stack_t {
ss_sp: signal_stack.as_mut_ptr().cast(),
ss_size: signal_stack.len(),
ss_flags: 0,
};
let (mut reader, writer) = pipe().unwrap();
let writer_fd = writer.as_raw_fd();
let reader_fd = reader.as_raw_fd();
let pid = clone_with_stack(
|| {
unsafe {
libc::alarm(5);
libc::close(reader_fd);
let limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = report_fault as *const () as usize;
action.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK | libc::SA_RESETHAND;
libc::sigemptyset(&mut action.sa_mask);
FAULT_FD.store(writer_fd, Ordering::Relaxed);
if libc::setrlimit(libc::RLIMIT_CORE, &limit) != 0
|| libc::sigaltstack(&alternate, std::ptr::null_mut()) != 0
|| libc::sigaction(libc::SIGSEGV, &action, std::ptr::null_mut()) != 0
{
return 101;
}
}
i32::from(recurse_into_guard(bottom))
},
libc::SIGCHLD,
&mut stack,
)
.unwrap();
drop(stack);
drop(signal_stack);
drop(writer);
let status = wait(pid);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).unwrap();
assert!(
matches!(status, WaitStatus::Signaled(_, Signal::SIGSEGV, _)),
"{status:?}"
);
assert_eq!(bytes.len(), 2 * std::mem::size_of::<usize>());
let (address, code) = bytes.split_at(std::mem::size_of::<usize>());
let address = usize::from_ne_bytes(address.try_into().unwrap());
let code = usize::from_ne_bytes(code.try_into().unwrap());
assert!(
(guard_start..bottom).contains(&address),
"fault at {address:#x} outside guard {guard_start:#x}..{bottom:#x}"
);
assert_eq!(
code, 2,
"expected SEGV_ACCERR for the protected guard mapping"
);
}
}