userfixer/wayblue-fix-89.sh
2024-12-01 23:30:03 -06:00

117 lines
4.7 KiB
Bash
Executable File

#! /bin/bash
#
# This script attempts to fix the following issue:
# https://github.com/wayblueorg/wayblue/issues/89
# More specifically, it does the following:
# * Iterates /etc/shadow and /etc/gshadow; and
# * For every entry that cannot be getent'd, delete it
#
# This script should be invoked before systemd-sysusers on system boot
#
# The reason for this is as follows:
# At time of writing, using rpm-ostree to build OCI container images fails to
# update /usr/lib/passwd and /usr/lib/group, instead dropping items in
# /usr/lib/sysusers.d for systemd-sysusers to process at boot time. This would
# fine under normal circumstances.
#
# HOWEVER. If you are coming from a distro that had entries in those /usr/lib
# files for that users/group, you will have entries in /etc/{,g}shadow for said
# users/groups.
#
# If an entry is present in /etc/shadow or /etc/gshadow that matches an object
# that systemd-sysusers is trying to add, it will fail and then abort further
# object processing. Thus, we remove objects that cannot be looked up, assuming
# that the cause is this disparity and that it will be smoothed out when
# systemd-sysusers next runs
#
set -euo pipefail
# Iterate over each file we're interested in
for file in /etc/shadow /etc/gshadow; do
# Prelim check for zero-byte files (shouldn't proc)
if ! [ -s "$file" ]; then
echo "File is missing or empty: $file"
continue
fi
# Prelim check to ensure we can read the file
if ! [ -r "$file" ]; then
echo "Unable to read file: $file"
continue
fi
# Prelim checks succeeded, move forward
echo "Parsing $file for junk"
# Read each line in the file to iterate over it
while read -r line; do
# This should never happen, but if for some reason we get an empty line,
# continue
[ -z "$line" ] && continue
# Per shadow(5), we are guaranteed that all characters leading up to the
# first colon are the user's/group's name. To that end, we'll do a bash
# string substitution to extract that first column.
name="${line%%:*}"
# Swith case here for what ents we care about. I don't think the array is
# strictly required but it affords us flexibility for zero cost so whatever.
ents=()
case "$file" in
*/shadow)
ents=("passwd")
;;
*/gshadow)
ents=("group")
;;
*)
echo "Unknown file to parse for junk: $file"
exit 101
;;
esac
# Now, we use getent to find a match for the shadow entry. It's at this point
# that one might ask why we're doing this instead of using grpck or something
# The answer is that those aren't nss-aware, whereas getent is. /etc/shadow
# and /etc/gshadow can have entries for things like passwords and group
# membership for groups that live in /usr/lib/passwd and /usr/lib/group,
# but grpck isn't aware of those and will obliterate their data from the
# shadow files if we're not careful.
#
# Thus, we use getent to parse through nsswitch.conf intelligently and tell
# us whether this object we're operating on is found elsewhere in the OS
matched_ent=""
for ent in "${ents[@]}"; do
[ -n "$matched_ent" ] && break
# We have to do || true here because getent exits nonzero if it fails to
# find an entity. We need to store its output so we can analyze the status
# of this command outside the loop -- we can't just "if ! getent"
result="$(getent "$ent" "$name" || true)"
[ -n "$result" ] && matched_ent="$result"
done
if [ -n "$matched_ent" ]; then
# getent found a matching entity and we don't care about this entry
# Just continue down the line
continue
else
# If we're at this point in the code path, we now know that we for-sure are
# operating on an entry that will cause systemd-sysusers to bail out
# on invocation. We are thus going to remove it.
echo "Analyzing broken entity: $name"
# First, we're going to pattern match the username against the systemd
# common core username regex. If this fails to match, we bail. I was unable
# to find a Fedora username that didn't match this but it's best to have
# this type of safety -- you never know what might happen.
# https://systemd.io/USER_NAMES/
if ! [[ $name =~ ^[a-z][a-z0-9-]{0,30}$ ]]; then
echo "Not touching nonconformant name: $name"
continue
fi
# We've succeeded in all our checks and for sure have a username loaded
# that isn't going to cause our regex to explode in terrifying ways.
# We're now going to load sed up and fire it at the shadowfile, making
# a backup along the way
echo "Removing from $file: $name"
sed --in-place=- \
"/^$name:/d" \
"$file"
fi
# Not useless use of cat. We're buffering the file here to avoid conditions
# where we edit the file we're reading and face issues.
done < <(cat "$file")
done