zfs_ctldir: make .zfs/snapshot/<name> btime the snapshot creation time

Before the automount, '.zfs/snapshot/<name>' is a placeholder inode
using simple_dir_inode_operations, which has no getattr, so statx(2)
reports no birth time. Once mounted, it is the snapshot's root
directory and reports that directory's crtime, i.e. the dataset
creation time. A consumer therefore gets either no birth time or the
dataset creation time, never the snapshot's, and the answer changes
when the snapshot is automounted.

Report the snapshot creation time as stx_btime consistently in both
states, so it can be read with a plain statx(2) on the path. Give
the placeholder its own inode_operations with a getattr that returns
the creation time zfsctl_inode_alloc() already stores, and override
the birth time of a mounted snapshot's root in zfs_getattr_fast()
next to the existing inode number override. The STATX_BTIME copy
moves from zpl_getattr() into zfs_getattr_fast() so the override is
not overwritten. Add snapdir_stat_btime to ZTS.

Reviewed-by: Brian Behlendorf <[email protected]>
Reviewed-by: Alexander Motin <[email protected]>
Signed-off-by: Ameer Hamza <[email protected]>
Closes #19093
This commit is contained in:
Ameer Hamza authored and GitHub committed 2026-09-15 14:53:12 -07:00
1 parent fa4bc4dec7
commit f5b2fc8e29
8 files changed
+148 -13

No files matched your search

