* [PATCH v4 01/10] ntfs: keep the EA presence flag after creating the first entry
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 02/10] ntfs: validate EA chains and link zero-terminated tails Baolin Liu
` (9 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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] 15+ messages in thread* [PATCH v4 02/10] ntfs: validate EA chains and link zero-terminated tails
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
2026-09-23 6:02 ` [PATCH v4 01/10] ntfs: keep the EA presence flag after creating the first entry Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 03/10] ntfs: propagate EA write and truncate errors Baolin Liu
` (8 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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, set and list paths. Keep ENOENT for a missing name and translate it
to ENODATA only at the xattr read boundary. Preserve lower-level read
errors.
Reuse the same full-chain validation in ntfs_listxattr(), including
size-only queries, before copying names. Reject malformed lists with
EUCLEAN instead of returning an empty or partial list.
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
fs/ntfs/ea.c | 81 +++++++++++++++++++++++++++++++---------------------
1 file changed, 48 insertions(+), 33 deletions(-)
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index bb97df3045a0..2ee700f8227e 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;
@@ -537,7 +558,7 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
goto out;
}
if (ea_info_size != sizeof(struct ea_information)) {
- err = -EIO;
+ err = -EUCLEAN;
goto out;
}
@@ -550,32 +571,26 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
goto out;
}
- if (ea_info_qsize > ea_buf_size || ea_info_qsize == 0)
+ if (ea_info_qsize > ea_buf_size) {
+ err = -EUCLEAN;
+ goto out;
+ }
+
+ /* Validate the entire chain, including size-only queries. */
+ err = ntfs_ea_lookup(ea_buf, ea_info_qsize, NULL, 0, &offset,
+ &ea_size);
+ if (err != -ENOENT)
+ goto out;
+ err = 0;
+ if (!ea_info_qsize)
goto out;
offset = 0;
do {
- if (ea_info_qsize - offset < sizeof(struct ea_attr)) {
- err = -EIO;
- goto out;
- }
-
p_ea = (const struct ea_attr *)&ea_buf[offset];
next = le32_to_cpu(p_ea->next_entry_offset);
ea_size = next ? next : (ea_info_qsize - offset);
- if (ea_size < sizeof(struct ea_attr) ||
- offset + ea_size > ea_info_qsize) {
- err = -EIO;
- goto out;
- }
-
- if ((int)p_ea->ea_name_length + 1 >
- ea_size - offsetof(struct ea_attr, ea_name)) {
- err = -EIO;
- goto out;
- }
-
if (buffer) {
if (ret + p_ea->ea_name_length + 1 > size) {
err = -ERANGE;
--
2.51.0
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH v4 03/10] ntfs: propagate EA write and truncate errors
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
2026-09-23 6:02 ` [PATCH v4 01/10] ntfs: keep the EA presence flag after creating the first entry Baolin Liu
2026-09-23 6:02 ` [PATCH v4 02/10] ntfs: validate EA chains and link zero-terminated tails Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 04/10] ntfs: preserve EA attribute lookup errors Baolin Liu
` (7 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 2ee700f8227e..98ca9e4f8d25 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] 15+ messages in thread* [PATCH v4 04/10] ntfs: preserve EA attribute lookup errors
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (2 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 03/10] ntfs: propagate EA write and truncate errors Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 05/10] ntfs: distinguish empty EA values from removal Baolin Liu
` (6 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 98ca9e4f8d25..6f7273f754ae 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] 15+ messages in thread* [PATCH v4 05/10] ntfs: distinguish empty EA values from removal
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (3 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 04/10] ntfs: preserve EA attribute lookup errors Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 06/10] ntfs: restore EA attributes when an update fails Baolin Liu
` (5 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 6f7273f754ae..03117749728b 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] 15+ messages in thread* [PATCH v4 06/10] ntfs: restore EA attributes when an update fails
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (4 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 05/10] ntfs: distinguish empty EA values from removal Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 07/10] ntfs: add fileattr query support Baolin Liu
` (4 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 03117749728b..8eafc7b53aab 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] 15+ messages in thread* [PATCH v4 07/10] ntfs: add fileattr query support
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (5 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 06/10] ntfs: restore EA attributes when an update fails Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 08/10] ntfs: persist immutable in the $LINUXFLAGS EA Baolin Liu
` (3 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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] 15+ messages in thread* [PATCH v4 08/10] ntfs: persist immutable in the $LINUXFLAGS EA
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (6 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 07/10] ntfs: add fileattr query support Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 09/10] ntfs: persist append-only " Baolin Liu
` (2 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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 $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>
---
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..173af66ae73f 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 8eafc7b53aab..daa3b430ca76 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_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);
@@ -852,6 +897,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 acb39c2a6fbc..5ec118e29425 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 e5846bf498b0..135e5be69e77 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 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;
+}
+
/*
* 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..9240f92b9279 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->linuxflags = 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_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 ff61bd402df0..d9168c7eb701 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;
@@ -145,6 +146,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 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] 15+ messages in thread* [PATCH v4 09/10] ntfs: persist append-only in the $LINUXFLAGS EA
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (7 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 08/10] ntfs: persist immutable in the $LINUXFLAGS EA Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-09-23 6:02 ` [PATCH v4 10/10] ntfs: persist nodump " Baolin Liu
2026-10-12 3:16 ` [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA liubaolin
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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
LINUXFLAGS. 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 173af66ae73f..fb9c89b9b493 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 ``$LINUXFLAGS`` 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 ``$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
+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 daa3b430ca76..4c99743c4dee 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -500,6 +500,8 @@ int ntfs_ea_get_linuxflags(struct inode *inode)
ni->linuxflags = le32_to_cpu(value);
if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)
inode->i_flags |= S_IMMUTABLE;
+ if (ni->linuxflags & NTFS_LINUXFLAGS_APPEND)
+ inode->i_flags |= S_APPEND;
return 0;
}
diff --git a/fs/ntfs/ea.h b/fs/ntfs/ea.h
index 5ec118e29425..b988b39f5f00 100644
--- a/fs/ntfs/ea.h
+++ b/fs/ntfs/ea.h
@@ -9,7 +9,9 @@
/* $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
+#define NTFS_LINUXFLAGS_APPEND BIT(1)
+#define NTFS_LINUXFLAGS_MASK (NTFS_LINUXFLAGS_IMMUTABLE | \
+ NTFS_LINUXFLAGS_APPEND)
extern const struct xattr_handler *const ntfs_xattr_handlers[];
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 135e5be69e77..cda8d02c130e 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 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_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->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE))
linuxflags |= NTFS_LINUXFLAGS_IMMUTABLE;
}
+ if (fa->flags & FS_APPEND_FL) {
+ new_fl |= S_APPEND;
+ linuxflags |= NTFS_LINUXFLAGS_APPEND;
+ }
mutex_lock(&ni->mrec_lock);
err = ntfs_ea_set_linuxflags(vi, linuxflags);
@@ -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] 15+ messages in thread* [PATCH v4 10/10] ntfs: persist nodump in the $LINUXFLAGS EA
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (8 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 09/10] ntfs: persist append-only " Baolin Liu
@ 2026-09-23 6:02 ` Baolin Liu
2026-10-12 3:16 ` [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA liubaolin
10 siblings, 0 replies; 15+ messages in thread
From: Baolin Liu @ 2026-09-23 6:02 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 LINUXFLAGS 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>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
---
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 fb9c89b9b493..d305fd4314d2 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 ``$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. 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 ``$LINUXFLAGS`` 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 b988b39f5f00..8f144fa4f9bb 100644
--- a/fs/ntfs/ea.h
+++ b/fs/ntfs/ea.h
@@ -10,8 +10,9 @@
/* $LINUXFLAGS stores these bits in a single little-endian 32-bit value. */
#define NTFS_LINUXFLAGS_IMMUTABLE BIT(0)
#define NTFS_LINUXFLAGS_APPEND BIT(1)
+#define NTFS_LINUXFLAGS_NODUMP BIT(2)
#define NTFS_LINUXFLAGS_MASK (NTFS_LINUXFLAGS_IMMUTABLE | \
- NTFS_LINUXFLAGS_APPEND)
+ NTFS_LINUXFLAGS_APPEND | NTFS_LINUXFLAGS_NODUMP)
extern const struct xattr_handler *const ntfs_xattr_handlers[];
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index cda8d02c130e..38309cbd98c2 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->linuxflags & NTFS_LINUXFLAGS_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 linuxflags = ni->linuxflags & ~NTFS_LINUXFLAGS_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;
linuxflags |= NTFS_LINUXFLAGS_APPEND;
}
+ if (fa->flags & FS_NODUMP_FL)
+ linuxflags |= NTFS_LINUXFLAGS_NODUMP;
mutex_lock(&ni->mrec_lock);
err = ntfs_ea_set_linuxflags(vi, linuxflags);
@@ -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->linuxflags & NTFS_LINUXFLAGS_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] 15+ messages in thread* Re: [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA
2026-09-23 6:02 [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA Baolin Liu
` (9 preceding siblings ...)
2026-09-23 6:02 ` [PATCH v4 10/10] ntfs: persist nodump " Baolin Liu
@ 2026-10-12 3:16 ` liubaolin
2026-10-12 4:43 ` Hyunchul Lee
10 siblings, 1 reply; 15+ messages in thread
From: liubaolin @ 2026-10-12 3:16 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
在 2026/9/23 14:02, Baolin Liu 写道:
> From: Baolin Liu <liubaolin@kylinos.cn>
>
> This series implements persistent immutable, append-only and nodump flags
> using a private $LINUXFLAGS EA in the standard NTFS $EA/$EA_INFORMATION
> attributes. No new NTFS attribute type or formatter changes are needed.
>
> The value is a little-endian u32 with immutable, append-only and nodump
> in bits 0, 1 and 2. Inode loading restores the flags. Unknown bits are
> preserved, and the entry is removed only when its entire value is zero.
> The setter protects system metadata and sys_immutable-derived immutable
> state, rejects direct writes to $LINUXFLAGS, and updates in-memory flags
> only after the EA update succeeds. Fileattr queries also report mount-wide
> case folding. Nodump is reported through both fileattr and statx.
>
> Changes since v3 (the standalone revision of patch 2):
> - Rename the private EA from $LXFLAGS to $LINUXFLAGS, as agreed with
> Hyunchul, to avoid the WSL-owned $LX namespace. Rename the related
> internal fields, helpers and constants, and update the documentation.
> - Include the v3 patch-2 changes: ntfs_listxattr() reuses full-chain
> validation, including size-only queries, and returns EUCLEAN for
> malformed EA structures while preserving lower-level read errors.
> - Resend the complete ten-patch series with Hyunchul's Reviewed-by tag.
>
> Changes since v2:
> - The EA rename above and the listxattr validation requested during
> review. The on-disk bit assignments and update/rollback behavior
> remain unchanged.
>
> Patch overview:
> - Patch 1 fixes the cached EA presence flag after creating or replacing
> the only entry.
> - Patch 2 validates EA chains and links zero-terminated tails before
> appending entries.
> - Patch 3 preserves EA write errors and reports truncate failures.
> - Patch 4 distinguishes missing attributes from lookup failures and
> rejects incomplete EA attribute pairs.
> - Patch 5 distinguishes empty EA values from deletion and rejects
> removal or replacement of absent entries.
> - Patch 6 prepares the new list before updating attributes, restores
> both old attributes on failure, and shuts down if restoration fails.
> - Patch 7 adds fileattr queries for existing inode flags and mount-wide
> case folding.
> - Patch 8 introduces the $LINUXFLAGS storage and inode-load paths, adds
> immutable persistence, and protects system-imposed immutable state.
> - Patch 9 extends the persistent interface to append-only.
> - Patch 10 adds persistent nodump and reports it through fileattr and
> statx.
>
> Testing on this revision:
> - QEMU tests covered flag and EA readback, inode cache reclaim,
> read-only/read-write remount, unmount/mount and fresh-kernel reload.
> Checked immutable write/unlink denial, append-only enforcement,
> normal I/O after clearing flags, and read-only SETFLAGS rejection.
> - Verified EPERM when clearing $Bitmap protection, including without
> sys_immutable, and when clearing sys_immutable-derived protection.
> $MFT rejects SETFLAGS with ENOTTY through its empty operations.
> Checked casefold reporting and STATX_ATTR_NODUMP.
> - Exercised first-EA creation/removal, XATTR_CREATE/REPLACE, zero-length
> values, zero-terminated tails, unrelated EA preservation, unknown-bit
> preservation, and direct LINUXFLAGS write/removal rejection.
> - listxattr tests covered exact names and sizes, zero-terminated and
> empty lists, short buffers, and malformed offsets, name terminators,
> values and entry headers. Size-only, full-buffer and tiny-buffer
> requests returned EUCLEAN for malformed chains after a successful open.
> - Malformed LINUXFLAGS length, chain offsets, name termination, value
> length and query length returned EUCLEAN. A corrupt tail following
> LINUXFLAGS prevented an update without changing the existing flags.
> An invalid resident EA_INFORMATION length rejected earlier by the
> underlying attribute parser retained that parser's EIO.
> - Fault injection covered allocation failure, failures after each EA
> attribute update and rollback failure. Tested creation, replacement
> and removal with no other EAs, resident EAs and an 8 KiB non-resident
> EA; checked old values and protection after failure and remount, and
> shutdown when restoration failed.
> - Additional injection checked ENOSPC write errors, short writes,
> truncate errors, ENOMEM/EIO lookup errors and missing EA attribute
> pairs, with old EA contents verified after remount.
> - Formatted a SanDisk USB partition inside QEMU using the final kernel,
> and created combined and individual flag probes through SETFLAGS.
> A fresh QEMU boot with both disk and mount read-only confirmed flags
> 0x70/0x10/0x20/0x40 and LINUXFLAGS values 7/1/2/4, directory nodump,
> absence of the cleared entry, unchanged canary EA and statx nodump.
>
> The USB was freshly formatted with the label LINUXFLAGS_V4. The final
> read-only check verified that $LINUXFLAGS is present with the expected
> four-byte values and that the obsolete $LXFLAGS entry is absent.
>
> Windows compatibility testing with the new $LINUXFLAGS name completed
> on Windows build 26200.9457 (ver: 10.0.26200.9457), using the LINUXFLAGS_V4
> USB volume (serial 3B3F-1D09). Windows mounted the volume and accessed
> the existing probes with dir/type. After changing the current directory
> to C:\, chkdsk G: /f completed all three stages and reported no filesystem
> problems and no further action required.
>
> After returning the USB to Linux, a fresh QEMU guest running the final
> v4 kernel checked it with both the virtual disk and NTFS mount read-only.
> GETFLAGS and EA reads confirmed combined flags 0x70 and individual flags
> 0x10/0x20/0x40, with four-byte $LINUXFLAGS values 7/1/2/4 unchanged.
> Directory nodump remained set, the cleared entry remained absent, the
> unrelated canary EA was unchanged, and statx still reported nodump.
> The obsolete $LXFLAGS entry was absent.
>
> The tested Windows mount/access/CHKDSK sequence therefore preserved the
> $LINUXFLAGS names, lengths and values on this Windows build.
>
> Baolin Liu (10):
> ntfs: keep the EA presence flag after creating the first entry
> ntfs: validate EA chains and link zero-terminated tails
> ntfs: propagate EA write and truncate errors
> ntfs: preserve EA attribute lookup errors
> ntfs: distinguish empty EA values from removal
> ntfs: restore EA attributes when an update fails
> ntfs: add fileattr query support
> ntfs: persist immutable in the $LINUXFLAGS EA
> ntfs: persist append-only in the $LINUXFLAGS EA
> ntfs: persist nodump in the $LINUXFLAGS EA
>
> Documentation/filesystems/ntfs.rst | 24 ++
> fs/ntfs/ea.c | 483 +++++++++++++++--------------
> fs/ntfs/ea.h | 9 +
> fs/ntfs/file.c | 125 +++++++-
> fs/ntfs/inode.c | 12 +-
> fs/ntfs/inode.h | 2 +
> fs/ntfs/namei.c | 2 +
> fs/ntfs/ntfs.h | 4 +
> 8 files changed, 423 insertions(+), 238 deletions(-)
>
Hi Namjae and Hyunchul,
Are there any remaining concerns or changes needed for this patch
series?
I'd be happy to address any feedback.
Thanks,
Baolin.
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA
2026-10-12 3:16 ` [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA liubaolin
@ 2026-10-12 4:43 ` Hyunchul Lee
2026-10-12 5:06 ` Namjae Jeon
0 siblings, 1 reply; 15+ messages in thread
From: Hyunchul Lee @ 2026-10-12 4:43 UTC (permalink / raw)
To: liubaolin; +Cc: linkinjeon, ntfs, linux-kernel, chenxiaosong, Baolin Liu
Hi Baolin,
>
> Hi Namjae and Hyunchul,
> Are there any remaining concerns or changes needed for this patch
> series?
> I'd be happy to address any feedback.
No, I think he maybe have missed these series.
>
> Thanks,
> Baolin.
>
--
Thanks,
Hyunchul
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA
2026-10-12 4:43 ` Hyunchul Lee
@ 2026-10-12 5:06 ` Namjae Jeon
2026-10-12 8:15 ` liubaolin
0 siblings, 1 reply; 15+ messages in thread
From: Namjae Jeon @ 2026-10-12 5:06 UTC (permalink / raw)
To: Hyunchul Lee; +Cc: liubaolin, ntfs, linux-kernel, chenxiaosong, Baolin Liu
> > Hi Namjae and Hyunchul,
> > Are there any remaining concerns or changes needed for this patch
> > series?
> > I'd be happy to address any feedback.
>
> No, I think he maybe have missed these series.
Sorry for missing this patches. I will appy them. If there are any
other patches I am missing, please let me know.
Thank you for the ntfs work!
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v4 00/10] ntfs: persist Linux file attributes in an EA
2026-10-12 5:06 ` Namjae Jeon
@ 2026-10-12 8:15 ` liubaolin
0 siblings, 0 replies; 15+ messages in thread
From: liubaolin @ 2026-10-12 8:15 UTC (permalink / raw)
To: Namjae Jeon, Hyunchul Lee; +Cc: ntfs, linux-kernel, chenxiaosong, Baolin Liu
在 2026/10/12 13:06, Namjae Jeon 写道:
>>> Hi Namjae and Hyunchul,
>>> Are there any remaining concerns or changes needed for this patch
>>> series?
>>> I'd be happy to address any feedback.
>>
>> No, I think he maybe have missed these series.
> Sorry for missing this patches. I will appy them. If there are any
> other patches I am missing, please let me know.
> Thank you for the ntfs work!
Hi Namjae and Hyunchul,
Thank you for your reply and for applying the patches.
Thanks again for your support.
Best regards,
Baolin.
^ permalink raw reply [flat|nested] 15+ messages in thread