Skip to content

Commit

Permalink
WIP: Add support for wrapping binaries (rpm, dracut, grubby)
Browse files Browse the repository at this point in the history
We need to be friendlier to people who are transitioning from
"traditional" yum managed systems.  This patchset starts to lay
out the groundwork for supporting "intercepting" binaries that
are in the tree.

To start with for example, we wrap `/usr/bin/rpm` and cause it
to drop privileges.  This way it can't corrupt anything; we're
not just relying on the read-only bind mount.  For example nothing
will accidentally get written to `/var/lib/rpm`.

Now a tricky thing with this one is we *do* want it to write if
we're in an unlocked state.

There are various other examples of binaries we want to intercept,
among them:

 - `grubby` -> `rpm-ostree kargs`
 - `dracut` -> `rpm-ostree initramfs`
 - `yum` -> well...we'll talk about that later
  • Loading branch information
cgwalters committed Apr 28, 2019
1 parent 9cb1f61 commit 0e57e53
Show file tree
Hide file tree
Showing 16 changed files with 400 additions and 1 deletion.
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 @@ -99,7 +100,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
4 changes: 4 additions & 0 deletions docs/manual/treefile.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ It supports the following parameters:
specific filesystem drivers are included. If not specified,
`--no-hostonly` will be used.

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

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

* `remove-from-packages`: Array, optional: Delete from specified packages
Expand Down
20 changes: 20 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ authors = ["Colin Walters <walters@verbum.org>", "Jonathan Lebon <jonathan@jlebo
edition = "2018"

[dependencies]
nix = "0.13.0"
failure = "0.1.3"
serde = "1.0.78"
serde_derive = "1.0.78"
Expand Down
101 changes: 101 additions & 0 deletions rust/src/cliwrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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 std::io::prelude::*;
use std::{io, path};
use failure::{Fallible, bail};

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

/// Location for the underlying (not wrapped) binaries.
pub(crate) static WRAP_DESTDIR : &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"];

/// Main entrypoint for cliwrap
fn cliwrap_main(args: &Vec<String>) -> Fallible<()> {
// 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 args : Vec<&str> = args.iter().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_wrapped(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) -> Fallible<()> {
rootfs_dfd.create_dir(WRAP_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!("{}/{}", WRAP_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);
f.write(b"#!/bin/sh\nexec /usr/bin/rpm-ostree cliwrap $0\n")?;
f.flush()?;
Ok(())
})
}

mod ffi {
use super::*;
use crate::ffiutil::*;
use glib;
use libc;
use failure::ResultExt;

#[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(|e| format!("During cli wrapper replacement: {}", e)), 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)
}
}
pub use self::ffi::*;
104 changes: 104 additions & 0 deletions rust/src/cliwrap/cliutil.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use failure::Fallible;
use libc;
use std::{thread, time, path};
use std::ffi::CString;
use nix::sys::statvfs;
use nix::unistd;

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 the current process is running as root.
pub fn is_unlocked() -> Fallible<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 {
unsafe { libc::getuid() == 0 }
}

/// Wrapper for execv which accepts strings
fn execvp_strs(argv0: &str, argv: &[&str]) -> Fallible<()> {
let argv0 = CString::new(argv0).unwrap();
let argv : Vec<CString> = argv.iter().map(|&v| CString::new(v).unwrap()).collect();
unistd::execvp(&argv0, &argv)?;
Ok(())
}

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

/// Wrapper for execv which accepts strings
pub fn exec_real_wrapped<T: AsRef<str> + std::fmt::Display>(bin_name: T, argv: &[T]) -> Fallible<()> {
let bin_name = bin_name.as_ref();
let real_bin = get_real_bin(bin_name);
let argv : Vec<&str> = std::iter::once(bin_name).chain(argv.iter().map(|v| v.as_ref())).collect();
execvp_strs(real_bin.as_str(), &argv)
}

/// 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],
) -> Fallible<()> {
// `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::WRAP_DESTDIR,
bin = target_bin,
delay = delay_s,
);
thread::sleep(time::Duration::from_secs(delay_s));
}

if drop_privileges {
let real_bin = get_real_bin(target_bin);
let real_argv : Vec<&str> = setpriv_argv.iter().map(|&v| v)
.chain(std::iter::once(real_bin.as_str()))
.chain(argv).collect();
execvp_strs("setpriv", &real_argv)
} else {
exec_real_wrapped(target_bin, &argv)
}
}
9 changes: 9 additions & 0 deletions rust/src/cliwrap/dracut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use failure::Fallible;

/// Primary entrypoint to running our wrapped `dracut` handling.
pub(crate) fn main(_argv: &[&str]) -> Fallible<()> {
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.");
std::process::exit(1);
}
8 changes: 8 additions & 0 deletions rust/src/cliwrap/grubby.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use failure::Fallible;

/// Primary entrypoint to running our wrapped `grubby` handling.
pub(crate) fn main(_argv: &[&str]) -> Fallible<()> {
eprintln!("This system is rpm-ostree based; grubby is not used.
Use `rpm-ostree kargs` instead.");
std::process::exit(1);
}
64 changes: 64 additions & 0 deletions rust/src/cliwrap/rpm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use clap::{App, Arg};
use failure::Fallible;

use crate::cliwrap::cliutil;

/// Set of RPM arguments that we know are safe to pass through,
/// or that we handle internally.
fn new_rpm_app<'r>() -> App<'r, 'static> {
let name = "cli-ostree-wrapper-rpm";
App::new(name)
.bin_name(name)
.version("0.1")
.about("Wrapper for rpm")
.arg(Arg::with_name("all").short("a"))
.arg(Arg::with_name("file").short("f"))
.arg(Arg::with_name("package").short("p"))
.arg(Arg::with_name("query").short("q"))
.arg(Arg::with_name("verify").short("V"))
.arg(Arg::with_name("version"))
}

/// Primary entrypoint to running our wrapped `rpm` handling.
pub(crate) fn main(argv: &[&str]) -> Fallible<()> {
if cliutil::is_unlocked()? {
// For now if we're unlocked, just directly exec rpm. In the future we
// may choose to take over installing a package live.
cliutil::exec_real_wrapped("rpm", argv)
} else {
let mut app = new_rpm_app();
let mut with_warning = true;
if let Ok(matches) = app.get_matches_from_safe_borrow(argv) {
// Implement custom option handling here
if matches.is_present("verify") {
println!("rpm --verify is not necessary for ostree-based systems.
All binaries in /usr are underneath a read-only bind mount.
If you wish to verify integrity, use `ostree fsck`.");
return Ok(())
}
with_warning = false;
}
cliutil::run_unprivileged(with_warning, "rpm", argv)
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qa() {
let app = new_rpm_app();
let argv = vec!["rpm", "-qa"];
let matches = app.get_matches_from_safe(&argv);
assert!(matches.is_ok());
}

#[test]
fn test_unknown() {
let app = new_rpm_app();
let argv = vec!["rpm", "--not-a-valid-arg"];
let matches = app.get_matches_from_safe(&argv);
assert!(matches.is_err());
}

}
2 changes: 2 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
mod ffiutil;
mod openat_utils;

mod cliwrap;
pub use cliwrap::*;
mod composepost;
pub use self::composepost::*;
mod journal;
Expand Down
Loading

0 comments on commit 0e57e53

Please sign in to comment.