+2
View File
@@ -86,6 +86,8 @@ extern const struct inode_operations zpl_ops_root;
extern const struct file_operations zpl_fops_snapdir;
extern const struct inode_operations zpl_ops_snapdir;
extern const struct inode_operations zpl_ops_snapdirs;
extern const struct file_operations zpl_fops_shares;
extern const struct inode_operations zpl_ops_shares;
+3 -1
View File
@@ -536,6 +536,8 @@ zfsctl_inode_alloc(zfsvfs_t *zfsvfs, uint64_t id,
zp->z_pflags = 0;
zp->z_mode = 0;
zp->z_sync_cnt = 0;
zp->z_btime.tv_sec = creation;
zp->z_btime.tv_nsec = 0;
ip->i_generation = 0;
ip->i_ino = id;
ip->i_mode = (S_IFDIR | S_IRWXUGO);
@@ -865,7 +867,7 @@ zfsctl_snapdir_lookup(struct inode *dip, const char *name, struct inode **ipp,
}
*ipp = zfsctl_inode_lookup(zfsvfs, ZFSCTL_INO_SNAPDIRS - id,
&simple_dir_operations, &simple_dir_inode_operations);
&simple_dir_operations, &zpl_ops_snapdirs);
if (*ipp == NULL)
error = SET_ERROR(ENOENT);
+18 -1
View File
@@ -1775,14 +1775,31 @@ zfs_getattr_fast(zidmap_t *idmap, u32 request_mask, struct inode *ip,
mutex_exit(&zp->z_lock);
#ifdef STATX_BTIME
if (request_mask & STATX_BTIME) {
sp->btime = zp->z_btime;
sp->result_mask |= STATX_BTIME;
}
#endif
/*
* Required to prevent NFS client from detecting different inode
* numbers of snapshot root dentry before and after snapshot mount.
* Likewise report the snapshot creation time as the birth time,
* matching the unmounted '.zfs/snapshot/<name>' directory.
*/
if (zfsvfs->z_issnap) {
if (ip->i_sb->s_root->d_inode == ip)
if (ip->i_sb->s_root->d_inode == ip) {
sp->ino = ZFSCTL_INO_SNAPDIRS -
dmu_objset_id(zfsvfs->z_os);
#ifdef STATX_BTIME
if (request_mask & STATX_BTIME) {
sp->btime.tv_sec = dsl_get_creation(
dmu_objset_ds(zfsvfs->z_os));
sp->btime.tv_nsec = 0;
}
#endif
}
}
zfs_exit(zfsvfs, FTAG);
+35 -3
View File
@@ -88,7 +88,7 @@ ZPL_IDMAP_IOP_DEFINE(int, zpl_root_getattr, 4,
const struct path *, path, struct kstat *, stat, u32, request_mask,
unsigned int, query_flags)
{
(void) request_mask, (void) query_flags;
(void) query_flags;
struct inode *ip = path->dentry->d_inode;
zpl_generic_fillattr(idmap, request_mask, ip, stat);
stat->atime = current_time(ip);
@@ -731,7 +731,7 @@ ZPL_IDMAP_IOP_DEFINE(int, zpl_snapdir_getattr, 4,
const struct path *, path, struct kstat *, stat, u32, request_mask,
unsigned int, query_flags)
{
(void) request_mask, (void) query_flags;
(void) query_flags;
struct inode *ip = path->dentry->d_inode;
zfsvfs_t *zfsvfs = ITOZSB(ip);
int error;
@@ -788,6 +788,38 @@ const struct inode_operations zpl_ops_snapdir = {
.mkdir = zpl_snapdir_mkdir,
};
/*
* Get unmounted '.zfs/snapshot/<name>' directory attributes.
*/
ZPL_IDMAP_IOP_DEFINE(int, zpl_snapdirs_getattr, 4,
const struct path *, path, struct kstat *, stat, u32, request_mask,
unsigned int, query_flags)
{
(void) query_flags;
struct inode *ip = path->dentry->d_inode;
znode_t *zp __maybe_unused = ITOZ(ip);
zpl_generic_fillattr(idmap, request_mask, ip, stat);
#ifdef STATX_BTIME
if ((request_mask & STATX_BTIME) && zp->z_btime.tv_sec != 0) {
stat->btime = zp->z_btime;
stat->result_mask |= STATX_BTIME;
}
#endif
return (0);
}
/*
* The '.zfs/snapshot/<name>' directory inode operations, used until the
* snapshot is mounted over it.
*/
const struct inode_operations zpl_ops_snapdirs = {
.lookup = simple_lookup,
.getattr = zpl_snapdirs_getattr,
};
static struct dentry *
zpl_shares_lookup(struct inode *dip, struct dentry *dentry,
unsigned int flags)
@@ -854,7 +886,7 @@ ZPL_IDMAP_IOP_DEFINE(int, zpl_shares_getattr, 4,
const struct path *, path, struct kstat *, stat, u32, request_mask,
unsigned int, query_flags)
{
(void) request_mask, (void) query_flags;
(void) query_flags;
struct inode *ip = path->dentry->d_inode;
zfsvfs_t *zfsvfs = ITOZSB(ip);
znode_t *dzp;
-7
View File
@@ -432,13 +432,6 @@ ZPL_IDMAP_IOP_DEFINE(int, zpl_getattr, 4,
error = -zfs_getattr_fast(idmap, request_mask, ip, stat);
#ifdef STATX_BTIME
if (request_mask & STATX_BTIME) {
stat->btime = zp->z_btime;
stat->result_mask |= STATX_BTIME;
}
#endif
#ifdef STATX_CHANGE_COOKIE
if (request_mask & STATX_CHANGE_COOKIE) {
/*
+1 -1
View File
@@ -238,7 +238,7 @@ tests = ['snapdir_admin_create_destroy', 'snapdir_admin_rename_destroy',
'snapdir_stat_no_mount', 'snapdir_mount_bind', 'snapdir_mount_move',
'snapdir_mount_expire_bind', 'snapdir_mount_expire_move',
'snapdir_mount_multiple', 'snapdir_mount_fd_hold', 'snapdir_mount_foreign',
'snapdir_mount_destroy_defer']
'snapdir_mount_destroy_defer', 'snapdir_stat_btime']
tags = ['functional', 'snapdir']
[tests/functional/syncfs:Linux]
+1
View File
@@ -2296,6 +2296,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/snapdir/snapdir_mount_no_setuid.ksh \
functional/snapdir/snapdir_mount_parallel.ksh \
functional/snapdir/snapdir_mount_unmount.ksh \
functional/snapdir/snapdir_stat_btime.ksh \
functional/snapdir/snapdir_stat_no_mount.ksh \
functional/snapshot/cleanup.ksh \
functional/snapshot/clone_001_pos.ksh \
@@ -0,0 +1,88 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# https://opensource.org/license/CDDL-1.0.
#
#
# Copyright (c) 2026 by iXsystems, Inc.
#
. $STF_SUITE/tests/functional/snapdir/snapdir.kshlib
. $STF_SUITE/tests/functional/snapdir/snapdir.cfg
verify_runnable "both"
log_assert "Verify .zfs/snapshot/<name> reports the snapshot creation time as" \
"its birth time whether or not the snapshot is mounted"
if ! is_linux ; then
log_unsupported "This test uses the Linux-only statx helper"
fi
function cleanup
{
destroy_pool $TESTPOOL
}
log_onexit cleanup
# The statx helper opens with O_PATH and never automounts, so it can observe
# the unmounted placeholder. It exits 2 if STATX_BTIME is not in stx_mask.
function check_btime # <path> <expected seconds> <description>
{
typeset out btime
out=$(statx btime $1 2>&1) || log_fail "$3: statx btime failed: $out"
btime=${out#btime: }
log_note "$3: btime $btime (snapshot creation $2)"
[[ ${btime%.*} -eq $2 && ${btime#*.} -eq 0 ]] ||
log_fail "$3: btime $btime != snapshot creation $2"
}
# Create a pool and a filesystem
create_pool $TESTPOOL $DISKS
log_must zfs create -o snapdir=visible -o mountpoint=$TESTDIR/fs \
$TESTPOOL/$TESTFS
# The creation property has one-second granularity; make sure the snapshot's
# differs from the filesystem root's crtime, which is what the mounted root
# used to report, so that the mounted-state check below is discriminating.
sleep 2
log_must zfs snapshot $TESTPOOL/$TESTFS@snap
typeset snap=$TESTPOOL/$TESTFS@snap
typeset snapdir=$TESTDIR/fs/$SNAPROOT/snap
typeset -i creation=$(get_prop creation $snap)
typeset fs_btime
fs_btime=$(statx btime $TESTDIR/fs) ||
log_fail "statx btime $TESTDIR/fs failed: $fs_btime"
fs_btime=${fs_btime#btime: }
log_must test ${fs_btime%.*} -lt $creation
# 1. Unmounted placeholder: reports the snapshot creation time, and
# statx itself must not have mounted the snapshot.
log_must test -z "$(get_mount_paths $snap)"
check_btime $snapdir $creation "unmounted"
log_must test -z "$(get_mount_paths $snap)"
typeset ino=$(statx ino $snapdir)
# 2. Mounted root: trigger the automount, same birth time, same inode
# number (zfs_getattr_fast() remaps both for the snapshot root).
log_must ls -l $snapdir
log_must test "$(get_mount_paths $snap)" == "$snapdir"
check_btime $snapdir $creation "mounted"
log_must test "$(statx ino $snapdir)" == "$ino"
# 3. After an explicit unmount the placeholder is back.
log_must umount $snapdir
log_must test -z "$(get_mount_paths $snap)"
check_btime $snapdir $creation "unmounted again"
log_pass "Snapshot dir birth time is the snapshot creation time in both states."