[PATCH v1 1/5] ntfs: initialize inherited ACLs after allocating the MFT record

From: Baolin Liu

Date: Tue Oct 27 2026 - 05:40:01 EST


From: Baolin Liu <liubaolin@xxxxxxxxxx>

ntfs_init_acl() can write EAs, but __ntfs_create() calls it before
allocating the new inode's MFT record. An inherited access ACL can appear
correct in the inode cache and disappear after eviction.

Initialize the ACL after allocating the MFT record and adding the standard
attributes. Release the parent and child record locks around the ACL
helpers to avoid recursive locking, then refresh the local mode from the
inode. On creation failure, remove any EA and EA_INFORMATION attributes
before freeing the MFT record.

Signed-off-by: Baolin Liu <liubaolin@xxxxxxxxxx>
---
QEMU tests used the complete five-patch series with NTFS debug and
lockdep enabled. An ordinary new file inherits an access ACL from a
parent default ACL containing a named-user entry; that access ACL remains
readable after inode eviction. An anonymous file's inherited access ACL
also remains readable after publication, cache eviction, read-only and
read-write remounts, unmount/mount and a fresh boot. Offline inspection
confirmed that the recorded base MFT record was freed after an injected
creation failure following ACL and WSL EA initialization.

fs/ntfs/namei.c | 44 +++++++++++++++++++++++++++++++++-----------
1 file changed, 33 insertions(+), 11 deletions(-)

diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 0db7da0..0c84a5c 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -422,6 +422,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
struct ntfs_volume *vol = dir_ni->vol;
struct ntfs_inode *ni;
bool rollback_data = false, rollback_sd = false, rollback_reparse = false;
+ bool rollback_ea = false;
struct file_name_attr *fn = NULL;
struct standard_information *si = NULL;
int err = 0, fn_len, si_len;
@@ -429,6 +430,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
struct mft_record *ni_mrec, *dni_mrec;
struct super_block *sb = dir_ni->vol->sb;
struct inode *rollback_data_vi = NULL, *rollback_sd_vi = NULL;
+ struct inode *rollback_ea_vi = NULL, *rollback_eai_vi = NULL;
__le64 parent_mft_ref;
u64 child_mft_ref;
__le16 ea_size;
@@ -469,17 +471,6 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d

mode = vi->i_mode;

-#ifdef CONFIG_NTFS_FS_POSIX_ACL
- if (!S_ISLNK(mode) && (sb->s_flags & SB_POSIXACL)) {
- err = ntfs_init_acl(idmap, vi, dir);
- if (err)
- goto err_out;
- } else
-#endif
- {
- vi->i_flags |= S_NOSEC;
- }
-
if (uid_valid(vol->uid))
vi->i_uid = vol->uid;

@@ -644,6 +635,24 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
goto err_out;
}

+ /* ACL EAs need the allocated MFT record and initialized attributes. */
+ rollback_ea = true;
+#ifdef CONFIG_NTFS_FS_POSIX_ACL
+ if (!S_ISLNK(mode) && (sb->s_flags & SB_POSIXACL)) {
+ /* The ACL helpers acquire both the parent's and child's locks. */
+ mutex_unlock(&dir_ni->mrec_lock);
+ mutex_unlock(&ni->mrec_lock);
+ err = ntfs_init_acl(idmap, vi, dir);
+ mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
+ mutex_lock_nested(&dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
+ if (err)
+ goto err_out;
+ mode = vi->i_mode;
+ }
+#endif
+ if (S_ISLNK(mode) || !(sb->s_flags & SB_POSIXACL))
+ vi->i_flags |= S_NOSEC;
+
err = ntfs_ea_set_wsl_inode(vi, dev, &ea_size,
NTFS_EA_UID | NTFS_EA_GID | NTFS_EA_MODE);
if (err)
@@ -718,6 +727,15 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
return ni;

err_out:
+ if (rollback_ea) {
+ if (ntfs_attr_exist(ni, AT_EA, AT_UNNAMED, 0) &&
+ ntfs_attr_remove(ni, AT_EA, AT_UNNAMED, 0, &rollback_ea_vi))
+ ntfs_error(sb, "Failed to roll back EA attribute.");
+ if (ntfs_attr_exist(ni, AT_EA_INFORMATION, AT_UNNAMED, 0) &&
+ ntfs_attr_remove(ni, AT_EA_INFORMATION, AT_UNNAMED, 0,
+ &rollback_eai_vi))
+ ntfs_error(sb, "Failed to roll back EA_INFORMATION attribute.");
+ }
if (rollback_sd) {
int rollback_err;

@@ -764,6 +782,10 @@ err_out:
mutex_unlock(&dir_ni->mrec_lock);
mutex_unlock(&ni->mrec_lock);

+ if (rollback_ea_vi)
+ iput(rollback_ea_vi);
+ if (rollback_eai_vi)
+ iput(rollback_eai_vi);
if (rollback_data_vi)
iput(rollback_data_vi);
if (rollback_sd_vi)