From 6acb99cb1bf9c63a2b0cdd9e1c54af048b328d48 Mon Sep 17 00:00:00 2001 From: tiehexue Date: Thu, 9 Jul 2026 03:02:10 +0800 Subject: [PATCH] Do not return ESTALE for open-unlinked files When the Linux fh_to_dentry() export operation is called for an open-unlinked file on a ZFS filesystem, an ESTALE error is returned. This behavior differs from other filesystem implementations on Linux (like eg. ext4, XFS), which grant access to inodes that are unlinked but still referenced. Update both the Linux and FreeBSD code to conform with the established Linux behavior. Reviewed-by: Brian Behlendorf Signed-off-by: tiehexue Closes #18699 Closes #18718 --- module/os/freebsd/zfs/zfs_vfsops.c | 6 +- module/os/freebsd/zfs/zfs_znode_os.c | 20 +-- module/os/linux/zfs/zfs_vfsops.c | 2 +- tests/runfiles/common.run | 4 + tests/zfs-tests/cmd/.gitignore | 1 + tests/zfs-tests/cmd/Makefile.am | 3 + tests/zfs-tests/cmd/file/file_unlink_fh.c | 145 ++++++++++++++++++ tests/zfs-tests/include/commands.cfg | 1 + tests/zfs-tests/tests/Makefile.am | 3 + .../tests/functional/unlink/cleanup.ksh | 30 ++++ .../tests/functional/unlink/setup.ksh | 32 ++++ .../unlink/unlinked_fh_accessible.ksh | 64 ++++++++ 12 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 tests/zfs-tests/cmd/file/file_unlink_fh.c create mode 100755 tests/zfs-tests/tests/functional/unlink/cleanup.ksh create mode 100755 tests/zfs-tests/tests/functional/unlink/setup.ksh create mode 100755 tests/zfs-tests/tests/functional/unlink/unlinked_fh_accessible.ksh diff --git a/module/os/freebsd/zfs/zfs_vfsops.c b/module/os/freebsd/zfs/zfs_vfsops.c index 4504f1ba1a4a..ed92bd362163 100644 --- a/module/os/freebsd/zfs/zfs_vfsops.c +++ b/module/os/freebsd/zfs/zfs_vfsops.c @@ -1861,10 +1861,6 @@ zfs_vget(vfs_t *vfsp, ino_t ino, int flags, vnode_t **vpp) if ((err = zfs_enter(zfsvfs, FTAG)) != 0) return (err); err = zfs_zget(zfsvfs, ino, &zp); - if (err == 0 && zp->z_unlinked) { - vrele(ZTOV(zp)); - err = EINVAL; - } if (err == 0) *vpp = ZTOV(zp); zfs_exit(zfsvfs, FTAG); @@ -2021,7 +2017,7 @@ zfs_fhtovp(vfs_t *vfsp, fid_t *fidp, int flags, vnode_t **vpp) zp_gen = zp_gen & gen_mask; if (zp_gen == 0) zp_gen = 1; - if (zp->z_unlinked || zp_gen != fid_gen) { + if (zp_gen != fid_gen) { dprintf("znode gen (%llu) != fid gen (%llu)\n", (u_longlong_t)zp_gen, (u_longlong_t)fid_gen); vrele(ZTOV(zp)); diff --git a/module/os/freebsd/zfs/zfs_znode_os.c b/module/os/freebsd/zfs/zfs_znode_os.c index ce24ca8f147f..0ababae2844b 100644 --- a/module/os/freebsd/zfs/zfs_znode_os.c +++ b/module/os/freebsd/zfs/zfs_znode_os.c @@ -1005,18 +1005,14 @@ again: */ ASSERT3P(zp, !=, NULL); ASSERT3U(zp->z_id, ==, obj_num); - if (zp->z_unlinked) { - err = SET_ERROR(ENOENT); - } else { - vp = ZTOV(zp); - /* - * Don't let the vnode disappear after - * ZFS_OBJ_HOLD_EXIT. - */ - VN_HOLD(vp); - *zpp = zp; - err = 0; - } + vp = ZTOV(zp); + /* + * Don't let the vnode disappear after + * ZFS_OBJ_HOLD_EXIT. + */ + VN_HOLD(vp); + *zpp = zp; + err = 0; sa_buf_rele(db, NULL); ZFS_OBJ_HOLD_EXIT(zfsvfs, obj_num); diff --git a/module/os/linux/zfs/zfs_vfsops.c b/module/os/linux/zfs/zfs_vfsops.c index f36bed8b7993..acfe2a6cf8bf 100644 --- a/module/os/linux/zfs/zfs_vfsops.c +++ b/module/os/linux/zfs/zfs_vfsops.c @@ -1683,7 +1683,7 @@ zfs_vget(struct super_block *sb, struct inode **ipp, fid_t *fidp) zp_gen = 1; if ((fid_gen == 0) && (zfsvfs->z_root == object)) fid_gen = zp_gen; - if (zp->z_unlinked || zp_gen != fid_gen) { + if (zp_gen != fid_gen) { dprintf("znode gen (%llu) != fid gen (%llu)\n", zp_gen, fid_gen); zrele(zp); diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 7b1d8e877274..e51f932b38e0 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -1081,6 +1081,10 @@ tags = ['functional', 'trim'] tests = ['truncate_001_pos', 'truncate_002_pos', 'truncate_timestamps'] tags = ['functional', 'truncate'] +[tests/functional/unlink] +tests = ['unlinked_fh_accessible'] +tags = ['functional', 'unlink'] + [tests/functional/upgrade] tests = ['upgrade_userobj_001_pos', 'upgrade_readonly_pool'] tags = ['functional', 'upgrade'] diff --git a/tests/zfs-tests/cmd/.gitignore b/tests/zfs-tests/cmd/.gitignore index c0785d9a28e8..7fbf7c5ed632 100644 --- a/tests/zfs-tests/cmd/.gitignore +++ b/tests/zfs-tests/cmd/.gitignore @@ -61,3 +61,4 @@ /idmap_util /statx /setlease +/file_unlink_fh diff --git a/tests/zfs-tests/cmd/Makefile.am b/tests/zfs-tests/cmd/Makefile.am index 3275c1358aa3..6b10c6ed5cd3 100644 --- a/tests/zfs-tests/cmd/Makefile.am +++ b/tests/zfs-tests/cmd/Makefile.am @@ -145,3 +145,6 @@ endif scripts_zfs_tests_bin_PROGRAMS += %D%/file_fadvise %C%_file_fadvise_SOURCES = %D%/file/file_fadvise.c + +scripts_zfs_tests_bin_PROGRAMS += %D%/file_unlink_fh +%C%_file_unlink_fh_SOURCES = %D%/file/file_unlink_fh.c diff --git a/tests/zfs-tests/cmd/file/file_unlink_fh.c b/tests/zfs-tests/cmd/file/file_unlink_fh.c new file mode 100644 index 000000000000..8e96c85dcfa8 --- /dev/null +++ b/tests/zfs-tests/cmd/file/file_unlink_fh.c @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or https://opensource.org/licenses/CDDL-1.0. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ + +/* + * Copyright 2026, tiehexue . All rights reserved. + */ + +/* + * Verify that a filehandle-based reopen of an open-unlinked file + * succeeds. On Linux this exercises zfs_vget() via fh_to_dentry; + * on FreeBSD it exercises zfs_fhtovp() via VFS_FHTOVP. + * + * Exit 0 on success, non-zero on failure. + * + * Most code copied from https://reviews.freebsd.org/D57982 + * and https://github.com/openzfs/zfs/issues/18699 + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __FreeBSD__ +#include +#include +#endif + +int +main(int argc, char *argv[]) +{ + const char *filename; + int file_fd = -1, handle_fd = -1; + int ret = 2; + + if (argc != 2) { + (void) fprintf(stderr, "usage: %s \n", argv[0]); + return (2); + } + filename = argv[1]; + + file_fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (file_fd < 0) { + perror("open"); + goto out; + } + +#ifdef __linux__ + struct file_handle *fhp; + int mount_fd, mnt_id; + + mount_fd = open(".", O_RDONLY | O_DIRECTORY); + if (mount_fd < 0) { + perror("open mount_fd"); + goto out; + } + + fhp = malloc(sizeof (struct file_handle) + 128); + if (fhp == NULL) { + perror("malloc"); + (void) close(mount_fd); + goto out; + } + fhp->handle_bytes = 128; + + /* Obtain filehandle while the file still has a name */ + if (name_to_handle_at(AT_FDCWD, filename, fhp, + &mnt_id, 0) < 0) { + perror("name_to_handle_at"); + free(fhp); + (void) close(mount_fd); + goto out; + } + + /* Unlink — file_fd keeps the inode alive */ + if (unlink(filename) < 0) { + perror("unlink"); + free(fhp); + (void) close(mount_fd); + goto out; + } + + /* Reopen via filehandle — exercises fh_to_dentry/zfs_vget */ + handle_fd = open_by_handle_at(mount_fd, fhp, O_RDONLY); + free(fhp); + (void) close(mount_fd); +#elif defined(__FreeBSD__) + fhandle_t fh; + + /* Obtain ZFS filehandle while the file still has a name */ + if (getfh(filename, &fh) < 0) { + perror("getfh"); + goto out; + } + + /* Unlink — file_fd keeps the vnode alive */ + if (unlink(filename) < 0) { + perror("unlink"); + goto out; + } + + /* Reopen via filehandle — exercises VFS_FHTOVP/zfs_fhtovp */ + handle_fd = fhopen(&fh, O_RDONLY); +#endif + + if (handle_fd < 0) { + (void) fprintf(stderr, "FAIL: filehandle reopen: %s\n", + strerror(errno)); + ret = 1; + } else { + (void) printf("filehandle reopen succeeded, fd=%d\n", + handle_fd); + ret = 0; + } + +out: + if (handle_fd >= 0) + (void) close(handle_fd); + if (file_fd >= 0) + (void) close(file_fd); + return (ret); +} diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 19dbb5202c3c..874d41bda77b 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -198,6 +198,7 @@ export ZFSTEST_FILES_COMMON='badsend file_append file_check file_trunc + file_unlink_fh file_write get_diff getversion diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 772c3883b43e..200ef5dab514 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -2283,6 +2283,9 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/truncate/truncate_001_pos.ksh \ functional/truncate/truncate_002_pos.ksh \ functional/truncate/truncate_timestamps.ksh \ + functional/unlink/cleanup.ksh \ + functional/unlink/setup.ksh \ + functional/unlink/unlinked_fh_accessible.ksh \ functional/upgrade/cleanup.ksh \ functional/upgrade/setup.ksh \ functional/upgrade/upgrade_projectquota_001_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/unlink/cleanup.ksh b/tests/zfs-tests/tests/functional/unlink/cleanup.ksh new file mode 100755 index 000000000000..fde27c58a0aa --- /dev/null +++ b/tests/zfs-tests/tests/functional/unlink/cleanup.ksh @@ -0,0 +1,30 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright 2026, tiehexue . All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib + +default_cleanup diff --git a/tests/zfs-tests/tests/functional/unlink/setup.ksh b/tests/zfs-tests/tests/functional/unlink/setup.ksh new file mode 100755 index 000000000000..d12dc0d11e73 --- /dev/null +++ b/tests/zfs-tests/tests/functional/unlink/setup.ksh @@ -0,0 +1,32 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright 2026, tiehexue . All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib + +DISK=${DISKS%% *} +default_setup_noexit $DISK +log_pass diff --git a/tests/zfs-tests/tests/functional/unlink/unlinked_fh_accessible.ksh b/tests/zfs-tests/tests/functional/unlink/unlinked_fh_accessible.ksh new file mode 100755 index 000000000000..88c262475e8e --- /dev/null +++ b/tests/zfs-tests/tests/functional/unlink/unlinked_fh_accessible.ksh @@ -0,0 +1,64 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright 2026, tiehexue . All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Verify that a filehandle-based reopen of an open-unlinked file +# succeeds. This tests that zfs_vget() / zfs_fhtovp() does not +# incorrectly reject znodes with z_unlinked when the inode/vnode +# is still referenced. +# +# STRATEGY: +# 1. Use the file_unlink_fh helper to create a file, obtain a +# filehandle (name_to_handle_at on Linux, getfh on FreeBSD), +# unlink the file, and then attempt to re-open it via +# open_by_handle_at (Linux) / fhopen (FreeBSD). +# 2. Verify that the reopen succeeds. +# + +verify_runnable "global" + +function cleanup +{ + cd "$cwd" || true + [[ -e $TESTDIR ]] && log_must rm -Rf $TESTDIR/* +} + +log_assert "filehandle reopen of open-unlinked file should succeed" + +log_onexit cleanup + +cwd=$PWD +log_must cd $TESTDIR + +# file_unlink_fh: creates file, gets filehandle, unlinks, reopens via +# open_by_handle_at (Linux) or fhopen (FreeBSD). Exits 0 on success. +log_must file_unlink_fh unlinked_fh_testfile + +log_pass "filehandle reopen of open-unlinked file succeeded"