tracexec 1.0.0

Tracer for execve{,at} and pre-exec behavior, launcher for debuggers.
#![allow(
  clippy::future_not_send, // We are not a library for now.
)]

#[cfg(feature = "ebpf")]
mod bpf;
mod log;
mod run_mode;

use std::{
  process,
  sync::Arc,
};

use clap::Parser;
use color_eyre::eyre::{
  OptionExt,
  bail,
};
use nix::unistd::Uid;
use signal_hook::consts::signal::*;
use tokio::sync::mpsc;
use tracexec_backend_ptrace::ptrace::BuildPtraceTracer;
use tracexec_core::{
  account,
  cli::{
    Cli,
    CliCommand,
    args::TracerEventArgs,
    config::{
      Config,
      ConfigLoadError,
    },
    options::Color,
  },
  elevate,
  event::{
    TracerEvent,
    TracerEventDetails,
    TracerMessage,
  },
  export::ExporterMetadata,
  is_current_kernel_ge,
  proc::BaselineInfo,
  tracer::{
    TracerBuilder,
    TracerMode,
  },
};
use tracexec_tui::{
  app::{
    App,
    PTracer,
  },
  theme::current_theme,
};

use crate::log::initialize_panic_handler;

// Avoid using tokio::main macro because we may need to restore env vars,
// which is not completely safe inside a multithreaded process.
fn main() -> color_eyre::Result<()> {
  let mut cli = Cli::parse();

  // Handle --elevate early, before any environment-sensitive setup.
  // This re-execs the process with elevated privileges and does not return.
  if cli.elevate {
    tracexec_core::elevate::elevate_and_reexec()?;
    unreachable!();
  }

  // Request the original environment from the unelevated parent. Only an
  // allowlisted subset is consulted by elevated tracexec itself; the complete
  // original environment is saved as data for the tracee's execve.
  if cli.restore_env_socket.is_some() && !Uid::effective().is_root() {
    bail!("--restore-env-socket is only available after privilege elevation");
  }
  let restored_env = cli
    .restore_env_socket
    .take()
    .map(|socket| elevate::request_env_from_parent(&socket))
    .transpose()?;
  let tracexec_env = restored_env
    .as_ref()
    .map(|env| elevate::filter_allowlisted_env(env));
  let tracexec_env_ref = tracexec_env.as_deref();
  let tracee_env = restored_env;

  // Apply project directory overrides from --elevate before anything
  // touches project_directory() (logging, config, themes).
  // The clap validation ensures that the three elevated_*_dir options are either all present or all absent.
  if let (Some(config_dir), Some(data_dir), Some(data_local_dir)) = (
    cli.elevated_config_dir.take(),
    cli.elevated_data_dir.take(),
    cli.elevated_data_local_dir.take(),
  ) {
    tracexec_core::cli::config::set_project_dir_overrides(config_dir, data_dir, data_local_dir);
  }

  if cli.color == Color::Auto && elevate::env_var_os(tracexec_env_ref, "NO_COLOR").is_some() {
    // Respect NO_COLOR if --color=auto
    cli.color = Color::Never;
  }
  if cli.color == Color::Always {
    owo_colors::control::set_should_colorize(true);
    color_eyre::install()?;
  } else if cli.color == Color::Never {
    owo_colors::control::set_should_colorize(false);
  } else {
    color_eyre::install()?;
  }
  initialize_panic_handler();
  log::initialize_logging(tracexec_env_ref)?;
  log::debug!("Commandline args: {:?}", cli);

  let runtime = tokio::runtime::Builder::new_multi_thread()
    .worker_threads(2)
    .enable_all()
    .build()?;
  runtime.block_on(async_main(cli, tracee_env, tracexec_env))
}

