ext4_setent() updates the inode number and file_type in an existing directory entry but did not handle the dirdata extension payload. Extend it with a src_fid parameter so that the source LUFID travels with the inode through rename. When src_fid is non-NULL and the destination slot carries a LUFID of the same on-disk size, the source payload is copied into the destination before the file_type merge. When src_fid is NULL (source has no LUFID), EXT4_DIRENT_LUFID is cleared from the destination entry. CFHASH bytes are left in place: CFHASH is a property of the destination filename, not the inode, and it always precedes CFHASH in the extension layout so its offset is unaffected by the LUFID copy. Both ext4_rename() and ext4_cross_rename() snapshot the source LUFID into a stack buffer before any setent() call, so that a cross-directory swap reads both sides before either is overwritten. Signed-off-by: Artem Blagodarenko --- fs/ext4/namei.c | 190 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 180 insertions(+), 10 deletions(-) diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c index 2ef4d430d18e..6bf553415707 100644 --- a/fs/ext4/namei.c +++ b/fs/ext4/namei.c @@ -3738,7 +3738,8 @@ static int ext4_rename_dir_finish(handle_t *handle, struct ext4_renament *ent, } static int ext4_setent(handle_t *handle, struct ext4_renament *ent, - unsigned ino, unsigned file_type) + unsigned ino, unsigned file_type, + const struct ext4_dirent_fid *src_fid) { int retval, retval2; @@ -3748,8 +3749,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); + } inode_inc_iversion(ent->dir); inode_set_mtime_to_ts(ent->dir, inode_set_ctime_current(ent->dir)); retval = ext4_mark_inode_dirty(handle, ent->dir); @@ -3786,7 +3836,7 @@ static void ext4_resetent(handle_t *handle, struct ext4_renament *ent, return; } - ext4_setent(handle, &old, ino, file_type); + ext4_setent(handle, &old, ino, file_type, NULL); brelse(old.bh); } @@ -3885,6 +3935,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; +} + +static void ext4_lufid_snap_release(struct ext4_lufid_snap *snap) +{ + if (snap->on_heap) + kfree(snap->fid); +} + /* * Anybody can rename anything with this: the permission checks are left to the * higher-level routines. @@ -3913,6 +4024,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir, struct inode *whiteout = NULL; int credits; u8 old_file_type; + struct ext4_lufid_snap old_snap = {}; if (new.inode && new.inode->i_nlink == 0) { EXT4_ERROR_INODE(new.inode, @@ -4018,27 +4130,60 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir, force_reread = (new.dir->i_ino == old.dir->i_ino && ext4_test_inode_flag(new.dir, EXT4_INODE_INLINE_DATA)); + if (ext4_has_feature_dirdata(old.dir->i_sb)) { + /* Snapshot before whiteout's ext4_setent() clears the LUFID + * flag in old.de; the snapshot is also needed for the !new.bh + * path below where we must carry the LUFID to the new entry. */ + retval = ext4_lufid_snapshot(old.de, + old.dir->i_sb->s_blocksize, + &old_snap); + if (retval) + goto end_rename; + if (old_snap.fid) + ext4_fc_mark_ineligible(old.dir->i_sb, + EXT4_FC_REASON_DIRDATA, handle); + else + old_file_type &= ~EXT4_DIRENT_LUFID; + } + if (whiteout) { /* * Do this before adding a new entry, so the old entry is sure * to be still pointing to the valid old entry. */ retval = ext4_setent(handle, &old, whiteout->i_ino, - EXT4_FT_CHRDEV); + 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) { + struct ext4_dentry_param *edp = NULL; + if (old_snap.fid) { + /* Wrap the snapshotted LUFID in an ext4_dentry_param so + * ext4_add_entry() picks it up via d_fsdata. */ + unsigned fid_size = old_snap.fid->df_header.ddh_length; + + edp = kmalloc(offsetof(struct ext4_dentry_param, + edp_dfid) + fid_size, GFP_NOFS); + if (!edp) { + retval = -ENOMEM; + goto end_rename; + } + edp->edp_magic = EXT4_LUFID_MAGIC; + memcpy(&edp->edp_dfid, old_snap.fid, fid_size); + } + new.dentry->d_fsdata = edp; retval = ext4_add_entry(handle, new.dentry, old.inode); + new.dentry->d_fsdata = NULL; + kfree(edp); if (retval) goto end_rename; } else { retval = ext4_setent(handle, &new, - old.inode->i_ino, old_file_type); + old.inode->i_ino, old_file_type, old_snap.fid); if (retval) goto end_rename; } @@ -4125,6 +4270,7 @@ static int ext4_rename(struct mnt_idmap *idmap, struct inode *old_dir, retval = 0; end_rename: + ext4_lufid_snap_release(&old_snap); if (whiteout) { if (retval) { ext4_resetent(handle, &old, @@ -4161,8 +4307,9 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry, .dentry = new_dentry, .inode = d_inode(new_dentry), }; - u8 new_file_type; + u8 new_file_type, old_de_file_type; int retval; + struct ext4_lufid_snap old_snap = {}, new_snap = {}; if ((ext4_test_inode_flag(new_dir, EXT4_INODE_PROJINHERIT) && !projid_eq(EXT4_I(new_dir)->i_projid, @@ -4241,12 +4388,33 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry, goto end_rename; } + if (ext4_has_feature_dirdata(old.dir->i_sb)) { + retval = ext4_lufid_snapshot(old.de, old.dir->i_sb->s_blocksize, + &old_snap); + if (retval) + goto end_rename; + retval = ext4_lufid_snapshot(new.de, new.dir->i_sb->s_blocksize, + &new_snap); + if (retval) + goto end_rename; + if (old_snap.fid || new_snap.fid) + ext4_fc_mark_ineligible(old.dir->i_sb, + EXT4_FC_REASON_DIRDATA, handle); + } + + old_de_file_type = old.de->file_type; + if (!old_snap.fid) + old_de_file_type &= ~EXT4_DIRENT_LUFID; new_file_type = new.de->file_type; - retval = ext4_setent(handle, &new, old.inode->i_ino, old.de->file_type); + if (!new_snap.fid) + new_file_type &= ~EXT4_DIRENT_LUFID; + retval = ext4_setent(handle, &new, old.inode->i_ino, old_de_file_type, + old_snap.fid); if (retval) goto end_rename; - retval = ext4_setent(handle, &old, new.inode->i_ino, new_file_type); + retval = ext4_setent(handle, &old, new.inode->i_ino, new_file_type, + new_snap.fid); if (retval) goto end_rename; @@ -4279,6 +4447,8 @@ static int ext4_cross_rename(struct inode *old_dir, struct dentry *old_dentry, retval = 0; end_rename: + ext4_lufid_snap_release(&old_snap); + ext4_lufid_snap_release(&new_snap); brelse(old.dir_bh); brelse(new.dir_bh); brelse(old.bh); -- 2.43.7