Fix wait4 failure of child process created with vfork

This commit is contained in:
Hui, Chunyang 2023-04-25 07:32:09 +00:00 committed by volcano
parent 0ba7f80b21
commit 0b0fed947c
5 changed files with 132 additions and 10 deletions

@ -66,7 +66,7 @@ pub fn do_exec(
table::add_thread(new_process_ref.main_thread().unwrap());
table::add_process(new_process_ref);
task::enqueue_and_exec(new_main_thread);
return vfork_return_to_parent(context, current_ref);
return vfork_return_to_parent(context, current_ref, None);
}
// Force exit all child threads of current process

@ -1,3 +1,4 @@
use crate::process::do_vfork::reap_zombie_child_created_with_vfork;
use crate::signal::constants::*;
use std::intrinsics::atomic_store;
@ -15,7 +16,8 @@ use crate::vm::USER_SPACE_VM_MANAGER;
pub fn do_exit_group(status: i32, curr_user_ctxt: &mut CpuContext) -> Result<isize> {
if is_vforked_child_process() {
let current = current!();
return vfork_return_to_parent(curr_user_ctxt as *mut _, &current);
let child_exit_status = TermStatus::Exited(status as u8);
return vfork_return_to_parent(curr_user_ctxt as *mut _, &current, Some(child_exit_status));
} else {
let term_status = TermStatus::Exited(status as u8);
current!().process().force_exit(term_status);
@ -89,6 +91,7 @@ fn exit_thread(term_status: TermStatus) {
fn exit_process(thread: &ThreadRef, term_status: TermStatus) {
let process = thread.process();
let pid = process.pid();
// Deadlock note: always lock parent first, then child.
@ -123,13 +126,16 @@ fn exit_process(thread: &ThreadRef, term_status: TermStatus) {
if parent_inner.is_none() {
debug_assert!(parent.pid() == 0);
let pid = process.pid();
let main_tid = pid;
table::del_thread(main_tid).expect("tid must be in the table");
table::del_process(pid).expect("pid must be in the table");
clean_pgrp_when_exit(process);
process_inner.exit(term_status, &idle_ref, &mut idle_inner);
// For vfork-and-exit children, just clean them to free the pid
let _ = reap_zombie_child_created_with_vfork(pid);
idle_inner.remove_zombie_child(pid);
return;
}
@ -138,6 +144,9 @@ fn exit_process(thread: &ThreadRef, term_status: TermStatus) {
process_inner.exit(term_status, &idle_ref, &mut idle_inner);
// For vfork-and-exit children, just clean them to free the pid
let _ = reap_zombie_child_created_with_vfork(pid);
//Send SIGCHLD to parent
send_sigchld_to(&parent);
@ -225,6 +234,9 @@ fn exit_process_for_execve(
// Let new_process to adopt the children of current process
process_inner.exit(term_status, &new_parent_ref, &mut new_parent_inner);
// For vfork-and-exit children, we don't need to adopt them here.
// Because the new parent process share the same pid with the old parent process.
// Remove current process from parent process' zombie list.
if parent_inner.is_none() {
debug_assert!(parent.pid() == 0);

@ -1,5 +1,5 @@
use super::untrusted_event::{set_event, wait_event};
use super::{ProcessRef, ThreadId, ThreadRef};
use super::{ProcessFilter, ProcessRef, TermStatus, ThreadId, ThreadRef};
use crate::fs::FileTable;
use crate::interrupt::broadcast_interrupts;
use crate::prelude::*;
@ -24,10 +24,21 @@ use std::mem;
// use vfork to replace fork. For multi-threaded applications, if vfork doesn't stop other child threads, the application
// will be more likely to fail because the child process directly uses the VM and the file table of the parent process.
// The exit status of the child process which directly calls exit after vfork.
struct ChildExitStatus {
pid: pid_t,
status: TermStatus,
}
lazy_static! {
// Store all the parents's file tables who call vfork. It will be recovered when the child exits or has its own task.
// K: parent pid, V: parent file table
static ref VFORK_PARENT_FILE_TABLES: SgxMutex<HashMap<pid_t, FileTable>> = SgxMutex::new(HashMap::new());
// Store all the child process's exit status which are created with vfork and directly exit without calling execve. Because
// these children process are only allocated with a pid, they are not managed by the usual way including exit and wait. Use
// this special structure to record these children.
// K: parent pid, V: exit children created with vfork.
static ref EXIT_CHILDREN_STATUS: SgxMutex<HashMap<pid_t, Vec<ChildExitStatus>>> = SgxMutex::new(HashMap::new());
}
thread_local! {
@ -94,9 +105,14 @@ pub fn is_vforked_child_process() -> bool {
pub fn vfork_return_to_parent(
mut context: *mut CpuContext,
current_ref: &ThreadRef,
child_exit_status: Option<TermStatus>, // If the child process exits, the exit status should be specified.
) -> Result<isize> {
let child_pid = restore_parent_process(context, current_ref)?;
if let Some(term_status) = child_exit_status {
record_exit_child(current_ref.process().pid(), child_pid as pid_t, term_status);
}
// Wake parent's child thread which are all sleeping
let current = current!();
let child_threads = current.process().threads();
@ -112,6 +128,17 @@ pub fn vfork_return_to_parent(
Ok(child_pid)
}
fn record_exit_child(parent_pid: pid_t, child_pid: pid_t, child_exit_status: TermStatus) {
let child_exit_status = ChildExitStatus::new(child_pid, child_exit_status);
let mut children_status = EXIT_CHILDREN_STATUS.lock().unwrap();
if let Some(children) = children_status.get_mut(&parent_pid) {
children.push(child_exit_status);
} else {
children_status.insert(parent_pid, vec![child_exit_status]);
}
}
fn restore_parent_process(mut context: *mut CpuContext, current_ref: &ThreadRef) -> Result<isize> {
let current_thread = current!();
let current_pid = current_ref.process().pid();
@ -201,3 +228,64 @@ pub fn handle_force_stop() {
}
}
}
// Wait4 unwaited child which are created with vfork and directly exit without calling execve.
pub fn wait4_exit_child_created_with_vfork(
parent_pid: pid_t,
child_filter: &ProcessFilter,
) -> Option<(pid_t, i32)> {
let mut children_status = EXIT_CHILDREN_STATUS.lock().unwrap();
if let Some(children) = children_status.get_mut(&parent_pid) {
let unwaited_child_idx = children.iter().position(|child| match child_filter {
ProcessFilter::WithAnyPid => true,
ProcessFilter::WithPid(pid) => pid == child.pid(),
ProcessFilter::WithPgid(pgid) => todo!(), // This case should be rare.
});
if let Some(child_idx) = unwaited_child_idx {
let child = children.remove(child_idx);
if children.is_empty() {
children_status.remove(&parent_pid);
}
return Some((*child.pid(), child.status().as_u32() as i32));
}
}
None
}
// Reap all unwaited child which are created with vfork and directly exit without calling execve.
pub fn reap_zombie_child_created_with_vfork(parent_pid: pid_t) -> Option<Vec<pid_t>> {
let mut children_status = EXIT_CHILDREN_STATUS.lock().unwrap();
let children = children_status.remove(&parent_pid);
if children.is_none() {
warn!("no vforked children found");
return None;
}
Some(
children
.unwrap()
.into_iter()
.map(|child| child.pid)
.collect(),
)
}
impl ChildExitStatus {
fn new(child_pid: pid_t, status: TermStatus) -> Self {
Self {
pid: child_pid,
status,
}
}
fn pid(&self) -> &pid_t {
&self.pid
}
fn status(&self) -> &TermStatus {
&self.status
}
}

@ -1,3 +1,4 @@
use super::do_vfork::wait4_exit_child_created_with_vfork;
use super::pgrp::clean_pgrp_when_exit;
use super::process::{ProcessFilter, ProcessInner};
use super::wait::Waiter;
@ -53,10 +54,6 @@ pub fn do_wait4(child_filter: &ProcessFilter, options: WaitOptions) -> Result<(p
})
.collect::<Vec<&ProcessRef>>();
if unwaited_children.len() == 0 {
return_errno!(ECHILD, "Cannot find any unwaited children");
}
// Return immediately if a child that we wait for has already exited
let zombie_child = unwaited_children
.iter()
@ -67,6 +64,14 @@ pub fn do_wait4(child_filter: &ProcessFilter, options: WaitOptions) -> Result<(p
return Ok((zombie_pid, exit_status));
}
// Check again for vfork-and-exit child process which doesn't have a real structure of a process
if let Some(child_status) = wait4_exit_child_created_with_vfork(process.pid(), child_filter) {
return Ok(child_status);
} else if unwaited_children.len() == 0 {
// No unwaited children or vforked children, return immediately
return_errno!(ECHILD, "Cannot find any unwaited children");
}
// TODO: Support these options
if !options.supported() {
warn!("Unsupported options contained. wait options: {:?}", options);

@ -9,12 +9,29 @@
// Note: This test intends to test the case that child process directly calls _exit()
// after vfork. "exit", "_exit" and returning from main function are different.
// And here the exit function must be "_exit" to prevent undefined bevaviour.
int test_vfork_exit() {
int test_vfork_exit_and_wait() {
int status = 0;
pid_t child_pid = vfork();
if (child_pid == 0) {
_exit(0);
} else {
printf ("Comming back to parent process from child with pid = %d\n", child_pid);
// vfork again
pid_t child_pid_2 = vfork();
if (child_pid_2 == 0) {
_exit(1);
} else {
printf ("Comming back to parent process from child with pid = %d\n", child_pid_2);
int ret = waitpid(child_pid, &status, WUNTRACED);
if (ret != child_pid || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
THROW_ERROR("wait child status error");
}
ret = waitpid(child_pid_2, &status, WUNTRACED);
if (ret != child_pid_2 || !WIFEXITED(status) || WEXITSTATUS(status) != 1) {
THROW_ERROR("wait child status error");
}
}
}
return 0;
}
@ -167,7 +184,7 @@ int test_vfork_stop_child_thread() {
}
static test_case_t test_cases[] = {
TEST_CASE(test_vfork_exit),
TEST_CASE(test_vfork_exit_and_wait),
TEST_CASE(test_multiple_vfork_execve),
TEST_CASE(test_vfork_isolate_file_table),
TEST_CASE(test_vfork_stop_child_thread),