Skip to content

Commit

Permalink
Merge pull request coreos#4913 from cgwalters/shadow-more
Browse files Browse the repository at this point in the history
shadow: Adjust all deployments
  • Loading branch information
cgwalters authored Apr 12, 2024
2 parents 5dd7dc9 + d40ba77 commit b18433b
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 5 deletions.
2 changes: 1 addition & 1 deletion rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ mod normalization;
mod origin;
mod ostree_prepareroot;
pub(crate) use self::origin::*;
mod passwd;
pub mod passwd;
use passwd::*;
mod console_progress;
pub(crate) use self::console_progress::*;
Expand Down
1 change: 1 addition & 0 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async fn inner_async_main(args: Vec<String>) -> Result<i32> {
match *arg {
// Add custom Rust commands here, and also in `libmain.cxx` if user-visible.
"countme" => rpmostree_rust::countme::entrypoint(args).map(|_| 0),
"fix-shadow-perms" => rpmostree_rust::passwd::fix_shadow_perms_entrypoint(args).map(|_| 0),
"cliwrap" => rpmostree_rust::cliwrap::entrypoint(args).map(|_| 0),
// A hidden wrapper to intercept some binaries in RPM scriptlets.
"scriptlet-intercept" => builtins::scriptlet_intercept::entrypoint(args).map(|_| 0),
Expand Down
124 changes: 124 additions & 0 deletions rust/src/passwd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const DEFAULT_MODE: u32 = 0o644;
static DEFAULT_PERMS: Lazy<Permissions> = Lazy::new(|| Permissions::from_mode(DEFAULT_MODE));
static PWGRP_SHADOW_FILES: &[&str] = &["shadow", "gshadow", "subuid", "subgid"];
static USRLIB_PWGRP_FILES: &[&str] = &["passwd", "group"];
// This stamp file signals the original fix which only changed the booted deployment
const SHADOW_MODE_FIXED_STAMP_OLD: &str = "etc/.rpm-ostree-shadow-mode-fixed.stamp";
// And this one is written by the newer logic that changes all deployments
const SHADOW_MODE_FIXED_STAMP: &str = "etc/.rpm-ostree-shadow-mode-fixed2.stamp";

// Lock/backup files that should not be in the base commit (TODO fix).
static PWGRP_LOCK_AND_BACKUP_FILES: &[&str] = &[
Expand Down Expand Up @@ -363,6 +367,86 @@ impl PasswdKind {
}
}

/// Due to a prior bug, the build system had some deployments with a world-readable
/// shadow file. This fixes a given deployment.
#[context("Fixing shadow permissions")]
pub(crate) fn fix_shadow_perms_in_root(root: &Dir) -> Result<bool> {
let zero_perms = Permissions::from_mode(0);
let mut changed = false;
for path in ["etc/shadow", "etc/shadow-", "etc/gshadow", "etc/gshadow-"] {
let metadata = if let Some(meta) = root
.symlink_metadata_optional(path)
.context("Querying metadata")?
{
meta
} else {
tracing::debug!("No path {path}");
continue;
};
let mode = metadata.mode() & !libc::S_IFMT;
// Don't touch the file if it's already correct
if mode == 0 {
continue;
}
let f = root.open(path).with_context(|| format!("Opening {path}"))?;
f.set_permissions(zero_perms.clone())
.with_context(|| format!("chmod: {path}"))?;
println!("Adjusted mode for {path}");
changed = true;
}
// Write our stamp file
root.write(SHADOW_MODE_FIXED_STAMP, "")
.context(SHADOW_MODE_FIXED_STAMP)?;
// And clean up the old one
root.remove_file_optional(SHADOW_MODE_FIXED_STAMP_OLD)
.with_context(|| format!("Removing old {SHADOW_MODE_FIXED_STAMP_OLD}"))?;
Ok(changed)
}

/// Due to a prior bug, the build system had some deployments with a world-readable
/// shadow file. This fixes all deployments.
pub(crate) fn fix_shadow_perms_in_sysroot(sysroot: &ostree::Sysroot) -> Result<bool> {
let deployments = sysroot.deployments();
// TODO add a nicer api for this to ostree-rs
let sysroot_fd =
Dir::reopen_dir(unsafe { &std::os::fd::BorrowedFd::borrow_raw(sysroot.fd()) })?;
let mut changed = false;
for deployment in deployments {
let path = sysroot.deployment_dirpath(&deployment);
let dir = sysroot_fd.open_dir(&path)?;
if fix_shadow_perms_in_root(&dir)
.with_context(|| format!("Deployment index={}", deployment.index()))?
{
println!(
"Adjusted shadow files in deployment index={} {}.{}",
deployment.index(),
deployment.csum(),
deployment.bootserial()
);
changed = true;
}
}
Ok(changed)
}

/// The main entrypoint for updating /etc/{,g}shadow permissions across
/// all deployments.
pub fn fix_shadow_perms_entrypoint(_args: &[&str]) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let sysroot = ostree::Sysroot::new_default();
sysroot.set_mount_namespace_in_use();
sysroot.lock()?;
sysroot.load(cancellable)?;
let changed = fix_shadow_perms_in_sysroot(&sysroot)?;
if changed {
// We already printed per deployment, so this one is just
// a debug-level log.
tracing::debug!("Updated shadow/gshadow permissions");
}
sysroot.unlock();
Ok(())
}

// This function writes the static passwd/group data from the treefile to the
// target root filesystem.
fn write_data_from_treefile(
Expand Down Expand Up @@ -1070,3 +1154,43 @@ impl PasswdEntries {
Ok(())
}
}

#[test]
fn test_shadow_perms() -> Result<()> {
let root = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir("etc")?;
root.write("etc/shadow", "some shadow")?;
root.write("etc/gshadow", "some gshadow")?;
root.set_permissions("etc/gshadow", Permissions::from_mode(0))?;

assert!(fix_shadow_perms_in_root(root)?);
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(root.try_exists(SHADOW_MODE_FIXED_STAMP)?);
// Verify idempotence
assert!(!fix_shadow_perms_in_root(root)?);
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(root.try_exists(SHADOW_MODE_FIXED_STAMP)?);

Ok(())
}

#[test]
/// Verify the scenario of updating from a previously fixed root
fn test_shadow_perms_from_orig_fix() -> Result<()> {
let root = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir("etc")?;
root.write("etc/shadow", "some shadow")?;
root.set_permissions("etc/shadow", Permissions::from_mode(0))?;
root.write("etc/gshadow", "some gshadow")?;
root.set_permissions("etc/gshadow", Permissions::from_mode(0))?;
// Write the original stamp file
root.write(SHADOW_MODE_FIXED_STAMP_OLD, "")?;

// No changes
assert!(!fix_shadow_perms_in_root(root)?);
// Except we should have updated to the new stamp file
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(root.try_exists(SHADOW_MODE_FIXED_STAMP)?);

Ok(())
}
12 changes: 8 additions & 4 deletions src/daemon/rpm-ostree-fix-shadow-mode.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
# This makes sure to fix permissions on systems that were deployed with the wrong permissions.
Description=Update permissions for /etc/shadow
Documentation=https://github.com/coreos/rpm-ostree-ghsa-2m76-cwhg-7wv6
ConditionPathExists=!/etc/.rpm-ostree-shadow-mode-fixed.stamp
# This new stamp file is written by the Rust code, and obsoletes
# the old /etc/.rpm-ostree-shadow-mode-fixed.stamp
ConditionPathExists=!/etc/.rpm-ostree-shadow-mode-fixed2.stamp
ConditionPathExists=/run/ostree-booted
# Because we read the sysroot
RequiresMountsFor=/boot
# Make sure this is started before any unprivileged (interactive) user has access to the system.
Before=systemd-user-sessions.service

[Service]
Type=oneshot
ExecStart=chmod --verbose 0000 /etc/shadow /etc/gshadow
ExecStart=-chmod --verbose 0000 /etc/shadow- /etc/gshadow-
ExecStart=touch /etc/.rpm-ostree-shadow-mode-fixed.stamp
ExecStart=rpm-ostree fix-shadow-perms
RemainAfterExit=yes
# So we can remount /sysroot writable in our own namespace
MountFlags=slave

[Install]
WantedBy=multi-user.target
80 changes: 80 additions & 0 deletions tests/kolainst/destructive/shadow
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
#
# Copyright (C) 2024 Red Hat Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

set -euo pipefail

. ${KOLA_EXT_DATA}/libtest.sh

set -x

cd $(mktemp -d)

service=rpm-ostree-fix-shadow-mode.service
stamp=/etc/.rpm-ostree-shadow-mode-fixed2.stamp

case "${AUTOPKGTEST_REBOOT_MARK:-}" in
"")