async fn async_main(
  mut cli: Cli,
  tracee_env: Option<tracexec_core::elevate::EnvVars>,
  tracexec_env: Option<tracexec_core::elevate::EnvVars>,
) -> color_eyre::Result<()> {
  if let Some(cwd) = &cli.cwd {
    std::env::set_current_dir(cwd)?;
  }
  let user = if let Some(user) = cli.user.as_deref() {
    if !Uid::effective().is_root() {
      bail!("--user option is only available when running tracexec as root!");
    }
    Some(account::user_from_name(user)?.ok_or_eyre("Failed to get user info")?)
  } else {
    None
  };
  // PTRACE_GET_SYSCALL_INFO requires at least linux 5.3.
  let min_support_kver = (5, 3);
  if !is_current_kernel_ge(min_support_kver)? {
    log::warn!(
      "Current kernel version is not supported! Minimum supported kernel version is {}.{}.",
      min_support_kver.0,
      min_support_kver.1
    );
    eprintln!(
      "Current kernel version is not supported! Minimum supported kernel version is {}.{}.",
      min_support_kver.0, min_support_kver.1
    );
  }
  if !cli.no_profile {
    match Config::load(cli.profile.clone()) {
      Ok(config) => cli.merge_config(config),
      Err(ConfigLoadError::NotFound) => (),
      Err(e) => Err(e)?,
    };
  }
  match cli.cmd {
    CliCommand::Log {
      cmd,
      tracing_args,
      modifier_args,
      ptrace_args,
      tracer_event_args,
      output,
    } => {
      let modifier_args = modifier_args.processed();
      let output = Cli::get_output(output, cli.color)?;
      let baseline = BaselineInfo::new_with_env(tracee_env.as_deref())?;
      let (tracer_tx, mut tracer_rx) = mpsc::unbounded_channel();
      let (tracer, token) = TracerBuilder::new()
        .mode(TracerMode::Log {
          foreground: tracing_args.foreground(),
        })
        .modifier(modifier_args)
        .user(user)
        .tracee_env(tracee_env)
        .tracer_tx(tracer_tx)
        .baseline(Arc::new(baseline))
        .filter(tracer_event_args.filter()?)
        .ptrace_options(&ptrace_args)
        .printer_from_cli(&tracing_args)
        .build_ptrace()?;
      let (_tracer, tracer_thread) = tracer.spawn(cmd, Some(output), token)?;
      loop {
        match tracer_rx.recv().await {
          Some(TracerMessage::Event(TracerEvent {
            details: TracerEventDetails::TraceeExit { exit_code, .. },
            ..
          })) => {
            tracing::debug!("Waiting for tracer thread to exit");
            tracer_thread.await??;
            process::exit(exit_code);
          }
          // channel closed abnormally.
          None | Some(TracerMessage::FatalError(_)) => {
            tracing::debug!("Waiting for tracer thread to exit");
            tracer_thread.await??;
            process::exit(1);
          }
          _ => (),
        }
      }
    }
    CliCommand::Tui {
      cmd,
      modifier_args,
      ptrace_args,
      tracer_event_args,
      tui_args,
      debugger_args,
    } => {
      let executable_path = std::env::current_exe()?;
      run_mode::initialize_tui(&tui_args, &executable_path)?;
      let modifier_args = modifier_args.processed();
      log::debug!(
        "should colorize: {}",
        owo_colors::control::should_colorize()
      );
      let (baseline, tracer_mode, pty_master) =
        run_mode::setup_tui_io(tui_args.tty(), tracee_env.as_deref())?;
      let tracing_args = run_mode::tui_log_args();
      let baseline = Arc::new(baseline);
      let (tracer_tx, tracer_rx) = mpsc::unbounded_channel();
      let (tracer, token) = TracerBuilder::new()
        .mode(tracer_mode)
        .modifier(modifier_args.clone())
        .user(user)
        .tracee_env(tracee_env)
        .tracer_tx(tracer_tx)
        .baseline(baseline.clone())
        .filter(tracer_event_args.filter()?)
        .ptrace_options(&ptrace_args)
        .printer_from_cli(&tracing_args)
        .build_ptrace()?;

      let frame_rate = tui_args.frame_rate.unwrap_or(60.);
      let (tracer, tracer_thread) = tracer.spawn(cmd, None, token)?;
      let mut app = App::new(
        Some(PTracer {
          tracer,
          debugger_args,
        }),
        &tracing_args,
        &modifier_args,
        tui_args,
        baseline,
        pty_master,
        current_theme(),
      )?;
      let mut tui = tracexec_tui::Tui::new()?.frame_rate(frame_rate);
      tui.enter(tracer_rx)?;
      app.run(&mut tui).await?;
      // Now when TUI exits, the tracer thread is still running.
      // options:
      // 1. Wait for the tracer thread to exit.
      // 2. Terminate the root process so that the tracer thread exits.
      // 3. Kill the root process so that the tracer thread exits.
      app.exit()?;
      tracexec_tui::restore_tui()?;
      tracer_thread.await??;
    }
    CliCommand::Collect {
      cmd,
      format,
      output,
      modifier_args,
      ptrace_args,
      exporter_args,
      foreground,
      no_foreground,
    } => {
      let modifier_args = modifier_args.processed();
      let output = Cli::get_output(output, cli.color)?;
      let tracing_args = run_mode::collect_log_args(foreground, no_foreground);
      let (tracer_tx, tracer_rx) = mpsc::unbounded_channel();
      let baseline = Arc::new(BaselineInfo::new_with_env(tracee_env.as_deref())?);
      let (tracer, token) = TracerBuilder::new()
        .mode(TracerMode::Log {
          foreground: tracing_args.foreground(),
        })
        .modifier(modifier_args)
        .user(user)
        .tracee_env(tracee_env)
        .tracer_tx(tracer_tx)
        .baseline(baseline.clone())
        .filter(TracerEventArgs::all().filter()?)
        .ptrace_options(&ptrace_args)
        .printer_from_cli(&tracing_args)
        .build_ptrace()?;
      let (tracer, tracer_thread) = tracer.spawn(cmd, None, token)?;
      run_mode::spawn_signal_handler([SIGTERM, SIGINT, SIGQUIT], move |signal| match signal {
        SIGTERM | SIGINT => tracer
          .request_termination()
          .expect("Failed to terminate tracer"),
        _ => unreachable!(),
      })?;
      let meta = ExporterMetadata {
        baseline,
        exporter_args,
      };
      let exit_code = run_mode::run_exporter(format, output, meta, tracer_rx).await?;
      tracer_thread.await??;
      process::exit(exit_code);
    }
    CliCommand::GenerateCompletions { shell } => {
      Cli::generate_completions(shell);
    }
    #[cfg(feature = "ebpf")]
    CliCommand::Ebpf { command } => {
      // TODO: warn if --user is set when not follow-forks
      bpf::main(command, user, cli.color, tracee_env, tracexec_env).await?;
    }
  }
  Ok(())
}