From nobody Mon Sep 28 02:56:32 2026 Received: from mail-244116.protonmail.ch (mail-244116.protonmail.ch [109.224.244.116]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B9D7E38AC8A; Thu, 27 Aug 2026 23:37:47 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=109.224.244.116 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873869; cv=none; b=MfU0hcNyQ0/cO+SuxatOZN4C1Qn9FoOe/jf9ziiQYIFa64E5qb84n2gOKYgCtwMenhTt783AVcNnBwDg33DpEis434Mj2svWST/BVi77maghVVoUsy1YZm4O9/X2nTiRawh42DAHhwvSfSVKTG5zhmevUktNphWpK1HafqhVnIY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873869; c=relaxed/simple; bh=lVioQ+2lnFeI8aV3vAxI2HD2Flp9WxEYrpxGWOPhUyA=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=uxxiN26jECJweg7Aldk+nJFjg0rP04nJJbSM5wcLD6tlLKlrtrEesW+wDsfKhncgQkOodHJ3HZXtAGwBiSgNoARfYICHEfLwORT3uqpcJLZ3bkO86AGFR+FeYCYVs+xUNVdZFeyl5VDOc0YeAD51ND3By260aVBvS29u0vI6MJ0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me; spf=pass smtp.mailfrom=pm.me; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b=V/HRzElS; arc=none smtp.client-ip=109.224.244.116 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pm.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b="V/HRzElS" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me; s=protonmail3; t=1787873858; x=1788133058; bh=lVioQ+2lnFeI8aV3vAxI2HD2Flp9WxEYrpxGWOPhUyA=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=V/HRzElSAm7yCy5cdGN4tJcQMeCjOSF9aJFu7l4uh6oSYbT+jIY2b6Df4Xv6lIytu eUDTli1uFpjRYMKztVfWqRxCQHYqqt0ExXWltIrwy/pGvz2WIz+scd37929dSxUCYD w0Kid8Npv1QLSNoNG6ocg8/CJHYRqjx1BVZeoKqAV7zJ22fPrUHQgTG4oxoHQB6E6o jKNjbpmqJF6Lk2Q5oGMkcAwz6ZBZ1ij0tRGpwxQJLckbXJ51QNLItqUqL2TGYON23J ymi23MJpIPgmik/tt0eYwPdB+0HOPafSh8Qwdw9JWg+b+q+/cBBd5AVhuEbLInoEOK 1yVnsSItw/ooA== Date: Thu, 27 Aug 2026 23:37:34 +0000 To: Miklos Szeredi , Stefan Hajnoczi , Vivek Goyal , German Maglione , Shuah Khan From: Aaron Paterson Cc: =?utf-8?Q?Eugenio_P=C3=A9rez?= , fuse-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org, linux-kselftest@vger.kernel.org, virtualization@lists.linux.dev, linux-kernel@vger.kernel.org, Aaron Paterson Subject: [PATCH 1/5] selftests/fuse: ignore the built acl cache test Message-ID: <0c85f57ce65ab1f2f408a17010dd3fd8c7d6730e.1787873791.git.apaterson@pm.me> In-Reply-To: References: Feedback-ID: 6356313:user:proton X-Pm-Message-ID: 7bb71e1f1f8238f29ac79159edf6bf3d4f9cdb73 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The Makefile builds fuse_acl_cache_test into the source directory when libfuse3 is present, but the binary is not ignored, so a build leaves the tree dirty. The two other programs built here are already listed. Signed-off-by: Aaron Paterson --- tools/testing/selftests/filesystems/fuse/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/te= sting/selftests/filesystems/fuse/.gitignore index fb51603fe419..25c779065806 100644 --- a/tools/testing/selftests/filesystems/fuse/.gitignore +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -1,4 +1,5 @@ # SPDX-License-Identifier: GPL-2.0-only +fuse_acl_cache_test fuse_mnt fusectl_test write_extend_eof_test --=20 2.55.0.553.g4ad8c266be From nobody Mon Sep 28 02:56:32 2026 Received: from mail-4322.protonmail.ch (mail-4322.protonmail.ch [185.70.43.22]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6C9893D890F; Thu, 27 Aug 2026 23:37:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.70.43.22 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873879; cv=none; b=NU9lHCr41n+tl4bBWzISukWIFTehgjoWy86E+yMjiAWcVNIqHl0PplAs2ZP7Jx3yzrpeVjNSTnIahfYJsq0iJO8hWuHnKvwubNvTpbTjdeHppzouqfFMz0Ke8LXPGf3kAbGoZHnQsNcrMQ2E1EU85ttAGahluVXQJVFO9bbbp3s= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873879; c=relaxed/simple; bh=+8Xt3nYlPyrQ87kYXz05YT1UtjZL8RO50P+bbLMB/SY=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=EW8F1jtRSL5w+jnt6m2QONQhRKDJFmSIp50wsWuuTWWGQicrKvA4eSjY1Bl1ODRAftXPKBIpCdhOB9q9Zuo3cL46EYYdzu+NyeKCVlwjQK2mMb4j1FMDQsk6VBdLsIQbNRr7BUsiXLmqQab2G/eJ8+DUFAAWPGqPURxyHiQb39E= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me; spf=pass smtp.mailfrom=pm.me; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b=INQLORai; arc=none smtp.client-ip=185.70.43.22 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pm.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b="INQLORai" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me; s=protonmail3; t=1787873869; x=1788133069; bh=+8Xt3nYlPyrQ87kYXz05YT1UtjZL8RO50P+bbLMB/SY=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=INQLORai9suJFTs88qEP3gSXfQxhwG8QyTsxm00cJbET8fw829mz+hslIRZvsY0Br 7ohE5Rshqir+dpPYn+C+FokNpB11phpLYps+T4Jp/bBs0T9C3LJtmWcoGscA1KhanU Q3sj6/TyHkBnDtLEs3ti10RS5uBNO5tSOFzvIsjK7AzQaHaNdjVH438pPzJwJ2yR0p WdVxfb4yOGaURV28wKTfskAW68Ajb5w65UxclwRHiEMMPcQZlPquSSn+XrliF0qWRe kS80XOlmKKa9ixa9sMNTpVkH0wdi7Qte4at6TR7aT0XSfNw9hNf0S+V/ns4efuMlNc OuZtoRwAPA4LQ== Date: Thu, 27 Aug 2026 23:37:43 +0000 To: Miklos Szeredi , Stefan Hajnoczi , Vivek Goyal , German Maglione , Shuah Khan From: Aaron Paterson Cc: =?utf-8?Q?Eugenio_P=C3=A9rez?= , fuse-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org, linux-kselftest@vger.kernel.org, virtualization@lists.linux.dev, linux-kernel@vger.kernel.org, Aaron Paterson Subject: [PATCH 2/5] selftests/fuse: name the libfuse3 flags for the library Message-ID: <1000f6fe22b08168ddebc910d8204cd5ec048fb5.1787873791.git.apaterson@pm.me> In-Reply-To: References: Feedback-ID: 6356313:user:proton X-Pm-Message-ID: f1115e9b3eacac6d00d08b7420bff58bde6659df Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The flags describe libfuse3 rather than the one test that currently uses them, so any further test needing the library can share them. Signed-off-by: Aaron Paterson --- tools/testing/selftests/filesystems/fuse/Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/test= ing/selftests/filesystems/fuse/Makefile index 95a1ee947ca7..1ea87008ef9e 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -6,10 +6,10 @@ TEST_GEN_PROGS :=3D fusectl_test TEST_GEN_PROGS +=3D write_extend_eof_test TEST_GEN_FILES :=3D fuse_mnt =20 -# fuse_acl_cache_test requires libfuse3; add it only when the library is p= resent. -ACL_CFLAGS :=3D $(shell pkg-config fuse3 --cflags 2>/dev/null) -ACL_LDLIBS :=3D $(shell pkg-config fuse3 --libs 2>/dev/null) -ifneq ($(ACL_CFLAGS),) +# These tests require libfuse3; add them only when the library is present. +FUSE3_CFLAGS :=3D $(shell pkg-config fuse3 --cflags 2>/dev/null) +FUSE3_LDLIBS :=3D $(shell pkg-config fuse3 --libs 2>/dev/null) +ifneq ($(FUSE3_CFLAGS),) TEST_GEN_PROGS +=3D fuse_acl_cache_test endif =20 @@ -30,5 +30,5 @@ endif $(OUTPUT)/fuse_mnt: CFLAGS +=3D $(VAR_CFLAGS) $(OUTPUT)/fuse_mnt: LDLIBS +=3D $(VAR_LDLIBS) =20 -$(OUTPUT)/fuse_acl_cache_test: CFLAGS +=3D $(ACL_CFLAGS) -$(OUTPUT)/fuse_acl_cache_test: LDLIBS +=3D $(ACL_LDLIBS) +$(OUTPUT)/fuse_acl_cache_test: CFLAGS +=3D $(FUSE3_CFLAGS) +$(OUTPUT)/fuse_acl_cache_test: LDLIBS +=3D $(FUSE3_LDLIBS) --=20 2.55.0.553.g4ad8c266be From nobody Mon Sep 28 02:56:32 2026 Received: from mail-07.mail-europe.com (mail-07.mail-europe.com [37.187.220.204]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A97163DF00B for ; Thu, 27 Aug 2026 23:38:14 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=37.187.220.204 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873896; cv=none; b=i6AHZ4LOn5F4HlmATp1CYVM7MfYKOy12W0VWJ55FOWaihoaTdQwL0Ba7sO/uYaJqfkuDW3p1FdkqSN/Jm46BSoD9rYQCYpTH9iUjKZf1og7cOK3Ne9mS2jOvsUb1nVbEQbp05FCf6H2qk5CKrJ0RrTixpT5Siz+OhjEdMkc7mXA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873896; c=relaxed/simple; bh=Wn3Kl1Em3pc3rLk/XZ/banG45sEBL5sAFBf8shoDkPk=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=LCduk8UmtjPwXps76vvKMwh69lvVgY6DuEe24FEbryZLuzIii4ss9+nM1wPmUAP3SlFIZG3gPdBRVKIu1YTkFYSRcqbgFBAzv5pNg1+1+3Be9o3KcZHx5mg3yi/4KKZitrRwF4pWE2I6fHS7ovsezgWiWeV1CqCV9Z5D7/H5YNU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me; spf=pass smtp.mailfrom=pm.me; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b=p25Vopf0; arc=none smtp.client-ip=37.187.220.204 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pm.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b="p25Vopf0" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me; s=protonmail3; t=1787873878; x=1788133078; bh=ZT19WzNgSAsSgc66j/15hWke6DZMz9tpdU2pv3QxgTU=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=p25Vopf0CgbgEyUmRKQkhfI3U3EYVlxQAav6CKD/p8phpVBDU76DxJXo38j2hLfur bFgduMRv9YL+oqurWJqp0IUCgBff3gFVKb4wzoVd0dI75ZiIrQAse5REiS2Dx35YYI 6Cng2zBh461Y1FsbCIK4oRF0O9xFPiWFXsW1QJq7Wgat/sjIsD2F9K9Mg+nrgcj5YZ NX0cKv28RF5ovyh9qaCla8CLNeA/CMSQn2BsioMv3nzbdpq8GTVIbbjFRT1ku+yGbV uEFjWUEJVAJ/hnXw6J5v+DxVxE7172inwlprjX8YknkZXNQHmfIN/0ATUjhH7jQx61 YpKf+L8fHwbmQ== Date: Thu, 27 Aug 2026 23:37:52 +0000 To: Miklos Szeredi , Stefan Hajnoczi , Vivek Goyal , German Maglione , Shuah Khan From: Aaron Paterson Cc: =?utf-8?Q?Eugenio_P=C3=A9rez?= , fuse-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org, linux-kselftest@vger.kernel.org, virtualization@lists.linux.dev, linux-kernel@vger.kernel.org, Aaron Paterson Subject: [PATCH 3/5] fuse: report a request refused for a live nodeid as stale Message-ID: <409f0fef91d68c38c52ecd77ccf50bf41b79ddad.1787873791.git.apaterson@pm.me> In-Reply-To: References: Feedback-ID: 6356313:user:proton X-Pm-Message-ID: 6eb734148bb6406ee0747e1afdb5c7e57afcf632 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A request naming a nodeid and nothing else is only sent for an inode the client has looked up and holds a reference to, and the server owes the client that inode until it is sent FUSE_FORGET. ENOENT to such a request describes a handle the server was obliged to honour rather than something that has gone away, and the caller cannot tell the difference: a file that never stopped existing is reported missing. Report it so the caller can recover. The path opens say EOPENSTALE. fuse has not needed it before, its only other use of a stale error being fuse_get_dentry(), where ESTALE answers an export handle that cannot be resolved, which is a different question. EOPENSTALE is what an open says when the cached information it started from has gone stale, and path_openat() decides what that means for the walk in progress, answering ECHILD under LOOKUP_RCU so it drops to REF-walk and ESTALE otherwise so the name is resolved again under LOOKUP_REVAL. NFS reports its own stale opens this way, in nfs4_file_open() and nfs_atomic_open(). The conversion sits at the two callers that opened by name rather than in fuse_file_open(), which fuse_priv_ioctl_prepare() also reaches: it opens the inode to serve FS_IOC_GETFLAGS and FS_IOC_FSGETXATTR and returns what it gets through vfs_fileattr_get(), with no open behind it to translate the internal errno. The getattr, setattr, readlink and statfs paths say ESTALE through one helper, which retry_estale() answers by repeating the lookup once under LOOKUP_REVAL. fs/namei.c, fs/open.c, fs/stat.c, fs/statfs.c, fs/utimes.c and fs/xattr.c all reach it. EOPENSTALE would be wrong for them, since path_openat() is the only place that translates it and nothing would outside an open. Requests carrying a name are left alone, since ENOENT is then ambiguous and is frequently what the caller asked to be told. fuse already reads it that way: fuse_unlink() and fuse_rmdir() treat ENOENT as grounds to invalidate the entry, which is a statement about the name rather than the inode. The xattr requests belong with those, carrying an attribute name whose absence a server may report as ENOENT rather than ENODATA. Requests against an already open descriptor are left alone as well, having no equivalent retry to reach, and FUSE_IOCTL and FUSE_POLL hand the server's errno to userspace verbatim. Observed with virtiofs on macOS, where a server releases an inode as soon as a rename displaces the name it was looked up by and roughly one open in eight during a rename race is refused while stat continues to describe the file. Signed-off-by: Aaron Paterson --- fs/fuse/dir.c | 10 ++++++++++ fs/fuse/file.c | 11 +++++++++++ fs/fuse/fuse_i.h | 19 +++++++++++++++++++ fs/fuse/inode.c | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index e49b4e874b15..217a03999050 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -1532,6 +1532,8 @@ static int fuse_do_getattr(struct mnt_idmap *idmap, s= truct inode *inode, if (stat) fuse_fillattr(idmap, inode, &outarg.attr, stat); } + } else { + err =3D fuse_stale_inode_err(err); } return err; } @@ -1848,6 +1850,7 @@ static int fuse_readlink_folio(struct inode *inode, s= truct folio *folio) =20 fuse_invalidate_atime(inode); =20 + res =3D fuse_stale_inode_err(res); if (res < 0) return res; =20 @@ -1910,6 +1913,12 @@ static int fuse_dir_open(struct inode *inode, struct= file *file) return err; =20 err =3D fuse_do_open(fm, get_node_id(inode), file, true); + /* + * As in fuse_open_common(): a path walk stands behind this open, + * so the refusal is EOPENSTALE for path_openat() to answer. + */ + if (err =3D=3D -ENOENT) + err =3D -EOPENSTALE; if (!err) { struct fuse_file *ff =3D file->private_data; =20 @@ -2249,6 +2258,7 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct d= entry *dentry, if (err) { if (err =3D=3D -EINTR) fuse_invalidate_attr(inode); + err =3D fuse_stale_inode_err(err); goto error; } =20 diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 8d6135a6108a..308b15c30f34 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -279,6 +279,17 @@ static int fuse_open(struct inode *inode, struct file = *file) fuse_set_nowrite(inode); =20 err =3D fuse_do_open(fm, get_node_id(inode), file, false); + /* + * This open reached the server through a path walk, so a refusal + * for the live nodeid is reported as EOPENSTALE rather than the + * ESTALE the nodeid-only requests say: path_openat() picks the + * cheapest retry for the walk in progress, ECHILD under LOOKUP_RCU + * and ESTALE otherwise. fuse_file_open() is left alone because + * fuse_priv_ioctl_prepare() reaches it to serve FS_IOC_GETFLAGS, + * with no open behind it to translate the internal errno. + */ + if (err =3D=3D -ENOENT) + err =3D -EOPENSTALE; if (!err) { ff =3D file->private_data; err =3D fuse_finish_open(inode, file); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index c8d4c5f3af7e..e6b2597d6dd6 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1250,6 +1250,25 @@ void fuse_inode_uncached_io_end(struct fuse_inode *f= i); int fuse_file_io_open(struct file *file, struct inode *inode); void fuse_file_io_release(struct fuse_file *ff, struct inode *inode); =20 +/* + * Report a request refused for a live nodeid as stale. + * + * A request that names a nodeid and nothing else is only sent for an + * inode the client has looked up and holds a reference to, and the + * server owes the client that inode until it is sent FUSE_FORGET. + * ENOENT from the server describes the inode itself rather than a name + * that has gone away. Report the handle as stale, which the caller + * answers by resolving the name again under LOOKUP_REVAL and acting on + * whatever it refers to now. A name that really has gone fails that + * second lookup and the caller still sees ENOENT. + */ +static inline int fuse_stale_inode_err(int err) +{ + if (err =3D=3D -ENOENT) + return -ESTALE; + return err; +} + /* file.c */ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid, unsigned int open_flags, bool isdir); diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index e9552be3637b..cde0a5335089 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -666,7 +666,7 @@ static int fuse_statfs(struct dentry *dentry, struct ks= tatfs *buf) err =3D fuse_simple_request(fm, &args); if (!err) convert_fuse_statfs(buf, &outarg.st); - return err; + return fuse_stale_inode_err(err); } =20 static struct fuse_sync_bucket *fuse_sync_bucket_alloc(void) --=20 2.55.0.553.g4ad8c266be From nobody Mon Sep 28 02:56:32 2026 Received: from mail-4322.protonmail.ch (mail-4322.protonmail.ch [185.70.43.22]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0F2DF361947 for ; Thu, 27 Aug 2026 23:38:10 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.70.43.22 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873892; cv=none; b=d9QX+DXEsoQLNpP+XSTXrQ/caK4jc69FZVmpkvqWe1/8Bv1JO0GKKLwANYGfKwltc11wazo4AIXMtbuF6omfeNxIRIqHcIncby01hWEmqWpyPD9MNv5TsTSYxCr6kKW239AIK+0XlqIaX4BgXx48QERI4xkMUZwR3kguQnilxaQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873892; c=relaxed/simple; bh=u9uSB5C9l0sYaylZRTtAj6/bhy3iGcTQlWoLbvkmzic=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=nX+mzvuxRA6wIrUfJ9o4d3LPMvU16hFPT9SKVo9i5lAH24n43qngtuDUanjCHWJzq654isvo6qUzz4/3ZQlO8ee1qzWfCZiWexgiyflM8bkyAaut4fmcyNMj1ef+e6Bbb3xZPM6uVhlmtk07ZiYKb9MXOP2gyvcLZTdEQl1Aijc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me; spf=pass smtp.mailfrom=pm.me; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b=U2Fk+P1Y; arc=none smtp.client-ip=185.70.43.22 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pm.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b="U2Fk+P1Y" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me; s=protonmail3; t=1787873887; x=1788133087; bh=IGbPUBi9oyDRfhnaGZFqJaZREQhYPBXk4OHpYnu9Zps=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=U2Fk+P1YIx3TDVmSX56N9BzUumOv+klshlV60TtHjJBR+1WBvspxhataYqtD8WS5A PJ7SwSm4jHDlrlxlSCHnpnBSktqcbrPyzfhrt9ZCLHAZ6nNT/ewTm/hopVYFfghk9b liJ1TYewrrqMngO7nJUlA3seprnseAqaCej0eb2ClELR76uCCrYWVjEBOerHXzUcrL zUSHl2Wboy1hFF6NK2epChRWvvMKDk/KatqjR9Y/JtQTwWLPkPl+FJZ4pYyjfwWRYF pHf6CbZoRVuZZxvLEME0oLoe3z9OPAjyhCsNj3euJo31Z8RBFFZ+xE57zYcNSAPzie E9fWOwB1BOBnw== Date: Thu, 27 Aug 2026 23:38:02 +0000 To: Miklos Szeredi , Stefan Hajnoczi , Vivek Goyal , German Maglione , Shuah Khan From: Aaron Paterson Cc: =?utf-8?Q?Eugenio_P=C3=A9rez?= , fuse-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org, linux-kselftest@vger.kernel.org, virtualization@lists.linux.dev, linux-kernel@vger.kernel.org, Aaron Paterson Subject: [PATCH 4/5] selftests/fuse: cover a request refused for a live nodeid Message-ID: <15e12ddd18fa9bfecdb454391e6684664d8da653.1787873791.git.apaterson@pm.me> In-Reply-To: References: Feedback-ID: 6356313:user:proton X-Pm-Message-ID: 9839bd7b55eee69534519bf70eeede290c9cf289 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a test that mounts a libfuse3 server which can be told to refuse a request for an inode the client still holds a reference to, and check that the caller recovers instead of being told the file is gone. The server answers ENOENT on demand for each request the fix converts, FUSE_OPEN, FUSE_GETATTR, FUSE_SETATTR, FUSE_READLINK and FUSE_STATFS, while continuing to serve every other request for the same inode, which is how a server that releases an inode too early behaves. open(), stat(), chmod(), readlink() and statfs() are then expected to succeed, having resolved the name again under LOOKUP_REVAL, and the request counters confirm the retry happened rather than the answer being served from cache. Each of those requests carries a nodeid, so an ENOENT answering one of them describes a handle rather than a name. FUSE_LOOKUP is the exception and is covered the other way round: a name the server does not have must still report ENOENT, since there the refusal is the answer. Signed-off-by: Aaron Paterson --- .../selftests/filesystems/fuse/.gitignore | 1 + .../selftests/filesystems/fuse/Makefile | 4 + .../filesystems/fuse/fuse_estale_test.c | 450 ++++++++++++++++++ 3 files changed, 455 insertions(+) create mode 100644 tools/testing/selftests/filesystems/fuse/fuse_estale_te= st.c diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/te= sting/selftests/filesystems/fuse/.gitignore index 25c779065806..9cb3048128d5 100644 --- a/tools/testing/selftests/filesystems/fuse/.gitignore +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only fuse_acl_cache_test fuse_mnt +fuse_estale_test fusectl_test write_extend_eof_test diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/test= ing/selftests/filesystems/fuse/Makefile index 1ea87008ef9e..f564cb37b3a5 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -11,6 +11,7 @@ FUSE3_CFLAGS :=3D $(shell pkg-config fuse3 --cflags 2>/de= v/null) FUSE3_LDLIBS :=3D $(shell pkg-config fuse3 --libs 2>/dev/null) ifneq ($(FUSE3_CFLAGS),) TEST_GEN_PROGS +=3D fuse_acl_cache_test +TEST_GEN_PROGS +=3D fuse_estale_test endif =20 include ../../lib.mk @@ -32,3 +33,6 @@ $(OUTPUT)/fuse_mnt: LDLIBS +=3D $(VAR_LDLIBS) =20 $(OUTPUT)/fuse_acl_cache_test: CFLAGS +=3D $(FUSE3_CFLAGS) $(OUTPUT)/fuse_acl_cache_test: LDLIBS +=3D $(FUSE3_LDLIBS) + +$(OUTPUT)/fuse_estale_test: CFLAGS +=3D $(FUSE3_CFLAGS) +$(OUTPUT)/fuse_estale_test: LDLIBS +=3D $(FUSE3_LDLIBS) diff --git a/tools/testing/selftests/filesystems/fuse/fuse_estale_test.c b/= tools/testing/selftests/filesystems/fuse/fuse_estale_test.c new file mode 100644 index 000000000000..82b842d3a5f7 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/fuse_estale_test.c @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test: a request refused for an inode the client still holds a reference= to + * + * FUSE_OPEN, FUSE_GETATTR, FUSE_SETATTR, FUSE_READLINK and FUSE_STATFS ca= rry a + * nodeid rather than a path. The client only sends them for an inode it = has + * already looked up and holds a reference to, and a server owes the clien= t that + * inode until it is sent FUSE_FORGET. A server that lets the inode go ea= rly, + * as one backing a shared directory does when the name is renamed over, a= nswers + * with ENOENT. + * + * On an unfixed kernel that ENOENT is passed out unchanged. The path wal= k has + * no reason to doubt it and the caller is told a file is missing when it = never + * stopped existing. Callers that read a missing file as an empty one act= on + * the emptiness. + * + * Fixed (fs/fuse/file.c and fs/fuse/dir.c): ENOENT becomes ESTALE, which + * describes the handle rather than the name. filename_lookup() and + * do_filp_open() already retry with LOOKUP_REVAL on ESTALE, so the name is + * resolved again and the inode it refers to now is used. A name that has + * genuinely gone away fails the retried lookup, so ENOENT still reaches a + * caller that deserves it. + * + * Only requests reachable through a path walk are covered, because the re= try + * is what makes ESTALE useful and the walk is what performs it. An opera= tion + * on a descriptor already open has no equivalent recovery. + * + * Test outline: + * 1. Mount a minimal FUSE fs holding one file. + * 2. The server refuses the first request of the kind under test and all= ows + * every one after it, standing in for a server that released the inod= e and + * has since resolved the name again. + * 3. openat() the file. + * Buggy: ENOENT reaches the caller, one open was asked for. FAIL. + * Fixed: the walk retries, the second open is allowed, the descripto= r is + * returned, two opens were asked for. PASS. + * 4. stat(), chmod(), readlink() and statfs() by name, which are the same + * recovery through FUSE_GETATTR, FUSE_SETATTR, FUSE_READLINK and + * FUSE_STATFS. Each is reached through a path walk, which is what ma= kes + * the retry available. + * 5. Open a name the server does not have at all. + * Both: ENOENT, because the lookup fails rather than the open, and a + * file that is absent must still look absent. + */ + +#define _GNU_SOURCE +#define FUSE_USE_VERSION 34 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../kselftest_harness.h" + +#define FILE_NAME "held" +#define LINK_NAME "held-link" +#define ABSENT_NAME "no-such-file" +#define FILE_INO 2 +#define LINK_INO 3 +#define CONTENTS "present\n" +#define LINK_TARGET FILE_NAME + +/* + * Which request the server refuses, and how many times. Shared with the + * daemon thread; one test runs at a time, so plain ints. + * + * Every one of these names an inode by nodeid rather than by name, so a + * refusal of any of them is describing a handle rather than a missing fil= e. + * FUSE_LOOKUP is deliberately absent: it carries a name, so its ENOENT is= an + * answer rather than a fault, and absent_name_still_reports_absent covers= it. + */ +enum refuse_what { + REFUSE_NOTHING, + REFUSE_OPEN, + REFUSE_GETATTR, + REFUSE_SETATTR, + REFUSE_READLINK, + REFUSE_STATFS, +}; + +static struct { + enum refuse_what what; + int refusals_left; + int opens_seen; + int getattrs_seen; + int setattrs_seen; + int readlinks_seen; + int statfss_seen; +} g_ds; + +/* True once, for the request under test, and then never again. */ +static bool refuse_now(enum refuse_what what) +{ + if (g_ds.what !=3D what || g_ds.refusals_left <=3D 0) + return false; + g_ds.refusals_left--; + return true; +} + +static void fill_attr(fuse_ino_t ino, struct stat *st) +{ + memset(st, 0, sizeof(*st)); + st->st_ino =3D ino; + /* + * Owned by whoever runs the test, so that chmod() is a request the + * kernel will carry through to the server rather than refuse itself. + */ + st->st_uid =3D getuid(); + st->st_gid =3D getgid(); + if (ino =3D=3D FUSE_ROOT_ID) { + st->st_mode =3D S_IFDIR | 0755; + st->st_nlink =3D 2; + } else if (ino =3D=3D LINK_INO) { + st->st_mode =3D S_IFLNK | 0777; + st->st_nlink =3D 1; + st->st_size =3D sizeof(LINK_TARGET) - 1; + } else { + st->st_mode =3D S_IFREG | 0644; + st->st_nlink =3D 1; + st->st_size =3D sizeof(CONTENTS) - 1; + } +} + +static void t_lookup(fuse_req_t req, fuse_ino_t parent, const char *name) +{ + struct fuse_entry_param e; + fuse_ino_t ino; + + if (parent !=3D FUSE_ROOT_ID) + ino =3D 0; + else if (!strcmp(name, FILE_NAME)) + ino =3D FILE_INO; + else if (!strcmp(name, LINK_NAME)) + ino =3D LINK_INO; + else + ino =3D 0; + + if (!ino) { + fuse_reply_err(req, ENOENT); + return; + } + + memset(&e, 0, sizeof(e)); + e.ino =3D ino; + e.attr_timeout =3D 0; + e.entry_timeout =3D 0; + fill_attr(ino, &e.attr); + fuse_reply_entry(req, &e); +} + +static void t_getattr(fuse_req_t req, fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat st; + + (void)fi; + /* The root is left alone; refusing it would break the mount itself. */ + if (ino =3D=3D FILE_INO) { + g_ds.getattrs_seen++; + if (refuse_now(REFUSE_GETATTR)) { + fuse_reply_err(req, ENOENT); + return; + } + } + fill_attr(ino, &st); + fuse_reply_attr(req, &st, 0); +} + +static void t_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *= fi) +{ + if (ino !=3D FILE_INO) { + fuse_reply_err(req, ENOENT); + return; + } + + g_ds.opens_seen++; + if (refuse_now(REFUSE_OPEN)) { + /* + * The inode is gone as far as this server is concerned, even + * though the client is holding a reference to it and asked by + * nodeid rather than by name. + */ + fuse_reply_err(req, ENOENT); + return; + } + fuse_reply_open(req, fi); +} + +static void t_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, + struct fuse_file_info *fi) +{ + size_t len =3D sizeof(CONTENTS) - 1; + + (void)fi; + if (ino !=3D FILE_INO) { + fuse_reply_err(req, ENOENT); + return; + } + if ((size_t)off >=3D len) { + fuse_reply_buf(req, NULL, 0); + return; + } + if (off + size > len) + size =3D len - off; + fuse_reply_buf(req, CONTENTS + off, size); +} + +static void t_setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, + int to_set, struct fuse_file_info *fi) +{ + struct stat st; + + (void)attr; + (void)to_set; + (void)fi; + if (ino =3D=3D FILE_INO) { + g_ds.setattrs_seen++; + if (refuse_now(REFUSE_SETATTR)) { + fuse_reply_err(req, ENOENT); + return; + } + } + fill_attr(ino, &st); + fuse_reply_attr(req, &st, 0); +} + +static void t_readlink(fuse_req_t req, fuse_ino_t ino) +{ + if (ino !=3D LINK_INO) { + fuse_reply_err(req, EINVAL); + return; + } + + g_ds.readlinks_seen++; + if (refuse_now(REFUSE_READLINK)) { + fuse_reply_err(req, ENOENT); + return; + } + fuse_reply_readlink(req, LINK_TARGET); +} + +static void t_statfs(fuse_req_t req, fuse_ino_t ino) +{ + struct statvfs sfs; + + (void)ino; + g_ds.statfss_seen++; + if (refuse_now(REFUSE_STATFS)) { + fuse_reply_err(req, ENOENT); + return; + } + + memset(&sfs, 0, sizeof(sfs)); + sfs.f_bsize =3D 512; + sfs.f_frsize =3D 512; + sfs.f_namemax =3D NAME_MAX; + fuse_reply_statfs(req, &sfs); +} + +static const struct fuse_lowlevel_ops fs_ops =3D { + .lookup =3D t_lookup, + .getattr =3D t_getattr, + .setattr =3D t_setattr, + .readlink =3D t_readlink, + .statfs =3D t_statfs, + .open =3D t_open, + .read =3D t_read, +}; + +static void *run_daemon(void *arg) +{ + fuse_session_loop((struct fuse_session *)arg); + return NULL; +} + +/* ---- kselftest harness ------------------------------------------------= --- */ + +FIXTURE(open_estale) { + struct fuse_session *se; + char mountpoint[PATH_MAX]; + char file_path[PATH_MAX]; + char link_path[PATH_MAX]; + char absent_path[PATH_MAX]; + pthread_t thread; +}; + +FIXTURE_SETUP(open_estale) +{ + char *fuse_argv[] =3D { "fuse_estale_test", NULL }; + struct fuse_args args =3D FUSE_ARGS_INIT(1, fuse_argv); + + memset(&g_ds, 0, sizeof(g_ds)); + g_ds.what =3D REFUSE_NOTHING; + g_ds.refusals_left =3D 1; + + strcpy(self->mountpoint, "/tmp/open_estale_test_XXXXXX"); + if (!mkdtemp(self->mountpoint)) + SKIP(return, "mkdtemp: %s", strerror(errno)); + + snprintf(self->file_path, sizeof(self->file_path), + "%s/" FILE_NAME, self->mountpoint); + snprintf(self->link_path, sizeof(self->link_path), + "%s/" LINK_NAME, self->mountpoint); + snprintf(self->absent_path, sizeof(self->absent_path), + "%s/" ABSENT_NAME, self->mountpoint); + + self->se =3D fuse_session_new(&args, &fs_ops, sizeof(fs_ops), NULL); + if (!self->se) { + rmdir(self->mountpoint); + SKIP(return, "fuse_session_new failed"); + } + + if (fuse_session_mount(self->se, self->mountpoint)) { + fuse_session_destroy(self->se); + rmdir(self->mountpoint); + SKIP(return, "fuse_session_mount failed (no fusermount3 or no privileges= )"); + } + + if (pthread_create(&self->thread, NULL, run_daemon, self->se)) { + fuse_session_unmount(self->se); + fuse_session_destroy(self->se); + rmdir(self->mountpoint); + SKIP(return, "pthread_create: %s", strerror(errno)); + } + + fuse_opt_free_args(&args); +} + +FIXTURE_TEARDOWN(open_estale) +{ + fuse_session_exit(self->se); + fuse_session_unmount(self->se); + pthread_join(self->thread, NULL); + fuse_session_destroy(self->se); + rmdir(self->mountpoint); +} + +TEST_F(open_estale, refused_open_is_retried) +{ + int fd; + + g_ds.what =3D REFUSE_OPEN; + + fd =3D open(self->file_path, O_RDONLY); + + /* + * The refusal describes a handle the server should have honoured, so + * the walk is entitled to resolve the name again and open what it + * refers to now. Reporting the file missing instead ends the walk. + */ + ASSERT_GE(fd, 0) { + TH_LOG("open failed with %s after %d open request(s)", + strerror(errno), g_ds.opens_seen); + } + EXPECT_EQ(2, g_ds.opens_seen); + close(fd); +} + +TEST_F(open_estale, refused_getattr_on_path_is_retried) +{ + struct stat st; + + g_ds.what =3D REFUSE_GETATTR; + + /* + * Reached by name, so the walk can resolve it again and ask a second + * time, the same recovery the open gets. + */ + ASSERT_EQ(0, stat(self->file_path, &st)) { + TH_LOG("stat failed with %s after %d getattr request(s)", + strerror(errno), g_ds.getattrs_seen); + } + EXPECT_GT(g_ds.getattrs_seen, 1); +} + +TEST_F(open_estale, refused_setattr_on_path_is_retried) +{ + g_ds.what =3D REFUSE_SETATTR; + + /* + * chmod() reaches the inode by name, so the same retry applies: the + * refusal describes a handle and the walk may resolve the name again. + */ + ASSERT_EQ(0, chmod(self->file_path, 0600)) { + TH_LOG("chmod failed with %s after %d setattr request(s)", + strerror(errno), g_ds.setattrs_seen); + } + EXPECT_GT(g_ds.setattrs_seen, 1); +} + +TEST_F(open_estale, refused_readlink_on_path_is_retried) +{ + char buf[PATH_MAX]; + ssize_t n; + + g_ds.what =3D REFUSE_READLINK; + + n =3D readlink(self->link_path, buf, sizeof(buf) - 1); + ASSERT_GE(n, 0) { + TH_LOG("readlink failed with %s after %d readlink request(s)", + strerror(errno), g_ds.readlinks_seen); + } + buf[n] =3D '\0'; + EXPECT_STREQ(LINK_TARGET, buf); + EXPECT_GT(g_ds.readlinks_seen, 1); +} + +TEST_F(open_estale, refused_statfs_on_path_is_retried) +{ + struct statfs sfs; + + g_ds.what =3D REFUSE_STATFS; + + /* + * statfs() describes the mount rather than the file, but it is still + * reached through a path walk, so a refusal that names a handle is + * retried the same way. + */ + ASSERT_EQ(0, statfs(self->file_path, &sfs)) { + TH_LOG("statfs failed with %s after %d statfs request(s)", + strerror(errno), g_ds.statfss_seen); + } + EXPECT_GT(g_ds.statfss_seen, 1); +} + +TEST_F(open_estale, absent_name_still_reports_absent) +{ + int fd; + + /* + * Here it is the lookup that fails rather than the open, so nothing is + * being described as stale and the caller must still be told the name + * is not there. + */ + fd =3D open(self->absent_path, O_RDONLY); + ASSERT_LT(fd, 0); + EXPECT_EQ(ENOENT, errno); +} + +TEST_HARNESS_MAIN --=20 2.55.0.553.g4ad8c266be From nobody Mon Sep 28 02:56:32 2026 Received: from mail-4322.protonmail.ch (mail-4322.protonmail.ch [185.70.43.22]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 2EE373D890F; Thu, 27 Aug 2026 23:38:20 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.70.43.22 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873902; cv=none; b=on/JJvhiT42fYcUM656b1utOKSoY00CsM2/BTGuJfkpnH6BsBZiqLwNZ52SWMn74RXPB1EDO0w1F81z309SMty0GvBWQtevDQiWozVa4MEKmWbo1g11FBtFCQ+lpr6xZoOaG0+aZu+pa9wddn5odOUgC6zKh31hD+7fbFbF/AMY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787873902; c=relaxed/simple; bh=1RwU5fvHVJ9sE/KHEQbCOKdjhF2yFRswREVupkyZDLc=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=a05STeJGa++LNYyERKUH4uCFBc2iLAbeMUgKJgJsXqWx/byBluEhaFf/Deck5mF6j524RDjHePwSMskb+nFM7J7ozHpAyqc3nlMYtYE8I/U3F9IzYxFNXl6PDkQkOzCH6vByFOlRTNjEja4J1Lt8WSIAO2KbsDG0iq5s226ny6I= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me; spf=pass smtp.mailfrom=pm.me; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b=rPbrrgIA; arc=none smtp.client-ip=185.70.43.22 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=pm.me Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=pm.me Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=pm.me header.i=@pm.me header.b="rPbrrgIA" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me; s=protonmail3; t=1787873898; x=1788133098; bh=GhkKQ/yNTlAm89/vtdaJit4+1X2iL5wuRhao6qGXmxk=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=rPbrrgIAABo5avGB3RGOdf27pddP+IENsOpgQR8pInOse2RqMFWg9kl3FdpC2Ec8A z1OfXYbndNEcEmoSEZ9MFosfEH1jMJrkjjOa/A0fCvL5hl+O9lofK6wA0y8OQw2ZSK oqSRtmTg01KwUaRoBnDs1mY5GgSjWWboEFH1NzI9xE7J79XrbYQ05tEELMzz6ZUyik ehFZI+RmwraM2dSa73vx4ldYcjgPD01CsuiTHdvDNsTQCFDbouP3w6nAcLsm9ypItz v9qvk9cuF0ow2flFE+i80pQikUrpq6PdnU6la94IBBW3sjveZTmfHALdBoS2bGd+FL rw+SgvLLHIfFA== Date: Thu, 27 Aug 2026 23:38:11 +0000 To: Miklos Szeredi , Stefan Hajnoczi , Vivek Goyal , German Maglione , Shuah Khan From: Aaron Paterson Cc: =?utf-8?Q?Eugenio_P=C3=A9rez?= , fuse-devel@lists.linux.dev, linux-fsdevel@vger.kernel.org, linux-kselftest@vger.kernel.org, virtualization@lists.linux.dev, linux-kernel@vger.kernel.org, Aaron Paterson Subject: [PATCH 5/5] virtiofs: report a request refused for a live nodeid as stale Message-ID: In-Reply-To: References: Feedback-ID: 6356313:user:proton X-Pm-Message-ID: 65c8d97bb95cda0d9675916e00fdfd03930ac4db Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A request naming a nodeid and nothing else is only sent for an inode the client has looked up and holds a reference to, and the server owes the client that inode until it is sent FUSE_FORGET. ENOENT to such a request describes a handle the server was obliged to honour rather than something that has gone away, and the caller cannot tell the difference. Report it at request completion so the caller can recover. The opens say EOPENSTALE, which is what an open says when the cached information it started from has gone stale. path_openat() decides what that means for the walk in progress, answering ECHILD under LOOKUP_RCU so it drops to REF-walk and ESTALE otherwise so the name is resolved again under LOOKUP_REVAL. NFS reports its own stale opens the same way. The rest say ESTALE, which retry_estale() answers by repeating the lookup once under LOOKUP_REVAL. fs/namei.c, fs/open.c, fs/stat.c, fs/statfs.c, fs/utimes.c and fs/xattr.c all reach it. EOPENSTALE would be wrong for these, since path_openat() is the only place that translates it and nothing would outside an open. Converted: FUSE_OPEN, FUSE_OPENDIR, FUSE_GETATTR, FUSE_SETATTR, FUSE_READLINK and FUSE_STATFS, the last sending no payload beside the nodeid. Not converted: - Requests carrying a name, where ENOENT is ambiguous and is often what the caller asked to be told. FUSE_LOOKUP reports a missing name in a living directory and the directory operations carry a parent nodeid beside one. The xattr requests carry an attribute name, and a server answering ENOENT rather than ENODATA for a missing attribute would have a correct reply turned into a retry that cannot succeed. - Requests against an already open descriptor, which have no equivalent retry to reach, and FUSE_IOCTL and FUSE_POLL, which hand the server's errno to userspace verbatim. commit 68b69fa0edb2 ("virtiofs: add FUSE protocol validation") already inspects replies at this point for servers that break the protocol. Nodeid lifetime is another rule a server can break. Found with a server that releases an inode as soon as a rename displaces the name it was looked up by, where roughly one open in eight during a rename race is refused while stat continues to describe the file. Signed-off-by: Aaron Paterson --- fs/fuse/virtio_fs.c | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/fs/fuse/virtio_fs.c b/fs/fuse/virtio_fs.c index f15e516ebcb5..7b03bf87c2ae 100644 --- a/fs/fuse/virtio_fs.c +++ b/fs/fuse/virtio_fs.c @@ -780,6 +780,66 @@ static bool virtio_fs_verify_response(struct fuse_req = *req, unsigned int len) return true; } =20 +/* + * Report a request refused for a live nodeid as stale. + * + * These requests carry a nodeid and no name, so the client only sends the= m for + * an inode it has already looked up and holds a reference to, and a serve= r owes + * the client that inode until it is sent FUSE_FORGET. A server answering = with + * ENOENT is describing a handle it was obliged to honour rather than a na= me + * that has gone away, and the caller has no reason to doubt it. + * + * Saying the handle is stale is something the caller knows how to answer:= it + * repeats the lookup under LOOKUP_REVAL and acts on whatever the name ref= ers to + * now. A name that genuinely has gone fails the retried lookup, so a call= er + * still learns it is gone. retry_estale() is what does this, and fs/namei= .c, + * fs/open.c, fs/stat.c, fs/statfs.c, fs/utimes.c and fs/xattr.c all reach= it, + * which is what makes the conversion useful rather than a rename of the e= rror. + * + * A request that names something which can itself be absent is left alone, + * because ENOENT is then ambiguous and is frequently the answer the caller + * asked for. FUSE_LOOKUP reports a missing name in a living directory and= the + * directory operations carry a parent nodeid beside one, so neither can be + * read as a statement about the inode. The extended attribute requests be= long + * with them: they carry an attribute name, and while a server should repo= rt a + * missing attribute as ENODATA, one that answers ENOENT instead would hav= e a + * correct reply turned into a retry that cannot succeed. + * + * Requests against an already open descriptor are also left alone, since = there + * is no equivalent retry to reach and converting the error would rename a + * failure rather than repair it, and FUSE_IOCTL and FUSE_POLL hand the + * server's errno to userspace verbatim. + */ +static void virtio_fs_fixup_stale_error(struct fuse_req *req) +{ + if (req->out.h.error !=3D -ENOENT) + return; + + switch (req->in.h.opcode) { + case FUSE_OPEN: + case FUSE_OPENDIR: + /* + * An open says EOPENSTALE, and path_openat() decides what the + * walk in progress should make of it: ECHILD under LOOKUP_RCU + * so it drops to REF-walk, ESTALE otherwise so the name is + * resolved again under LOOKUP_REVAL. Naming ESTALE here would + * take the second in both cases and skip a cheaper retry. + */ + req->out.h.error =3D -EOPENSTALE; + break; + case FUSE_GETATTR: + case FUSE_SETATTR: + case FUSE_READLINK: + case FUSE_STATFS: + /* + * Nothing translates EOPENSTALE outside the open path, so + * these say ESTALE directly, which retry_estale() answers. + */ + req->out.h.error =3D -ESTALE; + break; + } +} + /* Work function for request completion */ static void virtio_fs_request_complete(struct fuse_req *req, struct virtio_fs_vq *fsvq) @@ -810,6 +870,8 @@ static void virtio_fs_request_complete(struct fuse_req = *req, =20 clear_bit(FR_SENT, &req->flags); =20 + virtio_fs_fixup_stale_error(req); + fuse_request_end(req); spin_lock(&fsvq->lock); dec_in_flight_req(fsvq); --=20 2.55.0.553.g4ad8c266be