diff --git a/Cargo.toml b/Cargo.toml index a4b1f16..0be4f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ anyhow = "1.0.14" rstack = {version = "0.3.1", default-features = false, features = ["unwind"]} rustc-demangle = "0.1.16" cpp_demangle = "0.2.13" +rand = "" diff --git a/src/lib.rs b/src/lib.rs index 5bf7af8..3c4f9a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,11 @@ use magic::Magic; use serde::{Deserialize, Serialize}; use std::{mem, os::unix::io::RawFd}; use tiny_nix_ipc::Socket; +use std::path::PathBuf; pub struct Settings { pub capture_backtrace: bool, + pub fail_path: Option, } #[repr(C)] diff --git a/src/main.rs b/src/main.rs index 0a1da84..3839963 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,6 +187,9 @@ struct Opt { /// Child will inherit all environment vars visible to lxtrace #[structopt(long)] inherit_env: bool, + /// Accesses to this path will fail with some probability + #[structopt(long)] + fail_path: Option, } fn main() -> anyhow::Result<()> { @@ -196,6 +199,10 @@ fn main() -> anyhow::Result<()> { eprintln!("executable not provided"); exit(1); } + opt.fail_path = match opt.fail_path { + Some(path) => std::fs::canonicalize(path).ok(), + None => None + }; if opt.inherit_env { // TODO: probably this doesn't interact well with --env for (k, v) in std::env::vars_os() { @@ -233,6 +240,7 @@ fn main() -> anyhow::Result<()> { let payload = lxtrace::Payload::Cmd(cmd_args); let settings = lxtrace::Settings { capture_backtrace: opt.backtrace, + fail_path: opt.fail_path, }; if let Err(e) = lxtrace::run(payload, settings, sender) { diff --git a/src/tracer.rs b/src/tracer.rs index 66eeae9..e1325ee 100644 --- a/src/tracer.rs +++ b/src/tracer.rs @@ -12,20 +12,24 @@ use nix::{ unistd::Pid, }; use std::collections::HashMap; +use std::path::PathBuf; use tiny_nix_ipc::Socket; +use rand::Rng; struct ChildInfo { in_syscall: bool, + in_spoiled: bool, + spoiled_syscall: u64, } -fn decode_syscall_args(regs: libc::user_regs_struct) -> RawSyscall { +fn decode_syscall_args(regs: &libc::user_regs_struct) -> RawSyscall { let mut out = RawSyscall { syscall_id: 0, args: [0; 6], ret: 0, }; out.ret = regs.rax; - out.syscall_id = regs.orig_rax; + out.syscall_id = regs.orig_rax & 0xffffffu64; out.args[0] = regs.rdi; out.args[1] = regs.rsi; out.args[2] = regs.rdx; @@ -35,6 +39,26 @@ fn decode_syscall_args(regs: libc::user_regs_struct) -> RawSyscall { out } +fn spoil(pid: Pid, children: &mut HashMap, regs: libc::user_regs_struct) -> Result<(), nix::Error> { + let ci1 = children.get(&(pid.as_raw() as u32)).unwrap(); + let ci2 = ChildInfo { + in_syscall: ci1.in_syscall, + in_spoiled: true, + spoiled_syscall: regs.orig_rax, + }; + children.insert(pid.as_raw() as u32, ci2); + let mut regs = regs; + regs.rax = 39 /*getpid*/; + regs.orig_rax = 39 /*getpid*/; + nix::sys::ptrace::setregs(pid, regs) +} + +fn return_eio(pid: Pid, regs: &mut libc::user_regs_struct, syscall: u64) -> Result<(), nix::Error> { + regs.orig_rax = syscall; + regs.rax = -(nix::errno::Errno::EIO as i32 as i64) as u64; + nix::sys::ptrace::setregs(pid, *regs) +} + fn process_syscall( raw: &RawSyscall, proc: Pid, @@ -125,7 +149,7 @@ pub(crate) unsafe fn parent( ) .context("ptrace setoptions failed")?; - let new_child_info = ChildInfo { in_syscall: false }; + let new_child_info = ChildInfo { in_syscall: false, in_spoiled: false, spoiled_syscall: 0 }; children.insert(pid, new_child_info); let event = Event { @@ -148,19 +172,37 @@ pub(crate) unsafe fn parent( (true, WaitStatus::PtraceSyscall(_)) => { let cur_info = children.get(&pid).unwrap(); // it's guaranteed here that get() returns Some let started_syscall = !cur_info.in_syscall; + let is_spoiled = cur_info.in_spoiled; + let spoiled_syscall = cur_info.spoiled_syscall; let new_info = ChildInfo { in_syscall: started_syscall, + in_spoiled: false, + spoiled_syscall: 0, }; children.insert(pid, new_info); - let regs = nix::sys::ptrace::getregs(Pid::from_raw(pid as i32)) + let mut regs = nix::sys::ptrace::getregs(Pid::from_raw(pid as i32)) .context("ptrace getregs failed")?; - let params = decode_syscall_args(regs); + if is_spoiled { + return_eio(Pid::from_raw(pid as i32), &mut regs, spoiled_syscall).context("return -EIO failed")?; + } + let params = decode_syscall_args(®s); let def = magic.lookup_syscall_by_id(SyscallId(params.syscall_id as u32)); let child_pid = Pid::from_raw(pid as i32); let mut decoded_params = match def { Some(def) => process_syscall(¶ms, child_pid, magic, def), None => None, }; + if started_syscall && decoded_params.is_some() && (params.syscall_id == 2 /*open*/ || params.syscall_id == 257 /*openat*/) { //TODO: x86_64 only + let decoded_params = decoded_params.as_ref().unwrap(); + let arg_id = if params.syscall_id == 2 {0} else {1}; + let path: PathBuf = match &decoded_params.args[arg_id] { + crate::magic::ty::Value::String(s) => s, + _ => panic!("open/openat with non-string argument") + }.into(); + if settings.fail_path.is_some() && path.starts_with(settings.fail_path.as_ref().unwrap()) && rand::thread_rng().gen_range(0, 100) == 0 { + spoil(Pid::from_raw(pid as i32), &mut children, regs).context("spoil failed")?; + } + } decoded_params.as_mut().map(|p| { // attach backtrace if requested if settings.capture_backtrace {