From 24da33905c7115c4cbccd0afb2a469804e96467a Mon Sep 17 00:00:00 2001
From: Karel Zak <kzak@redhat.com>
Date: Tue, 16 Jun 2026 11:15:19 +0200
Subject: [PATCH 08/12] libmount: use fd-based fchownat/chmod in hook_owner

Replace path-based lchown()/chmod() with fd-based operations in the
X-mount.{owner,group,mode} post-mount hook.

For restricted users the fd_target is pinned in prepare_target() and
re-opened after mount in hook_attach_target() to point to the mounted
filesystem root.  For root a local O_PATH fd is opened.  Ownership is
changed via fchownat(fd, "", ..., AT_EMPTY_PATH), mode via
/proc/self/fd/N.

This prevents TOCTOU attacks where an ancestor directory is swapped
between mount and the chmod/chown operations.

CVE-2026-53612

Reported-by: Xinyao Hu <ctf_0x01@foxmail.com>
Signed-off-by: Karel Zak <kzak@redhat.com>
---
 libmount/src/hook_owner.c | 32 ++++++++++++++++++++++++--------
 1 file changed, 24 insertions(+), 8 deletions(-)

Index: util-linux-2.39.3/libmount/src/hook_owner.c
===================================================================
--- util-linux-2.39.3.orig/libmount/src/hook_owner.c
+++ util-linux-2.39.3/libmount/src/hook_owner.c
@@ -17,6 +17,7 @@
 #include <sched.h>
 
 #include "mountP.h"
+#include "pathnames.h"
 #include "fileutils.h"
 
 struct hook_data {
@@ -48,7 +49,7 @@ static int hook_post(
 {
 	struct hook_data *hd = (struct hook_data *) data;
 	const char *target;
-	int rc = 0;
+	int rc = 0, fd;
 
 	assert(cxt);
 
@@ -59,18 +60,33 @@ static int hook_post(
 	if (!target)
 		return 0;
 
+	/* fd_target is pinned in restricted mode (see prepare_target()),
+	 * for root open it here to keep chmod/chown fd-based too */
+	if (mnt_context_target_fd_required(cxt))
+		fd = mnt_context_get_target_fd(cxt);
+	else
+		fd = open(target, O_PATH | O_CLOEXEC);
+
+	if (fd < 0)
+		return -MNT_ERR_CHMOD;
+
 	if (hd->owner != (uid_t) -1 || hd->group != (uid_t) -1) {
-		DBG(CXT, ul_debugobj(cxt, " lchown(%s, %u, %u)", target, hd->owner, hd->group));
-		if (lchown(target, hd->owner, hd->group) == -1)
-			return -MNT_ERR_CHOWN;
+		DBG(CXT, ul_debugobj(cxt, " fchownat(%s, %u, %u)", target, hd->owner, hd->group));
+		if (fchownat(fd, "", hd->owner, hd->group, AT_EMPTY_PATH) == -1)
+			rc -MNT_ERR_CHOWN;
 	}
 
-	if (hd->mode != (mode_t) -1) {
-		DBG(CXT, ul_debugobj(cxt, " chmod(%s, %04o)", target, hd->mode));
-		if (chmod(target, hd->mode) == -1)
-			return -MNT_ERR_CHMOD;
+	if (!rc && hd->mode != (mode_t) -1) {
+		char buf[sizeof(_PATH_PROC_FDDIR) + 1 + sizeof(stringify_value(INT_MAX))];
+
+		snprintf(buf, sizeof(buf), _PATH_PROC_FDDIR "/%d", fd);
+		DBG(CXT, ul_debugobj(cxt, " chmod(%s, %04o)", buf, hd->mode));
+		if (chmod(buf, hd->mode) == -1)
+			rc = -MNT_ERR_CHMOD;
 	}
 
+	if (!mnt_context_target_fd_required(cxt))
+		close(fd);
 	return rc;
 }
 
