From 0d3d55975aa3492c62fd345eac38f41cd166c0b0 Mon Sep 17 00:00:00 2001 From: Karel Zak Date: Tue, 16 Jun 2026 11:13:54 +0200 Subject: [PATCH] libmount: add fd_target to context for TOCTOU prevention Add a pinned O_PATH target fd to libmnt_context with lazy-open getter mnt_context_get_target_fd() and mnt_context_close_target_fd(). The fd is opened via ul_open_no_symlinks() (RESOLVE_NO_SYMLINKS) to reject symlinks at any path component. The fd is closed on context reset. Signed-off-by: Karel Zak (cherry picked from commit 78a860982e036f38fe9c0b3344998df5ac2c2ff5) Upstream: https://github.com/util-linux/util-linux/commit/0b010025a0e429bc80355c94db86a843395d49e2 CVE: CVE-2026-53613 Signed-off-by: Thomas Perale --- libmount/src/context.c | 43 ++++++++++++++++++++++++++++++++ libmount/src/context_mount.c | 7 ++++++ libmount/src/hook_mount.c | 18 ++++++++++++- libmount/src/hook_mount_legacy.c | 3 +++ libmount/src/mountP.h | 7 ++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/libmount/src/context.c b/libmount/src/context.c index a0ca25f8b74..3ef40d6d85a 100644 --- a/libmount/src/context.c +++ b/libmount/src/context.c @@ -37,6 +37,7 @@ */ #include "mountP.h" +#include "fileutils.h" #include "strutils.h" #include "namespace.h" #include "match.h" @@ -67,6 +68,7 @@ struct libmnt_context *mnt_new_context(void) cxt->ns_orig.fd = -1; cxt->ns_tgt.fd = -1; cxt->ns_cur = &cxt->ns_orig; + cxt->fd_target = -1; cxt->map_linux = mnt_get_builtin_optmap(MNT_LINUX_MAP); cxt->map_userspace = mnt_get_builtin_optmap(MNT_USERSPACE_MAP); @@ -173,6 +175,7 @@ int mnt_reset_context(struct libmnt_context *cxt) cxt->map_userspace = mnt_get_builtin_optmap(MNT_USERSPACE_MAP); mnt_context_reset_status(cxt); + mnt_context_close_target_fd(cxt); mnt_context_deinit_hooksets(cxt); if (cxt->table_fltrcb) @@ -398,6 +401,46 @@ int mnt_context_is_restricted(struct libmnt_context *cxt) return cxt->restricted; } +int mnt_context_target_fd_required(struct libmnt_context *cxt) +{ + return mnt_context_is_restricted(cxt); +} + +int mnt_context_reopen_target_fd(struct libmnt_context *cxt) +{ + assert(cxt); + + if (!mnt_context_target_fd_required(cxt)) + return 0; + mnt_context_close_target_fd(cxt); + if (mnt_context_get_target_fd(cxt) < 0) + return -errno; + return 0; +} + +int mnt_context_get_target_fd(struct libmnt_context *cxt) +{ + assert(cxt); + + if (cxt->fd_target < 0) { + const char *target = mnt_fs_get_target(cxt->fs); + + if (target) + cxt->fd_target = ul_open_no_symlinks(target, + O_PATH | O_CLOEXEC, 0); + } + return cxt->fd_target; +} + +void mnt_context_close_target_fd(struct libmnt_context *cxt) +{ + assert(cxt); + + if (cxt->fd_target >= 0) + close(cxt->fd_target); + cxt->fd_target = -1; +} + /** * mnt_context_force_unrestricted: * @cxt: mount context diff --git a/libmount/src/context_mount.c b/libmount/src/context_mount.c index 28e01309d8c..77435684b4a 100644 --- a/libmount/src/context_mount.c +++ b/libmount/src/context_mount.c @@ -733,6 +733,13 @@ static int prepare_target(struct libmnt_context *cxt) if (rc == 0) rc = mnt_context_call_hooks(cxt, MNT_STAGE_PREP_TARGET); + if (rc == 0 + && mnt_context_target_fd_required(cxt) + && mnt_context_get_target_fd(cxt) < 0) { + DBG(CXT, ul_debugobj(cxt, "failed to pin target")); + rc = -errno; + } + if (!mnt_context_switch_ns(cxt, ns_old)) return -MNT_ERR_NAMESPACE; diff --git a/libmount/src/hook_mount.c b/libmount/src/hook_mount.c index 37179fb59ad..50e656e04af 100644 --- a/libmount/src/hook_mount.c +++ b/libmount/src/hook_mount.c @@ -541,9 +541,25 @@ static int hook_attach_target(struct libmnt_context *cxt, umount2(target, MNT_DETACH); } - rc = move_mount(api->fd_tree, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH); + /* fd_target is open in restricted mode (see prepare_target()) */ + if (mnt_context_target_fd_required(cxt)) { + int fd = mnt_context_get_target_fd(cxt); + + if (fd < 0) + return -errno; + rc = move_mount(api->fd_tree, "", fd, "", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH); + } else + rc = move_mount(api->fd_tree, "", AT_FDCWD, target, + MOVE_MOUNT_F_EMPTY_PATH); + hookset_set_syscall_status(cxt, "move_mount", rc == 0); + if (rc == 0) { + /* re-open to point to the mounted filesystem root */ + rc = mnt_context_reopen_target_fd(cxt); + } + return rc == 0 ? 0 : -errno; } diff --git a/libmount/src/hook_mount_legacy.c b/libmount/src/hook_mount_legacy.c index 94a8fc685eb..d6631b08864 100644 --- a/libmount/src/hook_mount_legacy.c +++ b/libmount/src/hook_mount_legacy.c @@ -248,6 +248,9 @@ static int hook_mount(struct libmnt_context *cxt, return rc; } + /* re-open to point to the mounted filesystem root */ + rc = mnt_context_reopen_target_fd(cxt); + cxt->syscall_status = 0; return rc; } diff --git a/libmount/src/mountP.h b/libmount/src/mountP.h index a4ce7c04268..97d0bf8fa79 100644 --- a/libmount/src/mountP.h +++ b/libmount/src/mountP.h @@ -443,6 +443,8 @@ struct libmnt_context unsigned int has_selinux_opt : 1; /* temporary for broken fsconfig() syscall */ unsigned int force_clone : 1; /* OPEN_TREE_CLONE */ + int fd_target; /* pinned target fd (RESOLVE_NO_SYMLINKS) */ + struct list_head hooksets_datas; /* global hooksets data */ struct list_head hooksets_hooks; /* global hooksets data */ }; @@ -640,6 +642,11 @@ extern int mnt_context_prepare_update(struct libmnt_context *cxt); extern int mnt_context_merge_mflags(struct libmnt_context *cxt); extern int mnt_context_update_tabs(struct libmnt_context *cxt); +extern int mnt_context_target_fd_required(struct libmnt_context *cxt); +extern int mnt_context_get_target_fd(struct libmnt_context *cxt); +extern void mnt_context_close_target_fd(struct libmnt_context *cxt); +extern int mnt_context_reopen_target_fd(struct libmnt_context *cxt); + extern int mnt_context_umount_setopt(struct libmnt_context *cxt, int c, char *arg); extern int mnt_context_mount_setopt(struct libmnt_context *cxt, int c, char *arg);