wip: proper signal handling

This commit is contained in:
Jake Hillion 2022-02-10 17:57:19 +00:00
parent 0bb5e7b021
commit 0b34ee2394
4 changed files with 241 additions and 33 deletions

69
Cargo.lock generated
View File

@ -252,11 +252,27 @@ version = "0.1.0"
dependencies = [
"async-std",
"env_logger",
"futures",
"log",
"nix",
"thiserror",
]
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.21"
@ -264,6 +280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -272,6 +289,17 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.21"
@ -293,6 +321,47 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
[[package]]
name = "futures-task"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gloo-timers"
version = "0.2.3"

View File

@ -10,5 +10,6 @@ log = "0.4"
env_logger = "0.9"
thiserror = "1"
async-std = "1"
futures = "0.3.17"
nix = "0.23.1"

View File

@ -1,7 +1,12 @@
use async_std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("{msg}: {src}")]
Nix { msg: &'static str, src: nix::Error },
#[error("io: {0}")]
Io(#[from] io::Error),
}

View File

@ -1,4 +1,4 @@
use log::{error, info};
use log::{debug, error, info, warn};
mod error;
@ -6,19 +6,33 @@ use error::Error;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::unix::io::{FromRawFd, RawFd};
use std::sync::Arc;
use async_std::fs::File;
use async_std::io::{self, ReadExt, WriteExt};
use async_std::sync::Mutex;
use async_std::task;
use futures::try_join;
use nix::sys::ptrace::{self, Options};
use nix::sys::ptrace::{self, Event, Options};
use nix::sys::signal::Signal;
use nix::sys::wait::{wait, WaitStatus};
use nix::unistd::Pid;
use nix::unistd::{self, ForkResult};
struct Child {
pid: Pid,
stdin: File,
stdout: File,
stderr: File,
}
struct ProcessDescription {
state: ProcessState,
parent: Option<Pid>,
name: Option<String>,
}
@ -28,33 +42,117 @@ enum ProcessState {
}
fn main() -> Result<(), Error> {
env_logger::init();
let env = env_logger::Env::new().filter_or("LOG", "info");
env_logger::init_from_env(env);
info!("forking child process...");
let child = unsafe {
let child = spawn_child()?;
task::block_on(async_main(child))
}
fn spawn_child() -> Result<Child, Error> {
let child_stdin = unistd::pipe().map_err(|e| Error::Nix {
msg: "pipe",
src: e,
})?;
let child_stdout = unistd::pipe().map_err(|e| Error::Nix {
msg: "pipe",
src: e,
})?;
let child_stderr = unistd::pipe().map_err(|e| Error::Nix {
msg: "pipe",
src: e,
})?;
let pid = unsafe {
match nix::unistd::fork().map_err(|e| Error::Nix {
msg: "fork",
src: e,
})? {
ForkResult::Parent { child } => child,
ForkResult::Child => {
let err = spawn_child();
let err = run_child(
vec!["/bin/sh", "-c", "/bin/true"],
child_stdin.0,
child_stdout.1,
child_stderr.1,
);
error!("error spawning child: {}", err);
std::process::exit(1);
}
}
};
task::block_on(async_main(child))
Ok(Child {
pid,
stdin: unsafe { File::from_raw_fd(child_stdin.1) },
stdout: unsafe { File::from_raw_fd(child_stdout.0) },
stderr: unsafe { File::from_raw_fd(child_stderr.0) },
})
}
async fn async_main(child: Pid) -> Result<(), Error> {
fn run_child(args: Vec<&str>, stdin: RawFd, stdout: RawFd, stderr: RawFd) -> Error {
enum Never {}
fn int(args: Vec<&str>, stdin: RawFd, stdout: RawFd, stderr: RawFd) -> Result<Never, Error> {
// swap 3 main fds
unistd::dup2(stdin, 0).map_err(|e| Error::Nix {
msg: "dup2",
src: e,
})?;
unistd::dup2(stdout, 1).map_err(|e| Error::Nix {
msg: "dup2",
src: e,
})?;
unistd::dup2(stderr, 2).map_err(|e| Error::Nix {
msg: "dup2",
src: e,
})?;
// ask to be traced (stops process)
ptrace::traceme().map_err(|e| Error::Nix {
msg: "PTRACE_TRACEME",
src: e,
})?;
unistd::execve::<_, &CStr>(
&CString::new(args[0]).unwrap(),
args.into_iter()
.map(CString::new)
.map(Result::unwrap)
.collect::<Vec<_>>()
.as_slice(),
&[],
)
.map_err(|e| Error::Nix {
msg: "execve",
src: e,
})?;
unreachable!("execve doesn't return on success")
}
match int(args, stdin, stdout, stderr) {
Ok(s) => match s {},
Err(e) => e,
}
}
async fn async_main(child: Child) -> Result<(), Error> {
let child_stdin = child.stdin;
let child_stdout = child.stdout;
let child_stderr = child.stderr;
let child = child.pid;
let children = Arc::new(Mutex::new({
let mut m = HashMap::new();
m.insert(
child,
ProcessDescription {
state: ProcessState::Traced,
parent: None,
name: Some("main".to_string()),
},
@ -62,6 +160,8 @@ async fn async_main(child: Pid) -> Result<(), Error> {
m
}));
let wait_loop = task::spawn(wait_loop(children.clone()));
ptrace::setoptions(
child,
Options::PTRACE_O_TRACECLONE
@ -70,10 +170,37 @@ async fn async_main(child: Pid) -> Result<(), Error> {
| Options::PTRACE_O_TRACEVFORK,
)
.map_err(|e| Error::Nix {
msg: "ptrace_setoptions",
msg: "PTRACE_SETOPTIONS",
src: e,
})?;
ptrace::cont(child, None).map_err(|e| Error::Nix {
msg: "PTRACE_CONT",
src: e,
})?;
try_join!(wait_loop, task::spawn(handle_input(child_stdin)))?;
Ok(())
}
async fn handle_input(mut stdin_pipe: File) -> Result<(), Error> {
let mut stdin = io::stdin();
let mut bytes_in = vec![0 as u8; 128];
let mut bytes_out = vec![0 as u8; 128];
loop {
let num_read = stdin.read(&mut bytes_in).await?;
for i in 0..num_read {
debug!("forwarded byte: {}", i);
bytes_out[i] = bytes_in[i];
}
stdin_pipe.write_all(&bytes_out[0..num_read]).await?;
}
}
async fn wait_loop(children: Arc<Mutex<HashMap<Pid, ProcessDescription>>>) -> Result<(), Error> {
while !children.lock().await.is_empty() {
let wait_result = wait().map_err(|e| Error::Nix {
msg: "wait",
@ -81,37 +208,43 @@ async fn async_main(child: Pid) -> Result<(), Error> {
})?;
match &wait_result {
WaitStatus::Signaled(pid, Signal::SIGTRAP, _) => {
ptrace::cont(*pid, None).map_err(|e| Error::Nix {
msg: "ptrace_cont",
src: e,
})?;
WaitStatus::PtraceEvent(pid, _, event) => match match_event(*event) {
Some(Event::PTRACE_EVENT_FORK) => info!("process {} forked", pid),
Some(Event::PTRACE_EVENT_VFORK) => info!("process {} vforked", pid),
Some(Event::PTRACE_EVENT_CLONE) => info!("process {} cloned", pid),
Some(Event::PTRACE_EVENT_EXEC) => info!("process {} execed", pid),
Some(Event::PTRACE_EVENT_VFORK_DONE) => {
info!("process {} returned from vfork", pid)
}
Some(Event::PTRACE_EVENT_EXIT) => info!("process {} exited", pid),
Some(Event::PTRACE_EVENT_SECCOMP) => {
info!("process {} triggered a seccomp rule", pid)
}
Some(Event::PTRACE_EVENT_STOP) => info!("process {} stopped", pid),
Some(e) => warn!("new ptrace event added: {:?}", e),
None => warn!("unrecognised ptrace event: {}", event),
},
WaitStatus::Stopped(pid, signal) => {
info!("process {} stopped with signal {}", pid, signal)
}
_ => unimplemented!(),
s => warn!("unhandled signal: {:?}", s),
};
}
Ok(())
}
fn spawn_child() -> Error {
fn int() -> Result<(), Error> {
ptrace::traceme().map_err(|e| Error::Nix {
msg: "traceme",
src: e,
})?;
let sh = CString::new("/bin/sh").unwrap();
unistd::execve::<_, &CStr>(&sh, vec![&sh].as_slice(), &[]).map_err(|e| Error::Nix {
msg: "execve",
src: e,
})?;
unreachable!()
fn match_event(v: i32) -> Option<Event> {
match v {
1 => Some(Event::PTRACE_EVENT_FORK),
2 => Some(Event::PTRACE_EVENT_VFORK),
3 => Some(Event::PTRACE_EVENT_CLONE),
4 => Some(Event::PTRACE_EVENT_EXEC),
5 => Some(Event::PTRACE_EVENT_VFORK_DONE),
6 => Some(Event::PTRACE_EVENT_EXIT),
7 => Some(Event::PTRACE_EVENT_SECCOMP),
128 => Some(Event::PTRACE_EVENT_STOP),
_ => None,
}
int().expect_err("execve doesn't return on success")
}
async fn wait_loop(desc: &mut HashMap<Pid, ProcessDescription>) {}