diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3924db5878..519881d931 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -14,6 +14,7 @@ glib-sys = "0.6.0" gio-sys = "0.6.0" glib = "0.5.0" tempfile = "3.0.3" +clap = "~2.28" openat = "0.1.15" curl = "0.4.14" c_utf8 = "0.1.0" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 724d0cc2ee..66b0b03689 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -17,6 +17,7 @@ */ extern crate c_utf8; +extern crate clap; extern crate curl; #[macro_use] extern crate failure; @@ -46,3 +47,5 @@ mod journal; pub use journal::*; mod utils; pub use utils::*; +mod sysusers; +pub use sysusers::*; diff --git a/rust/src/sysusers.rs b/rust/src/sysusers.rs new file mode 100644 index 0000000000..1361e90f82 --- /dev/null +++ b/rust/src/sysusers.rs @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2018 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 clap::{App, Arg}; +use failure::Fallible; +use std::borrow::Cow; +use std::io::prelude::*; +use std::{fmt, fs, io, process}; + +static AUTOPATH: &str = "usr/lib/sysusers.d/rpmostree-auto.conf"; + +#[derive(PartialEq, Debug)] +enum IdSpecification { + Unspecified, + Specified(u32), + Path(String), +} + +impl IdSpecification { + fn parse(buf: &str) -> Fallible { + if buf.starts_with("/") { + Ok(IdSpecification::Path(buf.to_string())) + } else if buf == "-" { + Ok(IdSpecification::Unspecified) + } else { + Ok(IdSpecification::Specified(buf.parse::()?)) + } + } + + fn format_sysusers<'a>(&'a self) -> Cow<'a, str> { + match self { + IdSpecification::Unspecified => Cow::Borrowed("-"), + IdSpecification::Specified(u) => Cow::Owned(format!("{}", u)), + IdSpecification::Path(ref s) => Cow::Borrowed(s), + } + } +} + +// For now, we don't parse all of the entry data; we'd need to handle quoting +// etc. See extract-word.c in the systemd source. All we need is the +// user/group → {u,g}id mapping to ensure we're not creating duplicate entries. +#[derive(PartialEq, Debug)] +enum PartialSysuserEntry { + User { name: String, uid: IdSpecification }, + Group { name: String, gid: IdSpecification }, + GroupMember { uname: String, gname: String }, +} + +/// The full version that we get from `useradd`. +#[derive(PartialEq, Debug)] +enum SysuserEntry { + User { + name: String, + uid: IdSpecification, + gecos: Option, + homedir: Option, + shell: Option, + }, + Group { + name: String, + gid: IdSpecification, + }, + GroupMember { + uname: String, + gname: String, + }, +} + +impl SysuserEntry { + fn format_sysusers(&self) -> String { + match self { + SysuserEntry::User { + name, + uid, + gecos, + homedir, + shell, + } => { + fn optional_quoted_string<'a>(s: &'a Option) -> Cow<'a, str> { + match s.as_ref() { + Some(s) => { + let mut elts = s.split_whitespace(); + let first = elts.next(); + if first.is_none() { + return Cow::Borrowed("-"); + } + match elts.next() { + Some(_) => Cow::Owned(format!("\"{}\"", s)), + None => Cow::Borrowed(s), + } + } + None => Cow::Borrowed("-"), + } + } + format!( + "u {} {} {} {} {}", + name, + uid.format_sysusers(), + optional_quoted_string(&gecos), + optional_quoted_string(&homedir), + optional_quoted_string(&shell), + ) + } + SysuserEntry::Group { name, gid } => format!("g {} {}", name, gid.format_sysusers()), + SysuserEntry::GroupMember { uname, gname } => format!("m {} {}", uname, gname), + } + } +} + +/// (Partially) parse single a single line from a sysusers.d file +fn parse_entry(line: &str) -> Fallible { + let err = || format_err!("Invalid sysusers entry: \"{}\"", line); + let mut elts = line.split_whitespace().fuse(); + match elts.next().ok_or_else(err)? { + "u" => { + let name = elts.next().ok_or_else(err)?.to_string(); + let uidstr = elts.next().ok_or_else(err)?; + let uid = IdSpecification::parse(uidstr)?; + Ok(PartialSysuserEntry::User { name, uid }) + } + "g" => { + let name = elts.next().ok_or_else(err)?.to_string(); + let gidstr = elts.next().ok_or_else(err)?; + let gid = IdSpecification::parse(gidstr)?; + Ok(PartialSysuserEntry::Group { name, gid }) + } + "m" => { + let uname = elts.next().ok_or_else(err)?.to_string(); + let gname = elts.next().ok_or_else(err)?.to_string(); + Ok(PartialSysuserEntry::GroupMember { uname, gname }) + } + _ => Err(err()), + } +} + +/// Parse a sysusers.d file (as a stream) +fn parse(stream: I) -> Fallible> { + let mut res = Vec::new(); + for line in stream.lines() { + let line = line?; + if line.starts_with("#") || line.is_empty() { + continue; + } + res.push(parse_entry(&line)?); + } + Ok(res) +} + +fn useradd<'a, I>(args: I) -> Fallible> +where + I: IntoIterator, +{ + let app = App::new("useradd") + .version("0.1") + .about("rpm-ostree useradd wrapper") + .arg(Arg::with_name("system").short("r")) + .arg(Arg::with_name("uid").short("u").takes_value(true)) + .arg(Arg::with_name("gid").short("g").takes_value(true)) + .arg(Arg::with_name("no-log-init").short("l")) + .arg(Arg::with_name("no-unique").short("o")) + .arg(Arg::with_name("groups").short("G").takes_value(true)) + .arg(Arg::with_name("home-dir").short("d").takes_value(true)) + .arg(Arg::with_name("comment").short("c").takes_value(true)) + .arg(Arg::with_name("shell").short("s").takes_value(true)) + .arg(Arg::with_name("username").takes_value(true).required(true)); + let matches = app.get_matches_from_safe(args)?; + + let mut uid = IdSpecification::Unspecified; + if let Some(ref uidstr) = matches.value_of("uid") { + uid = IdSpecification::Specified(uidstr.parse::()?); + } + let name = matches.value_of("username").unwrap().to_string(); + let gecos = matches.value_of("comment").map(|s| s.to_string()); + let homedir = matches.value_of("home-dir").map(|s| s.to_string()); + let shell = matches.value_of("shell").map(|s| s.to_string()); + + let mut res = vec![SysuserEntry::User { + name: name.to_string(), + uid, + gecos, + homedir, + shell, + }]; + if let Some(primary_group) = matches.value_of("gid") { + let is_numeric_gid = primary_group.parse::().is_ok(); + if !is_numeric_gid && primary_group != name { + bail!( + "Unable to represent user with group '{}' != username '{}'", + primary_group, + name + ); + } + } + if let Some(gnames) = matches.value_of("groups") { + for gname in gnames.split(",").filter(|&n| n != name) { + res.push(SysuserEntry::GroupMember { + uname: name.to_string(), + gname: gname.to_string(), + }); + } + } + Ok(res) +} + +fn groupadd<'a, I>(args: I) -> Fallible +where + I: IntoIterator, +{ + let app = App::new("useradd") + .version("0.1") + .about("rpm-ostree groupadd wrapper") + .arg(Arg::with_name("system").short("r")) + .arg(Arg::with_name("force").short("f")) + .arg(Arg::with_name("gid").short("g").takes_value(true)) + .arg(Arg::with_name("groupname").takes_value(true).required(true)); + let matches = app.get_matches_from_safe(args)?; + + let mut gid = IdSpecification::Unspecified; + if let Some(ref gidstr) = matches.value_of("gid") { + gid = IdSpecification::Specified(gidstr.parse::()?); + } + let name = matches.value_of("groupname").unwrap().to_string(); + Ok(SysuserEntry::Group { name, gid }) +} + +fn useradd_main(sysusers_file: &mut fs::File, args: &Vec<&str>) -> Fallible<()> { + eprintln!("useradd_main: {:?}", args); + let r = useradd(args.iter().map(|x| *x))?; + for elt in r { + writeln!(sysusers_file, "{}", elt.format_sysusers())?; + } + Ok(()) +} + +fn groupadd_main(sysusers_file: &mut fs::File, args: &Vec<&str>) -> Fallible<()> { + eprintln!("groupadd_main: {:?}", args); + let r = groupadd(args.iter().map(|x| *x))?; + writeln!(sysusers_file, "{}", r.format_sysusers())?; + Ok(()) +} + +fn process_useradd_invocation(rootfs: openat::Dir, mut argf: fs::File) -> Fallible<()> { + argf.seek(io::SeekFrom::Start(0))?; + eprintln!("useradd: {:?}", argf.metadata()?.len()); + let argf = io::BufReader::new(argf); + let mut lines = argf.lines(); + let autopath = ::std::path::Path::new(AUTOPATH); + loop { + let mode = match (&mut lines).next() { + Some(mode) => mode?, + None => return Ok(()), + }; + let mode = mode.as_str(); + eprintln!("useradd mode {:?}", mode); + if mode == "" { + return Ok(()) + } + let mut args = vec![mode.to_string()]; + for line in &mut lines { + let line = line?; + // Empty arg terminates an invocation + if line == "" { + break; + } + args.push(line); + }; + let args : Vec<&str> = args.iter().map(|v| v.as_str()).collect(); + let mut sysusers_autof = rootfs.append_file(AUTOPATH, 0644)?; + match mode { + "useradd" => useradd_main(&mut sysusers_autof, &args)?, + "groupadd" => groupadd_main(&mut sysusers_autof, &args)?, + x => bail!("Unknown command: {}", x), + }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // from "man sysusers.d" + static SYSUSERS1: &str = r###" +u httpd 404 "HTTP User" +u authd /usr/bin/authd "Authorization user" +u postgres - "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb +g input - - +m authd input +u root 0 "Superuser" /root /bin/zsh +"###; + + #[test] + fn test_parse() { + let buf = io::BufReader::new(SYSUSERS1.as_bytes()); + let r = parse(buf).unwrap(); + assert_eq!( + r[0], + PartialSysuserEntry::User { + name: "httpd".to_string(), + uid: IdSpecification::Specified(404) + } + ); + assert_eq!( + r[1], + PartialSysuserEntry::User { + name: "authd".to_string(), + uid: IdSpecification::Path("/usr/bin/authd".to_string()) + } + ); + assert_eq!( + r[2], + PartialSysuserEntry::User { + name: "postgres".to_string(), + uid: IdSpecification::Unspecified + } + ); + assert_eq!( + r[3], + PartialSysuserEntry::Group { + name: "input".to_string(), + gid: IdSpecification::Unspecified + } + ); + assert_eq!( + r[4], + PartialSysuserEntry::GroupMember { + uname: "authd".to_string(), + gname: "input".to_string() + } + ); + assert_eq!( + r[5], + PartialSysuserEntry::User { + name: "root".to_string(), + uid: IdSpecification::Specified(0) + } + ); + } + + #[test] + fn test_useradd_wesnoth() { + let r = useradd( + vec![ + "useradd", + "-c", + "Wesnoth server", + "-s", + "/sbin/nologin", + "-r", + "-d", + "/var/run/wesnothd", + "wesnothd", + ].iter() + .map(|v| *v), + ).unwrap(); + assert_eq!( + r, + vec![SysuserEntry::User { + name: "wesnothd".to_string(), + uid: IdSpecification::Unspecified, + gecos: Some("Wesnoth server".to_string()), + shell: Some("/sbin/nologin".to_string()), + homedir: Some("/var/run/wesnothd".to_string()), + }] + ); + assert_eq!(r.len(), 1); + assert_eq!( + r[0].format_sysusers(), + r##"wesnothd - "Wesnoth server" /var/run/wesnothd /sbin/nologin"## + ); + } + + #[test] + fn test_useradd_tss() { + let r = useradd( + vec![ + "useradd", + "-r", + "-u", + "59", + "-g", + "tss", + "-d", + "/dev/null", + "-s", + "/sbin/nologin", + "-c", + "comment", + "tss", + ].iter() + .map(|v| *v), + ).unwrap(); + assert_eq!(r.len(), 1); + assert_eq!( + r[0].format_sysusers(), + r##"tss 59 comment /dev/null /sbin/nologin"## + ); + } + + #[test] + fn test_groupadd_basics() { + assert_eq!( + groupadd(vec!["groupadd", "-r", "wireshark",].iter().map(|v| *v)).unwrap(), + SysuserEntry::Group { + name: "wireshark".to_string(), + gid: IdSpecification::Unspecified, + }, + ); + assert_eq!( + groupadd( + vec!["groupadd", "-r", "-g", "112", "vhostmd",] + .iter() + .map(|v| *v) + ).unwrap(), + SysuserEntry::Group { + name: "vhostmd".to_string(), + gid: IdSpecification::Specified(112), + }, + ); + } +} + +mod ffi { + use super::*; + use ffiutil::*; + use glib; + use libc; + use std::env; + use std::os::unix::io::FromRawFd; + + #[no_mangle] + pub extern "C" fn ror_sysusers_process_useradd(rootfs_dfd: libc::c_int, + useradd_fd: libc::c_int, + gerror: *mut *mut glib_sys::GError) -> libc::c_int { + let rootfs_dfd = ffi_view_openat_dir(rootfs_dfd); + // Ownership is transferred + let useradd_fd = unsafe { fs::File::from_raw_fd(useradd_fd) }; + int_glib_error(process_useradd_invocation(rootfs_dfd, useradd_fd), gerror) + } +} +pub use self::ffi::*; diff --git a/src/app/main.c b/src/app/main.c index 9508d878d3..7fe8e32e43 100644 --- a/src/app/main.c +++ b/src/app/main.c @@ -30,6 +30,7 @@ #include #include "rpmostree-builtins.h" +#include "rpmostree-rust.h" #include "rpmostree-polkit-agent.h" #include "libglnx.h" @@ -391,7 +392,6 @@ int main (int argc, char **argv) { - GCancellable *cancellable = g_cancellable_new (); RpmOstreeCommand *command; const char *command_name = NULL; g_autofree char *prgname = NULL; @@ -417,6 +417,8 @@ main (int argc, setlocale (LC_ALL, ""); + GCancellable *cancellable = g_cancellable_new (); + /* * Parse the global options. We rearrange the options as * necessary, in order to pass relevant options through diff --git a/src/libpriv/gresources.xml b/src/libpriv/gresources.xml index 371a24cea6..cbd7228b23 100644 --- a/src/libpriv/gresources.xml +++ b/src/libpriv/gresources.xml @@ -2,5 +2,7 @@ systemctl-wrapper.sh + useradd-wrapper.sh + groupadd-wrapper.sh diff --git a/src/libpriv/groupadd-wrapper.sh b/src/libpriv/groupadd-wrapper.sh new file mode 100755 index 0000000000..0106d1a24c --- /dev/null +++ b/src/libpriv/groupadd-wrapper.sh @@ -0,0 +1,12 @@ +#!/usr/bin/bash +# Used by rpmostree-core.c to intercept `groupadd` operations so +# we can convert to systemd-sysusers. +set -euo pipefail +ls /proc/self/fd/$RPMOSTREE_USERADD_FD >/dev/null +(echo groupadd + for x in "$@"; do + echo $x + done + echo +) > /proc/self/fd/$RPMOSTREE_USERADD_FD +exec /usr/sbin/groupadd.rpmostreesave "$@" diff --git a/src/libpriv/rpmostree-bwrap.c b/src/libpriv/rpmostree-bwrap.c index d1e0dbd147..8a6bda3a03 100644 --- a/src/libpriv/rpmostree-bwrap.c +++ b/src/libpriv/rpmostree-bwrap.c @@ -415,6 +415,15 @@ rpmostree_bwrap_setenv (RpmOstreeBwrap *bwrap, const char *name, const char *val g_subprocess_launcher_setenv (bwrap->launcher, name, value, TRUE); } +/* Transfer ownership of @source_fd to child at @target_fd */ +void +rpmostree_bwrap_take_fd (RpmOstreeBwrap *bwrap, + int source_fd, + int target_fd) +{ + g_subprocess_launcher_take_fd (bwrap->launcher, source_fd, target_fd); +} + /* Execute @bwrap, optionally capturing stdout or stderr. Must have been configured. After * executing this method, the @bwrap instance cannot be run again. */ @@ -425,27 +434,15 @@ rpmostree_bwrap_run_captured (RpmOstreeBwrap *bwrap, GCancellable *cancellable, GError **error) { - GSubprocessLauncher *launcher = bwrap->launcher; - - g_assert (!bwrap->executed); - bwrap->executed = TRUE; - - /* Set up our error message */ - const char *errmsg = glnx_strjoina ("Executing bwrap(", bwrap->child_argv0, ")"); - GLNX_AUTO_PREFIX_ERROR (errmsg, error); - - /* Add the final NULL */ - g_ptr_array_add (bwrap->argv, NULL); - if (stdout_buf) g_subprocess_launcher_set_flags (bwrap->launcher, G_SUBPROCESS_FLAGS_STDOUT_PIPE); if (stderr_buf) g_subprocess_launcher_set_flags (bwrap->launcher, G_SUBPROCESS_FLAGS_STDERR_PIPE); - g_subprocess_launcher_set_child_setup (launcher, bwrap_child_setup, bwrap, NULL); - g_autoptr(GSubprocess) subproc = - g_subprocess_launcher_spawnv (launcher, (const char *const*)bwrap->argv->pdata, - error); + const char *errmsg = glnx_strjoina ("Executing bwrap(", bwrap->child_argv0, ")"); + GLNX_AUTO_PREFIX_ERROR (errmsg, error); + + g_autoptr(GSubprocess) subproc = rpmostree_bwrap_execute (bwrap, error); if (!subproc) return FALSE; @@ -488,6 +485,20 @@ rpmostree_bwrap_run (RpmOstreeBwrap *bwrap, return rpmostree_bwrap_run_captured (bwrap, NULL, NULL, cancellable, error); } +GSubprocess * +rpmostree_bwrap_execute (RpmOstreeBwrap *bwrap, GError **error) +{ + g_autoptr(GSubprocessLauncher) launcher = g_steal_pointer (&bwrap->launcher); + g_assert (!bwrap->executed); + bwrap->executed = TRUE; + + /* Add the final NULL */ + g_ptr_array_add (bwrap->argv, NULL); + + g_subprocess_launcher_set_child_setup (launcher, bwrap_child_setup, bwrap, NULL); + return g_subprocess_launcher_spawnv (launcher, (const char *const*)bwrap->argv->pdata, error); +} + /* Execute /bin/true inside a bwrap container on the host */ gboolean rpmostree_bwrap_selftest (GError **error) diff --git a/src/libpriv/rpmostree-bwrap.h b/src/libpriv/rpmostree-bwrap.h index e13db71948..f75e2a4538 100644 --- a/src/libpriv/rpmostree-bwrap.h +++ b/src/libpriv/rpmostree-bwrap.h @@ -53,6 +53,10 @@ void rpmostree_bwrap_append_child_argva (RpmOstreeBwrap *bwrap, int argc, char * void rpmostree_bwrap_setenv (RpmOstreeBwrap *bwrap, const char *name, const char *value); +void rpmostree_bwrap_take_fd (RpmOstreeBwrap *bwrap, + int source_fd, + int target_fd); + void rpmostree_bwrap_set_child_setup (RpmOstreeBwrap *bwrap, GSpawnChildSetupFunc func, gpointer data); @@ -63,6 +67,8 @@ gboolean rpmostree_bwrap_run_captured (RpmOstreeBwrap *bwrap, GCancellable *cancellable, GError **error); +GSubprocess * rpmostree_bwrap_execute (RpmOstreeBwrap *bwrap, GError **error); + gboolean rpmostree_bwrap_run (RpmOstreeBwrap *bwrap, GCancellable *cancellable, GError **error); diff --git a/src/libpriv/rpmostree-core.c b/src/libpriv/rpmostree-core.c index be66639881..306be0338a 100644 --- a/src/libpriv/rpmostree-core.c +++ b/src/libpriv/rpmostree-core.c @@ -3669,6 +3669,71 @@ rpmostree_context_get_kernel_changed (RpmOstreeContext *self) return self->kernel_changed; } +/* Given a relative binary path e.g. usr/bin/systemctl, + * if it exists, replace it with a wrapper and record + * that we did the replacement in @replacements so we can + * undo it later. + */ +static gboolean +replace_with_wrapper (int rootfs_dfd, + const char *binpath, + GPtrArray *replacements, + GError **error) +{ + g_assert_cmpint (binpath[0], !=, '/'); + const char *binpath_saved = glnx_strjoina (binpath, ".rpmostreesave"); + if (renameat (rootfs_dfd, binpath, rootfs_dfd, binpath_saved) < 0) + { + /* Doesn't exist? Nothing to do */ + if (errno == ENOENT) + return TRUE; + else + return glnx_throw_errno_prefix (error, "rename(%s)", binpath); + } + else + { + const char *basename = glnx_basename (binpath); + const char *key = glnx_strjoina ("/rpmostree/", basename, "-wrapper.sh"); + g_autoptr(GBytes) wrapper = g_resources_lookup_data (key, G_RESOURCE_LOOKUP_FLAGS_NONE, error); + if (!wrapper) + return FALSE; + size_t len; + const guint8* buf = g_bytes_get_data (wrapper, &len); + if (!glnx_file_replace_contents_with_perms_at (rootfs_dfd, binpath, + buf, len, 0755, (uid_t) -1, (gid_t) -1, + GLNX_FILE_REPLACE_NODATASYNC, + NULL, error)) + return FALSE; + } + + g_ptr_array_add (replacements, g_strdup (binpath)); + + return TRUE; +} + +/* Reverse the effect of replace_with_wrapper() */ +static gboolean +undo_wrappers (int rootfs_dfd, + GPtrArray *replacements, + GError **error) +{ + g_autoptr(GString) buf = g_string_new (""); + + for (guint i = 0; i < replacements->len; i++) + { + const char *binpath = replacements->pdata[i]; + g_string_truncate (buf, 0); + g_string_append (buf, binpath); + g_string_append (buf, ".rpmostreesave"); + + if (!glnx_renameat (rootfs_dfd, buf->str, + rootfs_dfd, binpath, error)) + return FALSE; + } + + return TRUE; +} + gboolean rpmostree_context_assemble (RpmOstreeContext *self, GCancellable *cancellable, @@ -3914,6 +3979,10 @@ rpmostree_context_assemble (RpmOstreeContext *self, rpmostree_output_progress_end (&checkout_progress); + /* Ensure that some directories exist */ + if (!glnx_shutil_mkdir_p_at (tmprootfs_dfd, "usr/lib/sysusers.d", 0755, cancellable, error)) + return FALSE; + /* Some packages expect to be able to make temporary files here * for obvious reasons, but we otherwise make `/var` read-only. */ @@ -3945,7 +4014,6 @@ rpmostree_context_assemble (RpmOstreeContext *self, if (overlays->len > 0 || overrides_replace->len > 0) { gboolean have_passwd; - gboolean have_systemctl; g_autoptr(GPtrArray) passwdents_ptr = NULL; g_autoptr(GPtrArray) groupents_ptr = NULL; @@ -3964,7 +4032,7 @@ rpmostree_context_assemble (RpmOstreeContext *self, error)) return FALSE; - /* Also neuter systemctl - at least glusterfs for example calls `systemctl + /* Neuter systemctl - at least glusterfs for example calls `systemctl * start` in its %post which both violates Fedora policy and also will not * work with the rpm-ostree model. * See also https://github.com/projectatomic/rpm-ostree/issues/550 @@ -3973,30 +4041,14 @@ rpmostree_context_assemble (RpmOstreeContext *self, * point in the far future when we don't support CentOS7 we can drop * our wrapper script. If we remember. */ - if (renameat (tmprootfs_dfd, "usr/bin/systemctl", - tmprootfs_dfd, "usr/bin/systemctl.rpmostreesave") < 0) - { - if (errno == ENOENT) - have_systemctl = FALSE; - else - return glnx_throw_errno_prefix (error, "rename(usr/bin/systemctl)"); - } - else - { - have_systemctl = TRUE; - g_autoptr(GBytes) systemctl_wrapper = g_resources_lookup_data ("/rpmostree/systemctl-wrapper.sh", - G_RESOURCE_LOOKUP_FLAGS_NONE, - error); - if (!systemctl_wrapper) - return FALSE; - size_t len; - const guint8* buf = g_bytes_get_data (systemctl_wrapper, &len); - if (!glnx_file_replace_contents_with_perms_at (tmprootfs_dfd, "usr/bin/systemctl", - buf, len, 0755, (uid_t) -1, (gid_t) -1, - GLNX_FILE_REPLACE_NODATASYNC, - cancellable, error)) - return FALSE; - } + g_autoptr(GPtrArray) replaced_binaries = g_ptr_array_new_with_free_func (g_free); + if (!replace_with_wrapper (tmprootfs_dfd, "usr/bin/systemctl", replaced_binaries, error)) + return FALSE; + /* And we intercept useradd/groupadd to convert to systemd-sysusers */ + if (!replace_with_wrapper (tmprootfs_dfd, "usr/sbin/useradd", replaced_binaries, error)) + return FALSE; + if (!replace_with_wrapper (tmprootfs_dfd, "usr/sbin/groupadd", replaced_binaries, error)) + return FALSE; /* Necessary for unified core to work with semanage calls in %post, like container-selinux */ if (!rpmostree_rootfs_fixup_selinux_store_root (tmprootfs_dfd, cancellable, error)) @@ -4124,12 +4176,8 @@ rpmostree_context_assemble (RpmOstreeContext *self, !rpmostree_deployment_sanitycheck_true (tmprootfs_dfd, cancellable, error)) return FALSE; - if (have_systemctl) - { - if (!glnx_renameat (tmprootfs_dfd, "usr/bin/systemctl.rpmostreesave", - tmprootfs_dfd, "usr/bin/systemctl", error)) - return FALSE; - } + if (!undo_wrappers (tmprootfs_dfd, replaced_binaries, error)) + return FALSE; if (have_passwd) { diff --git a/src/libpriv/rpmostree-scripts.c b/src/libpriv/rpmostree-scripts.c index 6a1379587e..80120212c5 100644 --- a/src/libpriv/rpmostree-scripts.c +++ b/src/libpriv/rpmostree-scripts.c @@ -21,11 +21,14 @@ #include "config.h" #include +#include #include #include "rpmostree-output.h" #include "rpmostree-util.h" +#include "rpmostree-rust.h" #include "rpmostree-bwrap.h" #include +#include #include #include "libglnx.h" @@ -241,6 +244,7 @@ struct ChildSetupData { int stdin_fd; int stdout_fd; int stderr_fd; + int useradd_fd; }; static void @@ -258,6 +262,8 @@ script_child_setup (gpointer opaque) err (1, "dup2(stdout)"); if (data->stderr_fd >= 0 && dup2 (data->stderr_fd, STDERR_FILENO) < 0) err (1, "dup2(stderr)"); + if (data->useradd_fd >= 0 && fcntl (data->useradd_fd, F_SETFD, 0) < 0) + err (1, "fcntl(useradd_fd)"); } /* Print the output of a script, with each line prefixed with @@ -333,9 +339,11 @@ run_script_in_bwrap_container (int rootfs_fd, const char *postscript_path_container = glnx_strjoina ("/usr", postscript_name); const char *postscript_path_host = postscript_path_container + 1; g_autoptr(RpmOstreeBwrap) bwrap = NULL; + g_auto(GLnxTmpfile) useradd_tmpf = {0, }; gboolean created_var_lib_rpmstate = FALSE; glnx_autofd int stdout_fd = -1; glnx_autofd int stderr_fd = -1; + glnx_autofd int useradd_pipe_fd = -1; /* TODO - Create a pipe and send this to bwrap so it's inside the * tmpfs. Note the +1 on the path to skip the leading /. @@ -409,7 +417,8 @@ run_script_in_bwrap_container (int rootfs_fd, { struct ChildSetupData data = { .stdin_fd = stdin_fd, .stdout_fd = -1, - .stderr_fd = -1, }; + .stderr_fd = -1, + .useradd_fd = -1 }; /* Only try to log to the journal if we're already set up that way (normally * rpm-ostreed for host system management). Otherwise we might be in a Docker @@ -441,6 +450,16 @@ run_script_in_bwrap_container (int rootfs_fd, data.stdout_fd = data.stderr_fd = buffered_output.fd; } + /* Special API to pass useradd/groupadd invocations back up to the parent + * outside the root. + */ + if (!glnx_open_anonymous_tmpfile (O_RDWR | O_CLOEXEC, &useradd_tmpf, error)) + return FALSE; + data.useradd_fd = useradd_tmpf.fd; + { g_autofree char *useradd_env = g_strdup_printf ("%d", data.useradd_fd); + rpmostree_bwrap_setenv (bwrap, "RPMOSTREE_USERADD_FD", useradd_env); + } + data.all_fds_initialized = TRUE; rpmostree_bwrap_set_child_setup (bwrap, script_child_setup, &data); @@ -479,6 +498,14 @@ run_script_in_bwrap_container (int rootfs_fd, else dump_buffered_output_noerr (pkg_script, &buffered_output); + { + // Transfer ownership + int fd = useradd_tmpf.fd; + useradd_tmpf.initialized = FALSE; + if (!ror_sysusers_process_useradd (rootfs_fd, fd, error)) + return FALSE; + } + ret = TRUE; out: glnx_tmpfile_clear (&buffered_output); diff --git a/src/libpriv/useradd-wrapper.sh b/src/libpriv/useradd-wrapper.sh new file mode 100755 index 0000000000..629cf78a3e --- /dev/null +++ b/src/libpriv/useradd-wrapper.sh @@ -0,0 +1,13 @@ +#!/usr/bin/bash +# Used by rpmostree-core.c to intercept `useradd` operations so +# we can convert to systemd-sysusers. +set -euo pipefail +if ! ls /proc/self/fd/$RPMOSTREE_USERADD_FD >/dev/null; then + ls -al /proc/self/fd + exit 1 +fi +(echo useradd + for x in "$@"; do + echo $x + done) > /proc/self/fd/$RPMOSTREE_USERADD_FD +exec /usr/sbin/useradd.rpmostreesave "$@"