From f09c78bd6242599a3e6ebb6c88b1d857cc981147 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Wed, 23 Sep 2026 14:02:44 +0800 Subject: [PATCH] ntfs: persist immutable in the $LINUXFLAGS EA Add a fileattr setter for immutable and store its state in bit 0 of a little-endian u32 in the private $LINUXFLAGS EA. Reuse the standard NTFS EA attributes and restore the flag when loading an inode, so eviction and unmount do not discard the setting. Use $LINUXFLAGS to avoid the WSL-owned $LX namespace. Preserve unknown bits and delete the entry only when the entire value is zero. Reject malformed values with EUCLEAN. Reject direct xattr writes and removal to avoid bypassing fileattr permissions or stale cached state. Update the cached flags only after the EA update succeeds. Reject clearing immutable on system metadata files and files protected by sys_immutable, following inode-load policy. Do not persist a bit derived solely from this policy. Preserve append state without allowing it to be changed at this stage. Compression, encryption and mount-wide case folding remain read-only. Document the format and its Linux-only enforcement. Signed-off-by: Baolin Liu Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- Documentation/filesystems/ntfs.rst | 18 +++++- fs/ntfs/ea.c | 49 ++++++++++++++++ fs/ntfs/ea.h | 6 ++ fs/ntfs/file.c | 90 +++++++++++++++++++++++++++++- fs/ntfs/inode.c | 12 ++-- fs/ntfs/inode.h | 2 + fs/ntfs/namei.c | 3 +- fs/ntfs/ntfs.h | 2 + 8 files changed, 171 insertions(+), 11 deletions(-) diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst index fe42a9d22643..a40f60eef0f2 100644 --- a/Documentation/filesystems/ntfs.rst +++ b/Documentation/filesystems/ntfs.rst @@ -38,9 +38,21 @@ The project is available at: Linux file attributes ===================== -The driver supports querying compression, encryption, immutable, append-only -and mount-wide case folding through lsattr(1). Case folding is reported -according to the mount options. +The driver supports lsattr(1) and setting immutable with chattr(1). +Immutable is stored in bit 0 of a private ``$LINUXFLAGS`` extended attribute +within the standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value +is a 32-bit little-endian bitmask. Unknown bits are preserved; the entry +is removed when the entire value becomes zero. The setting survives +inode eviction, unmount and reboot. Direct writes or removal through +xattr interfaces are rejected; use chattr(1) instead. + +System metadata files and files protected by ``sys_immutable`` cannot +have their immutable protection cleared. Protection derived solely from +that policy is not stored in the EA. Compression, encryption, append-only +and mount-wide case folding are reported but cannot be changed through +chattr(1). + +Windows does not enforce the Linux immutable flag stored in this EA. Supported mount options ======================= diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c index 483082a7c86f..8f73590362e5 100644 --- a/fs/ntfs/ea.c +++ b/fs/ntfs/ea.c @@ -481,6 +481,51 @@ int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size, return err; } +int ntfs_ea_get_linuxflags(struct inode *inode) +{ + struct ntfs_inode *ni = NTFS_I(inode); + __le32 value; + int err; + + err = ntfs_get_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + &value, sizeof(value)); + if (err == -ENODATA) + return 0; + if (err < 0) + return err == -ERANGE ? -EUCLEAN : err; + if (err != sizeof(value)) + return -EUCLEAN; + + ni->linuxflags = le32_to_cpu(value); + if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE) + inode->i_flags |= S_IMMUTABLE; + return 0; +} + +int ntfs_ea_set_linuxflags(struct inode *inode, u32 linuxflags) +{ + struct ntfs_inode *ni = NTFS_I(inode); + __le32 value; + int err; + + if (linuxflags) { + value = cpu_to_le32(linuxflags); + err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + &value, sizeof(value), 0, NULL); + } else if (NInoHasEA(ni)) { + err = ntfs_set_ea(inode, "$LINUXFLAGS", sizeof("$LINUXFLAGS") - 1, + NULL, 0, XATTR_REPLACE, NULL); + if (err == -ENODATA) + err = 0; + } else { + err = 0; + } + + if (!err) + ni->linuxflags = linuxflags; + return err; +} + ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size) { struct inode *inode = d_inode(dentry); @@ -861,6 +906,10 @@ static int ntfs_setxattr(const struct xattr_handler *handler, if (NVolShutdown(ni->vol)) return -EIO; + /* Only fileattr_set may change the flags and their cached state. */ + if (!strcmp(name, "$LINUXFLAGS")) + return -EPERM; + if (ntfs_is_reserved_lxattr(name) && !capable(CAP_SYS_ADMIN)) return -EPERM; diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h index 690fafe181fb..c417ed9c556e 100644 --- a/fs/ntfs/ea.h +++ b/fs/ntfs/ea.h @@ -7,6 +7,10 @@ #define NTFS_EA_GID BIT(2) #define NTFS_EA_MODE BIT(3) +/* $LINUXFLAGS stores these bits in a single little-endian 32-bit value. */ +#define NTFS_LINUXFLAGS_IMMUTABLE BIT(0) +#define NTFS_LINUXFLAGS_MASK NTFS_LINUXFLAGS_IMMUTABLE + extern const struct xattr_handler *const ntfs_xattr_handlers[]; int ntfs_ea_set_wsl_not_symlink(struct ntfs_inode *ni, mode_t mode, dev_t dev); @@ -14,6 +18,8 @@ int ntfs_ea_get_wsl_inode(struct inode *inode, dev_t *rdevp, unsigned int flags, bool *has_lxmod); int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size, unsigned int flags); +int ntfs_ea_get_linuxflags(struct inode *inode); +int ntfs_ea_set_linuxflags(struct inode *inode, u32 linuxflags); ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size); #ifdef CONFIG_NTFS_FS_POSIX_ACL diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index ce3625a3533b..b000a53672a5 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -174,6 +174,87 @@ int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa) return 0; } +static bool ntfs_is_forced_immutable(struct inode *vi) +{ + struct ntfs_inode *ni = NTFS_I(vi); + + if (ni->mft_no < FILE_first_user && S_ISREG(vi->i_mode)) + return true; + + return NVolSysImmutable(ni->vol) && + (ni->flags & FILE_ATTR_SYSTEM) && + !S_ISFIFO(vi->i_mode) && !S_ISSOCK(vi->i_mode) && + !S_ISLNK(vi->i_mode); +} + +/* + * ntfs_fileattr_set - inode_operations::fileattr_set + * @idmap: idmap of the mount @dentry was found from + * @dentry: dentry to set the flags of + * @fa: flags to set + */ +int ntfs_fileattr_set(const struct mnt_idmap *idmap, struct dentry *dentry, + struct file_kattr *fa) +{ + struct inode *vi = d_inode(dentry); + struct ntfs_inode *ni = NTFS_I(vi); + u32 allowed = FS_IMMUTABLE_FL; + u32 readonly = 0; + u32 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_MASK; + unsigned int new_fl = 0; + bool forced_immutable; + int err; + + if (NVolShutdown(ni->vol)) + return -EIO; + + if (fileattr_has_fsx(fa)) + return -EOPNOTSUPP; + + if ((fa->flags & FS_APPEND_FL) != + (IS_APPEND(vi) ? FS_APPEND_FL : 0)) + return -EOPNOTSUPP; + allowed |= fa->flags & FS_APPEND_FL; + + /* chattr passes the unchanged read-only flags back to us too. */ + if (NInoCompressed(ni) || NInoWofCompressed(ni)) + readonly |= FS_COMPR_FL; + if (NInoEncrypted(ni)) + readonly |= FS_ENCRYPT_FL; + if ((fa->flags ^ readonly) & (FS_COMPR_FL | FS_ENCRYPT_FL)) + return -EOPNOTSUPP; + allowed |= readonly; + + /* Case folding is a read-only, mount-wide property. */ + if (!NVolCaseSensitive(ni->vol)) + allowed |= FS_CASEFOLD_FL; + if (fa->flags & ~allowed) + return -EOPNOTSUPP; + + forced_immutable = ntfs_is_forced_immutable(vi); + if (!(fa->flags & FS_IMMUTABLE_FL) && forced_immutable) + return -EPERM; + + if (fa->flags & FS_IMMUTABLE_FL) { + new_fl |= S_IMMUTABLE; + /* Do not persist an immutable bit derived from the mount. */ + if (!forced_immutable || + (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)) + linuxflags |= NTFS_LINUXFLAGS_IMMUTABLE; + } + + mutex_lock(&ni->mrec_lock); + err = ntfs_ea_set_linuxflags(vi, linuxflags); + mutex_unlock(&ni->mrec_lock); + if (err) + return err; + + inode_set_flags(vi, new_fl, S_IMMUTABLE); + inode_set_ctime_current(vi); + mark_inode_dirty(vi); + return 0; +} + struct ntfs_fsync_attr { struct list_head list; struct inode *inode; @@ -1456,9 +1537,10 @@ const struct file_operations ntfs_file_ops = { }; const struct inode_operations ntfs_file_inode_ops = { - .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .listxattr = ntfs_listxattr, .get_acl = ntfs_get_acl, .set_acl = ntfs_set_acl, @@ -1466,17 +1548,19 @@ const struct inode_operations ntfs_file_inode_ops = { }; const struct inode_operations ntfs_symlink_inode_operations = { - .fileattr_get = ntfs_fileattr_get, .get_link = ntfs_get_link, .setattr = ntfs_setattr, .listxattr = ntfs_listxattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, }; const struct inode_operations ntfs_special_inode_operations = { - .fileattr_get = ntfs_fileattr_get, .setattr = ntfs_setattr, .getattr = ntfs_getattr, .listxattr = ntfs_listxattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .get_acl = ntfs_get_acl, .set_acl = ntfs_set_acl, }; diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 8d9475788845..206db4e99ef1 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -537,6 +537,7 @@ void __ntfs_init_inode(struct super_block *sb, struct ntfs_inode *ni) ni->reparse_tag = 0; ni->reparse_flags = 0; ni->target = NULL; + ni->linuxflags = 0; ni->i_dealloc_clusters = 0; } @@ -727,10 +728,8 @@ static bool ntfs_non_resident_sizes_inconsistent(struct inode *vi, * Q: What locks are held when the function is called? * A: i_state has I_NEW set, hence the inode is locked, also * i_count is set to 1, so it is not going to go away - * i_flags is set to 0 and we have no business touching it. Only an ioctl() - * is allowed to write to them. We should of course be honouring them but - * we need to do that using the IS_* macros defined in include/linux/fs.h. - * In any case ntfs_read_locked_inode() has nothing to do with i_flags. + * i_flags is initialized from the persisted Linux file attributes and + * the driver's system-file protection policy before publishing the inode. * * Return 0 on success and -errno on error. */ @@ -934,6 +933,11 @@ static int ntfs_read_locked_inode(struct inode *vi) if (!err) { NInoSetHasEA(ni); ntfs_ea_get_wsl_inode(vi, &dev, flags, &has_lxmod); + err = ntfs_ea_get_linuxflags(vi); + if (err) + goto unm_err_out; + } else if (err != -ENOENT) { + goto unm_err_out; } if (ni->flags & FILE_ATTR_REPARSE_POINT) { diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h index 824cd6f79b7e..a18cbcf1c631 100644 --- a/fs/ntfs/inode.h +++ b/fs/ntfs/inode.h @@ -95,6 +95,7 @@ enum ntfs_inode_mutex_lock_class { * belongs. * @i_dealloc_clusters: delayed allocated clusters. * @target: symlink buffer. + * @linuxflags: Linux-specific file attributes loaded from the $LINUXFLAGS EA. */ struct ntfs_inode { rwlock_t size_lock; @@ -146,6 +147,7 @@ struct ntfs_inode { __le32 reparse_tag; __le32 reparse_flags; char *target; + u32 linuxflags; }; /* diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index c9dc26906617..62b01646c262 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -1660,7 +1660,6 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir, * Inode operations for directories. */ const struct inode_operations ntfs_dir_inode_ops = { - .fileattr_get = ntfs_fileattr_get, .lookup = ntfs_lookup, /* VFS: Lookup directory. */ .create = ntfs_create, .unlink = ntfs_unlink, @@ -1672,6 +1671,8 @@ const struct inode_operations ntfs_dir_inode_ops = { .listxattr = ntfs_listxattr, .setattr = ntfs_setattr, .getattr = ntfs_getattr, + .fileattr_get = ntfs_fileattr_get, + .fileattr_set = ntfs_fileattr_set, .symlink = ntfs_symlink, .mknod = ntfs_mknod, .link = ntfs_link, diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 8c344794f87e..fac72d40f583 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -186,6 +186,8 @@ extern const struct file_operations ntfs_empty_file_ops; extern const struct inode_operations ntfs_empty_inode_ops; int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa); +int ntfs_fileattr_set(const struct mnt_idmap *idmap, struct dentry *dentry, + struct file_kattr *fa); extern const struct export_operations ntfs_export_ops;