From: Sean Smith <defendthedisabled@gmail.com>
To: linux-fsdevel@vger.kernel.org
Cc: linux-ext4@vger.kernel.org, linux-btrfs@vger.kernel.org,
tytso@mit.edu, dsterba@suse.com, david@fromorbit.com,
brauner@kernel.org, osandov@osandov.com, almaz@kernel.org,
hirofumi@mail.parknet.co.jp, linkinjeon@kernel.org,
Sean Smith <DefendTheDisabled@gmail.com>
Subject: [PATCH 2/6] btrfs: add provenance time (ptime) support
Date: Sun, 5 Apr 2026 14:49:58 -0500 [thread overview]
Message-ID: <20260405195007.1306-3-DefendTheDisabled@gmail.com> (raw)
In-Reply-To: <20260405195007.1306-1-DefendTheDisabled@gmail.com>
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).
Signed-off-by: Sean Smith <DefendTheDisabled@gmail.com>
---
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(-)
diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h
index 73602ee8d..bac92f766 100644
--- a/fs/btrfs/btrfs_inode.h
+++ b/fs/btrfs/btrfs_inode.h
@@ -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;
diff --git a/fs/btrfs/delayed-inode.c b/fs/btrfs/delayed-inode.c
index 7e3d294a6..649de7c29 100644
--- a/fs/btrfs/delayed-inode.c
+++ b/fs/btrfs/delayed-inode.c
@@ -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))
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index 8ffbc40eb..7c8105ecf 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -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
diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c
index 13f1f3b52..dce80561a 100644
--- a/fs/btrfs/inode.c
+++ b/fs/btrfs/inode.c
@@ -4029,6 +4029,8 @@ static int btrfs_read_locked_inode(struct btrfs_inode *inode, struct btrfs_path
inode->i_otime_sec = btrfs_timespec_sec(leaf, &inode_item->otime);
inode->i_otime_nsec = btrfs_timespec_nsec(leaf, &inode_item->otime);
+ inode->i_ptime_sec = btrfs_timespec_sec(leaf, &inode_item->ptime);
+ inode->i_ptime_nsec = btrfs_timespec_nsec(leaf, &inode_item->ptime);
inode_set_bytes(vfs_inode, btrfs_inode_nbytes(leaf, inode_item));
inode->generation = btrfs_inode_generation(leaf, inode_item);
@@ -4220,6 +4222,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);
btrfs_set_inode_nbytes(leaf, item, inode_get_bytes(inode));
btrfs_set_inode_generation(leaf, item, BTRFS_I(inode)->generation);
@@ -5424,6 +5428,12 @@ static int btrfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
}
if (attr->ia_valid) {
+ if (attr->ia_valid & ATTR_PTIME) {
+ BTRFS_I(inode)->i_ptime_sec = attr->ia_ptime.tv_sec;
+ BTRFS_I(inode)->i_ptime_nsec = attr->ia_ptime.tv_nsec;
+ btrfs_set_fs_compat_ro(BTRFS_I(inode)->root->fs_info, PTIME);
+ }
+
setattr_copy(idmap, inode, attr);
inode_inc_iversion(inode);
ret = btrfs_dirty_inode(BTRFS_I(inode));
@@ -8007,6 +8017,8 @@ struct inode *btrfs_alloc_inode(struct super_block *sb)
ei->i_otime_sec = 0;
ei->i_otime_nsec = 0;
+ ei->i_ptime_sec = 0;
+ ei->i_ptime_nsec = 0;
inode = &ei->vfs_inode;
btrfs_extent_map_tree_init(&ei->extent_tree);
@@ -8159,6 +8171,14 @@ static int btrfs_getattr(struct mnt_idmap *idmap,
u32 bi_ro_flags = BTRFS_I(inode)->ro_flags;
stat->result_mask |= STATX_BTIME;
+ if (request_mask & STATX_PTIME) {
+ if (BTRFS_I(inode)->i_ptime_sec ||
+ BTRFS_I(inode)->i_ptime_nsec) {
+ stat->ptime.tv_sec = BTRFS_I(inode)->i_ptime_sec;
+ stat->ptime.tv_nsec = BTRFS_I(inode)->i_ptime_nsec;
+ stat->result_mask |= STATX_PTIME;
+ }
+ }
stat->btime.tv_sec = BTRFS_I(inode)->i_otime_sec;
stat->btime.tv_nsec = BTRFS_I(inode)->i_otime_nsec;
if (bi_flags & BTRFS_INODE_APPEND)
@@ -8675,6 +8695,28 @@ static int btrfs_rename(struct mnt_idmap *idmap,
btrfs_abort_transaction(trans, ret);
goto out_fail;
}
+ /*
+ * ptime rename-over preservation: if a file with no ptime
+ * is being renamed over a file that has ptime (the atomic
+ * save pattern: write-to-temp + rename over original),
+ * inherit the target's ptime so provenance survives.
+ */
+ if (new_inode && S_ISREG(old_inode->i_mode) &&
+ S_ISREG(new_inode->i_mode) && old_inode->i_nlink == 1 &&
+ !(flags & (RENAME_EXCHANGE | RENAME_WHITEOUT))) {
+ struct btrfs_inode *old_bi = BTRFS_I(old_inode);
+ struct btrfs_inode *new_bi = BTRFS_I(new_inode);
+ if (!old_bi->i_ptime_sec && !old_bi->i_ptime_nsec &&
+ (new_bi->i_ptime_sec || new_bi->i_ptime_nsec)) {
+ old_bi->i_ptime_sec = new_bi->i_ptime_sec;
+ old_bi->i_ptime_nsec = new_bi->i_ptime_nsec;
+ }
+ }
+ /* 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. */
+
ret = btrfs_update_inode(trans, BTRFS_I(old_inode));
if (unlikely(ret)) {
btrfs_abort_transaction(trans, ret);
diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c
index 6c40f48cc..7ed09af22 100644
--- a/fs/btrfs/tree-log.c
+++ b/fs/btrfs/tree-log.c
@@ -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
diff --git a/include/uapi/linux/btrfs.h b/include/uapi/linux/btrfs.h
index e8fd92789..d2c542425 100644
--- a/include/uapi/linux/btrfs.h
+++ b/include/uapi/linux/btrfs.h
@@ -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)
diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h
index fc29d2738..719c00363 100644
--- a/include/uapi/linux/btrfs_tree.h
+++ b/include/uapi/linux/btrfs_tree.h
@@ -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;
--
2.53.0
next prev parent reply other threads:[~2026-04-05 19:50 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-05 19:49 [RFC PATCH v1 0/6] provenance_time (ptime): a new settable timestamp for cross-filesystem provenance Sean Smith
2026-04-05 19:49 ` [PATCH 1/6] vfs: add provenance_time (ptime) infrastructure Sean Smith
2026-04-05 19:49 ` Sean Smith [this message]
2026-04-05 19:49 ` [PATCH 3/6] ntfs3: map ptime to NTFS creation time with rename-over Sean Smith
2026-04-05 19:50 ` [PATCH 4/6] ext4: add dedicated ptime field alongside i_crtime Sean Smith
2026-04-05 19:50 ` [PATCH 5/6] fat: map ptime to FAT creation time with rename-over Sean Smith
2026-04-05 19:50 ` [PATCH 6/6] exfat: map ptime to exFAT " Sean Smith
2026-04-05 22:54 ` [RFC PATCH v1 0/6] provenance_time (ptime): a new settable timestamp for cross-filesystem provenance Theodore Tso
2026-04-07 0:05 ` Sean Smith
2026-04-07 1:42 ` Darrick J. Wong
2026-04-07 6:06 ` Sean Smith
2026-04-07 15:17 ` Darrick J. Wong
2026-04-07 23:36 ` Theodore Tso
2026-04-08 2:54 ` Sean Smith
2026-04-08 13:33 ` Theodore Tso
2026-04-09 0:15 ` Sean Smith
2026-04-09 13:38 ` Christian Brauner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260405195007.1306-3-DefendTheDisabled@gmail.com \
--to=defendthedisabled@gmail.com \
--cc=almaz@kernel.org \
--cc=brauner@kernel.org \
--cc=david@fromorbit.com \
--cc=dsterba@suse.com \
--cc=hirofumi@mail.parknet.co.jp \
--cc=linkinjeon@kernel.org \
--cc=linux-btrfs@vger.kernel.org \
--cc=linux-ext4@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=osandov@osandov.com \
--cc=tytso@mit.edu \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.