[PATCH] ntfs3: do not trust setuid and device nodes from WSL metadata
From: aljojobypls
Date: Fri Oct 02 2026 - 02:36:26 EST
ntfs_get_wsl_perm() copies $LXMOD over the inode mode, including the
file type, setuid, setgid and the sticky bit, and $LXDEV supplies a
device number. Those attributes are stored on the volume.
A crafted NTFS image can therefore present a setuid-root binary or a
device node. cp -a and rsync -a copy that mode onto a normal Linux
filesystem. The nosuid,nodev flags on the ntfs3 mount do not travel
with the copy.
Keep the stored owner and the rwx bits. Keep the file type already
derived from the MFT, and drop setuid, setgid, the sticky bit and
device nodes. -o permissions restores the previous behavior for a
volume the operator trusts, such as a WSL disk that really contains
setuid binaries.
Fixes: be71b5cba2e6 ("fs/ntfs3: Add attrib operations")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Aljo Joby <aljojobypls@xxxxxxxxx>
---
Documentation/filesystems/ntfs3.rst | 9 ++++++++
fs/ntfs3/ntfs_fs.h | 1 +
fs/ntfs3/super.c | 7 ++++++
fs/ntfs3/xattr.c | 35 +++++++++++++++++++++++------
4 files changed, 45 insertions(+), 7 deletions(-)
diff --git a/Documentation/filesystems/ntfs3.rst
b/Documentation/filesystems/ntfs3.rst
index 2b86a9b3a..260f73228 100644
--- a/Documentation/filesystems/ntfs3.rst
+++ b/Documentation/filesystems/ntfs3.rst
@@ -109,6 +109,15 @@ this table marked with no it means default is
without **no**.
Kernel. Not to be confused with NTFS ACLs. The option specified as acl
enables support for POSIX ACLs.
+ * - permissions
+ - Trust the WSL metadata stored in \$LXUID, \$LXGID, \$LXMOD and \$LXDEV.
+ This includes the owner, setuid, setgid, the sticky bit and device
+ nodes. Without this option the driver still applies the stored owner
+ and the rwx bits, but it keeps the file type it read from the MFT and
+ drops setuid, setgid, the sticky bit and device nodes. Mount a volume
+ with this option only when you trust it, for example a WSL disk that
+ legitimately contains setuid binaries.
+
Todo list
=========
- Full journaling support over JBD. Currently journal replaying is supported
diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h
index 5811d89d6..267d980f7 100644
--- a/fs/ntfs3/ntfs_fs.h
+++ b/fs/ntfs3/ntfs_fs.h
@@ -111,6 +111,7 @@ struct ntfs_mount_options {
unsigned nocase : 1; /* case insensitive. */
unsigned delalloc : 1; /* delay allocation. */
unsigned ads : 1; /* ads support. */
+ unsigned permissions : 1; /* Trust on-disk WSL uid, mode, devices. */
};
/* Special value to unpack and deallocate. */
diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c
index f4a42a0c7..863126e87 100644
--- a/fs/ntfs3/super.c
+++ b/fs/ntfs3/super.c
@@ -275,6 +275,7 @@ enum Opt {
Opt_delalloc_bool,
Opt_ads,
Opt_ads_bool,
+ Opt_permissions,
Opt_err,
};
@@ -303,6 +304,7 @@ static const struct fs_parameter_spec
ntfs_fs_parameters[] = {
fsparam_bool("delalloc", Opt_delalloc_bool),
fsparam_flag("ads", Opt_ads),
fsparam_bool("ads", Opt_ads_bool),
+ fsparam_flag("permissions", Opt_permissions),
{}
};
// clang-format on
@@ -432,6 +434,9 @@ static int ntfs_fs_parse_param(struct fs_context *fc,
case Opt_ads_bool:
opts->ads = result.boolean;
break;
+ case Opt_permissions:
+ opts->permissions = 1;
+ break;
default:
/* Should not be here unless we forget add case. */
return -EINVAL;
@@ -805,6 +810,8 @@ static int ntfs_show_options(struct seq_file *m,
struct dentry *root)
seq_puts(m, ",delalloc");
if (opts->ads)
seq_puts(m, ",ads");
+ if (opts->permissions)
+ seq_puts(m, ",permissions");
return 0;
}
diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c
index 594ef6860..5db154513 100644
--- a/fs/ntfs3/xattr.c
+++ b/fs/ntfs3/xattr.c
@@ -1036,6 +1036,8 @@ void ntfs_get_wsl_perm(struct inode *inode)
{
size_t sz;
__le32 value[3];
+ struct ntfs_sb_info *sbi = inode->i_sb->s_fs_info;
+ umode_t disk_mode, type;
if (ntfs_get_ea(inode, "$LXUID", sizeof("$LXUID") - 1, &value[0],
sizeof(value[0]), &sz) == sizeof(value[0]) &&
@@ -1043,15 +1045,34 @@ void ntfs_get_wsl_perm(struct inode *inode)
sizeof(value[1]), &sz) == sizeof(value[1]) &&
ntfs_get_ea(inode, "$LXMOD", sizeof("$LXMOD") - 1, &value[2],
sizeof(value[2]), &sz) == sizeof(value[2])) {
- i_uid_write(inode, (uid_t)le32_to_cpu(value[0]));
- i_gid_write(inode, (gid_t)le32_to_cpu(value[1]));
- inode->i_mode = le32_to_cpu(value[2]);
+ disk_mode = le32_to_cpu(value[2]);
- if (ntfs_get_ea(inode, "$LXDEV", sizeof("$LXDEV") - 1,
- &value[0], sizeof(value),
- &sz) == sizeof(value[0])) {
- inode->i_rdev = le32_to_cpu(value[0]);
+ /*
+ * $LXUID/$LXGID/$LXMOD/$LXDEV are stored on the volume.
+ * A crafted image can claim to be a setuid-root binary or
+ * a device node. Those bits are copied onto a trusted
+ * filesystem by cp -a and rsync -a, even when this mount
+ * itself is nosuid,nodev. Trust them only with -o permissions.
+ */
+ if (sbi->options->permissions) {
+ i_uid_write(inode, (uid_t)le32_to_cpu(value[0]));
+ i_gid_write(inode, (gid_t)le32_to_cpu(value[1]));
+ inode->i_mode = disk_mode;
+
+ if (S_ISCHR(disk_mode) || S_ISBLK(disk_mode)) {
+ if (ntfs_get_ea(inode, "$LXDEV",
+ sizeof("$LXDEV") - 1, &value[0],
+ sizeof(value[0]),
+ &sz) == sizeof(value[0]))
+ inode->i_rdev = le32_to_cpu(value[0]);
+ }
+ return;
}
+
+ i_uid_write(inode, (uid_t)le32_to_cpu(value[0]));
+ i_gid_write(inode, (gid_t)le32_to_cpu(value[1]));
+ type = inode->i_mode & S_IFMT;
+ inode->i_mode = type | (disk_mode & 0777);
}
}
--
2.53.0