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 <liubaolin@kylinos.cn>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Baolin Liu 2026-09-23 14:02:44 +08:00 committed by Namjae Jeon
parent 5656fea4d6
commit f09c78bd62
8 changed files with 171 additions and 11 deletions

View File

@ -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
=======================

View File

@ -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;

View File

@ -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

View File

@ -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,
};

View File

@ -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) {

View File

@ -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;
};
/*

View File

@ -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,

View File

@ -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;