From 9863d8ea3a70d8958072282e678bad673cb4f9ab Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Mon, 25 Feb 2019 00:59:42 +0000 Subject: [PATCH] WIP: compose: Add `experimental/sysusers` option First, we add support for a new `experimental:` key. Then there's a new `sysusers` key underneath that. When enabled, we drop all of the other previous passwd handling. In practice the only one that was used was having static files. That is a pain to maintain. However, we need to statically assign non-zero uid/gid for any files that come from a base ostree commit. Anything else would mean the user/groups could be unpredictably assigned in different rpm-ostree runs. This code now checks for an errors out on that. In order to convert *fully* to sysusers, we install an interceptor for `useradd/groupadd` that talk back via a pipe to the compose process. These invocations then get translated to drop into a new` `sysusers.d/rpmostree-auto.conf` file. This way we don't need to require that every RPM have ported to sysusers.d. At the end, we drop everything in `/etc/passwd` and `/etc/group` except for the `root:` entries, relying on `systemd-sysusers` to readd everything at boot time. Closes: https://github.com/projectatomic/rpm-ostree/issues/49 --- docs/manual/treefile.md | 4 + rust/src/lib.rs | 2 + rust/src/sysusers.rs | 879 ++++++++++++++++++++++ rust/src/treefile.rs | 109 ++- src/app/rpmostree-compose-builtin-rojig.c | 2 +- src/app/rpmostree-compose-builtin-tree.c | 5 +- src/app/rpmostree-composeutil.c | 2 + src/libpriv/gresources.xml | 3 + src/libpriv/groupadd-wrapper.sh | 11 + src/libpriv/rpmostree-core.c | 122 ++- src/libpriv/rpmostree-passwd-util.c | 32 +- src/libpriv/rpmostree-postprocess.c | 81 +- src/libpriv/rpmostree-postprocess.h | 7 + src/libpriv/rpmostree-scripts.c | 29 +- src/libpriv/systemd-sysusers-wrapper.sh | 6 + src/libpriv/useradd-wrapper.sh | 9 + tests/compose-tests/test-sysusers.sh | 42 ++ 17 files changed, 1273 insertions(+), 72 deletions(-) create mode 100644 rust/src/sysusers.rs create mode 100755 src/libpriv/groupadd-wrapper.sh create mode 100755 src/libpriv/systemd-sysusers-wrapper.sh create mode 100755 src/libpriv/useradd-wrapper.sh create mode 100755 tests/compose-tests/test-sysusers.sh diff --git a/docs/manual/treefile.md b/docs/manual/treefile.md index bd48f321df..e36ef7a827 100644 --- a/docs/manual/treefile.md +++ b/docs/manual/treefile.md @@ -253,3 +253,7 @@ version of `rpm-ostree`. * `rojig`: Object, optional. Sub-keys are `name`, `summary`, `license`, and `description`. Of those, `name` and `license` are mandatory. + + * `sysusers`: boolean, optional: Defaults to `false`. Enable generation of + systemd sysusers.d entries based on `useradd` invocations. If enabled, + this overrides `preserve-passwd`. It also obsoletes `check-passwd`. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index af03118d21..f2978bed88 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -51,3 +51,5 @@ pub use journal::*; mod utils; pub use utils::*; mod openat_utils; +mod sysusers; +pub use sysusers::*; diff --git a/rust/src/sysusers.rs b/rust/src/sysusers.rs new file mode 100644 index 0000000000..570bfe1b1c --- /dev/null +++ b/rust/src/sysusers.rs @@ -0,0 +1,879 @@ +/* + * 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 libc; +use failure::{Fallible, ResultExt}; +use std::borrow::Cow; +use std::io::prelude::*; +use std::path::Path; +use std::{collections, fs, io, path}; +use std::collections::HashMap; + +use treefile::*; +use openat_utils::OpenatDirExt; + +static SYSUSERS_DIR: &str = "usr/lib/sysusers.d"; +static SYSUSERS_AUTO_NAME: &str = "rpmostree-auto.conf"; +static SYSUSERS_OVERRIDES_NAME: &str = "rpmostree-overrides.conf"; + +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +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), + } + } +} + +/// A generic name ↔ id pair used for both users and groups +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +struct PartialEntryValue { + raw: String, + name: String, + id: IdSpecification, + rest: String, +} + +// 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. +// Group members we just pass through +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +enum PartialSysuserEntry { + User(PartialEntryValue), + Group(PartialEntryValue), + GroupMember(String) +} + +/// The full version that we get from `useradd`. +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +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 raw = line.to_string(); + // Handle the group member special case; u/g are structurually the same. + if line.starts_with('m') { + return Ok(PartialSysuserEntry::GroupMember(raw)); + } + // We only parse out the type, name, and id. + let elts : Vec<&str> = line.split_whitespace().collect(); + if elts.len() < 3 { + return Err(err()) + } + let name = elts[1].to_string(); + let id = IdSpecification::parse(elts[2])?; + let rest = if elts.len() > 3 { + elts[3..].join(" ") + } else { + "".to_string() + }; + match elts[0] { + "u" => { + Ok(PartialSysuserEntry::User(PartialEntryValue { raw, name, id, rest })) + }, + "g" => { + Ok(PartialSysuserEntry::Group(PartialEntryValue { raw, name, id, rest })) + }, + "m" => { + // Handled above + unreachable!(); + }, + _ => Err(err()), + } +} + +/// Parse a sysusers.d file (as a stream) +fn parse_sysusers_stream(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-create-home").short("M")) + .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 W, args: &Vec<&str>) -> Fallible<()> { + 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 W, args: &Vec<&str>) -> Fallible<()> { + let r = groupadd(args.iter().map(|x| *x))?; + writeln!(sysusers_file, "{}", r.format_sysusers())?; + Ok(()) +} + +/// Called from rpmostree-scripts.c to implement a special API to extract +/// `useradd/groupadd` invocations from scripts. We add their data additionally +/// to a sysusers.d entry, letting the wrapper script invoke the underlying +/// utility to also add the data to /etc/{passwd,group}. (In the future we may +/// switch to invoking systemd-sysusers in the root instead). +/// +/// This function takes as input a file descriptor for an O_TMPFILE fd that has +/// newline-separated arguments. Note that we can get multiple invocations of +/// useradd/groupadd in a single call. +fn process_useradd_invocation(rootfs: openat::Dir, mut argf: fs::File) -> Fallible<()> { + argf.seek(io::SeekFrom::Start(0))?; + let argf = io::BufReader::new(argf); + let mut lines = argf.lines(); + let sysusers_dir = path::Path::new(SYSUSERS_DIR); + let autopath = sysusers_dir.join(SYSUSERS_AUTO_NAME); + let mut sysusers_autof = None; + loop { + let mode = match (&mut lines).next() { + Some(mode) => mode?, + None => break, + }; + let mode = mode.as_str(); + if mode == "" { + break + } + 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(); + if sysusers_autof.is_none() { + let f = rootfs.append_file(&autopath, 0644)?; + sysusers_autof = Some(io::BufWriter::new(f)); + } + match mode { + "useradd" => useradd_main(&mut sysusers_autof.as_mut().unwrap(), &args)?, + "groupadd" => groupadd_main(&mut sysusers_autof.as_mut().unwrap(), &args)?, + x => bail!("Unknown command: {}", x), + }; + } + if let Some(ref mut autof) = sysusers_autof { + autof.flush()?; + } + Ok(()) +} + +#[derive(Default)] +struct IndexedSysusers { + users: HashMap, + groups: HashMap, + members: collections::HashSet, +} + +impl IndexedSysusers { + fn new() -> Self { + Default::default() + } +} + +#[derive(Default)] +struct IdIndex { + uids: HashMap, + gids: HashMap, +} + +impl IdIndex { + fn new(sysusers: &IndexedSysusers) -> Self { + let mut uids = HashMap::new(); + let mut gids = HashMap::new(); + + for (name, value) in &sysusers.users { + if let IdSpecification::Specified(uid) = value.id { + uids.insert(uid, name.clone()); + gids.insert(uid, name.clone()); + } + } + for (name, value) in &sysusers.groups { + if let IdSpecification::Specified(gid) = value.id { + gids.insert(gid, name.clone()); + } + } + + Self { uids, gids } + } +} + +fn parse_sysusers_indexed(f: I, indexed: &mut IndexedSysusers) -> Fallible<()> { + let mut entries = parse_sysusers_stream(f)?; + for entry in entries.drain(..) { + match entry { + PartialSysuserEntry::User(u) => {indexed.users.insert(u.name.clone(), u.clone()); + indexed.groups.insert(u.name.clone(), u);}, + PartialSysuserEntry::Group(g) => {indexed.groups.insert(g.name.clone(), g);}, + PartialSysuserEntry::GroupMember(raw) => {indexed.members.insert(raw);}, + } + } + Ok(()) +} + +fn analyze_non_root_owned_file

