[PATCH v3 2/5] ntfs: preserve timestamps when creating hard links

From: Baolin Liu

Date: Fri Nov 06 2026 - 01:34:30 EST


From: Xuewen Wang <wangxuewen@xxxxxxxxxx>

ntfs_link() resets atime, mtime and ctime on both the source file and
the destination directory after creating a hard link. This overwrites
the source file's atime and mtime and the directory's atime.

Update only the source ctime and the destination directory mtime/ctime.
Use the same timestamp for the new FILE_NAME attribute, its directory
index entry and the source inode, so the new name does not retain the
old ctime. Set the inode's ctime only after the link succeeds and mark
both inodes dirty to queue their timestamps for writeback.

Keep the rename caller's existing timestamp behavior unchanged.

Reviewed-by: Hyunchul Lee <hyc.lee@xxxxxxxxx>
Signed-off-by: Xuewen Wang <wangxuewen@xxxxxxxxxx>
---
Tested in QEMU; generic/004 and generic/389 passed. Hard links preserved
atime/mtime and advanced ctime. Offline checks confirmed matching ctime
in STANDARD_INFORMATION, the new FILE_NAME and the directory index.

fs/ntfs/namei.c | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 14eaf7204ead..a83a648c8ff8 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -1244,6 +1244,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry)
* @dir_ni: ntfs inode for directory in which new link should be placed
* @name: unicode name of the new link
* @name_len: length of the name in unicode characters
+ * @ctime: metadata change time for the new FILE_NAME
*
* Create a new hard link. This involves adding an entry to the directory
* index and adding a new FILE_NAME attribute to the target inode.
@@ -1251,7 +1252,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry)
* Return 0 on success and -errno on error.
*/
static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
- __le16 *name, u8 name_len)
+ __le16 *name, u8 name_len, struct timespec64 ctime)
{
struct super_block *sb;
struct inode *vi = VFS_I(ni);
@@ -1313,7 +1314,7 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,

fn->creation_time = utc2ntfs(ni->i_crtime);
fn->last_data_change_time = utc2ntfs(inode_get_mtime(vi));
- fn->last_mft_change_time = utc2ntfs(inode_get_ctime(vi));
+ fn->last_mft_change_time = utc2ntfs(ctime);
fn->last_access_time = utc2ntfs(inode_get_atime(vi));
memcpy(fn->file_name, name, name_len * sizeof(__le16));

@@ -1464,7 +1465,8 @@ static int ntfs_rename(const struct mnt_idmap *idmap, struct inode *old_dir,
goto err_out;
}

- err = __ntfs_link(old_ni, new_dir_ni, uname_new, new_name_len);
+ err = __ntfs_link(old_ni, new_dir_ni, uname_new, new_name_len,
+ inode_get_ctime(old_inode));
if (err)
goto err_out;

@@ -1630,6 +1632,7 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
int uname_len;
int err;
struct ntfs_inode *ni = NTFS_I(vi), *dir_ni = NTFS_I(dir);
+ struct timespec64 ctime;

if (NVolShutdown(vol))
return -EIO;
@@ -1654,7 +1657,8 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
ihold(vi);
mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
mutex_lock_nested(&dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
- err = __ntfs_link(NTFS_I(vi), NTFS_I(dir), uname, uname_len);
+ ctime = current_time(vi);
+ err = __ntfs_link(ni, dir_ni, uname, uname_len, ctime);
if (err) {
mutex_unlock(&dir_ni->mrec_lock);
mutex_unlock(&ni->mrec_lock);
@@ -1664,12 +1668,13 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
}

inode_inc_iversion(dir);
- simple_inode_init_ts(dir);
+ inode_set_mtime_to_ts(dir, inode_set_ctime_current(dir));
+ mark_inode_dirty(dir);

inode_inc_iversion(vi);
- simple_inode_init_ts(vi);
+ inode_set_ctime_to_ts(vi, ctime);
+ mark_inode_dirty(vi);

- /* timestamp is already written, so mark_inode_dirty() is unneeded. */
d_instantiate(dentry, vi);
mutex_unlock(&dir_ni->mrec_lock);
mutex_unlock(&ni->mrec_lock);