libtest_prepare_fully_offline
libtest_enable_repover 0

systemctl status ${service} || true
rm -vf /etc/.rpm-ostree-shadow-mode*
chmod 0644 /etc/gshadow

# Verify running the service once fixes things
systemctl restart $service
assert_has_file "${stamp}"
assert_streq "$(stat -c '%f' /etc/gshadow)" 8000

# Now *undo* the fix, so that the current (then old) deployment
# is broken still, and ensure after reboot that it's fixed
# in both.

chmod 0644 /etc/gshadow
rm -vf /etc/.rpm-ostree*

booted_commit=$(rpm-ostree status --json | jq -r '.deployments[0].checksum')
ostree refs ${booted_commit} --create vmcheck2
rpm-ostree rebase :vmcheck2

/tmp/autopkgtest-reboot "1"
;;
"1")

systemctl status $service
assert_has_file "${stamp}"

verified=0
for f in $(ls /ostree/deploy/*/deploy/*/etc/{,g}shadow{,-}); do
verified=$(($verified + 1))
assert_streq "$(stat -c '%f' $f)" 8000
echo "ok ${f}"
done
assert_streq "$verified" 8

journalctl -b -u $service --grep="Adjusted shadow files in deployment" | tee out.txt
assert_streq "$(wc -l < out.txt)" 2

echo "ok shadow"

;;
*) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;;

esac

0 comments on commit b18433b

Please sign in to comment.