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

From: Baolin Liu

Date: Sat Oct 31 2026 - 01:32:50 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.

Initialize the new FILE_NAME ctime from the inode after ACL setup, since
writing the inherited ACL may advance the inode ctime. This also keeps
the directory index copy consistent with STANDARD_INFORMATION.

Signed-off-by: Baolin Liu <liubaolin@xxxxxxxxxx>
---
Tested the standalone five-patch series in QEMU with four CPUs, NTFS
debug and lockdep enabled. Ordinary files inherited an access ACL, and
published temporary-file ACLs remained readable after cache eviction,
remount and reboot. These checks used the existing cached default ACL
path; the separate cold-cache ACL defect is not fixed here.

Changes in v2:
- Initialize FILE_NAME ctime after ACL setup to avoid retaining the
pre-ACL timestamp.
- Refresh context for ntfs-next, including the updated early ACL
error path. The relocated ACL failure still uses MFT/EA rollback.
Existing attribute rollback defects are handled separately.

fs/ntfs/namei.c | 49 +++++++++++++++++++++++++++++++++++--------------
1 file changed, 35 insertions(+), 14 deletions(-)

diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index b46fe44b63b5..14eaf7204ead 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -422,6 +422,7 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in
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(const struct mnt_idmap *idmap, struct in
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,19 +471,6 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in

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) {
- iput(vi);
- return ERR_PTR(err);
- }
- } else
-#endif
- {
- vi->i_flags |= S_NOSEC;
- }
-
if (uid_valid(vol->uid))
vi->i_uid = vol->uid;

@@ -646,6 +635,24 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in
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)
@@ -679,7 +686,8 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in
if (NVolHideDotFiles(vol) && name_len > 0 && name[0] == cpu_to_le16('.'))
fn->file_attributes |= FILE_ATTR_HIDDEN;
fn->creation_time = fn->last_data_change_time = utc2ntfs(ni->i_crtime);
- fn->last_mft_change_time = fn->last_access_time = fn->creation_time;
+ fn->last_mft_change_time = utc2ntfs(inode_get_ctime(vi));
+ fn->last_access_time = fn->creation_time;
memcpy(fn->file_name, name, name_len * sizeof(__le16));

/* Add FILE_NAME attribute to inode. */
@@ -720,6 +728,15 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in
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;

@@ -766,6 +783,10 @@ static struct ntfs_inode *__ntfs_create(const struct mnt_idmap *idmap, struct in
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)