* [PATCH v2 01/10] ntfs: keep the EA presence flag after creating the first entry
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails Baolin Liu
` (8 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
The successful EA update path tests ea_info_qsize when updating
NInoHasEA, but this size excludes the appended entry. Creating the first
EA, or replacing the only entry, can therefore clear the cached presence
flag despite a successful update. Reads then return ENODATA until reload.
Include the appended entry in the size used for the presence test.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index b4fcfbe2da4c..bb97df3045a0 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -402,6 +402,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
if (packed_ea_size)
*packed_ea_size = p_ea_info->ea_length;
+ ea_info_qsize += new_ea_size;
mark_mft_record_dirty(ni);
out:
if (!err) {
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
2026-09-20 8:31 ` [PATCH v2 01/10] ntfs: keep the EA presence flag after creating the first entry Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-21 6:17 ` Namjae Jeon
2026-09-20 8:31 ` [PATCH v2 03/10] ntfs: propagate EA write and truncate errors Baolin Liu
` (7 subsequent siblings)
9 siblings, 1 reply; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
A zero next-entry offset terminates an EA list. Appending an entry
without linking the old tail makes the new entry unreachable. Invalid
entries are also treated as missing names, allowing setters to proceed
on corrupt metadata.
Validate the entire chain before modifying it and connect a zero tail
to the append position. Check name terminators and retain the full tail
span when removing an entry. Return EUCLEAN for malformed EA structures
and inconsistent information lengths, preserving that error through the
get and set paths. Keep ENOENT for a missing name and translate it to
ENODATA only at the xattr read boundary. Preserve lower-level read errors.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 49 +++++++++++++++++++++++++++++++++++--------------
1 file changed, 35 insertions(+), 14 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index bb97df3045a0..59160e49a53f 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -53,10 +53,13 @@ static int ntfs_ea_lookup(char *ea_buf, s64 ea_buf_size, const char *name,
loff_t offset, p_ea_size;
unsigned int next;
+ if (!ea_buf_size)
+ return -ENOENT;
+
offset = 0;
do {
if (ea_buf_size - offset < sizeof(struct ea_attr))
- break;
+ return -EUCLEAN;
p_ea = (const struct ea_attr *)&ea_buf[offset];
next = le32_to_cpu(p_ea->next_entry_offset);
@@ -64,31 +67,31 @@ static int ntfs_ea_lookup(char *ea_buf, s64 ea_buf_size, const char *name,
if (p_ea_size < sizeof(struct ea_attr) ||
offset + p_ea_size > ea_buf_size)
- break;
+ return -EUCLEAN;
if ((s64)p_ea->ea_name_length + 1 >
p_ea_size - offsetof(struct ea_attr, ea_name))
- break;
+ return -EUCLEAN;
actual_size = ALIGN(struct_size(p_ea, ea_name, 1 + p_ea->ea_name_length +
le16_to_cpu(p_ea->ea_value_length)), 4);
- if (actual_size > p_ea_size)
- break;
+ if (actual_size > p_ea_size ||
+ p_ea->ea_name[p_ea->ea_name_length])
+ return -EUCLEAN;
- if (p_ea->ea_name_length == name_len &&
+ if (name && p_ea->ea_name_length == name_len &&
!memcmp(p_ea->ea_name, name, name_len)) {
*ea_offset = offset;
- *ea_size = next ? next : actual_size;
+ *ea_size = p_ea_size;
if (ea_buf_size < *ea_offset + *ea_size)
- goto out;
+ return -EUCLEAN;
return 0;
}
offset += next;
} while (next > 0 && offset < ea_buf_size);
-out:
return -ENOENT;
}
@@ -126,7 +129,7 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
return PTR_ERR(p_ea_info);
if (ea_info_size != sizeof(struct ea_information)) {
kvfree(p_ea_info);
- return -EIO;
+ return -EUCLEAN;
}
ea_info_qlen = le32_to_cpu(p_ea_info->ea_query_length);
@@ -137,7 +140,7 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
return PTR_ERR(ea_buf);
if (ea_info_qlen > all_ea_size) {
- err = -EIO;
+ err = -EUCLEAN;
goto free_ea_buf;
}
@@ -162,7 +165,8 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
return ea_value_len;
}
- err = -ENODATA;
+ if (err == -ENOENT)
+ err = -ENODATA;
free_ea_buf:
kvfree(ea_buf);
return err;
@@ -216,7 +220,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
goto out;
}
if (ea_info_size != sizeof(struct ea_information)) {
- err = -EIO;
+ err = -EUCLEAN;
goto out;
}
@@ -255,12 +259,29 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
}
if (ea_info_qsize > all_ea_size) {
- err = -EIO;
+ err = -EUCLEAN;
goto out;
}
+ /* Validate the whole chain before modifying it, including its tail. */
+ err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &ea_off,
+ &ea_size);
+ if (err != -ENOENT)
+ goto out;
+ /* A zero tail offset must be linked before appending another EA. */
+ for (ea_off = 0; ea_off < ea_info_qsize; ea_off += ea_size) {
+ p_ea = (struct ea_attr *)(ea_buf + ea_off);
+ ea_size = le32_to_cpu(p_ea->next_entry_offset);
+ if (!ea_size) {
+ ea_size = ea_info_qsize - ea_off;
+ p_ea->next_entry_offset = cpu_to_le32(ea_size);
+ }
+ }
+
err = ntfs_ea_lookup(ea_buf, ea_info_qsize, name, name_len, &ea_off,
&ea_size);
+ if (err && err != -ENOENT)
+ goto out;
if (ea_info_qsize && !err) {
if (flags & XATTR_CREATE) {
err = -EEXIST;
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails
2026-09-20 8:31 ` [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails Baolin Liu
@ 2026-09-21 6:17 ` Namjae Jeon
2026-09-21 10:34 ` liubaolin
0 siblings, 1 reply; 17+ messages in thread
From: Namjae Jeon @ 2026-09-21 6:17 UTC (permalink / raw)
To: Baolin Liu; +Cc: hyc.lee, ntfs, linux-kernel, chenxiaosong, Baolin Liu
On Sun, Sep 20, 2026 at 5:31 PM Baolin Liu <liubaolin12138@163.com> wrote:
>
> From: Baolin Liu <liubaolin@kylinos.cn>
>
> A zero next-entry offset terminates an EA list. Appending an entry
> without linking the old tail makes the new entry unreachable. Invalid
> entries are also treated as missing names, allowing setters to proceed
> on corrupt metadata.
>
> Validate the entire chain before modifying it and connect a zero tail
> to the append position. Check name terminators and retain the full tail
> span when removing an entry. Return EUCLEAN for malformed EA structures
> and inconsistent information lengths, preserving that error through the
> get and set paths. Keep ENOENT for a missing name and translate it to
> ENODATA only at the xattr read boundary. Preserve lower-level read errors.
Could ntfs_listxattr() reuse the same chain validation and return
-EUCLEAN for malformed EA structures? And you can send only this patch
for v3.
Thanks.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails
2026-09-21 6:17 ` Namjae Jeon
@ 2026-09-21 10:34 ` liubaolin
0 siblings, 0 replies; 17+ messages in thread
From: liubaolin @ 2026-09-21 10:34 UTC (permalink / raw)
To: Namjae Jeon; +Cc: hyc.lee, ntfs, linux-kernel, chenxiaosong, Baolin Liu
在 2026/9/21 14:17, Namjae Jeon 写道:
> On Sun, Sep 20, 2026 at 5:31 PM Baolin Liu <liubaolin12138@163.com> wrote:
>>
>> From: Baolin Liu <liubaolin@kylinos.cn>
>>
>> A zero next-entry offset terminates an EA list. Appending an entry
>> without linking the old tail makes the new entry unreachable. Invalid
>> entries are also treated as missing names, allowing setters to proceed
>> on corrupt metadata.
>>
>> Validate the entire chain before modifying it and connect a zero tail
>> to the append position. Check name terminators and retain the full tail
>> span when removing an entry. Return EUCLEAN for malformed EA structures
>> and inconsistent information lengths, preserving that error through the
>> get and set paths. Keep ENOENT for a missing name and translate it to
>> ENODATA only at the xattr read boundary. Preserve lower-level read errors.
> Could ntfs_listxattr() reuse the same chain validation and return
> -EUCLEAN for malformed EA structures? And you can send only this patch
> for v3.
> Thanks.
Hi Namjae,
Thanks for your feedback. I've updated ntfs_listxattr() to reuse the
full-chain validation and return -EUCLEAN for malformed EA
structures,while preserving lower-level read errors.
I've submitted only this revised patch as v3, as requested. I
verified that patches 3-10 of v2 still apply cleanly without
modification, and completed build checks and QEMU tests covering normal
and malformed EA lists.
Please see:
"[PATCH v3 02/10] ntfs: validate EA chains and link zero-terminated
tails"
Thanks,
Baolin.
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2 03/10] ntfs: propagate EA write and truncate errors
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
2026-09-20 8:31 ` [PATCH v2 01/10] ntfs: keep the EA presence flag after creating the first entry Baolin Liu
2026-09-20 8:31 ` [PATCH v2 02/10] ntfs: validate EA chains and link zero-terminated tails Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 04/10] ntfs: preserve EA attribute lookup errors Baolin Liu
` (6 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
ntfs_write_ea() replaces negative write errors with EIO and ignores
errors when shrinking the attribute after writing a shorter list.
This can report success even though the final resize failed.
Preserve negative write errors, use EIO for short writes, and return
truncate failures to the caller.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 59160e49a53f..5c6facae4ed5 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -32,12 +32,12 @@ static int ntfs_write_ea(struct ntfs_inode *ni, __le32 type, char *value, s64 ea
written = ntfs_inode_attr_pwrite(ea_vi, ea_off, ea_size, value, false);
if (written != ea_size)
- err = -EIO;
+ err = written < 0 ? written : -EIO;
else {
struct ntfs_inode *ea_ni = NTFS_I(ea_vi);
if (need_truncate && ea_ni->data_size > ea_off + ea_size)
- ntfs_attr_truncate(ea_ni, ea_off + ea_size);
+ err = ntfs_attr_truncate(ea_ni, ea_off + ea_size);
mark_mft_record_dirty(ni);
}
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 04/10] ntfs: preserve EA attribute lookup errors
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (2 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 03/10] ntfs: propagate EA write and truncate errors Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 05/10] ntfs: distinguish empty EA values from removal Baolin Liu
` (5 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
ntfs_attr_exist() treats lookup failures as absence. Using it while
updating EAs can select creation or removal paths after an allocation
or I/O failure.
Use an EA-local lookup helper that preserves errors from mapping the
MFT record, allocating the search context and looking up the attribute.
Check both EA attributes before modifying either and return EUCLEAN if
only one exists. Keep the known presence state for the final update.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 51 ++++++++++++++++++++++++++++++++++++---------------
1 file changed, 36 insertions(+), 15 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 5c6facae4ed5..f5a112495fc9 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -181,6 +181,29 @@ static inline int ea_packed_size(const struct ea_attr *p_ea)
return 5 + p_ea->ea_name_length + le16_to_cpu(p_ea->ea_value_length);
}
+/* Unlike ntfs_attr_exist(), preserve lookup errors. */
+static int ntfs_ea_attr_lookup(struct ntfs_inode *ni, __le32 type)
+{
+ struct ntfs_attr_search_ctx *ctx;
+ struct mft_record *mrec;
+ int err;
+
+ mrec = map_mft_record(ni);
+ if (IS_ERR(mrec))
+ return PTR_ERR(mrec);
+ ctx = ntfs_attr_get_search_ctx(ni, mrec);
+ if (!ctx) {
+ err = -ENOMEM;
+ goto out;
+ }
+ err = ntfs_attr_lookup(type, AT_UNNAMED, 0, CASE_SENSITIVE,
+ 0, NULL, 0, ctx);
+ ntfs_attr_put_search_ctx(ctx);
+out:
+ unmap_mft_record(ni);
+ return err;
+}
+
/*
* Set a new EA, and set EA_INFORMATION accordingly
*
@@ -198,7 +221,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
{
struct ntfs_inode *ni = NTFS_I(inode);
struct ea_information *p_ea_info = NULL;
- int ea_packed, err = 0;
+ int ea_packed, err = 0, ea_err;
+ bool has_ea;
struct ea_attr *p_ea;
u32 ea_info_qsize = 0;
char *ea_buf = NULL;
@@ -211,7 +235,16 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
if (name_len > 255)
return -ENAMETOOLONG;
- if (ntfs_attr_exist(ni, AT_EA_INFORMATION, AT_UNNAMED, 0)) {
+ err = ntfs_ea_attr_lookup(ni, AT_EA_INFORMATION);
+ if (err && err != -ENOENT)
+ return err;
+ ea_err = ntfs_ea_attr_lookup(ni, AT_EA);
+ if (ea_err && ea_err != -ENOENT)
+ return ea_err;
+ if (!err != !ea_err)
+ return -EUCLEAN;
+ has_ea = !err;
+ if (has_ea) {
p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
&ea_info_size);
if (IS_ERR(p_ea_info)) {
@@ -230,15 +263,9 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
ea_buf = NULL;
goto out;
}
- if (!ea_buf) {
- ea_info_qsize = 0;
- kvfree(p_ea_info);
- goto create_ea_info;
- }
ea_info_qsize = le32_to_cpu(p_ea_info->ea_query_length);
} else {
-create_ea_info:
p_ea_info = kzalloc_obj(struct ea_information, GFP_NOFS);
if (!p_ea_info)
return -ENOMEM;
@@ -249,12 +276,6 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
if (err)
goto out;
- if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) {
- err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0);
- if (err)
- goto out;
- }
-
goto alloc_new_ea;
}
@@ -404,7 +425,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
/*
* no EA or EA_INFORMATION : add them
*/
- if (!ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0)) {
+ if (!has_ea) {
err = ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, ea_buf,
ea_info_qsize + new_ea_size);
if (err)
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 05/10] ntfs: distinguish empty EA values from removal
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (3 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 04/10] ntfs: preserve EA attribute lookup errors Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 06/10] ntfs: restore EA attributes when an update fails Baolin Liu
` (4 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
The EA setter identifies removal using a zero value length together
with XATTR_REPLACE. A replacement with a valid empty value is therefore
mistaken for deletion.
Use a NULL value to identify removal and keep zero-length values valid.
Reject removal or XATTR_REPLACE for an absent entry before creating
metadata, including when the file has no EA attributes.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index f5a112495fc9..15bfcdddc7ee 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -244,6 +244,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
if (!err != !ea_err)
return -EUCLEAN;
has_ea = !err;
+ if (!has_ea && (!value || (flags & XATTR_REPLACE)))
+ return -ENODATA;
if (has_ea) {
p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
&ea_info_size);
@@ -308,7 +310,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
err = -EEXIST;
goto out;
}
- if ((flags & XATTR_REPLACE) && !val_size) {
+ if (!value) {
old_ea_info = *p_ea_info;
old_ea_buf = kvmemdup(ea_buf, all_ea_size, GFP_NOFS);
if (!old_ea_buf) {
@@ -318,7 +320,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
}
/* Check the final $EA size before removing the old entry. */
- if (val_size &&
+ if (value &&
ntfs_attr_size_bounds_check(ni->vol, AT_EA,
ea_info_qsize - ea_size + new_ea_size)) {
err = -EFBIG;
@@ -327,7 +329,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
p_ea = (struct ea_attr *)(ea_buf + ea_off);
- if (val_size &&
+ if (value &&
le16_to_cpu(p_ea->ea_value_length) == val_size &&
!memcmp(p_ea->ea_name + p_ea->ea_name_length + 1, value,
val_size))
@@ -342,7 +344,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
ea_info_qsize -= ea_size;
p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize);
- if ((flags & XATTR_REPLACE) && !val_size && !ea_info_qsize) {
+ if (!value && !ea_info_qsize) {
err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0);
if (err)
goto out;
@@ -357,7 +359,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
goto out;
}
- if ((flags & XATTR_REPLACE) && !val_size) {
+ if (!value) {
err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, ea_info_qsize,
true);
if (err) {
@@ -378,7 +380,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
goto out;
}
} else {
- if (flags & XATTR_REPLACE) {
+ if (!value || (flags & XATTR_REPLACE)) {
err = -ENODATA;
goto out;
}
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 06/10] ntfs: restore EA attributes when an update fails
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (4 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 05/10] ntfs: distinguish empty EA values from removal Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 07/10] ntfs: add fileattr query support Baolin Liu
` (3 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
Creating or replacing an EA can leave EA data and information out of
sync if a later update fails. Existing deletion rollback ignores its
own errors, and creation changes metadata before preparing the new list.
Prepare the complete new list while retaining both original attributes.
Update EA data and information only after preparation succeeds, restore
both on failure, and publish the cached presence state only on success.
Keep structure errors as EUCLEAN and preserve operation errors. Mark the
volume erroneous and shut it down if either restoration fails.
This handles synchronous failures; it does not make the two updates
atomic across power loss or guarantee restoration on a failing device.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
fs/ntfs/ea.c | 349 ++++++++++++++++++++-------------------------------
1 file changed, 138 insertions(+), 211 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 15bfcdddc7ee..888b531e8174 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -204,262 +204,189 @@ static int ntfs_ea_attr_lookup(struct ntfs_inode *ni, __le32 type)
return err;
}
+/* A NULL value means that the attribute should not exist. */
+static int ntfs_ea_update_attr(struct ntfs_inode *ni, __le32 type,
+ char *value, s64 size)
+{
+ int err = ntfs_ea_attr_lookup(ni, type);
+
+ if (err && err != -ENOENT)
+ return err;
+ if (!value)
+ return err == -ENOENT ? 0 :
+ ntfs_attr_remove(ni, type, AT_UNNAMED, 0);
+ if (err == -ENOENT)
+ return ntfs_attr_add(ni, type, AT_UNNAMED, 0, value, size);
+ return ntfs_write_ea(ni, type, value, 0, size, true);
+}
+
/*
- * Set a new EA, and set EA_INFORMATION accordingly
- *
- * This is roughly the same as ZwSetEaFile() on Windows, however
- * the "offset to next" of the last EA should not be cleared.
- *
- * Consistency of the new EA is first checked.
- *
- * EA_INFORMATION is set first, and it is restored to its former
- * state if setting EA fails.
+ * Prepare the new EA list before changing either NTFS attribute. Keep the
+ * original values until both updates succeed, so a failed update can be
+ * rolled back. This does not provide atomicity across a crash or power loss.
*/
static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
const void *value, size_t val_size, int flags,
__le16 *packed_ea_size)
{
struct ntfs_inode *ni = NTFS_I(inode);
- struct ea_information *p_ea_info = NULL;
- int ea_packed, err = 0, ea_err;
- bool has_ea;
- struct ea_attr *p_ea;
- u32 ea_info_qsize = 0;
- char *ea_buf = NULL;
- char *new_ea_buf;
- char *old_ea_buf = NULL;
- struct ea_information old_ea_info;
- size_t new_ea_size = ALIGN(struct_size(p_ea, ea_name, 1 + name_len + val_size), 4);
- s64 ea_off, ea_info_size, all_ea_size, ea_size;
-
- if (name_len > 255)
+ struct ea_information *old_info = NULL, info = {};
+ char *old_ea = NULL, *ea = NULL;
+ struct ea_attr *entry;
+ s64 old_size = 0, info_size, offset = 0, entry_size = 0;
+ u32 size = 0, new_size, next;
+ size_t add_size;
+ int err, info_err, ea_err, packed = 0, needed = 0;
+ bool exists, remove = !value;
+
+ if (name_len > U8_MAX)
return -ENAMETOOLONG;
+ if (val_size > U16_MAX)
+ return -E2BIG;
err = ntfs_ea_attr_lookup(ni, AT_EA_INFORMATION);
if (err && err != -ENOENT)
return err;
- ea_err = ntfs_ea_attr_lookup(ni, AT_EA);
- if (ea_err && ea_err != -ENOENT)
- return ea_err;
- if (!err != !ea_err)
+ exists = !err;
+ err = ntfs_ea_attr_lookup(ni, AT_EA);
+ if (err && err != -ENOENT)
+ return err;
+ /* Do not silently discard an orphaned attribute. */
+ if (exists != !err)
return -EUCLEAN;
- has_ea = !err;
- if (!has_ea && (!value || (flags & XATTR_REPLACE)))
- return -ENODATA;
- if (has_ea) {
- p_ea_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
- &ea_info_size);
- if (IS_ERR(p_ea_info)) {
- err = PTR_ERR(p_ea_info);
- p_ea_info = NULL;
- goto out;
- }
- if (ea_info_size != sizeof(struct ea_information)) {
+
+ if (exists) {
+ old_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
+ &info_size);
+ if (IS_ERR(old_info))
+ return PTR_ERR(old_info);
+ if (info_size != sizeof(*old_info)) {
err = -EUCLEAN;
goto out;
}
-
- ea_buf = ntfs_attr_readall(ni, AT_EA, NULL, 0, &all_ea_size);
- if (IS_ERR(ea_buf)) {
- err = PTR_ERR(ea_buf);
- ea_buf = NULL;
+ old_ea = ntfs_attr_readall(ni, AT_EA, NULL, 0, &old_size);
+ if (IS_ERR(old_ea)) {
+ err = PTR_ERR(old_ea);
+ old_ea = NULL;
goto out;
}
-
- ea_info_qsize = le32_to_cpu(p_ea_info->ea_query_length);
- } else {
- p_ea_info = kzalloc_obj(struct ea_information, GFP_NOFS);
- if (!p_ea_info)
- return -ENOMEM;
-
- ea_info_qsize = 0;
- err = ntfs_attr_add(ni, AT_EA_INFORMATION, AT_UNNAMED, 0,
- (char *)p_ea_info, sizeof(struct ea_information));
- if (err)
+ size = le32_to_cpu(old_info->ea_query_length);
+ if (size > old_size) {
+ err = -EUCLEAN;
goto out;
-
- goto alloc_new_ea;
- }
-
- if (ea_info_qsize > all_ea_size) {
- err = -EUCLEAN;
- goto out;
+ }
}
- /* Validate the whole chain before modifying it, including its tail. */
- err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &ea_off,
- &ea_size);
+ /* Validate even the entries following the one being replaced. */
+ err = ntfs_ea_lookup(old_ea, size, NULL, 0, &offset, &entry_size);
if (err != -ENOENT)
goto out;
- /* A zero tail offset must be linked before appending another EA. */
- for (ea_off = 0; ea_off < ea_info_qsize; ea_off += ea_size) {
- p_ea = (struct ea_attr *)(ea_buf + ea_off);
- ea_size = le32_to_cpu(p_ea->next_entry_offset);
- if (!ea_size) {
- ea_size = ea_info_qsize - ea_off;
- p_ea->next_entry_offset = cpu_to_le32(ea_size);
- }
- }
-
- err = ntfs_ea_lookup(ea_buf, ea_info_qsize, name, name_len, &ea_off,
- &ea_size);
+ err = ntfs_ea_lookup(old_ea, size, name, name_len, &offset, &entry_size);
if (err && err != -ENOENT)
goto out;
- if (ea_info_qsize && !err) {
+ if (!err) {
if (flags & XATTR_CREATE) {
err = -EEXIST;
goto out;
}
- if (!value) {
- old_ea_info = *p_ea_info;
- old_ea_buf = kvmemdup(ea_buf, all_ea_size, GFP_NOFS);
- if (!old_ea_buf) {
- err = -ENOMEM;
- goto out;
- }
- }
-
- /* Check the final $EA size before removing the old entry. */
- if (value &&
- ntfs_attr_size_bounds_check(ni->vol, AT_EA,
- ea_info_qsize - ea_size + new_ea_size)) {
- err = -EFBIG;
- goto out;
- }
-
- p_ea = (struct ea_attr *)(ea_buf + ea_off);
-
- if (value &&
- le16_to_cpu(p_ea->ea_value_length) == val_size &&
- !memcmp(p_ea->ea_name + p_ea->ea_name_length + 1, value,
- val_size))
- goto out;
-
- le16_add_cpu(&p_ea_info->ea_length, 0 - ea_packed_size(p_ea));
-
- if (p_ea->flags & NEED_EA)
- le16_add_cpu(&p_ea_info->need_ea_count, -1);
-
- memmove((char *)p_ea, (char *)p_ea + ea_size, ea_info_qsize - (ea_off + ea_size));
- ea_info_qsize -= ea_size;
- p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize);
-
- if (!value && !ea_info_qsize) {
- err = ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0);
- if (err)
- goto out;
-
- err = ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0);
- if (err) {
- /* Restore the original $EA if $EA_INFORMATION removal failed. */
- ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, old_ea_buf,
- all_ea_size);
- ea_info_qsize = le32_to_cpu(old_ea_info.ea_query_length);
- }
- goto out;
- }
-
- if (!value) {
- err = ntfs_write_ea(ni, AT_EA, ea_buf, 0, ea_info_qsize,
- true);
- if (err) {
- ntfs_write_ea(ni, AT_EA, old_ea_buf, 0,
- all_ea_size, false);
- goto out;
- }
-
- err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info,
- 0, sizeof(struct ea_information), false);
- if (err) {
- ntfs_write_ea(ni, AT_EA, old_ea_buf, 0,
- all_ea_size, false);
- ntfs_write_ea(ni, AT_EA_INFORMATION,
- (char *)&old_ea_info, 0,
- sizeof(old_ea_info), false);
- }
- goto out;
+ entry = (struct ea_attr *)(old_ea + offset);
+ if (!remove && le16_to_cpu(entry->ea_value_length) == val_size &&
+ !memcmp(entry->ea_name + name_len + 1, value, val_size)) {
+ info = *old_info;
+ goto done;
}
} else {
- if (!value || (flags & XATTR_REPLACE)) {
+ if (remove || (flags & XATTR_REPLACE)) {
err = -ENODATA;
goto out;
}
+ offset = size;
+ entry_size = 0;
+ }
- if (ntfs_attr_size_bounds_check(ni->vol, AT_EA,
- ea_info_qsize + new_ea_size)) {
- err = -EFBIG;
+ add_size = remove ? 0 : ALIGN(sizeof(*entry) + name_len + 1 + val_size, 4);
+ new_size = size - entry_size + add_size;
+ if (new_size && ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_size)) {
+ err = -EFBIG;
+ goto out;
+ }
+ if (new_size) {
+ ea = kvzalloc(new_size, GFP_NOFS);
+ if (!ea) {
+ err = -ENOMEM;
goto out;
}
+ if (offset)
+ memcpy(ea, old_ea, offset);
+ if (size > offset + entry_size)
+ memcpy(ea + offset, old_ea + offset + entry_size,
+ size - offset - entry_size);
}
-alloc_new_ea:
- new_ea_buf = kvzalloc(ea_info_qsize + new_ea_size, GFP_NOFS);
- if (!new_ea_buf) {
- err = -ENOMEM;
- goto out;
+ size -= entry_size;
+
+ /* Link a zero-terminated old tail before appending the new entry. */
+ for (offset = 0; offset < size; offset += next) {
+ entry = (struct ea_attr *)(ea + offset);
+ next = le32_to_cpu(entry->next_entry_offset);
+ if (!next) {
+ next = size - offset;
+ entry->next_entry_offset = cpu_to_le32(next);
+ }
+ packed += ea_packed_size(entry);
+ if (entry->flags & NEED_EA)
+ needed++;
}
- if (ea_info_qsize)
- memcpy(new_ea_buf, ea_buf, ea_info_qsize);
- kvfree(ea_buf);
- ea_buf = new_ea_buf;
- p_ea = (struct ea_attr *)(ea_buf + ea_info_qsize);
-
- /*
- * EA and REPARSE_POINT compatibility not checked any more,
- * required by Windows 10, but having both may lead to
- * problems with earlier versions.
- */
- memcpy(p_ea->ea_name, name, name_len);
- p_ea->ea_name_length = name_len;
- p_ea->ea_name[name_len] = 0;
- memcpy(p_ea->ea_name + name_len + 1, value, val_size);
- p_ea->ea_value_length = cpu_to_le16(val_size);
- p_ea->next_entry_offset = cpu_to_le32(new_ea_size);
-
- ea_packed = le16_to_cpu(p_ea_info->ea_length) + ea_packed_size(p_ea);
- p_ea_info->ea_length = cpu_to_le16(ea_packed);
- p_ea_info->ea_query_length = cpu_to_le32(ea_info_qsize + new_ea_size);
-
- if (ea_packed > 0xffff) {
+ if (!remove) {
+ entry = (struct ea_attr *)(ea + size);
+ entry->next_entry_offset = cpu_to_le32(add_size);
+ entry->ea_name_length = name_len;
+ entry->ea_value_length = cpu_to_le16(val_size);
+ memcpy(entry->ea_name, name, name_len);
+ memcpy(entry->ea_name + name_len + 1, value, val_size);
+ packed += ea_packed_size(entry);
+ }
+ if (packed > U16_MAX) {
err = -EFBIG;
goto out;
}
-
- /*
- * no EA or EA_INFORMATION : add them
- */
- if (!has_ea) {
- err = ntfs_attr_add(ni, AT_EA, AT_UNNAMED, 0, ea_buf,
- ea_info_qsize + new_ea_size);
- if (err)
- goto out;
- } else {
- err = ntfs_write_ea(ni, AT_EA, ea_buf, 0,
- ea_info_qsize + new_ea_size, true);
- if (err)
- goto out;
- }
-
- err = ntfs_write_ea(ni, AT_EA_INFORMATION, (char *)p_ea_info, 0,
- sizeof(struct ea_information), false);
- if (err)
+ info.ea_length = cpu_to_le16(packed);
+ info.need_ea_count = cpu_to_le16(needed);
+ info.ea_query_length = cpu_to_le32(new_size);
+
+ /* All preparation is complete; errors from here require rollback. */
+ err = ntfs_ea_update_attr(ni, AT_EA, ea, new_size);
+ if (!err)
+ err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION,
+ new_size ? (char *)&info : NULL,
+ sizeof(info));
+ if (err) {
+ /* Try both restorations even if the first one fails. */
+ ea_err = ntfs_ea_update_attr(ni, AT_EA, old_ea, old_size);
+ info_err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION,
+ (char *)old_info, sizeof(info));
+ if (ea_err || info_err) {
+ NVolSetErrors(ni->vol);
+ NVolSetShutdown(ni->vol);
+ ntfs_error(inode->i_sb,
+ "Failed to restore EAs of inode 0x%llx (%d, %d); shutting down.",
+ ni->mft_no, ea_err, info_err);
+ err = -EIO;
+ }
goto out;
-
- if (packed_ea_size)
- *packed_ea_size = p_ea_info->ea_length;
- ea_info_qsize += new_ea_size;
+ }
+ size = new_size;
mark_mft_record_dirty(ni);
+done:
+ if (packed_ea_size)
+ *packed_ea_size = info.ea_length;
+ if (size)
+ NInoSetHasEA(ni);
+ else
+ NInoClearHasEA(ni);
out:
- if (!err) {
- if (ea_info_qsize > 0)
- NInoSetHasEA(ni);
- else
- NInoClearHasEA(ni);
- }
-
- kvfree(ea_buf);
- kvfree(old_ea_buf);
- kvfree(p_ea_info);
-
+ kvfree(ea);
+ kvfree(old_ea);
+ kvfree(old_info);
return err;
}
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 07/10] ntfs: add fileattr query support
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (5 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 06/10] ntfs: restore EA attributes when an update fails Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA Baolin Liu
` (2 subsequent siblings)
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
Add fileattr_get operations for files and directories, as well as the
existing symlink and special inode operation tables. Report compression,
encryption, immutable and append-only from cached inode state, and
FS_CASEFOLD_FL from the mount-wide case sensitivity policy.
This introduces querying only. It neither reads a private flag EA nor
allows userspace to change the reported flags.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
Documentation/filesystems/ntfs.rst | 8 ++++++++
fs/ntfs/file.c | 33 ++++++++++++++++++++++++++++++
fs/ntfs/namei.c | 1 +
fs/ntfs/ntfs.h | 2 ++
4 files changed, 44 insertions(+)
diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst
index 4bfa392daec6..0a4c43d6c890 100644
--- a/Documentation/filesystems/ntfs.rst
+++ b/Documentation/filesystems/ntfs.rst
@@ -9,6 +9,7 @@ The Linux NTFS filesystem driver
- Overview
- Utilities support
+ - Linux file attributes
- Supported mount options
@@ -34,6 +35,13 @@ The project is available at:
https://github.com/ntfsprogs-plus/ntfsprogs-plus
+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.
+
Supported mount options
=======================
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 99a2c7a5cf81..e5846bf498b0 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -15,6 +15,7 @@
#include <linux/posix_acl_xattr.h>
#include <linux/compat.h>
#include <linux/falloc.h>
+#include <linux/fileattr.h>
#include "lcnalloc.h"
#include "ntfs.h"
@@ -136,6 +137,35 @@ static int ntfs_file_release(struct inode *vi, struct file *filp)
return 0;
}
+/*
+ * ntfs_fileattr_get - inode_operations::fileattr_get
+ * @dentry: dentry to report the flags of
+ * @fa: filled in with the flags of @dentry
+ */
+int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa)
+{
+ struct inode *vi = d_inode(dentry);
+ struct ntfs_inode *ni = NTFS_I(vi);
+ u32 flags = 0;
+
+ if (NVolShutdown(ni->vol))
+ return -EIO;
+
+ if (NInoCompressed(ni) || NInoWofCompressed(ni))
+ flags |= FS_COMPR_FL;
+ if (NInoEncrypted(ni))
+ flags |= FS_ENCRYPT_FL;
+ if (vi->i_flags & S_IMMUTABLE)
+ flags |= FS_IMMUTABLE_FL;
+ if (vi->i_flags & S_APPEND)
+ flags |= FS_APPEND_FL;
+ if (!NVolCaseSensitive(ni->vol))
+ flags |= FS_CASEFOLD_FL;
+
+ fileattr_fill_flags(fa, flags);
+ return 0;
+}
+
/*
* ntfs_file_fsync - sync a file to disk
* @filp: file to be synced
@@ -1234,6 +1264,7 @@ 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,
.listxattr = ntfs_listxattr,
@@ -1243,12 +1274,14 @@ 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,
};
const struct inode_operations ntfs_special_inode_operations = {
+ .fileattr_get = ntfs_fileattr_get,
.setattr = ntfs_setattr,
.getattr = ntfs_getattr,
.listxattr = ntfs_listxattr,
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 3e0adb9a0ea4..ee451700b766 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -1548,6 +1548,7 @@ 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,
diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
index dd56894762a9..a5c5c18671d2 100644
--- a/fs/ntfs/ntfs.h
+++ b/fs/ntfs/ntfs.h
@@ -183,6 +183,8 @@ extern const struct inode_operations ntfs_dir_inode_ops;
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);
+
extern const struct export_operations ntfs_export_ops;
/*
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (6 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 07/10] ntfs: add fileattr query support Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-22 1:21 ` Hyunchul Lee
2026-09-20 8:31 ` [PATCH v2 09/10] ntfs: persist append-only " Baolin Liu
2026-09-20 8:31 ` [PATCH v2 10/10] ntfs: persist nodump " Baolin Liu
9 siblings, 1 reply; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
Add a fileattr setter for immutable and store its state in bit 0 of a
little-endian u32 in the private $LXFLAGS EA. Reuse the standard NTFS EA
attributes and restore the flag when loading an inode, so eviction and
unmount do not discard the setting.
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>
---
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 0a4c43d6c890..ced164216a5b 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 ``$LXFLAGS`` 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 888b531e8174..1e9b66e88655 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -482,6 +482,51 @@ int ntfs_ea_set_wsl_inode(struct inode *inode, dev_t rdev, __le16 *ea_size,
return err;
}
+int ntfs_ea_get_lxflags(struct inode *inode)
+{
+ struct ntfs_inode *ni = NTFS_I(inode);
+ __le32 value;
+ int err;
+
+ err = ntfs_get_ea(inode, "$LXFLAGS", sizeof("$LXFLAGS") - 1,
+ &value, sizeof(value));
+ if (err == -ENODATA)
+ return 0;
+ if (err < 0)
+ return err == -ERANGE ? -EUCLEAN : err;
+ if (err != sizeof(value))
+ return -EUCLEAN;
+
+ ni->lxflags = le32_to_cpu(value);
+ if (ni->lxflags & NTFS_LXFLAGS_IMMUTABLE)
+ inode->i_flags |= S_IMMUTABLE;
+ return 0;
+}
+
+int ntfs_ea_set_lxflags(struct inode *inode, u32 lxflags)
+{
+ struct ntfs_inode *ni = NTFS_I(inode);
+ __le32 value;
+ int err;
+
+ if (lxflags) {
+ value = cpu_to_le32(lxflags);
+ err = ntfs_set_ea(inode, "$LXFLAGS", sizeof("$LXFLAGS") - 1,
+ &value, sizeof(value), 0, NULL);
+ } else if (NInoHasEA(ni)) {
+ err = ntfs_set_ea(inode, "$LXFLAGS", sizeof("$LXFLAGS") - 1,
+ NULL, 0, XATTR_REPLACE, NULL);
+ if (err == -ENODATA)
+ err = 0;
+ } else {
+ err = 0;
+ }
+
+ if (!err)
+ ni->lxflags = lxflags;
+ return err;
+}
+
ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
{
struct inode *inode = d_inode(dentry);
@@ -858,6 +903,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, "$LXFLAGS"))
+ 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 acb39c2a6fbc..19aea162b909 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)
+/* $LXFLAGS stores these bits in a single little-endian 32-bit value. */
+#define NTFS_LXFLAGS_IMMUTABLE BIT(0)
+#define NTFS_LXFLAGS_MASK NTFS_LXFLAGS_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_lxflags(struct inode *inode);
+int ntfs_ea_set_lxflags(struct inode *inode, u32 lxflags);
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 e5846bf498b0..882d33b67560 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -166,6 +166,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(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 lxflags = ni->lxflags & ~NTFS_LXFLAGS_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->lxflags & NTFS_LXFLAGS_IMMUTABLE))
+ lxflags |= NTFS_LXFLAGS_IMMUTABLE;
+ }
+
+ mutex_lock(&ni->mrec_lock);
+ err = ntfs_ea_set_lxflags(vi, lxflags);
+ 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;
+}
+
/*
* ntfs_file_fsync - sync a file to disk
* @filp: file to be synced
@@ -1264,9 +1345,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,
@@ -1274,17 +1356,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 4375ad477809..6841dbc18a20 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -498,6 +498,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->lxflags = 0;
ni->i_dealloc_clusters = 0;
}
@@ -669,10 +670,8 @@ void ntfs_set_vfs_operations(struct inode *inode, mode_t mode, dev_t dev)
* 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.
*/
@@ -871,6 +870,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_lxflags(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 ff61bd402df0..2968ffa92eb0 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.
+ * @lxflags: Linux-specific file attributes loaded from the $LXFLAGS EA.
*/
struct ntfs_inode {
rwlock_t size_lock;
@@ -145,6 +146,7 @@ struct ntfs_inode {
__le32 reparse_tag;
__le32 reparse_flags;
char *target;
+ u32 lxflags;
};
/*
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index ee451700b766..de326886e046 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -1548,7 +1548,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,
@@ -1560,6 +1559,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 a5c5c18671d2..489bf031d1eb 100644
--- a/fs/ntfs/ntfs.h
+++ b/fs/ntfs/ntfs.h
@@ -184,6 +184,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(struct mnt_idmap *idmap, struct dentry *dentry,
+ struct file_kattr *fa);
extern const struct export_operations ntfs_export_ops;
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA
2026-09-20 8:31 ` [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA Baolin Liu
@ 2026-09-22 1:21 ` Hyunchul Lee
2026-09-22 3:26 ` liubaolin
0 siblings, 1 reply; 17+ messages in thread
From: Hyunchul Lee @ 2026-09-22 1:21 UTC (permalink / raw)
To: Baolin Liu; +Cc: linkinjeon, ntfs, linux-kernel, chenxiaosong, Baolin Liu
Hi Baolin,
2026년 9월 20일 (일) 오후 5:32, Baolin Liu <liubaolin12138@163.com>님이 작성:
>
> From: Baolin Liu <liubaolin@kylinos.cn>
>
> Add a fileattr setter for immutable and store its state in bit 0 of a
> little-endian u32 in the private $LXFLAGS EA. Reuse the standard NTFS EA
> attributes and restore the flag when loading an inode, so eviction and
> unmount do not discard the setting.
I think that the name $LXFLAGS is not appropriate. the prefix $LX
seems to be a WSL-owned namespace. The name could cause
confusion or a future semantics collision with Windows WSL.
Thanks,
Hyunchul
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA
2026-09-22 1:21 ` Hyunchul Lee
@ 2026-09-22 3:26 ` liubaolin
2026-09-22 5:25 ` Hyunchul Lee
0 siblings, 1 reply; 17+ messages in thread
From: liubaolin @ 2026-09-22 3:26 UTC (permalink / raw)
To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, chenxiaosong, Baolin Liu
在 2026/9/22 09:21, Hyunchul Lee 写道:
> Hi Baolin,
>
> 2026년 9월 20일 (일) 오후 5:32, Baolin Liu <liubaolin12138@163.com>님이 작성:
>>
>> From: Baolin Liu <liubaolin@kylinos.cn>
>>
>> Add a fileattr setter for immutable and store its state in bit 0 of a
>> little-endian u32 in the private $LXFLAGS EA. Reuse the standard NTFS EA
>> attributes and restore the flag when loading an inode, so eviction and
>> unmount do not discard the setting.
>
>
> I think that the name $LXFLAGS is not appropriate. the prefix $LX
> seems to be a WSL-owned namespace. The name could cause
> confusion or a future semantics collision with Windows WSL.
>
> Thanks,
> Hyunchul
Hi Hyunchul,
Thanks for pointing this out. I agree that we should avoid the $LX
prefix to prevent confusion or a future semantics collision with WSL.
Would $LINUXFLAGS be an acceptable name for this private EA?
If you agree, I'll update the code and documentation, rerun the
relevant tests, including the Windows NTFS and CHKDSK compatibility
tests with the new name, and submit the revised patches as v4.
I look forward to your feedback.
Thanks,
Baolin.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA
2026-09-22 3:26 ` liubaolin
@ 2026-09-22 5:25 ` Hyunchul Lee
2026-09-22 6:02 ` liubaolin
0 siblings, 1 reply; 17+ messages in thread
From: Hyunchul Lee @ 2026-09-22 5:25 UTC (permalink / raw)
To: liubaolin; +Cc: linkinjeon, ntfs, linux-kernel, chenxiaosong, Baolin Liu
2026년 9월 22일 (화) 오후 12:27, liubaolin <liubaolin12138@163.com>님이 작성:
>
>
>
> 在 2026/9/22 09:21, Hyunchul Lee 写道:
> > Hi Baolin,
> >
> > 2026년 9월 20일 (일) 오후 5:32, Baolin Liu <liubaolin12138@163.com>님이 작성:
> >>
> >> From: Baolin Liu <liubaolin@kylinos.cn>
> >>
> >> Add a fileattr setter for immutable and store its state in bit 0 of a
> >> little-endian u32 in the private $LXFLAGS EA. Reuse the standard NTFS EA
> >> attributes and restore the flag when loading an inode, so eviction and
> >> unmount do not discard the setting.
> >
> >
> > I think that the name $LXFLAGS is not appropriate. the prefix $LX
> > seems to be a WSL-owned namespace. The name could cause
> > confusion or a future semantics collision with Windows WSL.
> >
> > Thanks,
> > Hyunchul
>
> Hi Hyunchul,
>
> Thanks for pointing this out. I agree that we should avoid the $LX
> prefix to prevent confusion or a future semantics collision with WSL.
>
> Would $LINUXFLAGS be an acceptable name for this private EA?
>
> If you agree, I'll update the code and documentation, rerun the
> relevant tests, including the Windows NTFS and CHKDSK compatibility
> tests with the new name, and submit the revised patches as v4.
I agree. And others look good to me.
Feel free to add my review tag for this patch set.
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
>
> I look forward to your feedback.
>
> Thanks,
> Baolin.
>
--
Thanks,
Hyunchul
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA
2026-09-22 5:25 ` Hyunchul Lee
@ 2026-09-22 6:02 ` liubaolin
0 siblings, 0 replies; 17+ messages in thread
From: liubaolin @ 2026-09-22 6:02 UTC (permalink / raw)
To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, chenxiaosong, Baolin Liu
在 2026/9/22 13:25, Hyunchul Lee 写道:
> 2026년 9월 22일 (화) 오후 12:27, liubaolin <liubaolin12138@163.com>님이 작성:
>>
>>
>>
>> 在 2026/9/22 09:21, Hyunchul Lee 写道:
>>> Hi Baolin,
>>>
>>> 2026년 9월 20일 (일) 오후 5:32, Baolin Liu <liubaolin12138@163.com>님이 작성:
>>>>
>>>> From: Baolin Liu <liubaolin@kylinos.cn>
>>>>
>>>> Add a fileattr setter for immutable and store its state in bit 0 of a
>>>> little-endian u32 in the private $LXFLAGS EA. Reuse the standard NTFS EA
>>>> attributes and restore the flag when loading an inode, so eviction and
>>>> unmount do not discard the setting.
>>>
>>>
>>> I think that the name $LXFLAGS is not appropriate. the prefix $LX
>>> seems to be a WSL-owned namespace. The name could cause
>>> confusion or a future semantics collision with Windows WSL.
>>>
>>> Thanks,
>>> Hyunchul
>>
>> Hi Hyunchul,
>>
>> Thanks for pointing this out. I agree that we should avoid the $LX
>> prefix to prevent confusion or a future semantics collision with WSL.
>>
>> Would $LINUXFLAGS be an acceptable name for this private EA?
>>
>> If you agree, I'll update the code and documentation, rerun the
>> relevant tests, including the Windows NTFS and CHKDSK compatibility
>> tests with the new name, and submit the revised patches as v4.
>
> I agree. And others look good to me.
> Feel free to add my review tag for this patch set.
>
> Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Hi Hyunchul,
Thanks for your reply. I'll add your Reviewed-by tag to the v4 series.
Thanks,
Baolin.
>
>>
>> I look forward to your feedback.
>>
>> Thanks,
>> Baolin.
>>
>
>
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2 09/10] ntfs: persist append-only in the $LXFLAGS EA
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (7 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 08/10] ntfs: persist immutable in the $LXFLAGS EA Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
2026-09-20 8:31 ` [PATCH v2 10/10] ntfs: persist nodump " Baolin Liu
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
Extend the persistent fileattr interface to append-only using bit 1
of LXFLAGS. Restore S_APPEND on inode load and update it after successful
EA writes. Include append in the known-bit mask while retaining all other
unknown bits. Reuse the existing VFS capability checks and EA safeguards.
Document append-only alongside immutable. Both flags now survive inode
reclaim and remount without changing the NTFS attribute format.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
Documentation/filesystems/ntfs.rst | 10 +++++-----
fs/ntfs/ea.c | 2 ++
fs/ntfs/ea.h | 4 +++-
fs/ntfs/file.c | 13 ++++++-------
4 files changed, 16 insertions(+), 13 deletions(-)
diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst
index ced164216a5b..f00b618a3911 100644
--- a/Documentation/filesystems/ntfs.rst
+++ b/Documentation/filesystems/ntfs.rst
@@ -38,21 +38,21 @@ The project is available at:
Linux file attributes
=====================
-The driver supports lsattr(1) and setting immutable with chattr(1).
-Immutable is stored in bit 0 of a private ``$LXFLAGS`` extended attribute
+The driver supports lsattr(1) and setting immutable and append-only with chattr(1).
+Immutable is stored in bit 0 and append-only in bit 1 of a private ``$LXFLAGS`` 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
+is removed when the entire value becomes zero. These settings survive
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
+that policy is not stored in the EA. Compression, encryption
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.
+Windows does not enforce the Linux immutable and append-only flags stored in this EA.
Supported mount options
=======================
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 1e9b66e88655..ddac912de78c 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -500,6 +500,8 @@ int ntfs_ea_get_lxflags(struct inode *inode)
ni->lxflags = le32_to_cpu(value);
if (ni->lxflags & NTFS_LXFLAGS_IMMUTABLE)
inode->i_flags |= S_IMMUTABLE;
+ if (ni->lxflags & NTFS_LXFLAGS_APPEND)
+ inode->i_flags |= S_APPEND;
return 0;
}
diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h
index 19aea162b909..eac5ab4da44a 100644
--- a/fs/ntfs/ea.h
+++ b/fs/ntfs/ea.h
@@ -9,7 +9,9 @@
/* $LXFLAGS stores these bits in a single little-endian 32-bit value. */
#define NTFS_LXFLAGS_IMMUTABLE BIT(0)
-#define NTFS_LXFLAGS_MASK NTFS_LXFLAGS_IMMUTABLE
+#define NTFS_LXFLAGS_APPEND BIT(1)
+#define NTFS_LXFLAGS_MASK (NTFS_LXFLAGS_IMMUTABLE | \
+ NTFS_LXFLAGS_APPEND)
extern const struct xattr_handler *const ntfs_xattr_handlers[];
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 882d33b67560..5b2a31790275 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -190,7 +190,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
{
struct inode *vi = d_inode(dentry);
struct ntfs_inode *ni = NTFS_I(vi);
- u32 allowed = FS_IMMUTABLE_FL;
+ u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL;
u32 readonly = 0;
u32 lxflags = ni->lxflags & ~NTFS_LXFLAGS_MASK;
unsigned int new_fl = 0;
@@ -203,11 +203,6 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
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;
@@ -234,6 +229,10 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
(ni->lxflags & NTFS_LXFLAGS_IMMUTABLE))
lxflags |= NTFS_LXFLAGS_IMMUTABLE;
}
+ if (fa->flags & FS_APPEND_FL) {
+ new_fl |= S_APPEND;
+ lxflags |= NTFS_LXFLAGS_APPEND;
+ }
mutex_lock(&ni->mrec_lock);
err = ntfs_ea_set_lxflags(vi, lxflags);
@@ -241,7 +240,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
if (err)
return err;
- inode_set_flags(vi, new_fl, S_IMMUTABLE);
+ inode_set_flags(vi, new_fl, S_IMMUTABLE | S_APPEND);
inode_set_ctime_current(vi);
mark_inode_dirty(vi);
return 0;
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread* [PATCH v2 10/10] ntfs: persist nodump in the $LXFLAGS EA
2026-09-20 8:31 [PATCH v2 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (8 preceding siblings ...)
2026-09-20 8:31 ` [PATCH v2 09/10] ntfs: persist append-only " Baolin Liu
@ 2026-09-20 8:31 ` Baolin Liu
9 siblings, 0 replies; 17+ messages in thread
From: Baolin Liu @ 2026-09-20 8:31 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
From: Baolin Liu <liubaolin@kylinos.cn>
Store nodump in bit 2 of LXFLAGS and allow it through fileattr_set.
Report the cached persistent bit through fileattr_get and
STATX_ATTR_NODUMP, including the statx supported-attribute mask. Reuse
the existing EA update and inode-load paths; nodump needs no additional
inode flag enforcement.
Document the complete immutable, append-only and nodump bitmask.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
Documentation/filesystems/ntfs.rst | 34 +++++++++++++++++-------------
fs/ntfs/ea.h | 3 ++-
fs/ntfs/file.c | 11 ++++++++--
3 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/Documentation/filesystems/ntfs.rst b/Documentation/filesystems/ntfs.rst
index f00b618a3911..8b71bc6fca81 100644
--- a/Documentation/filesystems/ntfs.rst
+++ b/Documentation/filesystems/ntfs.rst
@@ -38,21 +38,25 @@ The project is available at:
Linux file attributes
=====================
-The driver supports lsattr(1) and setting immutable and append-only with chattr(1).
-Immutable is stored in bit 0 and append-only in bit 1 of a private ``$LXFLAGS`` 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. These settings survive
-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
-and mount-wide case folding are reported but cannot be changed through
-chattr(1).
-
-Windows does not enforce the Linux immutable and append-only flags stored in this EA.
+The driver supports lsattr(1) and chattr(1). Immutable, append-only and
+nodump are stored in a private ``$LXFLAGS`` extended attribute within the
+standard NTFS ``$EA``/``$EA_INFORMATION`` attributes. Its value is a
+32-bit little-endian bitmask: bit 0 is immutable, bit 1 is append-only,
+and bit 2 is nodump. Unknown bits are preserved when updating the flags;
+the entry is removed when the entire value becomes zero. These settings
+survive inode eviction, unmount and reboot. Direct writes to this EA
+through setxattr(2) or removexattr(2) are rejected; use chattr(1) instead.
+
+System metadata files and files protected by the ``sys_immutable`` mount
+option cannot have their immutable protection cleared with chattr(1).
+Protection derived solely from the mount option is not stored in the EA.
+Case folding is reported according to the mount options and cannot be
+configured per file. Compression and encryption are reported but cannot
+be changed through chattr(1).
+
+These Linux flags are enforced by Linux. Storing them in an NTFS EA does
+not make Windows enforce immutable, append-only or nodump semantics.
+
Supported mount options
=======================
diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h
index eac5ab4da44a..d66cdb4e2fe1 100644
--- a/fs/ntfs/ea.h
+++ b/fs/ntfs/ea.h
@@ -10,8 +10,9 @@
/* $LXFLAGS stores these bits in a single little-endian 32-bit value. */
#define NTFS_LXFLAGS_IMMUTABLE BIT(0)
#define NTFS_LXFLAGS_APPEND BIT(1)
+#define NTFS_LXFLAGS_NODUMP BIT(2)
#define NTFS_LXFLAGS_MASK (NTFS_LXFLAGS_IMMUTABLE | \
- NTFS_LXFLAGS_APPEND)
+ NTFS_LXFLAGS_APPEND | NTFS_LXFLAGS_NODUMP)
extern const struct xattr_handler *const ntfs_xattr_handlers[];
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 5b2a31790275..421dfa34ee7f 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -159,6 +159,8 @@ int ntfs_fileattr_get(struct dentry *dentry, struct file_kattr *fa)
flags |= FS_IMMUTABLE_FL;
if (vi->i_flags & S_APPEND)
flags |= FS_APPEND_FL;
+ if (ni->lxflags & NTFS_LXFLAGS_NODUMP)
+ flags |= FS_NODUMP_FL;
if (!NVolCaseSensitive(ni->vol))
flags |= FS_CASEFOLD_FL;
@@ -190,7 +192,7 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
{
struct inode *vi = d_inode(dentry);
struct ntfs_inode *ni = NTFS_I(vi);
- u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL;
+ u32 allowed = FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL;
u32 readonly = 0;
u32 lxflags = ni->lxflags & ~NTFS_LXFLAGS_MASK;
unsigned int new_fl = 0;
@@ -233,6 +235,8 @@ int ntfs_fileattr_set(struct mnt_idmap *idmap, struct dentry *dentry,
new_fl |= S_APPEND;
lxflags |= NTFS_LXFLAGS_APPEND;
}
+ if (fa->flags & FS_NODUMP_FL)
+ lxflags |= NTFS_LXFLAGS_NODUMP;
mutex_lock(&ni->mrec_lock);
err = ntfs_ea_set_lxflags(vi, lxflags);
@@ -507,9 +511,12 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path,
if (inode->i_flags & S_APPEND)
stat->attributes |= STATX_ATTR_APPEND;
+ if (ni->lxflags & NTFS_LXFLAGS_NODUMP)
+ stat->attributes |= STATX_ATTR_NODUMP;
stat->attributes_mask |= STATX_ATTR_COMPRESSED | STATX_ATTR_ENCRYPTED |
- STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND;
+ STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND |
+ STATX_ATTR_NODUMP;
/*
* If it's a compressed or encrypted file, NTFS currently
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread