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 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox