Adopt Rust postfork code

This adopts the Rust postfork code, bridging it from C++ exec module.

We use direct function calls for the bridge, rather than cxx/autocxx, so that we
can be sure that no memory allocations or other shenanigans are happening.
This commit is contained in:
ridiculousfish 2023-09-03 11:10:23 -07:00
parent c862a06874
commit 555171cb55
4 changed files with 102 additions and 59 deletions

View file

@ -120,7 +120,7 @@ set(FISH_SRCS
src/io.cpp src/io.cpp
src/null_terminated_array.cpp src/operation_context.cpp src/output.cpp src/null_terminated_array.cpp src/operation_context.cpp src/output.cpp
src/pager.cpp src/parse_execution.cpp src/parse_util.cpp src/pager.cpp src/parse_execution.cpp src/parse_util.cpp
src/parser.cpp src/parser_keywords.cpp src/path.cpp src/postfork.cpp src/parser.cpp src/parser_keywords.cpp src/path.cpp
src/proc.cpp src/reader.cpp src/screen.cpp src/proc.cpp src/reader.cpp src/screen.cpp
src/signals.cpp src/utf8.cpp src/signals.cpp src/utf8.cpp
src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp src/wcstringutil.cpp src/wgetopt.cpp src/wildcard.cpp

View file

@ -545,3 +545,60 @@ fn get_interpreter<'a>(command: &CStr, buffer: &'a mut [u8]) -> Option<&'a CStr>
}; };
Some(CStr::from_bytes_with_nul(&buffer[offset..idx.max(offset)]).unwrap()) Some(CStr::from_bytes_with_nul(&buffer[offset..idx.max(offset)]).unwrap())
} }
/// Set up redirections and signal handling in the child process.
mod ffi {
use super::*;
#[no_mangle]
pub extern "C" fn child_setup_process(
claim_tty_from: pid_t,
sigmask: *const libc::sigset_t,
is_forked: bool,
dup2s: *const Dup2List,
) -> i32 {
let sigmask = unsafe { sigmask.as_ref() };
let dup2s = unsafe { &*dup2s };
super::child_setup_process(claim_tty_from, sigmask, is_forked, dup2s)
}
#[no_mangle]
pub extern "C" fn safe_report_exec_error(
err: i32,
actual_cmd: *const c_char,
argvv: *const *const c_char,
envv: *const *const c_char,
) {
super::safe_report_exec_error(err, actual_cmd, argvv, envv)
}
#[no_mangle]
pub extern "C" fn execute_fork() -> pid_t {
super::execute_fork()
}
#[no_mangle]
pub extern "C" fn execute_setpgid(pid: pid_t, pgroup: pid_t, is_parent: bool) -> i32 {
super::execute_setpgid(pid, pgroup, is_parent)
}
#[no_mangle]
pub extern "C" fn report_setpgid_error(
err: i32,
is_parent: bool,
pid: pid_t,
desired_pgid: pid_t,
job_id: c_int,
command_str: *const c_char,
argv0_str: *const c_char,
) {
super::report_setpgid_error(
err,
is_parent,
pid,
desired_pgid,
job_id,
command_str,
argv0_str,
)
}
}

View file

@ -49,7 +49,6 @@
#include "null_terminated_array.h" #include "null_terminated_array.h"
#include "parse_tree.h" #include "parse_tree.h"
#include "parser.h" #include "parser.h"
#include "postfork.h"
#include "proc.h" #include "proc.h"
#include "reader.h" #include "reader.h"
#include "redirection.h" #include "redirection.h"
@ -145,6 +144,22 @@ bool is_thompson_shell_script(const char *path) {
return res; return res;
} }
// Implemented in postfork.rs. We don't use the FFI bridge to avoid the risk of allocations.
extern "C" {
void safe_report_exec_error(int err, const char *actual_cmd, const char *const *argv,
const char *const *envv);
int child_setup_process(pid_t claim_tty_from, const sigset_t *sigmask, bool is_forked,
const dup2_list_t *dup2s);
pid_t execute_fork();
int execute_setpgid(pid_t pid, pid_t pgroup, bool is_parent);
void report_setpgid_error(int err, bool is_parent, pid_t pid, pid_t desired_pgid, job_id_t job_id,
const char *cmd, const char *argv0);
}
/// This function is executed by the child process created by a call to fork(). It should be called /// This function is executed by the child process created by a call to fork(). It should be called
/// after \c child_setup_process. It calls execve to replace the fish process image with the command /// after \c child_setup_process. It calls execve to replace the fish process image with the command
/// specified in \c p. It never returns. Called in a forked child! Do not allocate memory, etc. /// specified in \c p. It never returns. Called in a forked child! Do not allocate memory, etc.
@ -244,10 +259,17 @@ static void internal_exec(env_stack_t &vars, job_t *j, const io_chain_t &block_i
return; return;
} }
sigset_t blocked_signals_storage;
sigemptyset(&blocked_signals_storage);
const sigset_t *blocked_signals = nullptr;
if (blocked_signals_for_job(*j, &blocked_signals_storage)) {
blocked_signals = &blocked_signals_storage;
}
// child_setup_process makes sure signals are properly set up. // child_setup_process makes sure signals are properly set up.
dup2_list_t redirs = dup2_list_resolve_chain_shim(all_ios); dup2_list_t redirs = dup2_list_resolve_chain_shim(all_ios);
if (child_setup_process(false /* not claim_tty */, *j, false /* not is_forked */, redirs) == if (child_setup_process(false /* not claim_tty */, blocked_signals, false /* not is_forked */,
0) { &redirs) == 0) {
// Decrement SHLVL as we're removing ourselves from the shell "stack". // Decrement SHLVL as we're removing ourselves from the shell "stack".
if (is_interactive_session()) { if (is_interactive_session()) {
auto shlvl_var = vars.get(L"SHLVL", ENV_GLOBAL | ENV_EXPORT); auto shlvl_var = vars.get(L"SHLVL", ENV_GLOBAL | ENV_EXPORT);
@ -408,6 +430,20 @@ static launch_result_t fork_child_for_process(const std::shared_ptr<job_t> &job,
pid_t claim_tty_from = pid_t claim_tty_from =
(p->leads_pgrp && job->group->wants_terminal()) ? getpgrp() : INVALID_PID; (p->leads_pgrp && job->group->wants_terminal()) ? getpgrp() : INVALID_PID;
// Decide if the job wants to set a custom sigmask.
sigset_t blocked_signals_storage;
sigemptyset(&blocked_signals_storage);
const sigset_t *blocked_signals = nullptr;
if (blocked_signals_for_job(*job, &blocked_signals_storage)) {
blocked_signals = &blocked_signals_storage;
}
// Narrow the command name for error reporting before fork,
// to avoid allocations in the forked child.
std::string narrow_cmd = wcs2string(job->command_wcstr());
std::string narrow_argv0 = wcs2string(p->argv0());
job_id_t job_id = job->job_id();
pid_t pid = execute_fork(); pid_t pid = execute_fork();
if (pid < 0) { if (pid < 0) {
return launch_result_t::failed; return launch_result_t::failed;
@ -420,18 +456,20 @@ static launch_result_t fork_child_for_process(const std::shared_ptr<job_t> &job,
if (p->leads_pgrp) { if (p->leads_pgrp) {
job->group->set_pgid(p->pid); job->group->set_pgid(p->pid);
} }
{ {
auto pgid = job->group->get_pgid(); auto pgid = job->group->get_pgid();
if (pgid) { if (pgid) {
if (int err = execute_setpgid(p->pid, pgid->value, is_parent)) { if (int err = execute_setpgid(p->pid, pgid->value, is_parent)) {
report_setpgid_error(err, is_parent, pgid->value, job.get(), p); report_setpgid_error(err, is_parent, p->pid, pgid->value, job_id,
narrow_cmd.c_str(), narrow_argv0.c_str());
} }
} }
} }
if (!is_parent) { if (!is_parent) {
// Child process. // Child process.
child_setup_process(claim_tty_from, *job, true, dup2s); child_setup_process(claim_tty_from, blocked_signals, true, &dup2s);
child_action(); child_action();
DIE("Child process returned control to fork_child lambda!"); DIE("Child process returned control to fork_child lambda!");
} }
@ -533,7 +571,7 @@ static launch_result_t exec_external_command(parser_t &parser, const std::shared
const char *actual_cmd = actual_cmd_str.c_str(); const char *actual_cmd = actual_cmd_str.c_str();
filename_ref_t file = parser.libdata().current_filename; filename_ref_t file = parser.libdata().current_filename;
#if FISH_USE_POSIX_SPAWN #if HAVE_SPAWN_H
// Prefer to use posix_spawn, since it's faster on some systems like OS X. // Prefer to use posix_spawn, since it's faster on some systems like OS X.
if (can_use_posix_spawn_for_job(j, dup2s)) { if (can_use_posix_spawn_for_job(j, dup2s)) {
++s_fork_count; // spawn counts as a fork+exec ++s_fork_count; // spawn counts as a fork+exec

View file

@ -1,52 +0,0 @@
// Functions that we may safely call after fork(), of which there are very few. In particular we
// cannot allocate memory, since we're insane enough to call fork from a multithreaded process.
#ifndef FISH_POSTFORK_H
#define FISH_POSTFORK_H
#include "config.h"
#ifdef HAVE_SPAWN_H
#include <spawn.h>
#endif
#ifndef FISH_USE_POSIX_SPAWN
#define FISH_USE_POSIX_SPAWN HAVE_SPAWN_H
#endif
#include "common.h"
#include "maybe.h"
struct Dup2List;
using dup2_list_t = Dup2List;
class job_t;
class process_t;
/// Tell the proc \p pid to join process group \p pgroup.
/// If \p is_child is true, we are the child process; otherwise we are fish.
/// Called by both parent and child; this is an unavoidable race inherent to Unix.
/// If is_parent is set, then we are the parent process and should swallow EACCESS.
/// \return 0 on success, an errno error code on failure.
int execute_setpgid(pid_t pid, pid_t pgroup, bool is_parent);
/// Report the error code \p err for a failed setpgid call.
/// Note not all errors should be reported; in particular EACCESS is expected and benign in the
/// parent only.
void report_setpgid_error(int err, bool is_parent, pid_t desired_pgid, const job_t *j,
const process_t *p);
/// Initialize a new child process. This should be called right away after forking in the child
/// process. This resets signal handlers and applies IO redirections.
///
/// If \p claim_tty_from is >= 0 and owns the tty, use tcsetpgrp() to claim it.
///
/// \return 0 on success, -1 on failure, in which case an error will be printed.
int child_setup_process(pid_t claim_tty_from, const job_t &job, bool is_forked,
const dup2_list_t &dup2s);
/// Call fork(), retrying on failure a few times.
pid_t execute_fork();
/// Report an error from failing to exec or posix_spawn a command.
void safe_report_exec_error(int err, const char *actual_cmd, const char *const *argv,
const char *const *envv);
#endif