Linux EXT4 FS development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Artem Blagodarenko" <artem.blagodarenko@gmail.com>,
	linux-ext4@vger.kernel.org
Cc: tytso@mit.edu
Subject: Re: [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries
Date: Tue, 03 Nov 2026 14:29:17 +0000	[thread overview]
Message-ID: <sashiko-outbox-195448@kernel.org> (raw)
In-Reply-To: <20261103134921.610047-13-ablagodarenko@thelustrecollective.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] ext4: rollback in ext4_dirdata_set_lufid() can fail and lose directory entry
- [High] ext4: premature fscrypt_has_encryption_key() check in ext4_dirdata_set_lufid()
- [Medium] ext4: missing validation of looked-up inode in ext4_dirdata_set_lufid()
- [Medium] ext4: check_sticky() positive return value leaked in ext4_dirdata_set_lufid()
--

commit 8661288e629af06ada9624f600ed1251cebe9539
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>

ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries

This commit introduces the EXT4_IOC_SET_LUFID ioctl and
ext4_dirdata_set_lufid() to attach LUFID dirdata to existing directory
entries by looking up the entry, deleting it, and re-creating it with the
LUFID payload.

> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 8f1a6c000373e..68c5caff4707e 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c

[ ... ]

> @@ -4825,6 +4858,245 @@ static int ext4_rename2(struct mnt_idmap *idmap,
>  	return ext4_rename(idmap, old_dir, old_dentry, new_dir, new_dentry, flags);
>  }
>  
> +/*
> + * ext4_dirdata_set_lufid() - Set LUFID data on an existing directory entry
> + * @dir:        parent directory inode
> + * @filename:   name of the file in the directory
> + * @namelen:    length of filename
> + * @edp:        pointer to initialized dentry param with LUFID data
> + *
> + * This function finds an existing directory entry, deletes it, and re-creates it
> + * with LUFID data attached. Used by the EXT4_IOC_SET_LUFID ioctl.
> + *
> + * Returns 0 on success, negative error code on failure.
> + */
> +int ext4_dirdata_set_lufid(struct mnt_idmap *idmap, struct inode *dir,
> +			   const char *filename, int namelen,
> +			   struct ext4_dentry_param *edp)
> +{
> +	struct super_block *sb = dir->i_sb;
> +	/* zero-init: safe to free on any path */
> +	struct ext4_filename new_fname = {}, old_fname = {};
> +	struct ext4_dir_entry_2 *de = NULL;
> +	struct buffer_head *bh = NULL;
> +	struct inode *inode = NULL;
> +	handle_t *handle = NULL;
> +	struct ext4_lufid_snap old_snap = {};
> +	struct qstr d_name = QSTR_INIT(filename, namelen);
> +	/* the name as stored on disk, for casefolded directories */
> +	struct fscrypt_str stored = FSTR_INIT(NULL, 0);
> +	struct qstr stored_name;
> +	const struct qstr *add_name = &d_name;
> +	__u32 ino;
> +	int err, err2;
> +
> +	if (!ext4_has_feature_dirdata(sb))
> +		return -EOPNOTSUPP;
> +
> +	if (namelen > EXT4_NAME_LEN)
> +		return -ENAMETOOLONG;
> +	if (namelen != strnlen(filename, namelen + 1))
> +		return -EINVAL;
> +
> +	/* the entry is re-created from its plaintext name */
> +	if (IS_ENCRYPTED(dir) && !fscrypt_has_encryption_key(dir))
> +		return -ENOKEY;

[Severity: High]
Won't this fscrypt_has_encryption_key(dir) check fail with -ENOKEY on an
encrypted directory even when the key is present in the keyring?

fscrypt_has_encryption_key() only checks whether dir->i_crypt_info has
already been populated:

include/linux/fscrypt.h:fscrypt_has_encryption_key() {
	return fscrypt_get_inode_info(inode) != NULL;
}

Opening a directory via ext4_dir_open() does not set up i_crypt_info, nor
does looking up a top-level encrypted directory in an unencrypted parent or
adding a key via FS_IOC_ADD_ENCRYPTION_KEY on an already-open directory fd.

Should ext4_dirdata_set_lufid() call fscrypt_prepare_readdir(dir) (or
fscrypt_get_encryption_info(dir)) before checking
fscrypt_has_encryption_key(dir)?

> +
> +	err = dquot_initialize(dir);
> +	if (err)
> +		return err;
> +
> +	/*
> +	 * The lookup and the change must both be under the directory lock.
> +	 * The entry is deleted and re-added in the same directory, so the
> +	 * target inode itself is not changed and is not locked.
> +	 */
> +	inode_lock_nested(dir, I_MUTEX_PARENT);
> +
> +	/*
> +	 * Look up the entry first to get the inode for the permission
> +	 * checks, without holding a journal handle.
> +	 */
> +	bh = ext4_find_entry(dir, &d_name, &de, NULL);
> +	if (IS_ERR(bh)) {
> +		err = PTR_ERR(bh);
> +		bh = NULL;
> +		goto out_unlock_dir;
> +	}
> +	if (!bh) {
> +		err = -ENOENT;
> +		goto out_unlock_dir;
> +	}
> +	ino = le32_to_cpu(de->inode);
> +	brelse(bh);
> +	bh = NULL;
> +	de = NULL;
> +
> +	inode = ext4_iget(sb, ino, EXT4_IGET_NORMAL);
> +	if (IS_ERR(inode)) {
> +		err = PTR_ERR(inode);
> +		inode = NULL;
> +		goto out_unlock_dir;
> +	}

[Severity: Medium]
Should this lookup validate ino and the returned inode the same way
ext4_lookup() does?

In ext4_lookup(), before and after calling ext4_iget(), it checks for
invalid inode numbers, entries pointing back to the parent directory itself,
and deleted inodes returning -ESTALE:

fs/ext4/namei.c:ext4_lookup() {
		if (!ext4_valid_inum(dir->i_sb, ino)) {
			EXT4_ERROR_INODE(dir, "bad inode number: %u", ino);
			return ERR_PTR(-EFSCORRUPTED);
		}
		if (unlikely(ino == dir->i_ino)) {
			EXT4_ERROR_INODE(dir, "'%pd' linked to parent dir",
					 dentry);
			return ERR_PTR(-EFSCORRUPTED);
		}
		inode = ext4_iget(dir->i_sb, ino, EXT4_IGET_NORMAL);
		if (inode == ERR_PTR(-ESTALE)) {
			EXT4_ERROR_INODE(dir,
					 "deleted inode referenced: %u",
					 ino);
			return ERR_PTR(-EFSCORRUPTED);
		}
}

Also, if a corrupted directory entry references an unlinked inode that is
still open in memory, ext4_iget() will return the cached inode with
inode->i_nlink == 0. Should ext4_dirdata_set_lufid() check for these cases
and report -EFSCORRUPTED instead of deleting and re-creating a corrupted
entry?

> +
> +	/* Enforce sticky-bit restriction: on a sticky directory only the
> +	 * directory owner, the file owner, or a privileged process may
> +	 * modify entries.  inode_permission(MAY_WRITE) alone does not cover
> +	 * this, so check it explicitly here as may_delete() does. */
> +	err = check_sticky(idmap, dir, inode);
> +	if (err)
> +		goto out_iput;

[Severity: Medium]
Does this leak a positive return value (+1) to userspace when sticky-bit
permission is denied?

check_sticky() calls __check_sticky(), which returns 0 when allowed and 1
(not -EPERM) when denied:

fs/namei.c:__check_sticky() {
	if (vfsuid_eq_kuid(i_uid_into_vfsuid(idmap, inode), fsuid))
		return 0;
	if (vfsuid_eq_kuid(i_uid_into_vfsuid(idmap, dir), fsuid))
		return 0;
	return !capable_wrt_inode_uidgid(idmap, inode, CAP_FOWNER);
}

Assigning that return value to err and jumping to out_iput causes
ext4_ioctl_set_lufid() to return 1 to userspace instead of -EPERM. Should
this set err = -EPERM when check_sticky(idmap, dir, inode) is non-zero?

[ ... ]

> +	err = ext4_delete_entry(handle, dir, de, bh);
> +	if (err)
> +		goto out_brelse;
> +
> +	brelse(bh);
> +	bh = NULL;
> +
> +	err = ext4_add_fname(handle, dir, &new_fname, inode);
> +	if (err) {
> +		/*
> +		 * The old entry fits in the space it was deleted from, so
> +		 * this needs no new block.
> +		 */
> +		err2 = ext4_add_fname(handle, dir, &old_fname, inode);
> +		if (err2)
> +			EXT4_ERROR_INODE(dir,
> +				"Failed to set LUFID on '%.*s' (err=%d) and failed to restore the original directory entry (err=%d); inode %llu may be orphaned",
> +				namelen, filename, err, err2,
> +				(unsigned long long)inode->i_ino);
> +		goto out_journal;
> +	}

[Severity: Critical]
Can the rollback call to ext4_add_fname(handle, dir, &old_fname, inode) fail
in an HTree-indexed directory and permanently lose the directory entry?

When looking up the existing entry, ext4_find_entry() ->
ext4_dx_find_entry() walks continuation leaf blocks via
ext4_htree_next_block() when a hash collision chain spans multiple blocks
(and __ext4_find_entry() can also fall back to a linear scan across all
directory blocks on casefolded directories or on ERR_BAD_DX_DIR).

However, when re-adding the entry, ext4_add_fname() -> ext4_dx_add_entry()
only probes the single primary leaf block returned by dx_probe() and never
walks continuation blocks:

fs/ext4/namei.c:ext4_dx_add_entry() {
	frame = dx_probe(fname, dir, NULL, frames);
	if (IS_ERR(frame))
		return PTR_ERR(frame);
	entries = frame->entries;
	at = frame->at;
	bh = ext4_read_dirblock(dir, dx_get_block(frame->at), DIRENT_HTREE);
	...
	err = add_dirent_to_buf(handle, fname, dir, inode, NULL, bh);
	if (err != -ENOSPC)
		goto cleanup;
	...
	de = do_split(handle, dir, &bh, frame, &fname->hinfo);
}

If the old entry resided in a continuation block (or was found via linear
fallback), ext4_delete_entry() frees space in that continuation block, while
the primary block returned by dx_probe() remains full. If splitting that
primary block in ext4_add_fname(&new_fname) then fails (for example due to
-ENOSPC, -EDQUOT, or reaching the maximum HTree level), the rollback call
ext4_add_fname(&old_fname) will probe the same full primary block, fail to
split it for the same reason, and commit the transaction with the entry
deleted and inode's link count leaked.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261103134921.610047-1-ablagodarenko@thelustrecollective.com?part=12

      reply	other threads:[~2026-11-03 14:29 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-03 13:49 [PATCH v14 00/12] Data in direntry (dirdata) feature Artem Blagodarenko
2026-11-03 13:49 ` [PATCH v14 01/12] ext4: validate count against limit in ext4_dx_csum_verify/_set Artem Blagodarenko
2026-11-03 13:56   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 02/12] ext4: use the e2fsck limit for htree index nodes Artem Blagodarenko
2026-11-03 13:56   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 03/12] ext4: replace ext4_dir_entry with ext4_dir_entry_2 Artem Blagodarenko
2026-11-03 14:05   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 04/12] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
2026-11-03 13:58   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 05/12] ext4: add dirdata format definitions and access helpers Artem Blagodarenko
2026-11-03 13:56   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 06/12] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
2026-11-03 14:18   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 07/12] ext4: add ext4_dir_entry_len() Artem Blagodarenko
2026-11-03 14:08   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 08/12] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
2026-11-03 14:20   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 09/12] ext4: dirdata feature Artem Blagodarenko
2026-11-03 14:27   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 10/12] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
2026-11-03 14:17   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 11/12] ext4: add dirdata set/get helpers Artem Blagodarenko
2026-11-03 14:18   ` sashiko-bot
2026-11-03 13:49 ` [PATCH v14 12/12] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
2026-11-03 14:29   ` sashiko-bot [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=sashiko-outbox-195448@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=artem.blagodarenko@gmail.com \
    --cc=linux-ext4@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=tytso@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