Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for wrapping binaries (rpm) #1789

Merged
merged 1 commit into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile-rpm-ostree.am
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ rpm_ostree_SOURCES = src/app/main.c \
src/app/rpmostree-builtin-reload.c \
src/app/rpmostree-builtin-rebase.c \
src/app/rpmostree-builtin-cancel.c \
src/app/rpmostree-builtin-cliwrap.c \
src/app/rpmostree-builtin-cleanup.c \
src/app/rpmostree-builtin-initramfs.c \
src/app/rpmostree-builtin-livefs.c \
Expand Down Expand Up @@ -102,7 +103,7 @@ librpmostree_rust_path = @abs_top_builddir@/target/@RUST_TARGET_SUBDIR@/librpmos
# If the target directory exists, and isn't owned by our uid, then
# we exit with a fatal error, since someone probably did `make && sudo make install`,
# and in this case cargo will download into ~/.root which we don't want.
LIBRPMOSTREE_RUST_SRCS = $(wildcard rust/src/*.rs) rust/cbindgen.toml
LIBRPMOSTREE_RUST_SRCS = $(shell find rust/src/ -name '*.rs') rust/cbindgen.toml
$(librpmostree_rust_path): Makefile $(LIBRPMOSTREE_RUST_SRCS)
cd $(top_srcdir)/rust && \
export CARGO_TARGET_DIR=@abs_top_builddir@/target && \
Expand Down
6 changes: 6 additions & 0 deletions docs/manual/treefile.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ It supports the following parameters:
specific filesystem drivers are included. If not specified,
`--no-hostonly` will be used.

* `cliwrap`: boolean, optional. Defaults to `false`. If enabled,
rpm-ostree will replace binaries such as `/usr/bin/rpm` with
wrappers that intercept unsafe operations, or adjust functionality.

The default is `false` out of conservatism; you likely want to enable this.

* `remove-files`: Array of files to delete from the generated tree.

* `remove-from-packages`: Array, optional: Delete from specified packages
Expand Down
143 changes: 143 additions & 0 deletions rust/src/cliwrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (C) 2019 Red Hat, Inc.
*
* 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; either version 2 of the License, or
* (at your option) any later version.
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

use anyhow::{bail, Result};
use std::io::prelude::*;
use std::{io, path};

use openat_ext::OpenatDirExt;
use rayon::prelude::*;
mod cliutil;
mod dracut;
mod grubby;
mod rpm;

/// Location for the underlying (not wrapped) binaries.
pub const CLIWRAP_DESTDIR: &'static str = "usr/libexec/rpm-ostree/wrapped";

/// Our list of binaries that will be wrapped. Must be a relative path.
static WRAPPED_BINARIES: &[&str] = &["usr/bin/rpm", "usr/bin/dracut", "usr/sbin/grubby"];

#[derive(Debug, PartialEq)]
pub(crate) enum RunDisposition {
Ok,
Warn,
Notice(String),
}

/// Main entrypoint for cliwrap
fn cliwrap_main(args: &Vec<String>) -> Result<()> {
// We'll panic here if the vector is empty, but that is intentional;
// the outer code should always pass us at least one arg.
let name = args[0].as_str();
let name = match std::path::Path::new(name).file_name() {
Some(name) => name,
None => bail!("Invalid wrapped binary: {}", name),
};
// We know we had a string from above
let name = name.to_str().unwrap();

let args: Vec<&str> = args.iter().skip(1).map(|v| v.as_str()).collect();

// If we're not booted into ostree, just run the child directly.
if !cliutil::is_ostree_booted() {
cliutil::exec_real_binary(name, &args)
} else {
match name {
"rpm" => self::rpm::main(&args),
"dracut" => self::dracut::main(&args),
"grubby" => self::grubby::main(&args),
_ => bail!("Unknown wrapped binary: {}", name),
}
}
}

/// Move the real binaries to a subdir, and replace them with
/// a shell script that calls our wrapping code.
fn write_wrappers(rootfs_dfd: &openat::Dir) -> Result<()> {
let destdir = std::path::Path::new(CLIWRAP_DESTDIR);
rootfs_dfd.ensure_dir(destdir.parent().unwrap(), 0o755)?;
rootfs_dfd.ensure_dir(destdir, 0o755)?;
WRAPPED_BINARIES.par_iter().try_for_each(|&bin| {
let binpath = path::Path::new(bin);

if !rootfs_dfd.exists(binpath)? {
return Ok(());
}

let name = binpath.file_name().unwrap().to_str().unwrap();
let destpath = format!("{}/{}", CLIWRAP_DESTDIR, name);
rootfs_dfd.local_rename(bin, destpath.as_str())?;

let f = rootfs_dfd.write_file(binpath, 0o755)?;
let mut f = io::BufWriter::new(f);
write!(
f,
"#!/bin/sh
# Wrapper created by rpm-ostree to override
# behavior of the underlying binary. For more
# information see `man rpm-ostree`. The real
# binary is now located at: {}
exec /usr/bin/rpm-ostree cliwrap $0 \"$@\"
",
binpath.to_str().unwrap()
)?;
f.flush()?;
Ok(())
})
}

mod ffi {
use super::*;
use crate::ffiutil::*;
use anyhow::Context;
use glib;
use lazy_static::lazy_static;
use libc;
use std::ffi::CString;

#[no_mangle]
pub extern "C" fn ror_cliwrap_write_wrappers(
rootfs_dfd: libc::c_int,
gerror: *mut *mut glib_sys::GError,
) -> libc::c_int {
let rootfs_dfd = ffi_view_openat_dir(rootfs_dfd);
int_glib_error(
write_wrappers(&rootfs_dfd).with_context(|| format!("cli wrapper replacement failed")),
gerror,
)
}

#[no_mangle]
pub extern "C" fn ror_cliwrap_entrypoint(
argv: *mut *mut libc::c_char,
gerror: *mut *mut glib_sys::GError,
) -> libc::c_int {
let v: Vec<String> = unsafe { glib::translate::FromGlibPtrContainer::from_glib_none(argv) };
int_glib_error(cliwrap_main(&v), gerror)
}

#[no_mangle]
pub extern "C" fn ror_cliwrap_destdir() -> *const libc::c_char {
lazy_static! {
static ref CLIWRAP_DESTDIR_C: CString = CString::new(CLIWRAP_DESTDIR).unwrap();
}
CLIWRAP_DESTDIR_C.as_ptr()
}
}
pub use self::ffi::*;
101 changes: 101 additions & 0 deletions rust/src/cliwrap/cliutil.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use anyhow::Result;
use nix::sys::statvfs;
use std::os::unix::process::CommandExt;
use std::{path, thread, time};

use crate::cliwrap;

/// Returns true if the current process is booted via ostree.
pub fn is_ostree_booted() -> bool {
path::Path::new("/run/ostree-booted").exists()
}

/// Returns true if /usr is not a read-only bind mount
pub fn is_unlocked() -> Result<bool> {
Ok(!statvfs::statvfs("/usr")?
.flags()
.contains(statvfs::FsFlags::ST_RDONLY))
}

/// Returns true if the current process is running as root.
pub fn am_privileged() -> bool {
nix::unistd::getuid() == nix::unistd::Uid::from_raw(0)
}

/// Return the absolute path to the underlying wrapped binary
fn get_real_binary_path(bin_name: &str) -> String {
format!("/{}/{}", cliwrap::CLIWRAP_DESTDIR, bin_name)
}

/// Wrapper for execv which accepts strings
pub fn exec_real_binary<T: AsRef<str> + std::fmt::Display>(bin_name: T, argv: &[T]) -> Result<()> {
let bin_name = bin_name.as_ref();
let real_bin = get_real_binary_path(bin_name);
let mut proc = std::process::Command::new(real_bin);
proc.args(argv.iter().map(|s| s.as_ref()));
Err(proc.exec().into())
}

/// Run a subprocess synchronously as user `bin` (dropping all capabilities).
pub fn run_unprivileged<T: AsRef<str>>(
with_warning: bool,
target_bin: &str,
argv: &[T],
) -> Result<()> {
// `setpriv` is in util-linux; we could do this internally, but this is easier.
let setpriv_argv = &[
"setpriv",
"--no-new-privs",
"--reuid=bin",
"--regid=bin",
"--init-groups",
"--bounding-set",
"-all",
"--",
];

let argv: Vec<&str> = argv.into_iter().map(AsRef::as_ref).collect();
let drop_privileges = am_privileged();
let app_name = "rpm-ostree";
if with_warning {
let delay_s = 5;
eprintln!(
"{name}: NOTE: This system is ostree based.",
name = app_name
);
if drop_privileges {
eprintln!(
r#"{name}: Dropping privileges as `{bin}` was executed with not "known safe" arguments."#,
name = app_name,
bin = target_bin
);
} else {
eprintln!(
r#"{name}: Wrapped binary "{bin}" was executed with not "known safe" arguments."#,
name = app_name,
bin = target_bin
);
}
eprintln!(
r##"{name}: You may invoke the real `{bin}` binary in `/{wrap_destdir}/{bin}`.
{name}: Continuing execution in {delay} seconds.
"##,
name = app_name,
wrap_destdir = cliwrap::CLIWRAP_DESTDIR,
bin = target_bin,
delay = delay_s,
);
thread::sleep(time::Duration::from_secs(delay_s));
}

if drop_privileges {
let real_bin = get_real_binary_path(target_bin);
let mut proc = std::process::Command::new("setpriv");
proc.args(setpriv_argv);
proc.arg(real_bin);
proc.args(argv);
Err(proc.exec().into())
} else {
exec_real_binary(target_bin, &argv)
}
}
17 changes: 17 additions & 0 deletions rust/src/cliwrap/dracut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use anyhow::Result;

use crate::cliwrap::cliutil;

/// Primary entrypoint to running our wrapped `dracut` handling.
pub(crate) fn main(argv: &[&str]) -> Result<()> {
eprintln!(
"This system is rpm-ostree based; initramfs handling is
integrated with the underlying ostree transaction mechanism.
Use `rpm-ostree initramfs` to control client-side initramfs generation."
);
if argv.len() > 0 {
Ok(cliutil::run_unprivileged(true, "dracut", argv)?)
} else {
std::process::exit(1);
}
}
10 changes: 10 additions & 0 deletions rust/src/cliwrap/grubby.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use anyhow::Result;

/// Primary entrypoint to running our wrapped `grubby` handling.
pub(crate) fn main(_argv: &[&str]) -> Result<()> {
eprintln!(
"This system is rpm-ostree based; grubby is not used.
Use `rpm-ostree kargs` instead."
);
std::process::exit(1);
}
Loading