Re: [PATCH 7/9] rename(): fix the locking of subdirectories

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Wed 22-11-23 19:36:50, Al Viro wrote:
> 	We should never lock two subdirectories without having taken
> ->s_vfs_rename_mutex; inode pointer order or not, the "order" proposed
> in 28eceeda130f "fs: Lock moved directories" is not transitive, with
> the usual consequences.
> 
> 	The rationale for locking renamed subdirectory in all cases was
> the possibility of race between rename modifying .. in a subdirectory to
> reflect the new parent and another thread modifying the same subdirectory.
> For a lot of filesystems that's not a problem, but for some it can lead
> to trouble (e.g. the case when short directory contents is kept in the
> inode, but creating a file in it might push it across the size limit
> and copy its contents into separate data block(s)).
> 
> 	However, we need that only in case when the parent does change -
> otherwise ->rename() doesn't need to do anything with .. entry in the
> first place.  Some instances are lazy and do a tautological update anyway,
> but it's really not hard to avoid.
> 
> Amended locking rules for rename():
> 	find the parent(s) of source and target
> 	if source and target have the same parent
> 		lock the common parent
> 	else
> 		lock ->s_vfs_rename_mutex
> 		lock both parents, in ancestor-first order; if neither
> 		is an ancestor of another, lock the parent of source
> 		first.
> 	find the source and target.
> 	if source and target have the same parent
> 		if operation is an overwriting rename of a subdirectory
> 			lock the target subdirectory
> 	else
> 		if source is a subdirectory
> 			lock the source
> 		if target is a subdirectory
> 			lock the target
> 	lock non-directories involved, in inode pointer order if both
> 	source and target are such.
> 
> That way we are guaranteed that parents are locked (for obvious reasons),
> that any renamed non-directory is locked (nfsd relies upon that),
> that any victim is locked (emptiness check needs that, among other things)
> and subdirectory that changes parent is locked (needed to protect the update
> of .. entries).  We are also guaranteed that any operation locking more
> than one directory either takes ->s_vfs_rename_mutex or locks a parent
> followed by its child.
> 
> Cc: stable@xxxxxxxxxxxxxxx
> Fixes: 28eceeda130f "fs: Lock moved directories"
> Signed-off-by: Al Viro <viro@xxxxxxxxxxxxxxxxxx>

Looks good to me and thanks for fixing this! Feel free to add:

Reviewed-by: Jan Kara <jack@xxxxxxx>

								Honza

