Add a new LOOPDEV_FL_NOFOLLOW flag for loop device context that
prevents symlink following in both path canonicalization and file open.
When set:
- loopcxt_set_backing_file() uses strdup() instead of
ul_canonicalize_path() (which calls realpath() and follows symlinks)
- loopcxt_setup_device() adds O_NOFOLLOW to open() flags
The flag is set for non-root (restricted) mount operations in
libmount's loop device hook. This prevents a TOCTOU race condition
where an attacker could replace the backing file (specified in
/etc/fstab) with a symlink to an arbitrary root-owned file between
path resolution and open().
Vulnerable Code Flow:
mount /mnt/point (non-root, SUID)
mount.c: sanitize_paths() on user args (mountpoint only)
mnt_context_mount()
mnt_context_prepare_mount()
mnt_context_apply_fstab() <-- source path from fstab
hooks run at MNT_STAGE_PREP_SOURCE
hook_loopdev.c: setup_loopdev()
backing_file = fstab source path ("/home/user/disk.img")
loopcxt_set_backing_file() <-- calls realpath() as ROOT
ul_canonicalize_path() <-- follows symlinks!
loopcxt_setup_device()
open(lc->filename, O_RDWR|O_CLOEXEC) <-- no O_NOFOLLOW
Two vulnerabilities in the path:
1) loopcxt_set_backing_file() calls ul_canonicalize_path() which uses
realpath() -- this follows symlinks as euid=0. If the attacker swaps
the file to a symlink before this call, lc->filename becomes the
resolved target path (e.g., /root/secret.img).
2) loopcxt_setup_device() opens lc->filename without O_NOFOLLOW. Even
if canonicalization happened correctly, the file can be swapped to a
symlink between canonicalize and open.
Addresses: https://github.com/util-linux/util-linux/security/advisories/GHSA-qq4x-vfq4-9h9g
Signed-off-by: Karel Zak <kzak@redhat.com>
LOOPDEV_FL_NOIOCTL = (1 << 6),
LOOPDEV_FL_DEVSUBDIR = (1 << 7),
LOOPDEV_FL_CONTROL = (1 << 8), /* system with /dev/loop-control */
- LOOPDEV_FL_SIZELIMIT = (1 << 9)
+ LOOPDEV_FL_SIZELIMIT = (1 << 9),
+ LOOPDEV_FL_NOFOLLOW = (1 << 10) /* O_NOFOLLOW, don't follow symlinks */
};
/*
if (!lc)
return -EINVAL;
- lc->filename = ul_canonicalize_path(filename);
+ if (lc->flags & LOOPDEV_FL_NOFOLLOW)
+ lc->filename = strdup(filename);
+ else
+ lc->filename = ul_canonicalize_path(filename);
if (!lc->filename)
return -errno;
if (lc->config.info.lo_flags & LO_FLAGS_DIRECT_IO)
flags |= O_DIRECT;
+ if (lc->flags & LOOPDEV_FL_NOFOLLOW)
+ flags |= O_NOFOLLOW;
if ((file_fd = open(lc->filename, mode | flags)) < 0) {
if (mode != O_RDONLY && (errno == EROFS || errno == EACCES))
}
DBG_OBJ(LOOP, cxt, ul_debug("not found; create a new loop device"));
- rc = loopcxt_init(&lc, 0);
+ rc = loopcxt_init(&lc,
+ mnt_context_is_restricted(cxt) ? LOOPDEV_FL_NOFOLLOW : 0);
if (rc)
goto done_no_deinit;
if (mnt_opt_has_value(loopopt)) {