Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(RHEL-17394) fstab-generator: Chase symlinks where possible #150

Merged
merged 7 commits into from
Feb 7, 2024
8 changes: 8 additions & 0 deletions man/systemd-fstab-generator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
for more information about special <filename>/etc/fstab</filename>
mount options this generator understands.</para>

<para>One special topic is handling of symbolic links. Historical init
implementations supported symlinks in <filename>/etc/fstab</filename>.
Because mount units will refuse mounts where the target is a symbolic link,
this generator will resolve any symlinks as far as possible when processing
<filename>/etc/fstab</filename> in order to enhance backwards compatibility.
If a symlink target does not exist at the time that this generator runs, it
is assumed that the symlink target is the final target of the mount.</para>

<para><filename>systemd-fstab-generator</filename> implements
<citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
</refsect1>
Expand Down
5 changes: 3 additions & 2 deletions man/systemd.mount.xml
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,9 @@

<varlistentry>
<term><varname>Where=</varname></term>
<listitem><para>Takes an absolute path of a directory of the
mount point. If the mount point does not exist at the time of
<listitem><para>Takes an absolute path of a directory for the
mount point; in particular, the destination cannot be a symbolic
link. If the mount point does not exist at the time of
mounting, it is created. This string must be reflected in the
unit filename. (See above.) This option is
mandatory.</para></listitem>
Expand Down
2 changes: 1 addition & 1 deletion src/escape/escape.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ static int parse_argv(int argc, char *argv[]) {

case ARG_TEMPLATE:

if (!unit_name_is_valid(optarg, true) || !unit_name_is_template(optarg)) {
if (!unit_name_is_valid(optarg, UNIT_NAME_TEMPLATE) || !unit_name_is_template(optarg)) {
log_error("Template name %s is not valid.", optarg);
return -EINVAL;
}
Expand Down
40 changes: 31 additions & 9 deletions src/fstab-generator/fstab-generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ static int write_idle_timeout(FILE *f, const char *where, const char *opts) {
static int add_mount(
const char *what,
const char *where,
const char *original_where,
const char *fstype,
const char *opts,
int passno,
Expand Down Expand Up @@ -329,10 +330,12 @@ static int add_mount(
fprintf(f,
"\n"
"[Mount]\n"
"What=%s\n"
"Where=%s\n",
what,
where);
"What=%s\n",
what);

if (original_where)
fprintf(f, "# Canonicalized from %s\n", original_where);
fprintf(f, "Where=%s\n", where);

if (!isempty(fstype) && !streq(fstype, "auto"))
fprintf(f, "Type=%s\n", fstype);
Expand Down Expand Up @@ -436,7 +439,7 @@ static int parse_fstab(bool initrd) {
}

while ((me = getmntent(f))) {
_cleanup_free_ char *where = NULL, *what = NULL;
_cleanup_free_ char *where = NULL, *what = NULL, *canonical_where = NULL;
bool noauto, nofail;
int k;

Expand All @@ -452,13 +455,29 @@ static int parse_fstab(bool initrd) {
continue;
}

where = initrd ? strappend("/sysroot/", me->mnt_dir) : strdup(me->mnt_dir);
where = strdup(me->mnt_dir);
if (!where)
return log_oom();

if (is_path(where))
if (is_path(where)) {
path_kill_slashes(where);

/* Follow symlinks here; see 5261ba901845c084de5a8fd06500ed09bfb0bd80 which makes sense for
* mount units, but causes problems since it historically worked to have symlinks in e.g.
* /etc/fstab. So we canonicalize here. Note that we use CHASE_NONEXISTENT to handle the case
* where a symlink refers to another mount target; this works assuming the sub-mountpoint
* target is the final directory. */
r = chase_symlinks(where, initrd ? "/sysroot" : NULL,
CHASE_PREFIX_ROOT | CHASE_NONEXISTENT,
&canonical_where);
if (r < 0) /* If we can't canonicalize we continue on as if it wasn't a symlink */
log_debug_errno(r, "Failed to read symlink target for %s, ignoring: %m", where);
else if (streq(canonical_where, where)) /* If it was fully canonicalized, suppress the change */
canonical_where = mfree(canonical_where);
else
log_debug("Canonicalized what=%s where=%s to %s", what, where, canonical_where);
}

noauto = fstab_test_yes_no_option(me->mnt_opts, "noauto\0" "auto\0");
nofail = fstab_test_yes_no_option(me->mnt_opts, "nofail\0" "fail\0");
log_debug("Found entry what=%s where=%s type=%s nofail=%s noauto=%s",
Expand All @@ -482,7 +501,8 @@ static int parse_fstab(bool initrd) {
post = SPECIAL_LOCAL_FS_TARGET;

k = add_mount(what,
where,
canonical_where ?: where,
canonical_where ? where: NULL,
me->mnt_type,
me->mnt_opts,
me->mnt_passno,
Expand Down Expand Up @@ -526,6 +546,7 @@ static int add_sysroot_mount(void) {
log_debug("Found entry what=%s where=/sysroot type=%s", what, strna(arg_root_fstype));
return add_mount(what,
"/sysroot",
NULL,
arg_root_fstype,
opts,
1,
Expand Down Expand Up @@ -583,13 +604,14 @@ static int add_sysroot_usr_mount(void) {
log_debug("Found entry what=%s where=/sysroot/usr type=%s", what, strna(arg_usr_fstype));
return add_mount(what,
"/sysroot/usr",
NULL,
arg_usr_fstype,
opts,
1,
false,
false,
false,
SPECIAL_INITRD_ROOT_FS_TARGET,
SPECIAL_INITRD_FS_TARGET,
"/proc/cmdline");
}

Expand Down
14 changes: 11 additions & 3 deletions src/shared/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -5967,6 +5967,7 @@ bool documentation_url_is_valid(const char *url) {
bool in_initrd(void) {
static int saved = -1;
struct statfs s;
int r;

if (saved >= 0)
return saved;
Expand All @@ -5981,9 +5982,16 @@ bool in_initrd(void) {
* emptying when transititioning to the main systemd.
*/

saved = access("/etc/initrd-release", F_OK) >= 0 &&
statfs("/", &s) >= 0 &&
is_temporary_fs(&s);
r = getenv_bool("SYSTEMD_IN_INITRD");
if (r < 0 && r != -ENXIO)
log_debug_errno(r, "Failed to parse $SYSTEMD_IN_INITRD, ignoring: %m");

if (r >= 0)
saved = r > 0;
else
saved = access("/etc/initrd-release", F_OK) >= 0 &&
statfs("/", &s) >= 0 &&
is_temporary_fs(&s);

return saved;
}
Expand Down
1 change: 1 addition & 0 deletions test/TEST-81-GENERATORS/Makefile
110 changes: 110 additions & 0 deletions test/TEST-81-GENERATORS/generator-utils.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later

link_endswith() {
[[ -h "${1:?}" && "$(readlink "${1:?}")" =~ ${2:?}$ ]]
}

link_eq() {
[[ -h "${1:?}" && "$(readlink "${1:?}")" == "${2:?}" ]]
}

# Get the value from a 'key=value' assignment
opt_get_arg() {
local arg

IFS="=" read -r _ arg <<< "${1:?}"
test -n "$arg"
echo "$arg"
}

in_initrd() {
[[ "${SYSTEMD_IN_INITRD:-0}" -ne 0 ]]
}

# Check if we're parsing host's fstab in initrd
in_initrd_host() {
in_initrd && [[ "${SYSTEMD_SYSROOT_FSTAB:-/dev/null}" != /dev/null ]]
}

in_container() {
systemd-detect-virt -qc
}

opt_filter() (
set +x
local opt split_options filtered_options

IFS="," read -ra split_options <<< "${1:?}"
for opt in "${split_options[@]}"; do
if [[ "$opt" =~ ${2:?} ]]; then
continue
fi

filtered_options+=("$opt")
done

IFS=","; printf "%s" "${filtered_options[*]}"
)

# Run the given generator $1 with target directory $2 - clean the target
# directory beforehand
run_and_list() {
local generator="${1:?}"
local out_dir="${2:?}"
local environ
local cmdline

# If $PID1_ENVIRON is set temporarily overmount /proc/1/environ with
# a temporary file that contains contents of $PID1_ENVIRON. This is
# necessary in cases where the generator reads the environment through
# getenv_for_pid(1, ...) or similar like getty-generator does.
#
# Note: $PID1_ENVIRON should be a NUL separated list of env assignments
if [[ -n "${PID1_ENVIRON:-}" ]]; then
environ="$(mktemp)"
echo -ne "${PID1_ENVIRON}\0" >"${environ:?}"
mount -v --bind "$environ" /proc/1/environ
fi

if [[ -n "${SYSTEMD_PROC_CMDLINE:-}" ]]; then
cmdline="$(mktemp)"
echo "$SYSTEMD_PROC_CMDLINE" >"$cmdline"
mount --bind "$cmdline" /proc/cmdline
fi

if [[ -n "${SYSTEMD_FSTAB:-}" ]]; then
touch /etc/fstab
mount --bind "$SYSTEMD_FSTAB" /etc/fstab
fi

if [[ -n "${SYSTEMD_SYSROOT_FSTAB:-}" ]]; then
mkdir -p /sysroot/etc
touch /sysroot/etc/fstab
mount --bind "$SYSTEMD_SYSROOT_FSTAB" /sysroot/etc/fstab
fi

rm -fr "${out_dir:?}"/*
mkdir -p "$out_dir"/{normal,early,late}
SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-debug}" "$generator" "$out_dir/normal" "$out_dir/early" "$out_dir/late"
ls -lR "$out_dir"

if [[ -n "${SYSTEMD_SYSROOT_FSTAB:-}" ]]; then
umount /sysroot/etc/fstab
rm -rf /sysroot
fi

if [[ -n "${SYSTEMD_FSTAB:-}" ]]; then
umount /etc/fstab
fi

if [[ -n "${SYSTEMD_PROC_CMDLINE:-}" ]]; then
umount /proc/cmdline
rm -f "$cmdline"
fi

if [[ -n "${environ:-}" ]]; then
umount /proc/1/environ
rm -f "$environ"
fi
}
84 changes: 84 additions & 0 deletions test/TEST-81-GENERATORS/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/bash
TEST_DESCRIPTION="Test systemd generators"

# shellcheck source=test/test-functions
. "$TEST_BASE_DIR/test-functions"

check_result_qemu() {
ret=1
mkdir -p $TESTDIR/root
mount ${LOOPDEV}p1 $TESTDIR/root
[[ -e $TESTDIR/root/testok ]] && ret=0
[[ -f $TESTDIR/root/failed ]] && cp -a $TESTDIR/root/failed $TESTDIR
[[ -f $TESTDIR/root/var/log/journal ]] && cp -a $TESTDIR/root/var/log/journal $TESTDIR
umount $TESTDIR/root
[[ -f $TESTDIR/failed ]] && cat $TESTDIR/failed
ls -l $TESTDIR/journal/*/*.journal
test -s $TESTDIR/failed && ret=$(($ret+1))
return $ret
}

test_run() {
if run_qemu; then
check_result_qemu || return 1
else
dwarn "can't run QEMU, skipping"
fi
if check_nspawn; then
run_nspawn
check_result_nspawn || return 1
else
dwarn "can't run systemd-nspawn, skipping"
fi
return 0
}

test_setup() {
create_empty_image
mkdir -p "${TESTDIR:?}/root"
mount "${LOOPDEV:?}p1" "$TESTDIR/root"

(
LOG_LEVEL=5
# shellcheck disable=SC2046
eval $(udevadm info --export --query=env --name="${LOOPDEV}p2")

setup_basic_environment

# mask some services that we do not want to run in these tests
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-hwdb-update.service"
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-journal-catalog-update.service"
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-networkd.service"
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-networkd.socket"
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-resolved.service"
ln -fs /dev/null "$initdir/etc/systemd/system/systemd-machined.service"

# setup the testsuite service
cat >"$initdir/etc/systemd/system/testsuite.service" <<EOF
[Unit]
Description=Testsuite service

[Service]
ExecStart=/bin/bash -x /testsuite.sh
Type=oneshot
StandardOutput=tty
StandardError=tty
NotifyAccess=all
EOF
cp generator-utils.sh testsuite*.sh "$initdir/"

setup_testsuite
) || return 1
setup_nspawn_root

ddebug "umount $TESTDIR/root"
umount "$TESTDIR/root"
}

test_cleanup() {
umount $TESTDIR/root 2>/dev/null
[[ $LOOPDEV ]] && losetup -d $LOOPDEV
return 0
}

do_test "$@"