Skip to contentSkip to conversation

provenance_time (ptime): a new settable timestamp for cross-filesystem provenance

RFC PATCH In review Sean Smith 4 participants 17 messages Apr 5 – Apr 9

Subsystems (7)
lore
Share & tools
lore

Sean Smith

author
#0 lore raw

cover provenance_time (ptime): a new settable timestamp for cross-filesystem provenance

This series adds provenance_time (ptime) -- a new settable inode timestamp that records when a file's content was first created, preserving this date across copies, moves, and application saves.

This is a working implementation of the concept I proposed in my RFC in March:
https://lore.kernel.org/linux-fsdevel/CAOx6djP4hb-Cd1Zk07SNfFfLc8irjNmbVqq+58h1Whz+h1wSFA@mail.gmail.com/T/#u

MOTIVATION

Linux has no mechanism to preserve original creation dates when files move between filesystems. Every copy resets btime to "now." For workflows involving document migration (NTFS to Btrfs, between ext4 volumes, to USB drives), creation date provenance is lost.

Since the March RFC, I attempted an xattr-based workaround (user.provenance_time) and found it structurally unworkable:

1. Application atomic saves destroy xattrs. Programs that save via write-to-temp + rename() replace the inode, permanently destroying all extended attributes. Only the VFS sees both inodes during rename -- no userspace mechanism can intercept this and copy metadata across.

2. Every tool in the copy chain must explicitly opt in to xattr preservation. cp requires --preserve=xattr, rsync requires -X, tar requires --xattrs. Each missing flag causes silent data loss. Transparent preservation through arbitrary tool flows is not achievable in userspace.

Atomic saves are the default behavior of mainstream applications (LibreOffice, Vim, Kate, etc.).

DESIGN

ptime is a separate timestamp from btime. btime remains immutable and forensic ("when was this inode born on this disk"). ptime is settable and portable ("when was this content first created").

This resolves the 2019 impasse: Dave Chinner's forensic argument for immutable btime is fully respected -- btime is untouched on native Linux filesystems. Ted Ts'o's March 2025 concept of a settable "crtime" alongside immutable btime is implemented in ext4 with dedicated i_ptime fields.

Two implementation categories:

Native (Btrfs, ext4): Dedicated on-disk ptime field. btime remains immutable. Full nanosecond precision.

Mapped (ntfs3, FAT32/vfat, exFAT): ptime reads/writes the existing creation time field. This matches Windows and macOS behavior, where creation time is already settable via standard APIs. No new on-disk structures needed.

Key VFS capability -- rename-over preservation: when rename() overwrites an existing file, the kernel copies ptime from the old file to the new file. This fixes the atomic-save xattr destruction problem at its root, for every application on every supported filesystem.

API

ptime is exposed through existing interfaces with minimal additions:

- statx: STATX_PTIME (0x00040000U) returns ptime in stx_ptime
- utimensat: AT_UTIME_PTIME (0x20000) flag with times[2] extension for setting ptime
- setattr_prepare: ATTR_PTIME (bit 19) / ATTR_PTIME_SET (bit 20)

The utimensat extension reuses Sandoval's 2019 pattern. For upstream, an extensible-struct syscall (utimensat2, following the clone3/openat2 convention) may be preferred -- I am open to guidance on the API design.

Permissions follow the existing utimensat model: file owner or CAP_FOWNER required.

TESTING

This has been running on EndeavourOS (kernel 6.19.11) for daily use. Test coverage:

- 10 xfstests (7 generic VFS + 3 Btrfs-specific): basic set/read, persistence, rename-over, permissions, utime-omit, chmod/truncate survival, snapshots, nlink guards, compat_ro

- Runtime tests across all 5 filesystems: set/read, rename-over, cp -a preservation, cross-FS copies (Btrfs, ext4, ntfs3, FAT32, exFAT)

KNOWN LIMITATIONS

- XFS: deferred (separate inode structure analysis needed)
- Btrfs send/receive: not yet patched for ptime
- glibc utimensat() wrapper: cannot pass ptime; tools use raw syscall()
- Btrfs compat_ro: writing ptime sets a compat_ro flag; unpatched kernels refuse RW mount (correct Btrfs behavior)

The userspace ecosystem (patched cp, rsync, tar, KDE Dolphin) and xfstests are available at:
https://github.com/DefendTheDisabled/linux-ptime

This implementation was developed using AI-assisted tooling for code generation, iterative review, and test infrastructure. I am responsible for review, testing, and sign-off.

Sean Smith (6):
vfs: add provenance_time (ptime) infrastructure
btrfs: add provenance time (ptime) support
ntfs3: map ptime to NTFS creation time with rename-over
ext4: add dedicated ptime field alongside i_crtime
fat: map ptime to FAT creation time with rename-over
exfat: map ptime to exFAT creation time with rename-over

 fs/attr.c                       |  6 +++-
 fs/btrfs/btrfs_inode.h          |  4 +++
 fs/btrfs/delayed-inode.c        |  4 +++
 fs/btrfs/fs.h                   |  3 +-
 fs/btrfs/inode.c                | 43 +++++++++++++++++++++++++
 fs/btrfs/tree-log.c             |  2 ++
 fs/btrfs/volumes.c              |  2 +-
 fs/exfat/file.c                 |  9 ++++++
 fs/exfat/namei.c                | 21 +++++++++++--
 fs/ext4/ext4.h                  |  3 ++
 fs/ext4/inode.c                 | 14 +++++++++
 fs/ext4/namei.c                 | 13 ++++++++
 fs/fat/file.c                   |  6 ++++
 fs/fat/namei_vfat.c             | 20 ++++++++++--
 fs/init.c                       |  2 +-
 fs/ntfs3/file.c                 | 13 ++++++++
 fs/ntfs3/frecord.c              |  8 +++++
 fs/ntfs3/namei.c                | 14 +++++++++
 fs/stat.c                       |  2 ++
 fs/utimes.c                     | 56 +++++++++++++++++++++++++--------
 include/linux/fs.h              |  5 ++-
 include/linux/stat.h            |  1 +
 include/uapi/linux/btrfs.h      |  1 +
 include/uapi/linux/btrfs_tree.h |  4 ++-
 include/uapi/linux/fcntl.h      |  3 ++
 include/uapi/linux/stat.h       |  4 ++-
 init/initramfs.c                |  2 +-
 27 files changed, 239 insertions(+), 26 deletions(-)
Signature · 1 line
2.53.0
author
#1 lore raw

PATCH 1/6 vfs: add provenance_time (ptime) infrastructure

Add a new settable inode timestamp, provenance_time (ptime), for tracking the original creation date of file content across filesystem boundaries.

ptime is distinct from btime (forensic, immutable): it records when file content first came into existence on any filesystem, and is designed to be set during cross-filesystem migration and preserved through copies.

VFS changes:
- ATTR_PTIME (bit 19) and ATTR_PTIME_SET (bit 20) in struct iattr
- STATX_PTIME (0x00040000) in struct statx at offset 0xC0
- AT_UTIME_PTIME (0x20000) flag for utimensat()
- ptime field in struct kstat
- Permission model matches mtime (owner or CAP_FOWNER)
- UTIME_NOW and UTIME_OMIT supported for ptime element
- All existing vfs_utimes() callers updated for new flags parameter

---
 fs/attr.c                  |  6 +++-
 fs/btrfs/volumes.c         |  2 +-
 fs/init.c                  |  2 +-
 fs/stat.c                  |  2 ++
 fs/utimes.c                | 56 +++++++++++++++++++++++++++++---------
 include/linux/fs.h         |  5 +++-
 include/linux/stat.h       |  1 +
 include/uapi/linux/fcntl.h |  3 ++
 include/uapi/linux/stat.h  |  4 ++-
 init/initramfs.c           |  2 +-
 10 files changed, 64 insertions(+), 19 deletions(-)
fs/attr.c +5 -1
index b9ec6b47b..7fa9c01d1 100644
@@ -206,7 +206,7 @@ int setattr_prepare(struct mnt_idmap *idmap, struct dentry *dentry,
 	}
 
 	/* Check for setting the inode time. */
