Skip to content

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

Merged
behlendorf merged 2 commits into
openzfs:masterfrom
truenas:snapdir-btime
Sep 15, 2026
Merged

behlendorf merged 2 commits into
openzfs:masterfrom
truenas:snapdir-btime

Conversation

@ixhamza

@ixhamza ixhamza commented Sep 10, 2026

Copy link
Copy Markdown
Member

Motivation and Context

.zfs/snapshot/<name> reports a different birth time depending on whether the snapshot is automounted. Before the automount it is a placeholder inode with the kernel's simple_dir_inode_operations, which has no getattr, so statx(2) returns no stx_btime at all (a/m/ctime are the snapshot creation time since a21ca18). Once mounted it is the snapshot's root directory and reports that directory's crtime, i.e. the dataset creation time. So a consumer that wants "when was this snapshot taken" from a plain statx(2) gets nothing or the dataset creation time, and the answer changes when the snapshot gets mounted. Reporting the snapshot creation time as stx_btime in both states gives one consistent value for the path; birth time is the natural field for it and the one least likely to affect existing consumers.

Description

The unmounted entry gets its own inode_operations table, zpl_ops_snapdirs, so it has a getattr at all. It reports the creation time that zfsctl_inode_alloc() already looked up for a/m/ctime, now also kept in z_btime. For the mounted case, zfs_getattr_fast() already fakes the inode number of a snapshot root so NFS sees the same fileid before and after the automount; the birth time is now faked in the same place, from dsl_get_creation() of the mounted dataset. To make that stick, the STATX_BTIME copy in zpl_getattr() had to move down into zfs_getattr_fast(), otherwise it would overwrite the override on the way out.

How Has This Been Tested?

Before the patch:

# zfs get -H -o value creation tank/ds@snap1
Wed Sep  9  8:28 2026
# P=/mnt/tank/ds/.zfs/snapshot/snap1

# before mounting (stat does not trigger the automount): no birth time
# stat -c 'birth=%w' $P
birth=-

# after mounting (ls into the directory triggers the automount):
# the root directory's crtime, i.e. the dataset creation time
# ls $P >/dev/null; stat -c 'birth=%w' $P
birth=2026-09-09 08:28:57.959575818 -0700

After the patch:

# before mounting: the snapshot creation time
# stat -c 'birth=%w' $P
birth=2026-09-09 08:28:58.000000000 -0700

# after mounting: same
# ls $P >/dev/null; stat -c 'birth=%w' $P
birth=2026-09-09 08:28:58.000000000 -0700
  • Also verified that .zfs, .zfs/snapshot and files inside the snapshot are unaffected, and that the value holds across rename, rollback, promote, send/recv and expiry unmount, including over NFSv4 and SMB.

  • ZTS: snapdir_stat_btime fails on an unpatched module at the first check and passes with the change; the snapdir, snapshot, crtime, stat and ctime groups pass. Also compiled against 6.1 headers.

Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Performance enhancement (non-breaking change which improves efficiency)
  • Code cleanup (non-breaking change which makes code smaller or more readable)
  • Quality assurance (non-breaking change which makes the code more robust against bugs)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Library ABI change (libzfs, libzfs_core, libnvpair and libzfsbootenv)
  • Documentation (a change to man pages or other documentation)

Checklist

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.

Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Comment thread module/os/linux/zfs/zpl_ctldir.c Outdated
@behlendorf behlendorf added the Status: Code Review Needed Ready for review and testing label Sep 10, 2026
Every getattr in this file passes request_mask on, so it is always used.

Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
@amotin amotin added Status: Accepted Ready to integrate (reviewed, tested) and removed Status: Code Review Needed Ready for review and testing labels Sep 15, 2026
@behlendorf
behlendorf merged commit f5b2fc8 into openzfs:master Sep 15, 2026
41 of 48 checks passed
ixhamza added a commit to truenas/zfs that referenced this pull request Sep 16, 2026
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.

Backport of the master change. zfs_getattr_fast() has no request_mask
on kernels before 6.6 here, so the birth time is filled regardless of
the mask on those.

Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Closes openzfs#19093
bugclerk pushed a commit to truenas/zfs that referenced this pull request Sep 16, 2026
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.

Backport of the master change. zfs_getattr_fast() has no request_mask
on kernels before 6.6 here, so the birth time is filled regardless of
the mask on those.

Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Closes openzfs#19093
(cherry picked from commit 5276573)
bugclerk pushed a commit to truenas/zfs that referenced this pull request Sep 16, 2026
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.

Backport of the master change. zfs_getattr_fast() has no request_mask
on kernels before 6.6 here, so the birth time is filled regardless of
the mask on those.

Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Closes openzfs#19093
(cherry picked from commit 5276573)
creatorcary pushed a commit to truenas/zfs that referenced this pull request Sep 16, 2026
#441)

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.

Backport of the master change. zfs_getattr_fast() has no request_mask
on kernels before 6.6 here, so the birth time is filled regardless of
the mask on those.

Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>

Closes openzfs#19093
(cherry picked from commit 5276573)

Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Co-authored-by: Ameer Hamza <ahamza@ixsystems.com>
creatorcary pushed a commit to truenas/zfs that referenced this pull request Sep 16, 2026
#442)

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.

Backport of the master change. zfs_getattr_fast() has no request_mask
on kernels before 6.6 here, so the birth time is filled regardless of
the mask on those.

Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>

Closes openzfs#19093
(cherry picked from commit 5276573)

Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Co-authored-by: Ameer Hamza <ahamza@ixsystems.com>
lundman pushed a commit to openzfsonwindows/openzfs that referenced this pull request Sep 27, 2026
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 <behlendorf1@llnl.gov>
Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
Closes openzfs#19093
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Accepted Ready to integrate (reviewed, tested)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants