From 78a860982e036f38fe9c0b3344998df5ac2c2ff5 Mon Sep 17 00:00:00 2001
From: Karel Zak <kzak@redhat.com>
Date: Tue, 16 Jun 2026 11:13:54 +0200
Subject: [PATCH 06/12] 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.

CVE-2026-53613

Signed-off-by: Karel Zak <kzak@redhat.com>
---
 libmount/src/context.c           | 43 ++++++++++++++++++++++++++++++++
 libmount/src/context_mount.c     |  7 ++++++
 libmount/src/hook_mount.c        | 15 ++++++++++-
 libmount/src/hook_mount_legacy.c |  3 +++
 libmount/src/mountP.h            |  7 ++++++
 5 files changed, 74 insertions(+), 1 deletion(-)

Index: util-linux-2.39.3/libmount/src/context.c
===================================================================
--- util-linux-2.39.3.orig/libmount/src/context.c
+++ util-linux-2.39.3/libmount/src/context.c
@@ -37,6 +37,7 @@
  */
 
 #include "mountP.h"
+#include "fileutils.h"
 #include "strutils.h"
 #include "namespace.h"
 #include "match.h"
@@ -65,6 +66,7 @@ struct libmnt_context *mnt_new_context(v
 	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);
@@ -171,6 +173,7 @@ int mnt_reset_context(struct libmnt_cont
 	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)
@@ -395,6 +398,46 @@ int mnt_context_is_restricted(struct lib
 	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
Index: util-linux-2.39.3/libmount/src/context_mount.c
===================================================================
--- util-linux-2.39.3.orig/libmount/src/context_mount.c
+++ util-linux-2.39.3/libmount/src/context_mount.c
@@ -726,6 +726,13 @@ static int prepare_target(struct libmnt_
 	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;
 
Index: util-linux-2.39.3/libmount/src/hook_mount.c
===================================================================
--- util-linux-2.39.3.orig/libmount/src/hook_mount.c
+++ util-linux-2.39.3/libmount/src/hook_mount.c
@@ -527,7 +527,16 @@ static int hook_attach_target(struct lib
 		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, "", 0);
+	} else
+		rc = move_mount(api->fd_tree, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH);
+
 	set_syscall_status(cxt, "move_mount", rc == 0);
 
 	return rc == 0 ? 0 : -errno;
Index: util-linux-2.39.3/libmount/src/hook_mount_legacy.c
===================================================================
--- util-linux-2.39.3.orig/libmount/src/hook_mount_legacy.c
+++ util-linux-2.39.3/libmount/src/hook_mount_legacy.c
@@ -248,6 +248,9 @@ static int hook_mount(struct libmnt_cont
 		return rc;
 	}
 
+	/* re-open to point to the mounted filesystem root */
+	rc = mnt_context_reopen_target_fd(cxt);
+
 	cxt->syscall_status = 0;
 	return rc;
 }
Index: util-linux-2.39.3/libmount/src/mountP.h
===================================================================
--- util-linux-2.39.3.orig/libmount/src/mountP.h
+++ util-linux-2.39.3/libmount/src/mountP.h
@@ -442,6 +442,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 */
 };
@@ -630,6 +632,11 @@ extern int mnt_context_prepare_update(st
 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);
 