> ---
>  .../filesystems/directory-locking.rst         | 29 ++++-----
>  Documentation/filesystems/locking.rst         |  5 +-
>  Documentation/filesystems/porting.rst         | 18 ++++++
>  fs/namei.c                                    | 60 ++++++++++++-------
>  4 files changed, 74 insertions(+), 38 deletions(-)
> 
> diff --git a/Documentation/filesystems/directory-locking.rst b/Documentation/filesystems/directory-locking.rst
> index dccd61c7c5c3..193c22687851 100644
> --- a/Documentation/filesystems/directory-locking.rst
> +++ b/Documentation/filesystems/directory-locking.rst
> @@ -22,13 +22,16 @@ exclusive.
>  3) object removal.  Locking rules: caller locks parent, finds victim,
>  locks victim and calls the method.  Locks are exclusive.
>  
> -4) rename() that is _not_ cross-directory.  Locking rules: caller locks the
> -parent and finds source and target.  We lock both (provided they exist).  If we
> -need to lock two inodes of different type (dir vs non-dir), we lock directory
> -first.  If we need to lock two inodes of the same type, lock them in inode
> -pointer order.  Then call the method.  All locks are exclusive.
> -NB: we might get away with locking the source (and target in exchange
> -case) shared.
> +4) rename() that is _not_ cross-directory.  Locking rules: caller locks
> +the parent and finds source and target.  Then we decide which of the
> +source and target need to be locked.  Source needs to be locked if it's a
> +non-directory; target - if it's a non-directory or about to be removed.
> +Take the locks that need to be taken, in inode pointer order if need
> +to take both (that can happen only when both source and target are
> +non-directories - the source because it wouldn't be locked otherwise
> +and the target because mixing directory and non-directory is allowed
> +only with RENAME_EXCHANGE, and that won't be removing the target).
> +After the locks had been taken, call the method.  All locks are exclusive.
>  
>  5) link creation.  Locking rules:
>  
> @@ -44,20 +47,17 @@ rules:
>  
>  	* lock the filesystem
>  	* lock parents in "ancestors first" order. If one is not ancestor of
> -	  the other, lock them in inode pointer order.
> +	  the other, lock the parent of source first.
>  	* find source and target.
>  	* if old parent is equal to or is a descendent of target
>  	  fail with -ENOTEMPTY
>  	* if new parent is equal to or is a descendent of source
>  	  fail with -ELOOP
> -	* Lock both the source and the target provided they exist. If we
> -	  need to lock two inodes of different type (dir vs non-dir), we lock
> -	  the directory first. If we need to lock two inodes of the same type,
> -	  lock them in inode pointer order.
> +	* Lock subdirectories involved (source before target).
> +	* Lock non-directories involved, in inode pointer order.
>  	* call the method.
>  
> -All ->i_rwsem are taken exclusive.  Again, we might get away with locking
> -the source (and target in exchange case) shared.
> +All ->i_rwsem are taken exclusive.
>  
>  The rules above obviously guarantee that all directories that are going to be
>  read, modified or removed by method will be locked by caller.
> @@ -67,6 +67,7 @@ If no directory is its own ancestor, the scheme above is deadlock-free.
>  
>  Proof:
>  
> +[XXX: will be updated once we are done massaging the lock_rename()]
>  	First of all, at any moment we have a linear ordering of the
>  	objects - A < B iff (A is an ancestor of B) or (B is not an ancestor
>          of A and ptr(A) < ptr(B)).
> diff --git a/Documentation/filesystems/locking.rst b/Documentation/filesystems/locking.rst
> index 7be2900806c8..bd12f2f850ad 100644
> --- a/Documentation/filesystems/locking.rst
> +++ b/Documentation/filesystems/locking.rst
> @@ -101,7 +101,7 @@ symlink:	exclusive
>  mkdir:		exclusive
>  unlink:		exclusive (both)
>  rmdir:		exclusive (both)(see below)
> -rename:		exclusive (all)	(see below)
> +rename:		exclusive (both parents, some children)	(see below)
>  readlink:	no
>  get_link:	no
>  setattr:	exclusive
> @@ -123,6 +123,9 @@ get_offset_ctx  no
>  	Additionally, ->rmdir(), ->unlink() and ->rename() have ->i_rwsem
>  	exclusive on victim.
>  	cross-directory ->rename() has (per-superblock) ->s_vfs_rename_sem.
> +	->unlink() and ->rename() have ->i_rwsem exclusive on all non-directories
> +	involved.
> +	->rename() has ->i_rwsem exclusive on any subdirectory that changes parent.
>  
>  See Documentation/filesystems/directory-locking.rst for more detailed discussion
>  of the locking scheme for directory operations.
> diff --git a/Documentation/filesystems/porting.rst b/Documentation/filesystems/porting.rst
> index 878e72b2f8b7..9100969e7de6 100644
> --- a/Documentation/filesystems/porting.rst
> +++ b/Documentation/filesystems/porting.rst
> @@ -1061,3 +1061,21 @@ export_operations ->encode_fh() no longer has a default implementation to
>  encode FILEID_INO32_GEN* file handles.
>  Filesystems that used the default implementation may use the generic helper
>  generic_encode_ino32_fh() explicitly.
> +
> +---
> +
> +**mandatory**
> +
> +If ->rename() update of .. on cross-directory move needs an exclusion with
> +directory modifications, do *not* lock the subdirectory in question in your
> +->rename() - it's done by the caller now [that item should've been added in
> +28eceeda130f "fs: Lock moved directories"].
> +
> +---
> +
> +**mandatory**
> +
> +On same-directory ->rename() the (tautological) update of .. is not protected
> +by any locks; just don't do it if the old parent is the same as the new one.
> +We really can't lock two subdirectories in same-directory rename - not without
> +deadlocks.
> diff --git a/fs/namei.c b/fs/namei.c
> index 71c13b2990b4..29bafbdb44ca 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -3021,20 +3021,14 @@ static struct dentry *lock_two_directories(struct dentry *p1, struct dentry *p2)
>  	p = d_ancestor(p2, p1);
>  	if (p) {
>  		inode_lock_nested(p2->d_inode, I_MUTEX_PARENT);
> -		inode_lock_nested(p1->d_inode, I_MUTEX_CHILD);
> +		inode_lock_nested(p1->d_inode, I_MUTEX_PARENT2);
>  		return p;
>  	}
>  
>  	p = d_ancestor(p1, p2);
> -	if (p) {
> -		inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
> -		inode_lock_nested(p2->d_inode, I_MUTEX_CHILD);
> -		return p;
> -	}
> -
> -	lock_two_inodes(p1->d_inode, p2->d_inode,
> -			I_MUTEX_PARENT, I_MUTEX_PARENT2);
> -	return NULL;
> +	inode_lock_nested(p1->d_inode, I_MUTEX_PARENT);
> +	inode_lock_nested(p2->d_inode, I_MUTEX_PARENT2);
> +	return p;
>  }
>  
>  /*
> @@ -4716,11 +4710,12 @@ SYSCALL_DEFINE2(link, const char __user *, oldname, const char __user *, newname
>   *
>   *	a) we can get into loop creation.
>   *	b) race potential - two innocent renames can create a loop together.
> - *	   That's where 4.4 screws up. Current fix: serialization on
> + *	   That's where 4.4BSD screws up. Current fix: serialization on
>   *	   sb->s_vfs_rename_mutex. We might be more accurate, but that's another
>   *	   story.
> - *	c) we have to lock _four_ objects - parents and victim (if it exists),
> - *	   and source.
> + *	c) we may have to lock up to _four_ objects - parents and victim (if it exists),
> + *	   and source (if it's a non-directory or a subdirectory that moves to
> + *	   different parent).
>   *	   And that - after we got ->i_mutex on parents (until then we don't know
>   *	   whether the target exists).  Solution: try to be smart with locking
>   *	   order for inodes.  We rely on the fact that tree topology may change
> @@ -4752,6 +4747,7 @@ int vfs_rename(struct renamedata *rd)
>  	bool new_is_dir = false;
>  	unsigned max_links = new_dir->i_sb->s_max_links;
>  	struct name_snapshot old_name;
> +	bool lock_old_subdir, lock_new_subdir;
>  
>  	if (source == target)
>  		return 0;
> @@ -4805,15 +4801,32 @@ int vfs_rename(struct renamedata *rd)
>  	take_dentry_name_snapshot(&old_name, old_dentry);
>  	dget(new_dentry);
>  	/*
> -	 * Lock all moved children. Moved directories may need to change parent
> -	 * pointer so they need the lock to prevent against concurrent
> -	 * directory changes moving parent pointer. For regular files we've
> -	 * historically always done this. The lockdep locking subclasses are
> -	 * somewhat arbitrary but RENAME_EXCHANGE in particular can swap
> -	 * regular files and directories so it's difficult to tell which
> -	 * subclasses to use.
> +	 * Lock children.
> +	 * The source subdirectory needs to be locked on cross-directory
> +	 * rename or cross-directory exchange since its parent changes.
> +	 * The target subdirectory needs to be locked on cross-directory
> +	 * exchange due to parent change and on any rename due to becoming
> +	 * a victim.
> +	 * Non-directories need locking in all cases (for NFS reasons);
> +	 * they get locked after any subdirectories (in inode address order).
> +	 *
> +	 * NOTE: WE ONLY LOCK UNRELATED DIRECTORIES IN CROSS-DIRECTORY CASE.
> +	 * NEVER, EVER DO THAT WITHOUT ->s_vfs_rename_mutex.
>  	 */
> -	lock_two_inodes(source, target, I_MUTEX_NORMAL, I_MUTEX_NONDIR2);
> +	lock_old_subdir = new_dir != old_dir;
> +	lock_new_subdir = new_dir != old_dir || !(flags & RENAME_EXCHANGE);
> +	if (is_dir) {
> +		if (lock_old_subdir)
> +			inode_lock_nested(source, I_MUTEX_CHILD);
> +		if (target && (!new_is_dir || lock_new_subdir))
> +			inode_lock(target);
> +	} else if (new_is_dir) {
> +		if (lock_new_subdir)
> +			inode_lock_nested(target, I_MUTEX_CHILD);
> +		inode_lock(source);
> +	} else {
> +		lock_two_nondirectories(source, target);
> +	}
>  
>  	error = -EPERM;
>  	if (IS_SWAPFILE(source) || (target && IS_SWAPFILE(target)))
> @@ -4861,8 +4874,9 @@ int vfs_rename(struct renamedata *rd)
>  			d_exchange(old_dentry, new_dentry);
>  	}
>  out:
> -	inode_unlock(source);
> -	if (target)
> +	if (!is_dir || lock_old_subdir)
> +		inode_unlock(source);
> +	if (target && (!new_is_dir || lock_new_subdir))
>  		inode_unlock(target);
>  	dput(new_dentry);
>  	if (!error) {
> -- 
> 2.39.2
> 
-- 
Jan Kara <jack@xxxxxxxx>
SUSE Labs, CR




[Index of Archives]     [Linux Ext4 Filesystem]     [Union Filesystem]     [Filesystem Testing]     [Ceph Users]     [Ecryptfs]     [NTFS 3]     [AutoFS]     [Kernel Newbies]     [Share Photos]     [Security]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux Cachefs]     [Reiser Filesystem]     [Linux RAID]     [NTFS 3]     [Samba]     [Device Mapper]     [CEPH Development]

  Powered by Linux