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 = [ dependencies = [
"async-std", "async-std",
"env_logger", "env_logger",
"futures",
"log", "log",
"nix", "nix",
"thiserror", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.21" version = "0.3.21"
@ -264,6 +280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -272,6 +289,17 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.21" version = "0.3.21"
@ -293,6 +321,47 @@ dependencies = [
"waker-fn", "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]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.3" version = "0.2.3"

View File

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

View File

@ -1,7 +1,12 @@
use async_std::io;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("{msg}: {src}")] #[error("{msg}: {src}")]
Nix { msg: &'static str, src: nix::Error }, 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; mod error;
@ -6,19 +6,33 @@ use error::Error;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::os::unix::io::{FromRawFd, RawFd};
use std::sync::Arc; use std::sync::Arc;
use async_std::fs::File;
use async_std::io::{self, ReadExt, WriteExt};
use async_std::sync::Mutex; use async_std::sync::Mutex;
use async_std::task; 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::signal::Signal;
use nix::sys::wait::{wait, WaitStatus}; use nix::sys::wait::{wait, WaitStatus};
use nix::unistd::Pid; use nix::unistd::Pid;
use nix::unistd::{self, ForkResult}; use nix::unistd::{self, ForkResult};
struct Child {
pid: Pid,
stdin: File,
stdout: File,
stderr: File,
}
struct ProcessDescription { struct ProcessDescription {
state: ProcessState,
parent: Option<Pid>, parent: Option<Pid>,
name: Option<String>, name: Option<String>,
} }
@ -28,33 +42,117 @@ enum ProcessState {
} }
fn main() -> Result<(), Error> { 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..."); 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 { match nix::unistd::fork().map_err(|e| Error::Nix {
msg: "fork", msg: "fork",
src: e, src: e,
})? { })? {
ForkResult::Parent { child } => child, ForkResult::Parent { child } => child,
ForkResult::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); error!("error spawning child: {}", err);
std::process::exit(1); 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 children = Arc::new(Mutex::new({
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert( m.insert(
child, child,
ProcessDescription { ProcessDescription {
state: ProcessState::Traced,
parent: None, parent: None,
name: Some("main".to_string()), name: Some("main".to_string()),
}, },
@ -62,6 +160,8 @@ async fn async_main(child: Pid) -> Result<(), Error> {
m m
})); }));
let wait_loop = task::spawn(wait_loop(children.clone()));
ptrace::setoptions( ptrace::setoptions(
child, child,
Options::PTRACE_O_TRACECLONE Options::PTRACE_O_TRACECLONE
@ -70,10 +170,37 @@ async fn async_main(child: Pid) -> Result<(), Error> {
| Options::PTRACE_O_TRACEVFORK, | Options::PTRACE_O_TRACEVFORK,
) )
.map_err(|e| Error::Nix { .map_err(|e| Error::Nix {
msg: "ptrace_setoptions", msg: "PTRACE_SETOPTIONS",
src: e, 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() { while !children.lock().await.is_empty() {
let wait_result = wait().map_err(|e| Error::Nix { let wait_result = wait().map_err(|e| Error::Nix {
msg: "wait", msg: "wait",
@ -81,37 +208,43 @@ async fn async_main(child: Pid) -> Result<(), Error> {
})?; })?;
match &wait_result { match &wait_result {
WaitStatus::Signaled(pid, Signal::SIGTRAP, _) => { WaitStatus::PtraceEvent(pid, _, event) => match match_event(*event) {
ptrace::cont(*pid, None).map_err(|e| Error::Nix { Some(Event::PTRACE_EVENT_FORK) => info!("process {} forked", pid),
msg: "ptrace_cont", Some(Event::PTRACE_EVENT_VFORK) => info!("process {} vforked", pid),
src: e, 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(()) Ok(())
} }
fn spawn_child() -> Error { fn match_event(v: i32) -> Option<Event> {
fn int() -> Result<(), Error> { match v {
ptrace::traceme().map_err(|e| Error::Nix { 1 => Some(Event::PTRACE_EVENT_FORK),
msg: "traceme", 2 => Some(Event::PTRACE_EVENT_VFORK),
src: e, 3 => Some(Event::PTRACE_EVENT_CLONE),
})?; 4 => Some(Event::PTRACE_EVENT_EXEC),
5 => Some(Event::PTRACE_EVENT_VFORK_DONE),
let sh = CString::new("/bin/sh").unwrap(); 6 => Some(Event::PTRACE_EVENT_EXIT),
7 => Some(Event::PTRACE_EVENT_SECCOMP),
unistd::execve::<_, &CStr>(&sh, vec![&sh].as_slice(), &[]).map_err(|e| Error::Nix { 128 => Some(Event::PTRACE_EVENT_STOP),
msg: "execve", _ => None,
src: e,
})?;
unreachable!()
} }
int().expect_err("execve doesn't return on success")
} }
async fn wait_loop(desc: &mut HashMap<Pid, ProcessDescription>) {}