-	if (ia_valid & (ATTR_MTIME_SET | ATTR_ATIME_SET | ATTR_TIMES_SET)) {
+	if (ia_valid & (ATTR_MTIME_SET | ATTR_ATIME_SET | ATTR_PTIME_SET | ATTR_TIMES_SET)) {
 		if (!inode_owner_or_capable(idmap, inode))
 			return -EPERM;
 	}
@@ -466,6 +466,10 @@ int notify_change(struct mnt_idmap *idmap, struct dentry *dentry,
 		attr->ia_mtime = timestamp_truncate(attr->ia_mtime, inode);
 	else
 		attr->ia_mtime = now;
+	if (ia_valid & ATTR_PTIME_SET)
+		attr->ia_ptime = timestamp_truncate(attr->ia_ptime, inode);
+	else if (ia_valid & ATTR_PTIME)
+		attr->ia_ptime = now;
 
 	if (ia_valid & ATTR_KILL_PRIV) {
 		error = security_inode_need_killpriv(dentry);
fs/btrfs/volumes.c +1 -1
index 052b830a0..0e81f2cc9 100644
@@ -2117,7 +2117,7 @@ static void update_dev_time(const char *device_path)
 	struct path path;
 
 	if (!kern_path(device_path, LOOKUP_FOLLOW, &path)) {
-		vfs_utimes(&path, NULL);
+		vfs_utimes(&path, NULL, 0);
 		path_put(&path);
 	}
 }
fs/init.c +1 -1
index e0f5429c0..e9a9f4d93 100644
@@ -254,7 +254,7 @@ int __init init_utimes(char *filename, struct timespec64 *ts)
 	error = kern_path(filename, 0, &path);
 	if (error)
 		return error;
-	error = vfs_utimes(&path, ts);
+	error = vfs_utimes(&path, ts, 0);
 	path_put(&path);
 	return error;
 }
fs/stat.c +2 -0
index 6c79661e1..9284bb753 100644
@@ -728,6 +728,8 @@ cp_statx(const struct kstat *stat, struct statx __user *buffer)
 	tmp.stx_atime.tv_nsec = stat->atime.tv_nsec;
 	tmp.stx_btime.tv_sec = stat->btime.tv_sec;
 	tmp.stx_btime.tv_nsec = stat->btime.tv_nsec;
+	tmp.stx_ptime.tv_sec = stat->ptime.tv_sec;
+	tmp.stx_ptime.tv_nsec = stat->ptime.tv_nsec;
 	tmp.stx_ctime.tv_sec = stat->ctime.tv_sec;
 	tmp.stx_ctime.tv_nsec = stat->ctime.tv_nsec;
 	tmp.stx_mtime.tv_sec = stat->mtime.tv_sec;
fs/utimes.c +43 -13

123 lines not on this page · raw message

include/linux/fs.h +4 -1
index a01621fa6..07719e216 100644
@@ -239,6 +239,8 @@ typedef int (dio_iodone_t)(struct kiocb *iocb, loff_t offset,
 #define ATTR_TIMES_SET	(1 << 16)
 #define ATTR_TOUCH	(1 << 17)
 #define ATTR_DELEG	(1 << 18) /* Delegated attrs. Don't break write delegations */
+#define ATTR_PTIME	(1 << 19) /* Set provenance time */
+#define ATTR_PTIME_SET	(1 << 20) /* Set provenance time to specific value */
 
 /*
  * Whiteout is represented by a char device.  The following constants define the
@@ -283,6 +285,7 @@ struct iattr {
 	struct timespec64 ia_atime;
 	struct timespec64 ia_mtime;
 	struct timespec64 ia_ctime;
+	struct timespec64 ia_ptime;
 
 	/*
 	 * Not an attribute, but an auxiliary info for filesystems wanting to
@@ -1814,7 +1817,7 @@ int vfs_mkobj(struct dentry *, umode_t,
 
 int vfs_fchown(struct file *file, uid_t user, gid_t group);
 int vfs_fchmod(struct file *file, umode_t mode);
-int vfs_utimes(const struct path *path, struct timespec64 *times);
+int vfs_utimes(const struct path *path, struct timespec64 *times, int flags);
 
 #ifdef CONFIG_COMPAT
 extern long compat_ptr_ioctl(struct file *file, unsigned int cmd,
include/linux/stat.h +1 -0
index e3d00e7bb..52272000c 100644
@@ -48,6 +48,7 @@ struct kstat {
 	struct timespec64 mtime;
 	struct timespec64 ctime;
 	struct timespec64 btime;			/* File creation time */
+	struct timespec64 ptime;			/* Provenance time */
 	u64		blocks;
 	u64		mnt_id;
 	u64		change_cookie;
include/uapi/linux/fcntl.h +3 -0
index aadfbf6e0..f80ce0295 100644
@@ -190,4 +190,7 @@ struct delegation {
 #define AT_EXECVE_CHECK		0x10000	/* Only perform a check if execution
 					   would be allowed. */
 
+/* Flag for utimensat(2): times[2] carries provenance time */
+#define AT_UTIME_PTIME		0x20000
+
 #endif /* _UAPI_LINUX_FCNTL_H */
include/uapi/linux/stat.h +3 -1
index 1686861aa..0c8db3715 100644
@@ -187,7 +187,8 @@ struct statx {
 	__u32	__spare2[1];
 
 	/* 0xc0 */
-	__u64	__spare3[8];	/* Spare space for future expansion */
+	struct statx_timestamp	stx_ptime;	/* File provenance time */
+	__u64	__spare3[6];		/* Spare space for future expansion */
 
 	/* 0x100 */
 };
@@ -219,6 +220,7 @@ struct statx {
 #define STATX_SUBVOL		0x00008000U	/* Want/got stx_subvol */
 #define STATX_WRITE_ATOMIC	0x00010000U	/* Want/got atomic_write_* fields */
 #define STATX_DIO_READ_ALIGN	0x00020000U	/* Want/got dio read alignment info */
+#define STATX_PTIME		0x00040000U	/* Want/got stx_ptime */
 
 #define STATX__RESERVED		0x80000000U	/* Reserved for future struct statx expansion */
 
init/initramfs.c +1 -1
index 6ddbfb17f..e066b1fee 100644
@@ -139,7 +139,7 @@ static void __init do_utime(char *filename, time64_t mtime)
 static void __init do_utime_path(const struct path *path, time64_t mtime)
 {
 	struct timespec64 t[2] = { { .tv_sec = mtime }, { .tv_sec = mtime } };
-	vfs_utimes(path, t);
+	vfs_utimes(path, t, 0);
 }
 
 static __initdata LIST_HEAD(dir_list);
Signature · 1 line
2.53.0
author
#2 lore raw

PATCH 2/6 btrfs: add provenance time (ptime) support

In reply to Sean Smith

Store ptime as a dedicated field in btrfs_inode_item reserved space:
struct btrfs_timespec (12 bytes) + __le32 pad (4 bytes) = 16 bytes, consuming 2 of 4 reserved __le64 slots, leaving 2 free.

In-memory: i_ptime_sec/i_ptime_nsec in struct btrfs_inode.
Persistence: delayed-inode read/write path (the primary persistence path for normal inodes, not fill_inode_item).
Tree-log: ptime written to log tree for fsync crash recovery. New inode: initialized to zero (ptime unset).

Getattr reports ptime only when non-zero (distinguishes unset from supported-but-zero). Setattr accepts ATTR_PTIME and sets BTRFS_FEATURE_COMPAT_RO_PTIME - old kernels see unknown compat_ro bit and refuse RW mount, protecting ptime data.

Rename-over preservation: when rename(source, target) replaces an existing regular file, if source has ptime=0 and target has ptime set, inherit target ptime to source. Guards: S_ISREG both sides, nlink==1, not RENAME_EXCHANGE/WHITEOUT. Atomic with rename transaction. Enables atomic-save survival (write-temp + rename).

---
 fs/btrfs/btrfs_inode.h          |  4 ++++
 fs/btrfs/delayed-inode.c        |  4 ++++
 fs/btrfs/fs.h                   |  3 ++-
 fs/btrfs/inode.c                | 42 +++++++++++++++++++++++++++++++++
 fs/btrfs/tree-log.c             |  2 ++
 include/uapi/linux/btrfs.h      |  1 +
 include/uapi/linux/btrfs_tree.h |  4 +++-
 7 files changed, 58 insertions(+), 2 deletions(-)
fs/btrfs/btrfs_inode.h +4 -0
index 73602ee8d..bac92f766 100644
@@ -334,6 +334,10 @@ struct btrfs_inode {
 	u64 i_otime_sec;
 	u32 i_otime_nsec;
 
+	/* Provenance time - original creation date of file content. */
+	u64 i_ptime_sec;
+	u32 i_ptime_nsec;
+
 	/* Hook into fs_info->delayed_iputs */
 	struct list_head delayed_iput;
 
fs/btrfs/delayed-inode.c +4 -0
index 7e3d294a6..649de7c29 100644
@@ -1887,6 +1887,8 @@ static void fill_stack_inode_item(struct btrfs_trans_handle *trans,
 
 	btrfs_set_stack_timespec_sec(&inode_item->otime, inode->i_otime_sec);
 	btrfs_set_stack_timespec_nsec(&inode_item->otime, inode->i_otime_nsec);
+	btrfs_set_stack_timespec_sec(&inode_item->ptime, inode->i_ptime_sec);
+	btrfs_set_stack_timespec_nsec(&inode_item->ptime, inode->i_ptime_nsec);
 }
 
 int btrfs_fill_inode(struct btrfs_inode *inode, u32 *rdev)
@@ -1935,6 +1937,8 @@ int btrfs_fill_inode(struct btrfs_inode *inode, u32 *rdev)
 
 	inode->i_otime_sec = btrfs_stack_timespec_sec(&inode_item->otime);
 	inode->i_otime_nsec = btrfs_stack_timespec_nsec(&inode_item->otime);
+	inode->i_ptime_sec = btrfs_stack_timespec_sec(&inode_item->ptime);
+	inode->i_ptime_nsec = btrfs_stack_timespec_nsec(&inode_item->ptime);
 
 	vfs_inode->i_generation = inode->generation;
 	if (S_ISDIR(vfs_inode->i_mode))
fs/btrfs/fs.h +2 -1
index 8ffbc40eb..7c8105ecf 100644
@@ -284,7 +284,8 @@ enum {
 	(BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE |	\
 	 BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE_VALID | \
 	 BTRFS_FEATURE_COMPAT_RO_VERITY |		\
-	 BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE)
+	 BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE | \
+	 BTRFS_FEATURE_COMPAT_RO_PTIME)
 
 #define BTRFS_FEATURE_COMPAT_RO_SAFE_SET	0ULL
 #define BTRFS_FEATURE_COMPAT_RO_SAFE_CLEAR	0ULL
fs/btrfs/inode.c +42 -0

78 lines not on this page · raw message

fs/btrfs/tree-log.c +2 -0
index 6c40f48cc..7ed09af22 100644
@@ -4640,6 +4640,8 @@ static void fill_inode_item(struct btrfs_trans_handle *trans,
 
 	btrfs_set_timespec_sec(leaf, &item->otime, BTRFS_I(inode)->i_otime_sec);
 	btrfs_set_timespec_nsec(leaf, &item->otime, BTRFS_I(inode)->i_otime_nsec);
+	btrfs_set_timespec_sec(leaf, &item->ptime, BTRFS_I(inode)->i_ptime_sec);
+	btrfs_set_timespec_nsec(leaf, &item->ptime, BTRFS_I(inode)->i_ptime_nsec);
 
 	/*
 	 * We do not need to set the nbytes field, in fact during a fast fsync
include/uapi/linux/btrfs.h +1 -0
index e8fd92789..d2c542425 100644
@@ -313,6 +313,7 @@ struct btrfs_ioctl_fs_info_args {
  * reducing mount time for large filesystem due to better locality.
  */
 #define BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE	(1ULL << 3)
+#define BTRFS_FEATURE_COMPAT_RO_PTIME			(1ULL << 4)
 
 #define BTRFS_FEATURE_INCOMPAT_MIXED_BACKREF	(1ULL << 0)
 #define BTRFS_FEATURE_INCOMPAT_DEFAULT_SUBVOL	(1ULL << 1)
include/uapi/linux/btrfs_tree.h +3 -1
index fc29d2738..719c00363 100644
@@ -890,7 +890,9 @@ struct btrfs_inode_item {
 	 * a little future expansion, for more than this we can
 	 * just grow the inode item and version it
 	 */
-	__le64 reserved[4];
+	struct btrfs_timespec ptime;
+	__le32 __reserved_pad;
+	__le64 reserved[2];
 	struct btrfs_timespec atime;
 	struct btrfs_timespec ctime;
 	struct btrfs_timespec mtime;
Signature · 1 line
2.53.0
author
#3 lore raw

PATCH 3/6 ntfs3: map ptime to NTFS creation time with rename-over

In reply to Sean Smith

Map ptime to the NTFS Date Created field in $STANDARD_INFORMATION. This is a mapped-ptime implementation: setting ptime overwrites the creation time. Justified because Windows treats NTFS creation time as mutable via SetFileTime() - it was never truly immutable.

Getattr: report NTFS creation time as ptime.
Setattr: write ptime to NTFS creation time via frecord cr_time path.
Rename-over: save target creation time before unlink, restore to
source after rename. Replicates Windows behavior where creation time survives application atomic saves.

Round-trip: NTFS Date Created -> Btrfs ptime -> NTFS Date Created preserves the original creation date through cross-FS copies.

---
 fs/ntfs3/file.c    | 13 +++++++++++++
 fs/ntfs3/frecord.c |  8 ++++++++
 fs/ntfs3/namei.c   | 14 ++++++++++++++
 3 files changed, 35 insertions(+)
fs/ntfs3/file.c +13 -0
index 13d014b87..8688a48b1 100644
@@ -161,6 +161,13 @@ int ntfs_getattr(struct mnt_idmap *idmap, const struct path *path,
 
 	stat->result_mask |= STATX_BTIME;
 	stat->btime = ni->i_crtime;
+
+	/* Map NTFS creation time to ptime (provenance time) */
+	if (request_mask & STATX_PTIME) {
+		stat->ptime = ni->i_crtime;
+		stat->result_mask |= STATX_PTIME;
+	}
+
 	stat->blksize = ni->mi.sbi->cluster_size; /* 512, 1K, ..., 2M */
 
 	if (inode->i_flags & S_IMMUTABLE)
@@ -857,6 +864,12 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		i_size_write(inode, newsize);
 	}
 
+	/* Accept ptime and store as NTFS creation time */
+	if (ia_valid & ATTR_PTIME) {
+		ni->i_crtime = attr->ia_ptime;
+		ni->ni_flags |= NI_FLAG_UPDATE_PARENT;
+	}
+
 	setattr_copy(idmap, inode, attr);
 
 	if (mode != inode->i_mode) {
fs/ntfs3/frecord.c +8 -0
index d5bbd47e1..b164b2f50 100644
@@ -3197,6 +3197,14 @@ int ni_write_inode(struct inode *inode, int sync, const char *hint)
 			modified = true;
 		}
 
+		/* Write creation time (ptime maps to NTFS cr_time) */
+		ts = ni->i_crtime;
+		dup.cr_time = kernel2nt(&ts);
+		if (std->cr_time != dup.cr_time) {
+			std->cr_time = dup.cr_time;
+			modified = true;
+		}
+
 		dup.fa = ni->std_fa;
 		if (std->fa != dup.fa) {
 			std->fa = dup.fa;
fs/ntfs3/namei.c +14 -0
index b2af8f695..40d06884f 100644
@@ -292,6 +292,16 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *dir,
 		return -EINVAL;
 	}
 
+	/* ptime rename-over: save target creation time before unlink */
+	struct timespec64 saved_crtime = {};
+	bool inherit_crtime = false;
+
+	if (new_inode && S_ISREG(inode->i_mode) &&
+	    S_ISREG(new_inode->i_mode) && inode->i_nlink == 1) {
+		saved_crtime = ntfs_i(new_inode)->i_crtime;
+		inherit_crtime = true;
+	}
+
 	if (new_inode) {
 		/* Target name exists. Unlink it. */
 		dget(new_dentry);
@@ -330,6 +340,10 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *dir,
 
 	err = ni_rename(dir_ni, new_dir_ni, ni, de, new_de);
 	if (!err) {
+		/* ptime rename-over: inherit target creation time */
+		if (inherit_crtime)
+			ni->i_crtime = saved_crtime;
+
 		simple_rename_timestamp(dir, dentry, new_dir, new_dentry);
 		mark_inode_dirty(inode);
 		mark_inode_dirty(dir);
Signature · 1 line
2.53.0
author
#4 lore raw

PATCH 4/6 ext4: add dedicated ptime field alongside i_crtime

In reply to Sean Smith

Add i_ptime (__le32) and i_ptime_extra (__le32) to the ext4 on-disk inode structure after i_projid. Total: 8 bytes in the extended inode area. i_crtime remains untouched as immutable birth time.

This is a native-ptime implementation: ptime and btime are separate fields. On 256-byte inodes (modern default), both fit easily. On 128-byte inodes, ptime is silently unavailable (same graceful degradation as i_crtime via EXT4_FITS_IN_INODE).

Uses existing EXT4_EINODE_GET_XTIME/SET_XTIME macros for read/write.
Rename-over: when a file with ptime=0 replaces a file with ptime set, inherit target ptime (same zero-sentinel logic as Btrfs).

---
 fs/ext4/ext4.h  |  3 +++
 fs/ext4/inode.c | 14 ++++++++++++++
 fs/ext4/namei.c | 13 +++++++++++++
 3 files changed, 30 insertions(+)
fs/ext4/ext4.h +3 -0
index f1c476303..5c2812637 100644
@@ -860,6 +860,8 @@ struct ext4_inode {
 	__le32  i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
 	__le32  i_version_hi;	/* high 32 bits for 64-bit version */
 	__le32	i_projid;	/* Project ID */
+	__le32	i_ptime;		/* Provenance time */
+	__le32	i_ptime_extra;	/* extra Provenance time (nsec << 2 | epoch) */
 };
 
 #define EXT4_EPOCH_BITS 2
@@ -1136,6 +1138,7 @@ struct ext4_inode_info {
 	 * struct timespec64 i_{a,c,m}time in the generic inode.
 	 */
 	struct timespec64 i_crtime;
+	struct timespec64 i_ptime;
 
 	/* mballoc */
 	atomic_t i_prealloc_active;
fs/ext4/inode.c +14 -0
index 625cfbf61..15b6b6dc6 100644
@@ -4753,6 +4753,7 @@ static int ext4_fill_raw_inode(struct inode *inode, struct ext4_inode *raw_inode
 	EXT4_INODE_SET_MTIME(inode, raw_inode);
 	EXT4_INODE_SET_ATIME(inode, raw_inode);
 	EXT4_EINODE_SET_XTIME(i_crtime, ei, raw_inode);
+	EXT4_EINODE_SET_XTIME(i_ptime, ei, raw_inode);
 
 	raw_inode->i_dtime = cpu_to_le32(ei->i_dtime);
 	raw_inode->i_flags = cpu_to_le32(ei->i_flags & 0xFFFFFFFF);
@@ -5409,6 +5410,7 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
 	EXT4_INODE_GET_ATIME(inode, raw_inode);
 	EXT4_INODE_GET_MTIME(inode, raw_inode);
 	EXT4_EINODE_GET_XTIME(i_crtime, ei, raw_inode);
+	EXT4_EINODE_GET_XTIME(i_ptime, ei, raw_inode);
 
 	if (likely(!test_opt2(inode->i_sb, HURD_COMPAT))) {
 		u64 ivers = le32_to_cpu(raw_inode->i_disk_version);
@@ -6061,6 +6063,9 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	if (!error) {
 		if (inc_ivers)
 			inode_inc_iversion(inode);
+		if (attr->ia_valid & ATTR_PTIME)
+			EXT4_I(inode)->i_ptime = attr->ia_ptime;
+
 		setattr_copy(idmap, inode, attr);
 		mark_inode_dirty(inode);
 	}
@@ -6114,6 +6119,15 @@ int ext4_getattr(struct mnt_idmap *idmap, const struct path *path,
 		stat->btime.tv_nsec = ei->i_crtime.tv_nsec;
 	}
 
+	/* Report ptime from dedicated field, not crtime */
+	if ((request_mask & STATX_PTIME) &&
+	    EXT4_FITS_IN_INODE(raw_inode, ei, i_ptime) &&
+	    (ei->i_ptime.tv_sec || ei->i_ptime.tv_nsec)) {
+		stat->result_mask |= STATX_PTIME;
+		stat->ptime.tv_sec = ei->i_ptime.tv_sec;
+		stat->ptime.tv_nsec = ei->i_ptime.tv_nsec;
+	}
+
 	/*
 	 * Return the DIO alignment restrictions if requested.  We only return
 	 * this information when requested, since on encrypted files it might
fs/ext4/namei.c +13 -0
index c4b5e252a..1bfe4df24 100644
@@ -3942,6 +3942,19 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 	 * rename.
 	 */
 	inode_set_ctime_current(old.inode);
+
+	/* ptime rename-over: preserve ptime across atomic saves */
+	if (new.inode && S_ISREG(old.inode->i_mode) &&
+	    S_ISREG(new.inode->i_mode) && old.inode->i_nlink == 1 &&
+	    !(flags & RENAME_WHITEOUT)) {
+		struct ext4_inode_info *old_ei = EXT4_I(old.inode);
+		struct ext4_inode_info *new_ei = EXT4_I(new.inode);
+
+		if (!old_ei->i_ptime.tv_sec && !old_ei->i_ptime.tv_nsec &&
+		    (new_ei->i_ptime.tv_sec || new_ei->i_ptime.tv_nsec))
+			old_ei->i_ptime = new_ei->i_ptime;
+	}
+
 	retval = ext4_mark_inode_dirty(handle, old.inode);
 	if (unlikely(retval))
 		goto end_rename;
Signature · 1 line
2.53.0
author
#5 lore raw

PATCH 5/6 fat: map ptime to FAT creation time with rename-over

In reply to Sean Smith

Map ptime to the FAT/VFAT creation time field. Only active on VFAT (long filename) mounts since plain FAT12/FAT16 lack creation time. FAT32 creation time has 2-second precision.

Getattr: report creation time as ptime (VFAT only, via isvfat check).
Setattr: write ptime to i_crtime.
Rename-over: save target creation time before detach, restore to
source after attach. Preserves creation time across atomic saves.

---
 fs/fat/file.c       |  6 ++++++
 fs/fat/namei_vfat.c | 20 ++++++++++++++++++--
 2 files changed, 24 insertions(+), 2 deletions(-)
fs/fat/file.c +6 -0
index 4fc49a614..9d1fcc554 100644
@@ -413,6 +413,10 @@ int fat_getattr(struct mnt_idmap *idmap, const struct path *path,
 		stat->result_mask |= STATX_BTIME;
 		stat->btime = MSDOS_I(inode)->i_crtime;
 	}
+	if (sbi->options.isvfat && (request_mask & STATX_PTIME)) {
+		stat->result_mask |= STATX_PTIME;
+		stat->ptime = MSDOS_I(inode)->i_crtime;
+	}
 
 	return 0;
 }
@@ -564,6 +568,8 @@ int fat_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 		fat_truncate_time(inode, &attr->ia_mtime, S_MTIME);
 	attr->ia_valid &= ~(ATTR_ATIME|ATTR_CTIME|ATTR_MTIME);
 
+	if (attr->ia_valid & ATTR_PTIME)
+		MSDOS_I(inode)->i_crtime = attr->ia_ptime;
 	setattr_copy(idmap, inode, attr);
 	mark_inode_dirty(inode);
 out:
fs/fat/namei_vfat.c +18 -2
index 47ff083cf..f1e2eadf8 100644
@@ -975,8 +975,24 @@ static int vfat_rename(struct inode *old_dir, struct dentry *old_dentry,
 	}
 	inode_inc_iversion(new_dir);
 
-	fat_detach(old_inode);
-	fat_attach(old_inode, new_i_pos);
+	/* ptime rename-over: save target creation time */
+	{
+		struct timespec64 saved_crtime = {};
+		bool inherit_crtime = false;
+
+		if (new_inode && S_ISREG(old_inode->i_mode) &&
+		    S_ISREG(new_inode->i_mode) && old_inode->i_nlink == 1) {
+			saved_crtime = MSDOS_I(new_inode)->i_crtime;
+			inherit_crtime = true;
+		}
+
+		fat_detach(old_inode);
+		fat_attach(old_inode, new_i_pos);
+
+		if (inherit_crtime)
+			MSDOS_I(old_inode)->i_crtime = saved_crtime;
+	}
+
 	err = vfat_sync_ipos(new_dir, old_inode);
 	if (err)
 		goto error_inode;
Signature · 1 line
2.53.0
author
#6 lore raw

PATCH 6/6 exfat: map ptime to exFAT creation time with rename-over

In reply to Sean Smith

Map ptime to the exFAT creation time field. exFAT creation time has 10ms precision.

Getattr: report creation time as ptime.
Setattr: write ptime to i_crtime.
Rename-over: save target creation time before __exfat_rename, restore
after. Preserves creation time across atomic saves.

---
 fs/btrfs/inode.c |  3 ++-
 fs/exfat/file.c  |  9 +++++++++
 fs/exfat/namei.c | 21 ++++++++++++++++++---
 3 files changed, 29 insertions(+), 4 deletions(-)
fs/btrfs/inode.c +2 -1
index dce80561a..918dfd4c5 100644
@@ -8715,7 +8715,8 @@ static int btrfs_rename(struct mnt_idmap *idmap,
 		/* Note: if rename fails below, ptime mutation is harmless —
 		 * the source file keeps its previous ptime=0 semantics since
 		 * the rename didn't complete. The in-memory value will be
-		 * overwritten on next inode read from disk. */
+		 * overwritten on next inode read from disk.
+		 */
 
 		ret = btrfs_update_inode(trans, BTRFS_I(old_inode));
 		if (unlikely(ret)) {
fs/exfat/file.c +9 -0
index 536c8078f..b6438bd79 100644
@@ -277,6 +277,11 @@ int exfat_getattr(struct mnt_idmap *idmap, const struct path *path,
 	stat->result_mask |= STATX_BTIME;
 	stat->btime.tv_sec = ei->i_crtime.tv_sec;
 	stat->btime.tv_nsec = ei->i_crtime.tv_nsec;
+	if (request_mask & STATX_PTIME) {
+		stat->result_mask |= STATX_PTIME;
+		stat->ptime.tv_sec = ei->i_crtime.tv_sec;
+		stat->ptime.tv_nsec = ei->i_crtime.tv_nsec;
+	}
 	stat->blksize = EXFAT_SB(inode->i_sb)->cluster_size;
 	return 0;
 }
@@ -337,6 +342,10 @@ int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	if (attr->ia_valid & ATTR_SIZE)
 		inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
 
+	if (attr->ia_valid & ATTR_PTIME) {
+		struct exfat_inode_info *exi = EXFAT_I(inode);
+		exi->i_crtime = attr->ia_ptime;
+	}
 	setattr_copy(idmap, inode, attr);
 	exfat_truncate_inode_atime(inode);
 
fs/exfat/namei.c +18 -3
index dfe957493..9c0b59e00 100644
@@ -1262,9 +1262,24 @@ static int exfat_rename(struct mnt_idmap *idmap,
 	old_inode = old_dentry->d_inode;
 	new_inode = new_dentry->d_inode;
 
-	err = __exfat_rename(old_dir, EXFAT_I(old_inode), new_dir, new_dentry);
-	if (err)
-		goto unlock;
+	/* ptime rename-over: save target creation time */
+	{
+		struct timespec64 saved_crtime = {};
+		bool inherit_crtime = false;
+
+		if (new_inode && S_ISREG(old_inode->i_mode) &&
+		    S_ISREG(new_inode->i_mode) && old_inode->i_nlink == 1) {
+			saved_crtime = EXFAT_I(new_inode)->i_crtime;
+			inherit_crtime = true;
+		}
+
+		err = __exfat_rename(old_dir, EXFAT_I(old_inode), new_dir, new_dentry);
+		if (err)
+			goto unlock;
+
+		if (inherit_crtime)
+			EXFAT_I(old_inode)->i_crtime = saved_crtime;
+	}
 
 	inode_inc_iversion(new_dir);
 	simple_rename_timestamp(old_dir, old_dentry, new_dir, new_dentry);
Signature · 1 line
2.53.0
maintainer
#7 lore raw
In reply to Sean Smith
Quoted 6 lines · Sean Smith

1. Application atomic saves destroy xattrs. Programs that save via write-to-temp + rename() replace the inode, permanently destroying all extended attributes. Only the VFS sees both inodes during rename -- no userspace mechanism can intercept this and copy metadata across.

The VFS could potentially copy the xattr on a rename, no?

Quoted 5 lines

2. Every tool in the copy chain must explicitly opt in to xattr preservation. cp requires --preserve=xattr, rsync requires -X, tar requires --xattrs. Each missing flag causes silent data loss. Transparent preservation through arbitrary tool flows is not achievable in userspace.

But this is true for your proposed ptime as well. You have to change every single tool to copy over the ptime. Worse, you have to change the format of tar in a non-standard on-disk format change to support this new ptime timestamp. And rsync will require a non-standard protocol change to support the new timestamp.

Quoted 2 lines

Atomic saves are the default behavior of mainstream applications (LibreOffice, Vim, Kate, etc.).

You will also have to change mainstream applications to copy ptime from the original file to the file.new before the atomic rename. Using ptime doesn't change this. So you will need to make this non-standard, Linux-specific change to all of these mainstream applications.

Is it worth it? It's a huge amount of cost being spread across a very large part of the open source ecosystem just this fairly narrow use case. Personally, I'm not convinced it's worth the effort.

- Ted

author
#8 lore raw

[written with AI assistance]

On Sun, Apr 05, 2026 at 06:54:42PM -0400, Theodore Tso wrote:

Thanks for the substantive engagement — it helps clarify where the proposal needs to justify itself.

Quoted 9 lines · Sean Smith
Quoted 6 lines

1. Application atomic saves destroy xattrs. Programs that save via write-to-temp + rename() replace the inode, permanently destroying all extended attributes. Only the VFS sees both inodes during rename -- no userspace mechanism can intercept this and copy metadata across.

The VFS could potentially copy the xattr on a rename, no?

It could, but even scoping to user.* means adding conditional xattr-copy logic into every filesystem's rename handler — with dynamic allocation and xattr tree lookups on a hot path. ptime avoids this: one inline inode field, clear semantics, same VFS patterns as atime/mtime/btime.

Quoted 11 lines
Quoted 5 lines

2. Every tool in the copy chain must explicitly opt in to xattr preservation. cp requires --preserve=xattr, rsync requires -X, tar requires --xattrs. Each missing flag causes silent data loss. Transparent preservation through arbitrary tool flows is not achievable in userspace.

But this is true for your proposed ptime as well. You have to change every single tool to copy over the ptime. Worse, you have to change the format of tar in a non-standard on-disk format change to support this new ptime timestamp. And rsync will require a non-standard protocol change to support the new timestamp.

You are right that copy tools require patches. If ptime only improved the copy-tool situation, I would agree it does not justify new kernel surface over xattrs.

The structural difference is in the default adoption path. xattr preservation is permanently per-invocation opt-in: each tool call needs the correct flag, and the default is to drop them. A kernel timestamp exposed through statx/utimensat follows the same API pattern as mtime — standard libraries and tools naturally evolve to preserve all standard timestamps by default. ptime has a path to default-preservation that xattrs structurally cannot reach.

On the formats: the tar patch uses a vendor-prefixed PAX header (SCHILY.ptime), backward-compatible — old readers ignore it cleanly. The rsync patch plugs into the existing --crtimes machinery that already supports macOS and Cygwin.

Quoted 8 lines
Quoted 2 lines

Atomic saves are the default behavior of mainstream applications (LibreOffice, Vim, Kate, etc.).

You will also have to change mainstream applications to copy ptime from the original file to the file.new before the atomic rename. Using ptime doesn't change this. So you will need to make this non-standard, Linux-specific change to all of these mainstream applications.

This is where the cover letter was not clear enough, and it is the core reason ptime must be a kernel timestamp.

The patches implement rename-over preservation in all 5 filesystem rename handlers. When rename(source, target) replaces an existing file, and the source has ptime=0 (the default for any newly-created temp file) while the target has ptime != 0, the filesystem copies the target's ptime to the source before destroying the target's inode. This runs inside the rename transaction, atomic with the rename itself.

Most GUI applications — LibreOffice, Kate, Qt and GNOME apps — save via write-to-temp + rename-over-original. For these, ptime survives automatically with no application changes:

1. App writes to temp file (ptime = 0)
2. rename(temp, document.odt)
3. Kernel: source ptime=0, target!=0 -> copies ptime
4. ptime preserved. No app change.

This is not universal: editors that use rename-away + create-new (Vim with default backupcopy=no, Emacs) do not trigger rename-over, and the spec documents this as a known limitation. But the write-to-temp + rename-over pattern is the dominant GUI save path, and the kernel handles it transparently — something no xattr mechanism can provide without application cooperation.

Quoted 3 lines

Is it worth it? It's a huge amount of cost being spread across a very large part of the open source ecosystem just this fairly narrow use case. Personally, I'm not convinced it's worth the effort.

I think the use case is broader than I conveyed. Any workflow that copies files from NTFS, APFS, or HFS+ onto native Linux filesystems loses user-visible creation time unless carried out-of-band. This affects personal migrations, enterprise backups, dual-boot users, and professional workflows in photography, legal, scientific data, and media production. Windows, macOS, and SMB have supported a settable creation timestamp for decades — Linux is the outlier.

Users already expend significant resources working around this gap — metadata manifests, scripts to stamp creation dates into filenames or xattrs, side-channel databases — or simply accept the data loss. The cost is already being paid, continuously and redundantly across the ecosystem. One upstream investment in ptime converts that distributed ongoing cost into a bounded effort.

ptime is separate from btime by design: it preserves btime's value as immutable forensic metadata while providing a settable timestamp that travels with file content across filesystem boundaries.

On ecosystem cost: the kernel surface is ~240 lines across 28 files. For context, I am a disabled Medicaid recipient who came to this from a disability rights litigation workflow — I need file provenance preserved across an NTFS-to-Btrfs migration for legal work. The complete implementation — kernel patches across 5 filesystems, tool patches, and xfstests — was produced in a few days using agentic development tools, which suggests the adoption cost may be meaningfully lower than traditional estimates as these tools become available across the ecosystem.

I understand a new timestamp is permanent API surface and the bar should be high. My claim is that rename-over preservation — automatic ptime survival through application saves, without application changes — makes this materially different from an xattr workaround, and justifies that cost.

Sean

#9 lore raw

[drop almaz because the kernel.org mailer immediately refused]

Quoted 39 lines · Sean Smith

[written with AI assistance]

On Sun, Apr 05, 2026 at 06:54:42PM -0400, Theodore Tso wrote:

Thanks for the substantive engagement — it helps clarify where the proposal needs to justify itself.

Quoted 9 lines · Sean Smith
Quoted 6 lines

1. Application atomic saves destroy xattrs. Programs that save via write-to-temp + rename() replace the inode, permanently destroying all extended attributes. Only the VFS sees both inodes during rename -- no userspace mechanism can intercept this and copy metadata across.

The VFS could potentially copy the xattr on a rename, no?

It could, but even scoping to user.* means adding conditional xattr-copy logic into every filesystem's rename handler — with dynamic allocation and xattr tree lookups on a hot path. ptime avoids this: one inline inode field, clear semantics, same VFS patterns as atime/mtime/btime.

Quoted 11 lines
Quoted 5 lines

2. Every tool in the copy chain must explicitly opt in to xattr preservation. cp requires --preserve=xattr, rsync requires -X, tar requires --xattrs. Each missing flag causes silent data loss. Transparent preservation through arbitrary tool flows is not achievable in userspace.

But this is true for your proposed ptime as well. You have to change every single tool to copy over the ptime. Worse, you have to change the format of tar in a non-standard on-disk format change to support this new ptime timestamp. And rsync will require a non-standard protocol change to support the new timestamp.

You are right that copy tools require patches. If ptime only improved the copy-tool situation, I would agree it does not justify new kernel surface over xattrs.

Quoted 8 lines

The structural difference is in the default adoption path. xattr preservation is permanently per-invocation opt-in: each tool call needs the correct flag, and the default is to drop them. A kernel timestamp exposed through statx/utimensat follows the same API pattern as mtime — standard libraries and tools naturally evolve to preserve all standard timestamps by default. ptime has a path to default-preservation that xattrs structurally cannot reach.

"Standard"... I was about to write a sardonic reply here, but then I remembred that Linux finally *does* have a standard means to transfer some of those newer file attributes: file_getattr/file_setattr.

(Go Andrey!)

So, I guess all you really need to do is extend struct file_attr and now userspace has a fairly convenient means to propagate the provenance time. :)

Quoted 35 lines

On the formats: the tar patch uses a vendor-prefixed PAX header (SCHILY.ptime), backward-compatible — old readers ignore it cleanly. The rsync patch plugs into the existing --crtimes machinery that already supports macOS and Cygwin.

Quoted 8 lines
Quoted 2 lines

Atomic saves are the default behavior of mainstream applications (LibreOffice, Vim, Kate, etc.).

You will also have to change mainstream applications to copy ptime from the original file to the file.new before the atomic rename. Using ptime doesn't change this. So you will need to make this non-standard, Linux-specific change to all of these mainstream applications.

This is where the cover letter was not clear enough, and it is the core reason ptime must be a kernel timestamp.

The patches implement rename-over preservation in all 5 filesystem rename handlers. When rename(source, target) replaces an existing file, and the source has ptime=0 (the default for any newly-created temp file) while the target has ptime != 0, the filesystem copies the target's ptime to the source before destroying the target's inode. This runs inside the rename transaction, atomic with the rename itself.

Most GUI applications — LibreOffice, Kate, Qt and GNOME apps — save via write-to-temp + rename-over-original. For these, ptime survives automatically with no application changes:

1. App writes to temp file (ptime = 0)
2. rename(temp, document.odt)
3. Kernel: source ptime=0, target!=0 -> copies ptime
4. ptime preserved. No app change.

Quoted 7 lines

This is not universal: editors that use rename-away + create-new (Vim with default backupcopy=no, Emacs) do not trigger rename-over, and the spec documents this as a known limitation. But the write-to-temp + rename-over pattern is the dominant GUI save path, and the kernel handles it transparently — something no xattr mechanism can provide without application cooperation.

So does the provenance time cover just the file's contents, or the other attributes and xattrs?

The reason I ask is, does the ptime get copied over for an FICLONE, which maps all of one file's data blocks into another?

And by extension, would it also need to be exchanged if you told XFS_IOC_EXCHANGE_RANGE to exchange all contents between two files?

(I know, I know, you said XFS was TBDHBD ;))

Last question: Is the provenance time only useful if the file is immutable? Either directly via chattr +i, or by enabling fsverity?

--D

Quoted 45 lines
Quoted 3 lines

Is it worth it? It's a huge amount of cost being spread across a very large part of the open source ecosystem just this fairly narrow use case. Personally, I'm not convinced it's worth the effort.

I think the use case is broader than I conveyed. Any workflow that copies files from NTFS, APFS, or HFS+ onto native Linux filesystems loses user-visible creation time unless carried out-of-band. This affects personal migrations, enterprise backups, dual-boot users, and professional workflows in photography, legal, scientific data, and media production. Windows, macOS, and SMB have supported a settable creation timestamp for decades — Linux is the outlier.

Users already expend significant resources working around this gap — metadata manifests, scripts to stamp creation dates into filenames or xattrs, side-channel databases — or simply accept the data loss. The cost is already being paid, continuously and redundantly across the ecosystem. One upstream investment in ptime converts that distributed ongoing cost into a bounded effort.

ptime is separate from btime by design: it preserves btime's value as immutable forensic metadata while providing a settable timestamp that travels with file content across filesystem boundaries.

On ecosystem cost: the kernel surface is ~240 lines across 28 files. For context, I am a disabled Medicaid recipient who came to this from a disability rights litigation workflow — I need file provenance preserved across an NTFS-to-Btrfs migration for legal work. The complete implementation — kernel patches across 5 filesystems, tool patches, and xfstests — was produced in a few days using agentic development tools, which suggests the adoption cost may be meaningfully lower than traditional estimates as these tools become available across the ecosystem.

I understand a new timestamp is permanent API surface and the bar should be high. My claim is that rename-over preservation — automatic ptime survival through application saves, without application changes — makes this materially different from an xattr workaround, and justifies that cost.

Sean

author
#10 lore raw

[written with AI assistance]

Quoted 9 lines · Darrick J. Wong

"Standard"... I was about to write a sardonic reply here, but then I remembred that Linux finally *does* have a standard means to transfer some of those newer file attributes: file_getattr/file_setattr.

(Go Andrey!)

So, I guess all you really need to do is extend struct file_attr and now userspace has a fairly convenient means to propagate the provenance time. 🙂

Thank you for pointing to file_getattr/file_setattr — this is a significantly better API path than our utimensat extension. The size-versioned struct file_attr eliminates the glibc times[2] limitation entirely, which was one of the main upstream concerns with the current approach.

We will investigate extending struct file_attr with ptime fields for v2. The on-disk storage across all 5 filesystems and the rename-over preservation are API-independent and would remain unchanged. The change is re-plumbing the userspace write path from utimensat to file_setattr.

Two design questions:

Would you recommend fa_ptime_sec (__u64) + fa_ptime_nsec (__u32) matching the statx timespec pattern, or a different representation?

Pali Rohar has announced plans for mask fields in file_attr. Should we coordinate with his mask work so ptime can be selectively set without read-modify-write?

Quoted 2 lines

So does the provenance time cover just the file's contents, or the other attributes and xattrs?

Content only. ptime records when the file's data first came into existence. Metadata changes (permissions, owner, xattrs) update ctime but leave ptime unchanged. This matches the semantics of Windows Date Created and macOS creation time.

Quoted 2 lines

The reason I ask is, does the ptime get copied over for an FICLONE, which maps all of one file's data blocks into another?

It should, conceptually — the content's provenance doesn't change when you clone it. Currently FICLONE shares data extents but does not copy inode metadata (timestamps, permissions), so ptime would not be automatically preserved. The calling tool (e.g., cp --reflink) handles timestamp copying separately via the write path.

The question is whether FICLONE should be enhanced to copy ptime from source to destination at the kernel level — similar to how rename-over preserves ptime. There is an argument for it: if the kernel handles provenance during clone, tools don't need to know. But FICLONE doesn't currently copy mtime either, so adding ptime alone would be inconsistent. Worth discussing.

Btrfs subvolume snapshots are a different case — they do preserve ptime because the inodes are COW copies of the originals.

Quoted 2 lines

And by extension, would it also need to be exchanged if you told XFS_IOC_EXCHANGE_RANGE to exchange all contents between two files?

Yes — if the content moves, the provenance should move with it. If files A and B exchange data extents, their ptimes should swap. ptime follows the content, not the inode identity.

Quoted 1 line

(I know, I know, you said XFS was TBDHBD ;))

Worth considering for a future XFS implementation — and the file_attr route you suggested would give XFS a clean integration path for ptime alongside FICLONE and EXCHANGE_RANGE.

Quoted 2 lines

Last question: Is the provenance time only useful if the file is immutable? Either directly via chattr +i, or by enabling fsverity?

No — ptime is useful regardless of mutability. It records when the document was born, the same way Windows Date Created works. Editing a document updates mtime but not the creation date. Both are independently valuable:

ptime: "This file was first created March 15, 2019"
mtime: "It was last modified today"
btime: "This inode was created when I copied it here"

Immutable files (chattr +i, fsverity) are a special case where ptime has extra forensic strength — the content provably hasn't changed since the provenance date. But for the primary use case — preserving creation dates across cross-platform migrations — mutability doesn't diminish ptime's value. A document's creation date remains meaningful regardless of subsequent edits.

Sean

#11 lore raw
Quoted 27 lines · Sean Smith

[written with AI assistance]

Quoted 9 lines · Darrick J. Wong

"Standard"... I was about to write a sardonic reply here, but then I remembred that Linux finally *does* have a standard means to transfer some of those newer file attributes: file_getattr/file_setattr.

(Go Andrey!)

So, I guess all you really need to do is extend struct file_attr and now userspace has a fairly convenient means to propagate the provenance time. 🙂

Thank you for pointing to file_getattr/file_setattr — this is a significantly better API path than our utimensat extension. The size-versioned struct file_attr eliminates the glibc times[2] limitation entirely, which was one of the main upstream concerns with the current approach.

We will investigate extending struct file_attr with ptime fields for v2. The on-disk storage across all 5 filesystems and the rename-over preservation are API-independent and would remain unchanged. The change is re-plumbing the userspace write path from utimensat to file_setattr.

Quoted 5 lines

Two design questions:

Would you recommend fa_ptime_sec (__u64) + fa_ptime_nsec (__u32) matching the statx timespec pattern, or a different representation?

That uses less space in the struct, so yes.

Quoted 3 lines

Pali Rohar has announced plans for mask fields in file_attr. Should we coordinate with his mask work so ptime can be selectively set without read-modify-write?

fa_xflags already provides that coverage for the other fields in struct file_attr, e.g. a getattr caller can ignore fa_extsize if FS_XFLAG_EXTSIZE isn't set; and setattr will (iirc) reject nonzero fa_extsize if FS_XFLAG_EXTSIZE isn't set.

Pali Rohar's work would make it possible to discover which fa_xflags fields are settable or clearable for a given file.

Quoted 19 lines
Quoted 2 lines

So does the provenance time cover just the file's contents, or the other attributes and xattrs?

Content only. ptime records when the file's data first came into existence. Metadata changes (permissions, owner, xattrs) update ctime but leave ptime unchanged. This matches the semantics of Windows Date Created and macOS creation time.

Quoted 2 lines

The reason I ask is, does the ptime get copied over for an FICLONE, which maps all of one file's data blocks into another?

It should, conceptually — the content's provenance doesn't change when you clone it. Currently FICLONE shares data extents but does not copy inode metadata (timestamps, permissions), so ptime would not be automatically preserved. The calling tool (e.g., cp --reflink) handles timestamp copying separately via the write path.

Quoted 7 lines

The question is whether FICLONE should be enhanced to copy ptime from source to destination at the kernel level — similar to how rename-over preserves ptime. There is an argument for it: if the kernel handles provenance during clone, tools don't need to know. But FICLONE doesn't currently copy mtime either, so adding ptime alone would be inconsistent. Worth discussing.

FICLONE is currently treated like a write, which means that mtime is updated on the destination file. For filesystems supporting ptime you'd probably want the kernel to copy the ptime from src to dest after the remapping completes.

(Same for exchange-range)

Quoted 31 lines

Btrfs subvolume snapshots are a different case — they do preserve ptime because the inodes are COW copies of the originals.

Quoted 2 lines

And by extension, would it also need to be exchanged if you told XFS_IOC_EXCHANGE_RANGE to exchange all contents between two files?

Yes — if the content moves, the provenance should move with it. If files A and B exchange data extents, their ptimes should swap. ptime follows the content, not the inode identity.

Quoted 1 line

(I know, I know, you said XFS was TBDHBD ;))

Worth considering for a future XFS implementation — and the file_attr route you suggested would give XFS a clean integration path for ptime alongside FICLONE and EXCHANGE_RANGE.

Quoted 2 lines

Last question: Is the provenance time only useful if the file is immutable? Either directly via chattr +i, or by enabling fsverity?

No — ptime is useful regardless of mutability. It records when the document was born, the same way Windows Date Created works. Editing a document updates mtime but not the creation date. Both are independently valuable:

ptime: "This file was first created March 15, 2019"
mtime: "It was last modified today"
btime: "This inode was created when I copied it here"

Quoted 7 lines

Immutable files (chattr +i, fsverity) are a special case where ptime has extra forensic strength — the content provably hasn't changed since the provenance date. But for the primary use case — preserving creation dates across cross-platform migrations — mutability doesn't diminish ptime's value. A document's creation date remains meaningful regardless of subsequent edits.

Got it.

--D

Quoted 4 lines

Sean

maintainer
#12 lore raw
In reply to Sean Smith
Quoted 7 lines · Sean Smith

The patches implement rename-over preservation in all 5 filesystem rename handlers. When rename(source, target) replaces an existing file, and the source has ptime=0 (the default for any newly-created temp file) while the target has ptime != 0, the filesystem copies the target's ptime to the source before destroying the target's inode. This runs inside the rename transaction, atomic with the rename itself.

Yelch. This is so *very* non-Unixy / non-POSIX / non-Linux.

I understand why it's convenient for your particular use case, but rename(2) is fundamentally an operation which works on directory entries. We don't copy over extended attributes, or Posix ACL's, or Unix permission mode bits, because (a) that would violate POSIX and historical Unix behavior, and (b) because rename(2) is fundamentally a directory entry operation. This is despite the fact that it be more convenient if we didn't have to copy over things like extended attributes, ACL's, permission mode bits, etc. So you want to make an
exception for ptime? Yelch. Just Yelch.

- Ted

author
#13 lore raw
Quoted 12 lines · Theodore Tso

Yelch. This is so *very* non-Unixy / non-POSIX / non-Linux.

I understand why it's convenient for your particular use case, but rename(2) is fundamentally an operation which works on directory entries. We don't copy over extended attributes, or Posix ACL's, or Unix permission mode bits, because (a) that would violate POSIX and historical Unix behavior, and (b) because rename(2) is fundamentally a directory entry operation. This is despite the fact that it be more convenient if we didn't have to copy over things like extended attributes, ACL's, permission mode bits, etc. So you want to make an
exception for ptime? Yelch. Just Yelch.

Quoted 1 line

- Ted

[written by Sean]

Finding an alternative to using rename() to transfer ptime between inodes during atomic saves seems beyond the scope of what I can address as someone who relies upon AI agents to review and modify code. From what my agents have been able to explain to me, the options here are using rename() to transfer ptime via the kernel, or using renameat2 to require each application to manually preserve/transfer ptime. The latter is, well, the phrase herding cats might be an understatement of the difficulty involved.

As an individual, I don't see how I could convince every major open-source and closed source application developer for Linux to code their application to preserve and transfer ptime. That seems like something only leadership in the community has any chance of doing, and even then, that would be a very slow-moving process.

It also doesn't solve the immediate needs of increasing number of users who are trying to ditch Windows for Linux. Windows 11 has pushed one too many people too far, and they, like me, have had enough.

Maybe senior developers can find an alternative to rename() that works. I can cross my fingers and hope. Discussing matters with my agents we couldn't find one. The need for ptime is very real, and the code in my patch gets the job done, but a solution that respects conventions requires a level of expertise I don't have. Perhaps in 4-8 months when my AI harness is more mature and smarter models are available we can solve this.

If the rename() code making atomic saves work prevents an upstream merge, I understand. It would be messy adding ptime to the kernel only to have it disappear anytime any unpatched application modified a file. Avoiding such failure modes is why it seemed necessary to take a kernel level approach.

Additionally, the reason an xattr ptime isn't a viable solution is because applications do not reliably support xattr transfer. Thus it would not seem likely ptime would receive better support.

I can patch every application I use which is open-source, or I can patch the kernel. Rational analysis requires that I patch the kernel.

I'll continue using the rename-over approach in my own system, and maintaining a github repo so that others who need it can patch their own kernels. As the phrase goes, it's better than nothing. If there's a path that respects conventions, I'm genuinely interested in guidance.

- Sean

maintainer
#14 lore raw
Quoted 7 lines · Sean Smith

Finding an alternative to using rename() to transfer ptime between inodes during atomic saves seems beyond the scope of what I can address as someone who relies upon AI agents to review and modify code....

It also doesn't solve the immediate needs of increasing number of users who are trying to ditch Windows for Linux. Windows 11 has pushed one too many people too far, and they, like me, have had enough.

I understand that you have one goal which seems more important than anything else (to you). But consider what might happen if you ask an AI, "How can we solve the paper clip shortage problem?", or "How do we carry out regime change in Iran?" AI models which don't consider secord order effects (or used by people who have different priorities than others) can result in... suboptimal results.

And this is why it's important why it's important to have humans in the loop instead of blindly trusting AI models.

I'd also suggest that you consider the value of patience. Linux is 35 years old as of this writing. One of the reasons why Linux has stuck around this long is because we take the long view. Sure, it might take longer to shift the ecosystem to use some new interface or new feature; but everyone will have to live with muddled interface semantics *forever*.

Quoted 1 line

The need for ptime is very real....

It's important for *you*. But the vast majority of Linux users are not Windows refugees. (Ask your AI models to explain the significance of the phrase, "This is the year of the Linux Desktop".) Even for Windows users, it is not all clear that Windows-style File Creation time is that important for those users. MacOS doesn't have this Windows-style timestamp support, and it hasn't stopped many users from switching from Windows to MacOS.

Quoted 1 line

... and the code in my patch gets the job done...

But it doesn't. Bastardizing the semantics of the rename interface doesn't completely solve the problem you've articulated. In particular, all of the userspace programs which need to create new files --- tar file extraction, unzip, file copying, etc., still need to be changed.

This will require changing userspace applications. So why not use that approach to address your problem statement?

Quoted 2 lines

I can patch every application I use which is open-source, or I can patch the kernel. Rational analysis requires that I patch the kernel.

That's certainly your perogative, and that's the beauty of Open Source. You're free to patch the software that you use on your system.

Cheers,

- Ted

author
#15 lore raw
Quoted 14 lines · Theodore Tso

AI models which don't consider secord order effects (or used by people who have different priorities than others) can result in... suboptimal results.>
And this is why it's important why it's important to have humans in the loop instead of blindly trusting AI models.

I'd also suggest that you consider the value of patience. Linux is 35 years old as of this writing. One of the reasons why Linux has stuck around this long is because we take the long view. Sure, it might take longer to shift the ecosystem to use some new interface or new feature; but everyone will have to live with muddled interface semantics *forever*.

Quoted 3 lines

MacOS doesn't have this Windows-style timestamp support, and it hasn't stopped many users from switching from Windows to MacOS.

I think this thread demonstrates the value of humans in the loop. Despite my efforts to be careful, my earlier claim that "Windows, macOS, and SMB have supported a settable creation timestamp for decades — Linux is the outlier" was wrong. macOS doesn't preserve date created on copy either. Windows is now the outlier. Working with AI, it feels like drowning in information which no single individual has the time or ability to fully verify. Your years of experience caught what I and agents did not.

From the sound of it, the issues with rename() will block an upstream merge of a kernel-based ptime. I can maintain my kernel patch on GitHub for those that want it. The file_attr design work that came out of Darrick Wong's engagement was productive and worth doing despite this outcome.

I see long-term value in our discussion. As agents become more involved in projects, this LKML thread will establish ptime as a solution to this specific problem. Someone migrating from Windows to Linux who asks their agents how to preserve date created are going to find this thread.

The tools I used to build my kernel patch didn't exist six months ago. My AI harness is just as much a dependency, and it didn't exist three months ago. My harness undergoes rapid development and increases in capabilities, even without newer, smarter models releasing every few months.

I think there's a real risk of a negative feedback loop. As implementation costs drop, people who could contribute to upstream will find it easier to build and maintain a custom stack rather than try to get changes through a multi-year review process. I think experienced developers need to lead a shift in how contributions are reviewed and organized. I think the key challenge for maintainers is figuring out how to work with a new class of contributor, and direct them so they work in a coordinated manner.

Thanks for taking the time to engage with me.

- Sean

#16 lore raw
In reply to Theodore Tso
Quoted 10 lines · Theodore Ts'o
Quoted 7 lines · Sean Smith

The patches implement rename-over preservation in all 5 filesystem rename handlers. When rename(source, target) replaces an existing file, and the source has ptime=0 (the default for any newly-created temp file) while the target has ptime != 0, the filesystem copies the target's ptime to the source before destroying the target's inode. This runs inside the rename transaction, atomic with the rename itself.

Yelch. This is so *very* non-Unixy / non-POSIX / non-Linux.

I think you meant to type "N", "A", and "K".