EcryptFS development
 help / color / mirror / Atom feed
From: Tyler Hicks <code@tyhicks.com>
To: Nickolai Zeldovich <nickolai@csail.mit.edu>
Cc: ecryptfs@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v1] ecryptfs: release the inode when lookup fails to read the file size
Date: Mon, 19 Oct 2026 17:28:30 -0500	[thread overview]
Message-ID: <ataZjvBo29sDyej3@yaupon> (raw)
In-Reply-To: <20261018023840.798718-1-nickolai@csail.mit.edu>

On 2026-10-17 22:38:39, Nickolai Zeldovich wrote:
> When eCryptfs cannot open the lower file of a regular file during
> lookup, the lookup fails and leaks the eCryptfs inode it got, together
> with that inode's reference on the lower inode.  Every failed lookup
> leaks one more inode, and unmounting reports "VFS: Busy inodes after
> unmount of ecryptfs" (a BUG with CONFIG_BUG_ON_DATA_CORRUPTION).  The
> lower open fails, for example, with -ETXTBSY while the lower file is
> being executed, or when an LSM or the lower file system's ->open()
> refuses it; any user who can look up the name can repeat it.  A
> concurrent lookup that found the new inode before it was unhashed
> waits for I_NEW forever.
> 
> ecryptfs_lookup_interpose() gets the inode from iget5_locked() and,
> for a regular file, reads the plaintext size with
> ecryptfs_i_size_read() before unlocking a new inode.  On failure it
> marks the inode bad and returns, but it neither clears I_NEW nor drops
> the reference it holds.  The error path came with the deferred
> unlock_new_inode() in commit 3b06b3ebf441 ("eCryptfs: Fix new inode
> race condition").
> 
> Unlock a new inode and drop the reference on that path, as
> iget_failed() does for a new inode.
> 
> Tested on x86_64 with a lower file on tmpfs that a process is
> executing: 1000 failed stat() calls left 1000 inodes allocated and the
> unmount warned about busy inodes; with the patch no inode is left and
> the unmount is clean.
> 
> Fixes: 3b06b3ebf441 ("eCryptfs: Fix new inode race condition")
> Assisted-by: LLM
> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>

Hello - Thank you for the fix and the level of testing that you
performed. Christian Brauner recently sent out a fix for the same issue.
I've already acked his fix but please take a look to see if you disagree
with anything there. It is slightly different than what you proposed.

 https://lore.kernel.org/ecryptfs/20261012-work-fs-sb_rdonly-v1-1-60743a3dc326@kernel.org/

If you have review feedback on Christian's patch, please reply in that
thread. Thanks!

Tyler

> ---
> 
> Notes:
>     Reproducer (CONFIG_ECRYPT_FS, CONFIG_TMPFS, CONFIG_KEYS; runs as /init
>     in QEMU, root; the key is a dummy auth_tok, nothing gets encrypted):
>     
>       gcc -O2 -static -o init ecryptfs_inew.c   # packed as /init in an initramfs
>       qemu-system-x86_64 -m 1G -nographic -kernel bzImage -initrd initramfs.cpio \
>           -append "console=ttyS0 rdinit=/init"
>     
>     Before:
>       stat: Text file busy
>       inodes left by 1000 failed lookups: 1000
>       VFS: Busy inodes after unmount of ecryptfs (ecryptfs)   (console)
>       unmounted
>     After:
>       stat: Text file busy
>       inodes left by 1000 failed lookups: 0
>       unmounted
>     
>     The same leak was reported on the list as "[BUG] ecryptfs leaks an inode
>     on lookup failure -> kernel BUG in generic_shutdown_super()" (Farhad
>     Alemi, 2026-08-27), with -ETXTBSY from the lower open.
>     
>     /*
>      * Look up a lower file that is being executed through eCryptfs 1000 times,
>      * count the inodes left behind, then unmount.
>      * Runs as /init in QEMU; needs CONFIG_ECRYPT_FS, CONFIG_TMPFS, CONFIG_KEYS.
>      */
>     #include <fcntl.h>
>     #include <linux/keyctl.h>
>     #include <stdio.h>
>     #include <sys/mount.h>
>     #include <sys/reboot.h>
>     #include <sys/stat.h>
>     #include <sys/sendfile.h>
>     #include <sys/syscall.h>
>     #include <unistd.h>
>     
>     static long inodes(void)
>     {
>     	long n = -1;
>     	FILE *f = fopen("/proc/sys/fs/inode-nr", "r");
>     
>     	if (f) {
>     		if (fscanf(f, "%ld", &n) != 1)
>     			n = -1;
>     		fclose(f);
>     	}
>     	return n;
>     }
>     
>     int main(int argc, char **argv)
>     {
>     	static char tok[1024];
>     	int fd, in, i;
>     	long n;
>     	struct stat st;
>     
>     	if (argc > 1) {		/* the copy in the lower directory */
>     		pause();
>     		return 0;
>     	}
>     
>     	mount("proc", "/proc", "proc", 0, NULL);
>     	mkdir("/lower", 0755);
>     	mkdir("/upper", 0755);
>     	mount("tmpfs", "/lower", "tmpfs", 0, NULL);
>     	in = open("/init", O_RDONLY);
>     	fstat(in, &st);
>     	fd = open("/lower/f", O_CREAT | O_WRONLY, 0755);
>     	sendfile(fd, in, NULL, st.st_size);
>     	close(fd);
>     	close(in);
>     	if (!fork()) {
>     		execl("/lower/f", "f", "sleep", NULL);
>     		_exit(1);
>     	}
>     	sleep(1);
>     
>     	tok[0] = 0x04;		/* auth_tok version 0.4, ECRYPTFS_PASSWORD */
>     	if (syscall(SYS_add_key, "user", "0123456789abcdef", tok, sizeof(tok),
>     		    KEY_SPEC_USER_KEYRING) < 0)
>     		perror("add_key");
>     	if (mount("/lower", "/upper", "ecryptfs", 0,
>     		  "ecryptfs_sig=0123456789abcdef,ecryptfs_cipher=aes,ecryptfs_key_bytes=16"))
>     		perror("mount ecryptfs");
>     
>     	n = inodes();
>     	for (i = 0; i < 1000; i++)
>     		if (stat("/upper/f", &st) && i == 0)
>     			printf("stat: %m\n");
>     	printf("inodes left by 1000 failed lookups: %ld\n", inodes() - n);
>     	if (umount("/upper"))
>     		perror("umount");
>     	printf("unmounted\n");
>     	fflush(stdout);
>     	reboot(RB_POWER_OFF);
>     	return 0;
>     }
> 
>  fs/ecryptfs/inode.c | 3 +++
>  1 file changed, 3 insertions(+)
> 
> diff --git a/fs/ecryptfs/inode.c b/fs/ecryptfs/inode.c
> index 525297c7ebd8..d7e90192efcf 100644
> --- a/fs/ecryptfs/inode.c
> +++ b/fs/ecryptfs/inode.c
> @@ -363,6 +363,9 @@ static struct dentry *ecryptfs_lookup_interpose(struct dentry *dentry,
>  		rc = ecryptfs_i_size_read(dentry, inode);
>  		if (rc) {
>  			make_bad_inode(inode);
> +			if (inode_state_read_once(inode) & I_NEW)
> +				unlock_new_inode(inode);
> +			iput(inode);
>  			return ERR_PTR(rc);
>  		}
>  	}
> 
> base-commit: 58161ead29e50d70a317ea26b472e0c7508e69da
> -- 
> 2.53.0
> 

      reply	other threads:[~2026-10-19 22:28 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-18  2:38 [PATCH v1] ecryptfs: release the inode when lookup fails to read the file size Nickolai Zeldovich
2026-10-19 22:28 ` Tyler Hicks [this message]

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=ataZjvBo29sDyej3@yaupon \
    --to=code@tyhicks.com \
    --cc=ecryptfs@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=nickolai@csail.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox