[PATCH 2/2] dlm: require CAP_SYS_ADMIN for dlm-monitor device

Haofeng Li <[email protected]> Thu, 16 Jul 2026 15:21:52 +0800
Newsgroups dev.linux.lists.gfs2,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
monitor_device_open() in fs/dlm/user.c performs only
atomic_inc(&dlm_monitor_opened) and sets dlm_monitor_unused = 0; it
does no capability check.  monitor_device_close() does
atomic_dec_and_test(&dlm_monitor_opened) and, when the count reaches
zero, calls dlm_stop_lockspaces() — which stops every lockspace on
the node.  The miscdevice is also registered with no .mode field.

Attack chain (when the device node is reachable by an unprivileged
opener — see mitigation note below):

  1. attacker open("/dev/dlm-monitor") with no cap check; the
     global counter goes 0 -> 1
  2. attacker close(fd); atomic_dec_and_test reaches zero again
     and dlm_stop_lockspaces() runs -> every DLM lockspace on the
     local node is stopped.  Other cluster members then observe
     the node losing its lockspaces (membership / recovery side
     effects), so the impact is not strictly local to GFS2 /
     OCFS2 / lvmlockd / cluster-md workloads on this node.
  variant: attacker holds the fd open indefinitely to suppress
     the intended stop when dlm_controld later closes its own fd
     (inverse abuse — recovery / shutdown stalls)

Mitigation: devtmpfs creates /dev/dlm-monitor as 0600 root:root on
a stock kernel, so unprivileged open is blocked by the node mode,
not by a kernel cap check.  The gap is real wherever the node is
reachable (udev MODE=0666, container bind-mount, fd via SCM_RIGHTS,
or any setup where dlm_controld shares its monitor fd).

Reproduction (kernel 7.2.0-rc3, dlm loaded, no live lockspace):

  # ./exploit_h3   # as root
  [*] lockspace devices present: 0
  [!!!] AUTH BYPASS: opened /dev/dlm-monitor, no cap check (fd=3)
  [VULNERABLE] monitor open auth bypass demonstrated

  $ setpriv --reuid 65534 --regid 65534 ./exploit_h3
  [OK ] open denied: Permission denied   # node 0600, not cap check

The destructive close path is opt-in in the PoX
(--i-know-it-stops-lockspaces); we did not drive it here.  Driving
the close path on a node with active lockspaces would stop them;
on this throw-away node there are none, but we keep the opt-in gate
so the same PoX is safe to re-run on production-like clusters.

Fix: gate monitor_device_open() on capable(CAP_SYS_ADMIN) and set
.mode = 0600 on monitor_device, matching the dlm_controld-only
intended usage.

Signed-off-by: Haofeng Li <[email protected]>
---
 fs/dlm/user.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/fs/dlm/user.c b/fs/dlm/user.c
index a8ed4c8fdc5b..cd7e142ca670 100644
--- a/fs/dlm/user.c
+++ b/fs/dlm/user.c
@@ -4,6 +4,7 @@
  */
 
 #include <linux/miscdevice.h>
+#include <linux/capability.h>
 #include <linux/init.h>
 #include <linux/wait.h>
 #include <linux/file.h>
@@ -910,6 +911,10 @@ static int ctl_device_close(struct inode *inode, struct file *file)
 
 static int monitor_device_open(struct inode *inode, struct file *file)
 {
+	/* dlm_controld is the only expected opener; last close stops LS. */
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
 	atomic_inc(&dlm_monitor_opened);
 	dlm_monitor_unused = 0;
 	return 0;
@@ -958,6 +963,7 @@ static struct miscdevice monitor_device = {
 	.name  = "dlm-monitor",
 	.fops  = &monitor_device_fops,
 	.minor = MISC_DYNAMIC_MINOR,
+	.mode  = 0600,
 };
 
 int __init dlm_user_init(void)
-- 
2.25.1