>(p: P, meta: &libc::stat, index: &IdIndex) -> Option { + let p = p.as_ref(); + if meta.st_uid != 0 { + if !index.uids.contains_key(&meta.st_uid) { + return Some(format!("sysusers: No static entry for owner {} of {:?}", meta.st_uid, p)) + } + } + if meta.st_gid != 0 { + if !index.gids.contains_key(&meta.st_gid) { + return Some(format!("sysusers: No static entry for group {} of {:?}", meta.st_gid, p)) + } + } + None +} + +fn find_nonstatic_ownership_recurse

>(dfd: &openat::Dir, p: P, index: &IdIndex, result: &mut Vec) -> Fallible<()> { + let p = p.as_ref(); + for child in dfd.list_dir(p)? { + let child = child?; + let childp = p.join(child.file_name()); + let meta = dfd.metadata(&childp)?; + let stat = meta.stat(); + if stat.st_uid != 0 || stat.st_gid != 0 { + if let Some(err) = analyze_non_root_owned_file(&childp, stat, index) { + result.push(err); + } + } + if meta.simple_type() == openat::SimpleType::Dir { + find_nonstatic_ownership_recurse(dfd, &childp, index, result)?; + } + } + Ok(()) +} + +/// Given a root directory and a sysusers database, find +/// any files owned by a uid that wasn't allocated statically. +fn find_nonstatic_ownership(root: &openat::Dir, users: &IndexedSysusers) -> Fallible<()> { + let index = IdIndex::new(users); + let mut errs = Vec::new(); + find_nonstatic_ownership_recurse(root, ".", &index, &mut errs)?; + if errs.is_empty() { + return Ok(()) + } + for err in errs { + eprintln!("{}", err); + } + bail!("Non-static uid/gid assignments for files found") +} + +/// Iterate over any sysusers files from RPMs (not created by us). +fn index_sysusers(sysusers_dir: &openat::Dir, skip_self: bool) -> Fallible { + let mut other_indexed = IndexedSysusers::new(); + // Load and index our *other* sysusers.d entries + for child in sysusers_dir.list_dir(".")? { + let child = child?; + let name = child.file_name(); + if sysusers_dir.get_file_type(&child)? != openat::SimpleType::File { + continue; + }; + if skip_self && (name == SYSUSERS_AUTO_NAME || name == SYSUSERS_OVERRIDES_NAME) { + continue; + } + let f = sysusers_dir.open_file(name)?; + let mut f = io::BufReader::new(f); + parse_sysusers_indexed(&mut f, &mut other_indexed)?; + } + Ok(other_indexed) +} + +// Replace just the id specification component of a raw sysusers.d string. +// This is a hack since we don't have a proper parser that handles quoting; if +// we did we could round trip. +fn replace_id_specification(partial: &PartialSysuserEntry, new_id: u32) -> String { + match partial { + PartialSysuserEntry::User(u) => { + format!("u {} {} {}", u.name, new_id, u.rest) + }, + PartialSysuserEntry::Group(g) => { + format!("g {} {} {}", g.name, new_id, g.rest) + }, + PartialSysuserEntry::GroupMember(raw) => raw.clone(), + } +} + +type SysusersOverrides = HashMap; + +fn replace_id_with_override(entry: &PartialSysuserEntry, + required_overrides: &mut collections::HashSet<&str>, + overrides: &SysusersOverrides, + out: &mut W) -> Fallible<()> + where W : io::Write +{ + let (name, raw) = + match entry { + PartialSysuserEntry::User(u) => (u.name.as_str(), u.raw.as_str()), + PartialSysuserEntry::Group(g) => (g.name.as_str(), g.raw.as_str()), + _ => unreachable!(), + }; + if required_overrides.remove(name) { + let id = overrides.get(name).unwrap(); + let rewritten = replace_id_specification(entry, *id); + out.write(rewritten.as_bytes())?; + } else { + out.write(raw.as_bytes())?; + } + Ok(()) +} + +fn rewrite_sysuser_file(sysusers_dir: &openat::Dir, + fname: &str, + required_user_overrides: &mut collections::HashSet<&str>, + required_group_overrides: &mut collections::HashSet<&str>, + user_overrides: &SysusersOverrides, + group_overrides: &SysusersOverrides) -> Fallible<()> { + let inf = sysusers_dir.open_file(fname)?; + let inf = io::BufReader::new(inf); + let tmpf_path = Path::new(fname).with_file_name(format!("{}.tmp", fname)); + let tmpf = sysusers_dir.write_file(&tmpf_path, 0644)?; + let mut f = io::BufWriter::new(tmpf); + + for line in inf.lines() { + let line = line?; + if line.starts_with("#") || line.is_empty() { + f.write(line.as_bytes())?; + } else { + let entry = parse_entry(&line)?; + match &entry { + PartialSysuserEntry::User(_) => { + replace_id_with_override(&entry, required_user_overrides, + user_overrides, + &mut f)?; + }, + PartialSysuserEntry::Group(_) => { + replace_id_with_override(&entry, required_group_overrides, + group_overrides, + &mut f)?; + }, + PartialSysuserEntry::GroupMember(raw) => { + f.write(raw.as_bytes())?; + }, + } + } + f.write(b"\n")?; + } + + f.flush()?; + sysusers_dir.local_rename(&tmpf_path, fname)?; + + Ok(()) +} + +fn rewrite_sysusers_with_overrides(sysusers_dir: &openat::Dir, + user_overrides: &SysusersOverrides, + group_overrides: &SysusersOverrides) -> Fallible<()> { + fn getkeys(overrides: &SysusersOverrides) -> collections::HashSet<&str> { + overrides.keys().map(|v|v.as_str()).collect() + } + let mut required_user_overrides = getkeys(user_overrides); + let mut required_group_overrides = getkeys(group_overrides); + + for child in sysusers_dir.list_dir(".")? { + if let Some(ref name) = child?.file_name().to_str() { + if !name.ends_with(".conf") { + continue; + } + + rewrite_sysuser_file(&sysusers_dir, name, &mut required_user_overrides, + &mut required_group_overrides, + user_overrides, + group_overrides).with_context(|e| + format!("Rewriting {}: {}", name, e) + )?; + } + } + + fn check_overrides(isgroup: bool, + required: collections::HashSet<&str>) -> Fallible<()> { + if !required.is_empty() { + eprintln!("Failed to find sysusers.d entries for {} overrides:", + if isgroup { "group" } else { "user" }); + + for k in required { + eprintln!(" {}", k); + } + bail!("Some sysusers.d entries not found"); + } + Ok(()) + } + check_overrides(false, required_user_overrides)?; + check_overrides(true, required_group_overrides)?; + + Ok(()) +} + +// Loop over the sysusers.d directory, creating two +// indexed versions; one of all the sysusers.d entries +// we didn't create, and one with ours. We want +// to remove our duplicates. +pub fn post_useradd(rootfs: &openat::Dir, tf: &Treefile) -> Fallible<()> { + let sysusers_dirpath = path::Path::new(SYSUSERS_DIR); + let sysusers_dir = rootfs.sub_dir(sysusers_dirpath)?; + + let mut other_indexed = index_sysusers(&sysusers_dir, true)?; + + // Load our auto-generated sysusers.d entries + let mut my_entries = + if let Some(f) = sysusers_dir.open_file_optional(SYSUSERS_AUTO_NAME)? { + let mut f = io::BufReader::new(f); + parse_sysusers_stream(&mut f)? + } else { + let mut f = io::BufReader::new("".as_bytes()); + parse_sysusers_stream(&mut f)? + }; + + // Rewrite our sysusers.d file, dropping any entries + // which duplicate ones defined by the system. + let f = sysusers_dir.write_file(SYSUSERS_AUTO_NAME, 0644)?; + let mut f = io::BufWriter::new(f); + f.write(b"# This file was automatically generated by rpm-ostree\n\ + # from intercepted invocations of /usr/sbin/useradd/groupadd.\n")?; + for entry in my_entries.drain(..) { + match entry { + PartialSysuserEntry::User(v) => { + if !other_indexed.users.contains_key(&v.name) { + f.write(v.raw.as_bytes())?; + f.write(b"\n")?; + } + }, + PartialSysuserEntry::Group(v) => { + if !other_indexed.groups.contains_key(&v.name) { + f.write(v.raw.as_bytes())?; + f.write(b"\n")?; + other_indexed.groups.insert(v.name.clone(), v); + } + }, + PartialSysuserEntry::GroupMember(g) => { + f.write(g.as_bytes())?; + f.write(b"\n")?; + other_indexed.members.insert(g); + }, + }; + } + f.flush()?; + + let empty_overrides = HashMap::new(); + let (user_overrides, group_overrides) = + if let Some(ref experimental) = tf.parsed.experimental { + (experimental.sysusers_users.as_ref().unwrap_or(&empty_overrides), + experimental.sysusers_groups.as_ref().unwrap_or(&empty_overrides)) + } else { + (&empty_overrides, &empty_overrides) + }; + if !user_overrides.is_empty() || !group_overrides.is_empty() { + rewrite_sysusers_with_overrides(&sysusers_dir, user_overrides, group_overrides)?; + } + Ok(()) +} + +pub fn replace_file_contents_at>(dfd: &openat::Dir, path: P, contents: &[u8]) -> Fallible<()> { + let path = path.as_ref(); + dfd.remove_file(path)?; + let mut f = dfd.new_file(path, 0644)?; + f.write(contents)?; + f.flush()?; + Ok(()) +} + +// Clear out /etc/passwd and /etc/group now; they should +// always be populated by the sysusers entries. +pub fn final_postprocess(rootfs: &openat::Dir) -> Fallible<()> { + let sysusers_dirpath = path::Path::new(SYSUSERS_DIR); + let sysusers_dir = rootfs.sub_dir(sysusers_dirpath)?; + let indexed = index_sysusers(&sysusers_dir, false)?; + + find_nonstatic_ownership(&rootfs, &indexed)?; + + replace_file_contents_at(rootfs, "usr/etc/passwd", b"root:x:0:0:root:/var/roothome:/bin/bash\n")?; + replace_file_contents_at(rootfs, "usr/etc/shadow", b"root:!locked::0:99999:7:::\n")?; + replace_file_contents_at(rootfs, "usr/etc/group", b"root:x:0\n")?; + replace_file_contents_at(rootfs, "usr/etc/gshadow", b"root:::\n")?; + Ok(()) +} + +#[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 make_buf = || io::BufReader::new(SYSUSERS1.as_bytes()); + let lines : Vec = make_buf().lines().filter_map(|line| { + let line = line.unwrap(); + if !line.is_empty() { + Some(line.to_string()) + } else { + None + } + }).collect(); + let buf = make_buf(); + let r = parse_sysusers_stream(buf).unwrap(); + assert_eq!( + r[0], + PartialSysuserEntry::User(PartialEntryValue { + raw: lines[0].clone(), + name: "httpd".to_string(), + id: IdSpecification::Specified(404), + rest: r#""HTTP User""#.to_string(), + }) + ); + assert_eq!( + r[1], + PartialSysuserEntry::User(PartialEntryValue { + raw: lines[1].clone(), + name: "authd".to_string(), + id: IdSpecification::Path("/usr/bin/authd".to_string()), + rest: r#""Authorization user""#.to_string(), + }) + ); + assert_eq!( + r[2], + PartialSysuserEntry::User(PartialEntryValue { + raw: lines[2].clone(), + name: "postgres".to_string(), + id: IdSpecification::Unspecified, + rest: r#""Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb"#.to_string(), + }) + ); + assert_eq!( + r[3], + PartialSysuserEntry::Group(PartialEntryValue { + raw: lines[3].clone(), + name: "input".to_string(), + id: IdSpecification::Unspecified, + rest: "-".to_string(), + }) + ); + assert_eq!( + r[4], + PartialSysuserEntry::GroupMember(lines[4].clone()) + ); + assert_eq!( + r[5], + PartialSysuserEntry::User(PartialEntryValue { + raw: lines[5].clone(), + name: "root".to_string(), + id: IdSpecification::Specified(0), + rest: r#""Superuser" /root /bin/zsh"#.to_string(), + }) + ); + } + + #[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##"u 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##"u 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), + }, + ); + } + + + #[test] + fn test_replace_id_specification() -> Fallible<()> { + let entry = parse_entry(r##"u wesnothd - "Wesnoth server" /var/run/wesnothd /sbin/nologin"##)?; + let replaced = replace_id_specification(&entry, 42); + match parse_entry(&replaced)? { + PartialSysuserEntry::User(PartialEntryValue { name, id, .. }) => { + assert_eq!(name, "wesnothd"); + assert_eq!(id, IdSpecification::Specified(42)); + }, + _ => unreachable!(), + }; + Ok(()) + } +} + +mod ffi { + use super::*; + use ffiutil::*; + use libc; + 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) + } + + #[no_mangle] + pub extern "C" fn ror_sysusers_post_useradd(rootfs_dfd: libc::c_int, + tf: *mut Treefile, + gerror: *mut *mut glib_sys::GError) -> libc::c_int { + let rootfs_dfd = ffi_view_openat_dir(rootfs_dfd); + assert!(!tf.is_null()); + let tf = unsafe { &mut *tf }; + int_glib_error(post_useradd(&rootfs_dfd, tf).with_context(|e| format!("sysusers post-pre: {}", e)), gerror) + } + + #[no_mangle] + pub extern "C" fn ror_sysusers_final_postprocess(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(final_postprocess(&rootfs_dfd).with_context(|e| format!("sysusers final post: {}", e)), gerror) + } +} +pub use self::ffi::*; diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index 4da73e96b4..284d2aae6e 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -48,7 +48,7 @@ pub struct Treefile { _workdir: openat::Dir, primary_dfd: openat::Dir, #[allow(dead_code)] // Not used in tests - parsed: TreeComposeConfig, + pub(crate) parsed: TreeComposeConfig, // This is a copy of rojig.name to avoid needing to convert to CStr when reading rojig_name: Option, rojig_spec: Option, @@ -296,6 +296,19 @@ fn merge_vec_field(dest: &mut Option>, src: &mut Option>) { } } +/// Merge a vector field by appending. This semantic was originally designed for +/// the `packages` key. +fn merge_hashmap_field(dest: &mut Option>, src: &mut Option>) + where K: std::cmp::Eq + std::hash::Hash +{ + if let Some(mut srcm) = src.take() { + if let Some(ref mut destm) = dest { + srcm.extend(destm.drain()); + } + *dest = Some(srcm); + } +} + /// Given two configs, merge them. fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) { macro_rules! merge_basics { @@ -343,6 +356,18 @@ fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) { remove_files, remove_from_packages ); + + if let Some(mut src_experimental) = src.experimental.take() { + if let Some(ref mut dest_experimental) = dest.experimental { + merge_basic_field(&mut dest_experimental.sysusers, &mut src_experimental.sysusers); + merge_hashmap_field(&mut dest_experimental.sysusers_users, + &mut src_experimental.sysusers_users); + merge_hashmap_field(&mut dest_experimental.sysusers_groups, + &mut src_experimental.sysusers_groups); + } else { + dest.experimental = Some(src_experimental) + } + } } /// Merge the treefile externals. There are currently only two keys that @@ -407,12 +432,13 @@ fn add_files_path_is_valid(path: &str) -> bool { impl Treefile { /// The main treefile creation entrypoint. - fn new_boxed( + pub fn new_boxed( filename: &Path, basearch: Option<&str>, workdir: openat::Dir, ) -> Fallible> { - let parsed = treefile_parse_recurse(filename, basearch, 0)?; + let mut parsed = treefile_parse_recurse(filename, basearch, 0)?; + parsed.config.canonicalize(); Treefile::validate_config(&parsed.config)?; let dfd = openat::Dir::open(filename.parent().unwrap())?; let (rojig_name, rojig_spec) = if let Some(rojig) = parsed.config.rojig.as_ref() { @@ -532,7 +558,7 @@ impl Default for BootLocation { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] enum CheckPasswdType { #[serde(rename = "none")] None, @@ -544,7 +570,7 @@ enum CheckPasswdType { Data, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] struct CheckPasswd { #[serde(rename = "type")] variant: CheckPasswdType, @@ -554,6 +580,15 @@ struct CheckPasswd { // entries: OptionString>, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +/// Defines a static mapping between a username or a group name and a numeric +/// value to use in /etc/passwd or /etc/group (indirectly via systemd-sysusers). +pub(crate) struct SysusersOverride { + name: String, + id: u32 +} + #[derive(Serialize, Deserialize, Debug)] struct Rojig { name: String, @@ -566,7 +601,7 @@ struct Rojig { // Option. The defaults live in the code (e.g. machineid-compat defaults // to `true`). #[derive(Serialize, Deserialize, Debug)] -struct TreeComposeConfig { +pub(crate) struct TreeComposeConfig { // Compose controls #[serde(rename = "ref")] #[serde(skip_serializing_if = "Option::is_none")] @@ -677,6 +712,10 @@ struct TreeComposeConfig { #[serde(flatten)] legacy_fields: LegacyTreeComposeConfigFields, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "experimental")] + pub(crate) experimental: Option, + #[serde(flatten)] extra: HashMap, } @@ -693,6 +732,18 @@ struct LegacyTreeComposeConfigFields { automatic_version_prefix: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct ExperimentalTreeComposeConfigFields { + #[serde(skip_serializing_if = "Option::is_none")] + sysusers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "sysusers-users")] + pub(crate) sysusers_users: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "sysusers-groups")] + pub(crate) sysusers_groups: Option>, +} + impl TreeComposeConfig { /// Look for use of legacy/renamed fields and migrate them to the new field. fn migrate_legacy_fields(mut self) -> Fallible { @@ -716,6 +767,20 @@ impl TreeComposeConfig { Ok(self) } + + /// Some options override others; perform those overrides. + fn canonicalize(&mut self) { + // If sysusers is enabled, disable all the other passwd/group related bits. + if let Some(ref experimental) = self.experimental { + if let Some(true) = experimental.sysusers { + self.preserve_passwd = Some(false); + self.check_passwd = Some(CheckPasswd { variant: CheckPasswdType:: None, filename: None }); + self.check_groups = self.check_passwd.clone(); + self.ignore_removed_users = None; + self.ignore_removed_groups = None; + } + } + } } #[cfg(test)] @@ -780,6 +845,27 @@ remove-files: assert!(treefile.remove_files.unwrap().len() == 2); } + #[test] + fn basic_valid_sysusers() { + let mut buf = VALID_PRELUDE.to_string(); + buf.push_str( + r###" +sysusers: true +sysusers-users: + foo: 42 + bar: 43 +"###, + ); + let buf = buf.as_bytes(); + let mut input = io::BufReader::new(buf); + let treefile = + treefile_parse_stream(InputFormat::YAML, &mut input, Some(ARCH_X86_64)).unwrap(); + let overrides = treefile.sysusers_users.unwrap(); + assert_eq!(overrides.len(), 2); + assert_eq!(*overrides.get("foo").unwrap(), 42); + assert_eq!(*overrides.get("bar").unwrap(), 43); + } + #[test] fn basic_js_valid() { let mut input = io::BufReader::new(VALID_PRELUDE_JS.as_bytes()); @@ -1086,6 +1172,17 @@ mod ffi { } } + #[no_mangle] + pub extern "C" fn ror_treefile_get_sysusers(tf: *mut Treefile) -> bool { + assert!(!tf.is_null()); + let tf = unsafe { &mut *tf }; + if let Some(ref experimental) = tf.parsed.experimental { + experimental.sysusers.unwrap_or(false) + } else { + false + } + } + #[no_mangle] pub extern "C" fn ror_treefile_get_rojig_name(tf: *mut Treefile) -> *const libc::c_char { assert!(!tf.is_null()); diff --git a/src/app/rpmostree-compose-builtin-rojig.c b/src/app/rpmostree-compose-builtin-rojig.c index e26a464460..e62f3e3d10 100644 --- a/src/app/rpmostree-compose-builtin-rojig.c +++ b/src/app/rpmostree-compose-builtin-rojig.c @@ -520,7 +520,7 @@ impl_write_rojig (RpmOstreeRojigCompose *self, return FALSE; if (!rpmostree_rootfs_postprocess_common (self->rootfs_dfd, cancellable, error)) return FALSE; - if (!rpmostree_postprocess_final (self->rootfs_dfd, self->treefile, TRUE, + if (!rpmostree_postprocess_final (self->rootfs_dfd, self->treefile_rs, self->treefile, TRUE, cancellable, error)) return FALSE; diff --git a/src/app/rpmostree-compose-builtin-tree.c b/src/app/rpmostree-compose-builtin-tree.c index 731139c210..703a9321df 100644 --- a/src/app/rpmostree-compose-builtin-tree.c +++ b/src/app/rpmostree-compose-builtin-tree.c @@ -966,7 +966,8 @@ impl_commit_tree (RpmOstreeTreeComposeContext *self, return FALSE; if (!rpmostree_rootfs_postprocess_common (self->rootfs_dfd, cancellable, error)) return FALSE; - if (!rpmostree_postprocess_final (self->rootfs_dfd, self->treefile, self->unified_core_and_fuse, + if (!rpmostree_postprocess_final (self->rootfs_dfd, self->treefile_rs, + self->treefile, self->unified_core_and_fuse, cancellable, error)) return FALSE; @@ -1184,7 +1185,7 @@ rpmostree_compose_builtin_postprocess (int argc, return FALSE; if (!rpmostree_rootfs_postprocess_common (rootfs_dfd, cancellable, error)) return FALSE; - if (!rpmostree_postprocess_final (rootfs_dfd, treefile, opt_unified_core, + if (!rpmostree_postprocess_final (rootfs_dfd, treefile_rs, treefile, opt_unified_core, cancellable, error)) return FALSE; return TRUE; diff --git a/src/app/rpmostree-composeutil.c b/src/app/rpmostree-composeutil.c index 2f9426585c..3d776299bd 100644 --- a/src/app/rpmostree-composeutil.c +++ b/src/app/rpmostree-composeutil.c @@ -282,6 +282,8 @@ rpmostree_composeutil_get_treespec (RpmOstreeContext *ctx, * to the final one right before commit as usual. */ g_key_file_set_boolean (treespec, "tree", "selinux", FALSE); } + if (ror_treefile_get_sysusers (treefile_rs)) + g_key_file_set_boolean (treespec, "tree", "sysusers", TRUE); const char *input_ref = NULL; if (!_rpmostree_jsonutil_object_get_optional_string_member (treedata, "ref", &input_ref, error)) diff --git a/src/libpriv/gresources.xml b/src/libpriv/gresources.xml index 371a24cea6..42e6f76a0d 100644 --- a/src/libpriv/gresources.xml +++ b/src/libpriv/gresources.xml @@ -2,5 +2,8 @@ systemctl-wrapper.sh + useradd-wrapper.sh + groupadd-wrapper.sh + systemd-sysusers-wrapper.sh diff --git a/src/libpriv/groupadd-wrapper.sh b/src/libpriv/groupadd-wrapper.sh new file mode 100755 index 0000000000..c58ddc1f90 --- /dev/null +++ b/src/libpriv/groupadd-wrapper.sh @@ -0,0 +1,11 @@ +#!/usr/bin/bash +# Used by rpmostree-core.c to intercept `groupadd` operations so +# we can convert to systemd-sysusers. +set -euo pipefail +(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-core.c b/src/libpriv/rpmostree-core.c index df5d551c2a..4c17552d78 100644 --- a/src/libpriv/rpmostree-core.c +++ b/src/libpriv/rpmostree-core.c @@ -290,6 +290,7 @@ rpmostree_treespec_new_from_keyfile (GKeyFile *keyfile, tf_bind_boolean (keyfile, &builder, "documentation", TRUE); tf_bind_boolean (keyfile, &builder, "recommends", TRUE); tf_bind_boolean (keyfile, &builder, "selinux", TRUE); + tf_bind_boolean (keyfile, &builder, "sysusers", FALSE); ret->spec = g_variant_builder_end (&builder); ret->dict = g_variant_dict_new (ret->spec); @@ -3690,6 +3691,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, @@ -3961,12 +4027,19 @@ rpmostree_context_assemble (RpmOstreeContext *self, return glnx_throw_errno_prefix (error, "symlinkat"); } + gboolean sysusers; + g_assert (g_variant_dict_lookup (self->spec->dict, "sysusers", "b", &sysusers)); + if (sysusers) + { + if (!glnx_shutil_mkdir_p_at (tmprootfs_dfd, "usr/lib/sysusers.d", 0755, cancellable, error)) + return FALSE; + } + /* NB: we're not running scripts right now for removals, so this is only for overlays and * replacements */ 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; @@ -3985,7 +4058,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 @@ -3994,28 +4067,17 @@ 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 + 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; + if (sysusers) { - 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) + /* 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; - 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)) + if (!replace_with_wrapper (tmprootfs_dfd, "usr/bin/systemd-sysusers", replaced_binaries, error)) return FALSE; } @@ -4055,6 +4117,14 @@ rpmostree_context_assemble (RpmOstreeContext *self, rpmostree_output_progress_end_msg (&task, "%u done", n_pre_scripts_run); } + if (sysusers) + { + if (!ror_sysusers_post_useradd (tmprootfs_dfd, self->treefile_rs, error)) + return FALSE; + if (!rpmostree_postprocess_run_sysusers (tmprootfs_dfd, TRUE, cancellable, error)) + return FALSE; + } + if (faccessat (tmprootfs_dfd, "etc/passwd", F_OK, 0) == 0) { g_autofree char *contents = @@ -4145,12 +4215,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-passwd-util.c b/src/libpriv/rpmostree-passwd-util.c index 1424bd732f..0b9cbefe6b 100644 --- a/src/libpriv/rpmostree-passwd-util.c +++ b/src/libpriv/rpmostree-passwd-util.c @@ -680,6 +680,7 @@ rpmostree_check_passwd_groups (gboolean passwd, /* See "man 5 passwd" We just make sure the name and uid/gid match, and that none are missing. don't care about GECOS/dir/shell. + If sysusers is enabled, this delegates to that. */ gboolean rpmostree_check_passwd (OstreeRepo *repo, @@ -690,9 +691,16 @@ rpmostree_check_passwd (OstreeRepo *repo, GCancellable *cancellable, GError **error) { - return rpmostree_check_passwd_groups (TRUE, repo, rootfs_fd, treefile_rs, - treedata, previous_commit, - cancellable, error); + if (ror_treefile_get_sysusers (treefile_rs)) + { + return ror_sysusers_final_postprocess (rootfs_fd, error); + } + else + { + return rpmostree_check_passwd_groups (TRUE, repo, rootfs_fd, treefile_rs, + treedata, previous_commit, + cancellable, error); + } } /* See "man 5 group" We just need to make sure the name and gid match, @@ -1077,6 +1085,7 @@ rpmostree_passwd_compose_prep (int rootfs_dfd, GCancellable *cancellable, GError **error) { + GLNX_AUTO_PREFIX_ERROR ("Preparing passwd/group", error); gboolean generate_from_previous = TRUE; if (!_rpmostree_jsonutil_object_get_optional_boolean_member (treedata, "preserve-passwd", &generate_from_previous, error)) @@ -1343,13 +1352,22 @@ rpmostree_passwddb_open (int rootfs, GCancellable *cancellable, GError **error) if (!add_passwd_to_hash (rootfs, "usr/etc/passwd", ret->users, error)) return NULL; - if (!add_passwd_to_hash (rootfs, "usr/lib/passwd", ret->users, error)) - return NULL; + if (!glnx_fstatat_allow_noent (rootfs, "usr/lib/passwd", NULL, 0, error)) + return FALSE; + const gboolean have_usrlib_passwd = (errno == 0); + if (have_usrlib_passwd) + { + if (!add_passwd_to_hash (rootfs, "usr/lib/passwd", ret->users, error)) + return NULL; + } if (!add_groups_to_hash (rootfs, "usr/etc/group", ret->groups, error)) return NULL; - if (!add_groups_to_hash (rootfs, "usr/lib/group", ret->groups, error)) - return NULL; + if (have_usrlib_passwd) + { + if (!add_groups_to_hash (rootfs, "usr/lib/group", ret->groups, error)) + return NULL; + } return g_steal_pointer (&ret); } diff --git a/src/libpriv/rpmostree-postprocess.c b/src/libpriv/rpmostree-postprocess.c index 2d018b4f5d..78f0b61089 100644 --- a/src/libpriv/rpmostree-postprocess.c +++ b/src/libpriv/rpmostree-postprocess.c @@ -66,13 +66,19 @@ run_bwrap_mutably (int rootfs_fd, GError **error) { /* For scripts, it's /etc, not /usr/etc */ - if (!glnx_renameat (rootfs_fd, "usr/etc", rootfs_fd, "etc", error)) + if (!glnx_fstatat_allow_noent (rootfs_fd, "etc", NULL, 0, error)) return FALSE; - /* But leave a compat symlink, as we used to bind mount, so scripts - * could still use that too. - */ - if (symlinkat ("../etc", rootfs_fd, "usr/etc") < 0) - return glnx_throw_errno_prefix (error, "symlinkat"); + gboolean renamed_usretc = (errno == ENOENT); + if (renamed_usretc) + { + if (!glnx_renameat (rootfs_fd, "usr/etc", rootfs_fd, "etc", error)) + return FALSE; + /* But leave a compat symlink, as we used to bind mount, so scripts + * could still use that too. + */ + if (symlinkat ("../etc", rootfs_fd, "usr/etc") < 0) + return glnx_throw_errno_prefix (error, "symlinkat"); + } RpmOstreeBwrapMutability mut = unified_core_mode ? RPMOSTREE_BWRAP_MUTATE_ROFILES : RPMOSTREE_BWRAP_MUTATE_FREELY; @@ -102,10 +108,13 @@ run_bwrap_mutably (int rootfs_fd, return FALSE; /* Remove the symlink and swap back */ - if (!glnx_unlinkat (rootfs_fd, "usr/etc", 0, error)) - return FALSE; - if (!glnx_renameat (rootfs_fd, "etc", rootfs_fd, "usr/etc", error)) - return FALSE; + if (renamed_usretc) + { + if (!glnx_unlinkat (rootfs_fd, "usr/etc", 0, error)) + return FALSE; + if (!glnx_renameat (rootfs_fd, "etc", rootfs_fd, "usr/etc", error)) + return FALSE; + } return TRUE; } @@ -266,6 +275,18 @@ rpmostree_postprocess_run_depmod (int rootfs_dfd, return TRUE; } +gboolean +rpmostree_postprocess_run_sysusers (int rootfs_dfd, + gboolean unified_core_mode, + GCancellable *cancellable, + GError **error) +{ + char *child_argv[] = { "systemd-sysusers", NULL }; + if (!run_bwrap_mutably (rootfs_dfd, "systemd-sysusers.rpmostreesave", child_argv, unified_core_mode, cancellable, error)) + return FALSE; + return TRUE; +} + /* Handle the kernel/initramfs, which can be in at least 2 different places: * - /boot (CentOS, Fedora treecompose before we suppressed kernel.spec's %posttrans) * - /usr/lib/modules (Fedora treecompose without kernel.spec's %posttrans) @@ -958,6 +979,7 @@ postprocess_selinux_policy_store_location (int rootfs_dfd, */ gboolean rpmostree_postprocess_final (int rootfs_dfd, + RORTreefile *treefile_rs, JsonObject *treefile, gboolean unified_core_mode, GCancellable *cancellable, @@ -1004,27 +1026,32 @@ rpmostree_postprocess_final (int rootfs_dfd, error)) return FALSE; - g_print ("Migrating /usr/etc/passwd to /usr/lib/\n"); - if (!rpmostree_passwd_migrate_except_root (rootfs_dfd, RPM_OSTREE_PASSWD_MIGRATE_PASSWD, NULL, - cancellable, error)) - return FALSE; + const gboolean sysusers = ror_treefile_get_sysusers (treefile_rs); - g_autoptr(GHashTable) preserve_groups_set = NULL; - if (treefile && json_object_has_member (treefile, "etc-group-members")) + if (!sysusers) { - JsonArray *etc_group_members = json_object_get_array_member (treefile, "etc-group-members"); - preserve_groups_set = _rpmostree_jsonutil_jsarray_strings_to_set (etc_group_members); - } + g_print ("Migrating /usr/etc/passwd to /usr/lib/\n"); + if (!rpmostree_passwd_migrate_except_root (rootfs_dfd, RPM_OSTREE_PASSWD_MIGRATE_PASSWD, NULL, + cancellable, error)) + return FALSE; - g_print ("Migrating /usr/etc/group to /usr/lib/\n"); - if (!rpmostree_passwd_migrate_except_root (rootfs_dfd, RPM_OSTREE_PASSWD_MIGRATE_GROUP, - preserve_groups_set, - cancellable, error)) - return FALSE; + g_autoptr(GHashTable) preserve_groups_set = NULL; + if (treefile && json_object_has_member (treefile, "etc-group-members")) + { + JsonArray *etc_group_members = json_object_get_array_member (treefile, "etc-group-members"); + preserve_groups_set = _rpmostree_jsonutil_jsarray_strings_to_set (etc_group_members); + } - /* NSS configuration to look at the new files */ - if (!replace_nsswitch (rootfs_dfd, cancellable, error)) - return glnx_prefix_error (error, "nsswitch replacement"); + g_print ("Migrating /usr/etc/group to /usr/lib/\n"); + if (!rpmostree_passwd_migrate_except_root (rootfs_dfd, RPM_OSTREE_PASSWD_MIGRATE_GROUP, + preserve_groups_set, + cancellable, error)) + return FALSE; + + /* NSS configuration to look at the new files */ + if (!replace_nsswitch (rootfs_dfd, cancellable, error)) + return glnx_prefix_error (error, "nsswitch replacement"); + } if (selinux) { diff --git a/src/libpriv/rpmostree-postprocess.h b/src/libpriv/rpmostree-postprocess.h index f01d2a0eaa..b95013c345 100644 --- a/src/libpriv/rpmostree-postprocess.h +++ b/src/libpriv/rpmostree-postprocess.h @@ -66,6 +66,12 @@ rpmostree_postprocess_run_depmod (int rootfs_fd, GCancellable *cancellable, GError **error); +gboolean +rpmostree_postprocess_run_sysusers (int rootfs_fd, + gboolean unified_core_mode, + GCancellable *cancellable, + GError **error); + gboolean rpmostree_prepare_rootfs_get_sepolicy (int dfd, OstreeSePolicy **out_sepolicy, @@ -86,6 +92,7 @@ rpmostree_prepare_rootfs_for_commit (int src_rootfs_dfd, gboolean rpmostree_postprocess_final (int rootfs_dfd, + RORTreefile *treefile_rs, JsonObject *treefile, gboolean unified_core_mode, GCancellable *cancellable, diff --git a/src/libpriv/rpmostree-scripts.c b/src/libpriv/rpmostree-scripts.c index 0fc1618049..8c4099ed37 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,10 +339,12 @@ 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; gboolean created_run_ostree_booted = 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 /. @@ -423,7 +431,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 @@ -455,6 +464,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); @@ -493,6 +512,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/systemd-sysusers-wrapper.sh b/src/libpriv/systemd-sysusers-wrapper.sh new file mode 100755 index 0000000000..dc0557c2a7 --- /dev/null +++ b/src/libpriv/systemd-sysusers-wrapper.sh @@ -0,0 +1,6 @@ +#!/usr/bin/bash +# Used by rpmostree-core.c to intercept `systemd-sysusers` invocations +# that are done inline; we need the invocations to see our fixed uid/gid +# mappings. So we will ensure the ones from disk are used instead. + +# (nothing) diff --git a/src/libpriv/useradd-wrapper.sh b/src/libpriv/useradd-wrapper.sh new file mode 100755 index 0000000000..7e8066da80 --- /dev/null +++ b/src/libpriv/useradd-wrapper.sh @@ -0,0 +1,9 @@ +#!/usr/bin/bash +# Used by rpmostree-core.c to intercept `useradd` operations so +# we can convert to systemd-sysusers. +set -euo pipefail +(echo useradd + for x in "$@"; do + echo $x + done) > /proc/self/fd/$RPMOSTREE_USERADD_FD +# exec /usr/sbin/useradd.rpmostreesave "$@" diff --git a/tests/compose-tests/test-sysusers.sh b/tests/compose-tests/test-sysusers.sh new file mode 100755 index 0000000000..3ae810e484 --- /dev/null +++ b/tests/compose-tests/test-sysusers.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -xeuo pipefail + +dn=$(cd $(dirname $0) && pwd) +. ${dn}/libcomposetest.sh + +prepare_compose_test "sysusers" +cat >sysusers.yaml </dev/null; then + fatal "Found /usr/lib/passwd" +fi + +ostreels /usr/lib/sysusers.d/rpmostree-auto.conf + +echo "ok sysusers"