All of lore.kernel.org
 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 v13 09/11] ext4: add dirdata LUFID support for directory entry rename
Date: Thu, 29 Oct 2026 12:47:34 +0000	[thread overview]
Message-ID: <sashiko-outbox-189309@kernel.org> (raw)
In-Reply-To: <20261029122413.230409-10-ablagodarenko@thelustrecollective.com>

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] ext4: directory corruption in ext4_resetent() after RENAME_WHITEOUT failure
- [Medium] ext4: double-fetch of ddh_length in ext4_lufid_snapshot()
--

commit 05f91dc592ccfff134532dbb86b63e7c812e5a35
Author: Artem Blagodarenko <artem.blagodarenko@gmail.com>

ext4: add dirdata LUFID support for directory entry rename

This commit extends ext4_setent(), ext4_rename(), and ext4_cross_rename() to
snapshot and preserve dirdata LUFID payloads across directory entry renames.

> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index 30bbf6a56bf64..433bee757ea4c 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c

[ ... ]

> @@ -3762,8 +3758,57 @@ static int ext4_setent(handle_t *handle, struct ext4_renament *ent,
>  	if (retval)
>  		return retval;
>  	ent->de->inode = cpu_to_le32(ino);
> -	if (ext4_has_feature_filetype(ent->dir->i_sb))
> -		ent->de->file_type = file_type;
> +	if (ext4_has_feature_filetype(ent->dir->i_sb)) {
> +		/* Copy the source LUFID payload into the destination slot when
> +		 * both carry a LUFID of the same on-disk size.  LUFID is
> +		 * inode-specific and must travel with the inode through rename.
> +		 * CFHASH bytes are left in-place: CFHASH is a function of the
> +		 * destination filename, and its bytes sit at the correct offset
> +		 * because LUFID always precedes CFHASH in the extension layout.
> +		 * When src_fid is NULL, the LUFID flag is synced to the LUFID
> +		 * bit already present in file_type: callers that clear LUFID
> +		 * (rename of a non-LUFID inode, whiteout) pass file_type with
> +		 * LUFID=0; ext4_resetent passes the original file_type so the
> +		 * flag is restored for error recovery without overwriting the
> +		 * LUFID bytes that are still intact on disk. */
> +		if (ext4_has_feature_dirdata(ent->dir->i_sb)) {
> +			if (src_fid && (ent->de->file_type & EXT4_DIRENT_LUFID)) {
> +				unsigned int rec_len =
> +					ext4_rec_len_from_disk(ent->de->rec_len,
> +						ent->dir->i_sb->s_blocksize);
> +				unsigned int ddh_off =
> +					EXT4_BASE_DIR_LEN + ent->de->name_len + 1;
> +				struct ext4_dirent_data_header *ddh =
> +					(struct ext4_dirent_data_header *)
> +					((char *)ent->de + ddh_off);
> +				unsigned int copy_len = src_fid->df_header.ddh_length;
> +
> +				if (ddh_off + sizeof(*ddh) > rec_len ||
> +				    ddh->ddh_length != copy_len ||
> +				    ddh_off + copy_len > rec_len) {
> +					/* Cannot copy: clear the flag so the
> +					 * slot does not advertise a stale LUFID
> +					 * from the old inode. */
> +					ent->de->file_type &= ~EXT4_DIRENT_LUFID;
> +				} else {
> +					memcpy(ddh, src_fid, copy_len);
> +				}
> +			} else if (!src_fid) {
> +				/* Sync the LUFID flag with what file_type requests.
> +				 * For normal rename (source has no LUFID) and for
> +				 * the whiteout setent, file_type carries LUFID=0,
> +				 * so we clear the stale flag.  For ext4_resetent
> +				 * (error recovery), file_type is the original
> +				 * file_type with LUFID=1, so we restore the flag —
> +				 * the LUFID bytes are still on disk untouched. */
> +				ent->de->file_type =
> +					(ent->de->file_type & ~EXT4_DIRENT_LUFID) |
> +					(file_type & EXT4_DIRENT_LUFID);
> +			}
> +		}
> +		ent->de->file_type = (file_type & EXT4_FT_MASK) |
> +				     (ent->de->file_type & ~EXT4_FT_MASK);
> +	}

[Severity: High]
Are the LUFID bytes in old.de guaranteed to remain untouched on disk if a
RENAME_WHITEOUT operation fails and rolls back via ext4_resetent()?

In ext4_rename(), when whiteout is non-NULL, ext4_setent() is called on old
with EXT4_FT_CHRDEV and src_fid = NULL before ext4_add_entry() runs:

	if (whiteout) {
		retval = ext4_setent(handle, &old, whiteout->i_ino,
				     EXT4_FT_CHRDEV, NULL);
		if (retval)
			goto end_rename;
		retval = ext4_mark_inode_dirty(handle, whiteout);
		if (unlikely(retval))
			goto end_rename;
	}
	if (!new.bh) {
		new.dentry->d_fsdata = edp;
		retval = ext4_add_entry(handle, new.dentry, old.inode);

Because that first ext4_setent() call clears EXT4_DIRENT_LUFID in
old.de->file_type, ext4_dir_entry_len() computes the active length of old.de
without its LUFID payload.

When !new.bh and old.dir == new.dir (same-directory rename with
RENAME_WHITEOUT), ext4_add_entry() can reuse or discard those LUFID bytes:

- ext4_find_dest_de() and ext4_insert_dentry_data() treat old.de's LUFID
  bytes as unused slack space, shrink old.de->rec_len to the smaller active
  length (smaller than ddh_off + ddh_length), and overwrite old.de's LUFID
  bytes with the new directory entry.

- During an htree block split in make_indexed_dir() or do_split(),
  dx_move_dirents() and dx_pack_dirents() copy only the active length for
  old.de, discarding its LUFID payload and shrinking old.de->rec_len.

- Additionally, after a later patch in this series adds
  ext4_setent_compact_exts(), if old.de carried both EXT4_DIRENT_LUFID and
  EXT4_DIRENT_CFHASH, the whiteout ext4_setent() call slides the CFHASH
  record over the LUFID slot at ddh_off.

If ext4_add_entry() (for example, returning -ENOSPC when the directory or
filesystem is full) or a later step in ext4_rename() fails and jumps to
end_rename, ext4_lufid_snap_release(&old_snap) frees old_snap and
ext4_resetent() calls ext4_setent(handle, &old, ino, file_type, NULL) with
old_file_type:

end_rename:
	kfree(edp);
	ext4_lufid_snap_release(&old_snap);
	if (whiteout) {
		if (retval) {
			ext4_resetent(handle, &old,
				      old.inode->i_ino, old_file_type);

Wouldn't the !src_fid branch in ext4_setent() then unconditionally set
EXT4_DIRENT_LUFID back on old.de->file_type without checking whether
old.de->rec_len still encompasses the LUFID or restoring old_snap.fid,
leaving a corrupted directory entry on disk?

[ ... ]

> @@ -3899,6 +3944,67 @@ static struct inode *ext4_whiteout_for_rename(struct mnt_idmap *idmap,
>  	return wh;
>  }
>  
> +/*
> + * Stack buffer sized for 1-byte header + 4 × 16-byte FIDs, covering virtually
> + * all real-world LUFID payloads without a heap allocation.  ddh_length is u8
> + * so the absolute maximum is 255 bytes; larger payloads fall back to kmalloc.
> + */
> +#define EXT4_LUFID_INLINE_SIZE  65
> +
> +struct ext4_lufid_snap {
> +	struct ext4_dirent_fid *fid;	/* NULL or valid; never ERR_PTR */
> +	bool on_heap;
> +	u8 buf[EXT4_LUFID_INLINE_SIZE];
> +};
> +
> +/*
> + * ext4_lufid_snapshot - copy the LUFID record from a directory entry
> + *
> + * Fills @snap with a copy of the on-disk LUFID record.  For payloads up to
> + * EXT4_LUFID_INLINE_SIZE bytes the inline buffer is used; larger payloads
> + * fall back to a heap allocation.  Returns 0 on success (snap->fid is NULL
> + * when no LUFID is present), or -ENOMEM when a heap allocation fails.
> + * Call ext4_lufid_snap_release() when done.
> + */
> +static int ext4_lufid_snapshot(struct ext4_dir_entry_2 *de,
> +			       unsigned int blocksize,
> +			       struct ext4_lufid_snap *snap)
> +{
> +	unsigned int ddh_off = EXT4_BASE_DIR_LEN + de->name_len + 1;
> +	unsigned int rec_len = ext4_rec_len_from_disk(de->rec_len, blocksize);
> +	struct ext4_dirent_fid *disk_fid;
> +	unsigned int dlen;
> +
> +	snap->fid = NULL;
> +	snap->on_heap = false;
> +
> +	if (!(de->file_type & EXT4_DIRENT_LUFID))
> +		return 0;
> +	if (ddh_off + sizeof(disk_fid->df_header) > rec_len)
> +		return 0;
> +	disk_fid = (struct ext4_dirent_fid *)((char *)de + ddh_off);
> +	dlen = disk_fid->df_header.ddh_length;
> +	if (dlen < sizeof(disk_fid->df_header) || ddh_off + dlen > rec_len)
> +		return 0;
> +
> +	if (dlen <= sizeof(snap->buf)) {
> +		memcpy(snap->buf, disk_fid, dlen);
> +		snap->fid = (struct ext4_dirent_fid *)snap->buf;
> +	} else {
> +		snap->fid = kmemdup(disk_fid, dlen, GFP_NOFS);
> +		if (!snap->fid)
> +			return -ENOMEM;
> +		snap->on_heap = true;
> +	}
> +	return 0;
> +}

[Severity: Medium]
Can a concurrent modification to the underlying buffer_head cause a
double-fetch of ddh_length in ext4_lufid_snapshot() and an out-of-bounds read
from snap->fid?

ext4_lufid_snapshot() reads dlen from disk_fid->df_header.ddh_length in the
directory buffer, validates dlen against rec_len, and then copies dlen bytes
from disk_fid into snap->buf or a kmemdup(..., GFP_NOFS) buffer (which may
sleep in reclaim before copying). During memcpy() or kmemdup(),
disk_fid->df_header.ddh_length is fetched a second time from the buffer into
snap->fid->df_header.ddh_length.

If the buffer data is concurrently modified via the underlying block device
page cache (the same threat model guarded against with
READ_ONCE(ddh->ddh_length) in the earlier commit "ext4: add dirdata format
definitions and access helpers"), snap->fid->df_header.ddh_length can become
larger than dlen (up to 255).

When ext4_rename() or ext4_setent() later reads
old_snap.fid->df_header.ddh_length or src_fid->df_header.ddh_length:

	if (!new.bh && old_snap.fid) {
		unsigned fid_size = old_snap.fid->df_header.ddh_length;

		edp = kmalloc(offsetof(struct ext4_dentry_param,
				       edp_dfid) + fid_size, GFP_NOFS);
		...
		memcpy(&edp->edp_dfid, old_snap.fid, fid_size);
	}

and in ext4_setent():

	unsigned int copy_len = src_fid->df_header.ddh_length;
	...
	memcpy(ddh, src_fid, copy_len);

wouldn't memcpy() read past the end of old_snap.buf or the dlen-byte
kmemdup() allocation? Should ext4_lufid_snapshot() use
READ_ONCE(disk_fid->df_header.ddh_length) and explicitly set
snap->fid->df_header.ddh_length = dlen after the copy?

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

  reply	other threads:[~2026-10-29 12:47 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 12:23 [PATCH v13 00/11] Data in direntry (dirdata) feature Artem Blagodarenko
2026-10-29 12:24 ` [PATCH v13 01/11] ext4: validate count against limit in ext4_dx_csum_verify/_set Artem Blagodarenko
2026-10-29 12:37   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 02/11] ext4: replace ext4_dir_entry with ext4_dir_entry_2 Artem Blagodarenko
2026-10-29 12:34   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 03/11] ext4: add ext4_dir_entry_is_tail() Artem Blagodarenko
2026-10-29 12:31   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 04/11] ext4: add dirdata format definitions and access helpers Artem Blagodarenko
2026-10-29 12:31   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 05/11] ext4: refactor dx_root to support variable dirent sizes Artem Blagodarenko
2026-10-29 12:49   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 06/11] ext4: add ext4_dir_entry_len() Artem Blagodarenko
2026-10-29 12:50   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 07/11] ext4: rename ext4_dir_rec_len() and clarify dirdata usage Artem Blagodarenko
2026-10-29 12:50   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 08/11] ext4: dirdata feature Artem Blagodarenko
2026-10-29 12:51   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 09/11] ext4: add dirdata LUFID support for directory entry rename Artem Blagodarenko
2026-10-29 12:47   ` sashiko-bot [this message]
2026-10-29 12:24 ` [PATCH v13 10/11] ext4: add dirdata set/get helpers Artem Blagodarenko
2026-10-29 12:44   ` sashiko-bot
2026-10-29 12:24 ` [PATCH v13 11/11] ext4: Add EXT4_IOC_SET_LUFID ioctl for setting LUFID on directory entries Artem Blagodarenko
2026-10-29 12:58   ` sashiko-bot

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-189309@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.