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
prev parent 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.