pacwrap/pacwrap/src/exec.rs

379 lines
12 KiB
Rust

/*
* pacwrap
*
* Copyright (C) 2023-2024 Xavier Moffett <sapphirus@azorium.net>
* SPDX-License-Identifier: GPL-3.0-only
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
fmt::{Display, Formatter},
fs::{remove_file, File},
os::unix::io::AsRawFd,
path::Path,
process::{Child, Command},
thread,
time::Duration,
vec::Vec,
};
use command_fds::{CommandFdExt, FdMapping};
use nix::{
sys::signal::{kill, Signal},
unistd::Pid,
};
use signal_hook::iterator::Signals;
use pacwrap_core::{
config::{
self,
register::{register_dbus, register_filesystems, register_permissions},
ContainerHandle,
ContainerType::Slice,
Dbus,
},
constants::{
BWRAP_EXECUTABLE,
DBUS_PROXY_EXECUTABLE,
DBUS_SOCKET,
DEFAULT_PATH,
IS_COLOR_TERMINAL,
SIGNAL_LIST,
XDG_RUNTIME_DIR,
},
err,
error,
exec::{
args::{Argument, ExecutionArgs},
fakeroot_container,
path::check_path,
seccomp::{configure_bpf_program, provide_bpf_program},
utils::{decode_info_json, wait_on_container},
ExecutionError,
ExecutionType::Interactive,
},
impl_error,
utils::{
self,
arguments::{Arguments, InvalidArgument, Operand as Op},
check_root,
env_var,
TermControl,
},
Error,
ErrorGeneric,
ErrorKind,
ErrorTrait,
Result,
};
static SOCKET_SLEEP_DURATION: Duration = Duration::from_micros(500);
#[derive(Debug)]
enum ExecError {
NestedNamespaceEnablement,
ConsoleSessionRetention,
SeccompDisablement,
}
impl_error!(ExecError);
impl Display for ExecError {
fn fmt(&self, fmter: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match self {
Self::SeccompDisablement => write!(fmter, "Disabling seccomp filtering can allow for sandbox escape."),
Self::NestedNamespaceEnablement =>
write!(fmter, "Namespace nesting has been known in the past to enable container escape vulnerabilities."),
Self::ConsoleSessionRetention => write!(
fmter,
"Retaining a console session is known to allow for container escape. See CVE-2017-5226 for details."
),
}
}
}
enum ExecParams<'a> {
FakeRoot(i8, bool, Vec<&'a str>, ContainerHandle<'a>),
Container(i8, bool, Vec<&'a str>, ContainerHandle<'a>),
}
impl<'a> ExecParams<'a> {
fn parse(args: &'a mut Arguments) -> Result<Self> {
let mut verbosity: i8 = 0;
let mut shell = matches!(args[0], Op::Value("shell"));
let mut root = false;
let mut container = None;
let mut pos = 1;
for str in args.inner() {
if str.starts_with("-") || *str == "run" || *str == "shell" {
pos += 1;
continue;
}
break;
}
while let Some(arg) = args.next() {
match arg {
Op::Long("root") | Op::Short('r') => root = true,
Op::Long("shell") | Op::Short('s') => shell = true,
Op::Long("verbose") | Op::Short('v') => verbosity += 1,
Op::LongPos(_, str) | Op::ShortPos(_, str) | Op::Value(str) =>
if container.is_none() {
container = Some(str);
break;
},
_ => args.invalid_operand()?,
}
}
let handle = match container {
Some(container) => config::provide_handle(container)?,
None => err!(InvalidArgument::TargetUnspecified)?,
};
let runtime = args.into_inner(pos);
if let (Slice, false, ..) = (handle.metadata().container_type(), root, shell) {
err!(ErrorKind::Message("Execution in container filesystem segments is not supported."))?
}
check_root()?;
Ok(match root {
true => Self::FakeRoot(verbosity, shell, runtime, handle),
false => Self::Container(verbosity, shell, runtime, handle),
})
}
}
pub fn execute<'a>(args: &'a mut Arguments<'a>) -> Result<()> {
match ExecParams::parse(args)? {
ExecParams::FakeRoot(verbosity, true, _, handle) => execute_fakeroot(&handle, None, verbosity),
ExecParams::FakeRoot(verbosity, false, args, handle) => execute_fakeroot(&handle, Some(args), verbosity),
ExecParams::Container(verbosity, true, _, handle) => execute_container(&handle, vec!["bash"], true, verbosity),
ExecParams::Container(verbosity, false, args, handle) => execute_container(&handle, args, false, verbosity),
}
}
fn execute_container(ins: &ContainerHandle, arguments: Vec<&str>, shell: bool, verbosity: i8) -> Result<()> {
let mut exec = ExecutionArgs::new();
let mut jobs: Vec<Child> = Vec::new();
let cfg = ins.config();
let vars = ins.vars();
let dbus = !cfg.dbus().is_empty();
if !cfg.allow_forking() {
exec.push_env(Argument::DieWithParent);
}
match !cfg.enable_userns() {
true => exec.push_env(Argument::DisableNamespaces),
false => error!(ExecError::NestedNamespaceEnablement).warn(),
}
match !cfg.retain_session() {
true => exec.push_env(Argument::NewSession),
false => error!(ExecError::ConsoleSessionRetention).warn(),
}
match shell && *IS_COLOR_TERMINAL {
true => exec.env("TERM", "xterm"),
false => exec.env("TERM", "dumb"),
}
if dbus {
jobs.push(instantiate_dbus_proxy(cfg.dbus(), &mut exec)?);
}
exec.env("XDG_RUNTIME_DIR", &XDG_RUNTIME_DIR);
register_filesystems(cfg.filesystem(), vars, &mut exec)?;
register_permissions(cfg.permissions(), &mut exec)?;
let path = match exec.obtain_env("PATH") {
Some(var) => var,
None => {
exec.env("PATH", DEFAULT_PATH);
DEFAULT_PATH
}
};
let path_vec: Vec<&str> = path.split(":").collect();
let info_pipe = os_pipe::pipe().unwrap();
let info_fd = info_pipe.1.as_raw_fd();
let sec_pipe = os_pipe::pipe().unwrap();
let sec_fd = match cfg.seccomp() {
true => provide_bpf_program(configure_bpf_program(cfg), &sec_pipe.0, sec_pipe.1).unwrap(),
false => {
error!(ExecError::SeccompDisablement).warn();
0
}
};
let term_control = TermControl::new(0);
let mut proc = Command::new(BWRAP_EXECUTABLE);
let proc = if sec_fd == 0 {
proc.env_clear()
.args(exec.arguments())
.arg("--info-fd")
.arg(info_fd.to_string())
.fd_mappings(vec![FdMapping {
parent_fd: info_fd,
child_fd: info_fd,
}])
.unwrap()
} else {
proc.env_clear()
.args(exec.arguments())
.arg("--info-fd")
.arg(info_fd.to_string())
.arg("--seccomp")
.arg(sec_fd.to_string())
.fd_mappings(vec![
FdMapping {
parent_fd: info_fd,
child_fd: info_fd,
},
FdMapping {
parent_fd: sec_fd,
child_fd: sec_fd,
},
])
.unwrap()
};
match verbosity {
0 => (),
1 => eprintln!("Arguments:\t {arguments:?}\n{ins:?}"),
_ => eprintln!("Arguments:\t {arguments:?}\n{ins:?}\n{exec:?}"),
}
check_path(ins, &arguments, path_vec)?;
match proc.args(arguments).spawn() {
Ok(child) => wait_on_container(
child,
term_control,
decode_info_json(info_pipe)?,
*cfg.allow_forking(),
match !jobs.is_empty() {
true => Some(jobs),
false => None,
},
signal_trap,
cleanup,
),
Err(err) => err!(ErrorKind::ProcessInitFailure(BWRAP_EXECUTABLE, err.kind())),
}
}
fn execute_fakeroot(ins: &ContainerHandle, arguments: Option<Vec<&str>>, verbosity: i8) -> Result<()> {
let arguments = match arguments {
None => vec!["bash"],
Some(args) => args,
};
if verbosity > 0 {
eprintln!("Arguments:\t {arguments:?}\n{ins:?}");
}
check_path(ins, &arguments, vec!["/usr/bin", "/bin"])?;
fakeroot_container(Interactive, Some(signal_trap), ins, arguments)
}
fn signal_trap(bwrap_pid: i32) {
let mut signals = Signals::new(*SIGNAL_LIST).unwrap();
thread::Builder::new()
.name("pacwrap-signal".to_string())
.spawn(move || {
let proc: &str = &format!("/proc/{}/", bwrap_pid);
let proc = Path::new(proc);
for _ in signals.forever() {
if proc.exists() {
kill(Pid::from_raw(bwrap_pid), Signal::SIGKILL).unwrap();
}
}
})
.unwrap();
}
fn instantiate_dbus_proxy(per: &[Box<dyn Dbus>], args: &mut ExecutionArgs) -> Result<Child> {
let dbus_socket_path = format!("/run/user/{}/bus", nix::unistd::geteuid());
let dbus_session = env_var("DBUS_SESSION_BUS_ADDRESS")?;
register_dbus(per, args)?;
create_placeholder(&DBUS_SOCKET)?;
match Command::new(DBUS_PROXY_EXECUTABLE)
.arg(dbus_session)
.arg(&*DBUS_SOCKET)
.args(args.get_dbus())
.spawn()
{
Ok(mut child) => {
let mut increment: u8 = 0;
args.robind(&DBUS_SOCKET, &dbus_socket_path);
args.symlink(&dbus_socket_path, "/run/dbus/system_bus_socket");
args.env("DBUS_SESSION_BUS_ADDRESS", &format!("unix:path={dbus_socket_path}"));
/*
* This blocking code is required to prevent a downstream race condition with
* bubblewrap. Unless xdg-dbus-proxy is passed improper parameters, this while loop
* shouldn't almost ever increment more than once or twice.
*
* With a sleep duration of 500 microseconds, we check the socket 200 times before failure.
*
* ADDENDUM: Upon further examination of bubblewrap's code, it is not possible to ask bubblewrap
* to wait on a FD prior to instantiating the filesystem bindings.
*/
while !check_socket(&DBUS_SOCKET, &increment, &mut child)? {
increment += 1;
}
Ok(child)
}
Err(error) => err!(ErrorKind::ProcessInitFailure(DBUS_PROXY_EXECUTABLE, error.kind()))?,
}
}
fn check_socket(socket: &String, increment: &u8, process_child: &mut Child) -> Result<bool> {
if increment == &200 {
process_child.kill().ok();
remove_file(&*DBUS_SOCKET).prepend_io(|| DBUS_SOCKET.to_string())?;
err!(ExecutionError::SocketTimeout(socket.into()))?
}
thread::sleep(SOCKET_SLEEP_DURATION);
Ok(utils::check_socket(socket))
}
fn create_placeholder(path: &str) -> Result<()> {
match File::create(path) {
Ok(file) => {
drop(file);
Ok(())
}
Err(error) => err!(ErrorKind::IOError(path.into(), error.kind())),
}
}
fn cleanup() -> Result<()> {
if Path::new(&*DBUS_SOCKET).exists() {
remove_file(&*DBUS_SOCKET).prepend_io(|| DBUS_SOCKET.to_string())?;
}
Ok(())
}