Re: [PATCH] Docs: fs: prove directory locking more formally

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

 



> Instead of proof by contradiction, this proof more intuitively explains
> the order that the locking rules want to enforce. If the developers want
> to update the locking rules, they would be forced to come up with the
> corresponding order that justifies deadlock-freedom, otherwise, the
> proof won't pass. As a evidence of effectiveness, the authors of RefFS
> paper firstly report the bug from commit 28eceeda130f ("fs: Lock moved
> directories") through this formal approach.

Any comments on this proof? BTW, it would be okay to keep both the
by-contradiction proof and by-order proof as both proofs may find their
audience.

Mo Zou <lostzoumo@xxxxxxxxx> 于2024年4月13日周六 00:10写道:
>
> The directory locking proof should and could be made more formal.
> Specifically, the locking rules for rename are very intricate and
> subtle. The current proof shows deadlock-freedom by contradiction, i.e.,
> if a deadlock is possible, where could go wrong. There is no denying
> that the proof is detailed with all possible cases considered. However,
> it gives less intuition on why the locking rules are correct and may be
> hard to maintain. Developers and even experts may still make mistakes
> without realizing that the proof is no longer correct. See commit
> 28eceeda130f ("fs: Lock moved directories") for such a case.
>
> A recent academia paper RefFS from OSDI 2024 has proposed a formal way
> to prove the correctness of locking scheme with machine-checkable proof
> (see https://ipads.se.sjtu.edu.cn/projects/reffs for a preprint of the
> paper). The core idea is to formally define the locking order of the
> rules. Actually, the existing proof has the same idea but fails to
> define the internal ranking for directories. The difficulty is that the
> ranking for directories is not static but dynamic. In this patch, we
> follow the idea from the paper to dynamically define the internal
> ranking of directories and shows that operations always take directories
> in order of increasing rank so that deadlocks are never possible.
> Instead of proof by contradiction, this proof more intuitively explains
> the order that the locking rules want to enforce. If the developers want
> to update the locking rules, they would be forced to come up with the
> corresponding order that justifies deadlock-freedom, otherwise, the
> proof won't pass. As a evidence of effectiveness, the authors of RefFS
> paper firstly report the bug from commit 28eceeda130f ("fs: Lock moved
> directories") through this formal approach.
>
> Signed-off-by: Mo Zou <lostzoumo@xxxxxxxxx>
> ---
>  .../filesystems/directory-locking.rst         | 224 +++++++++---------
>  1 file changed, 113 insertions(+), 111 deletions(-)
>
> diff --git a/Documentation/filesystems/directory-locking.rst b/Documentation/filesystems/directory-locking.rst
> index 05ea387bc9fb..94e358d71986 100644
> --- a/Documentation/filesystems/directory-locking.rst
> +++ b/Documentation/filesystems/directory-locking.rst
> @@ -44,7 +44,7 @@ For our purposes all operations fall in 6 classes:
>         * decide which of the source and target need to be locked.
>           The 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 (exlusive), in inode pointer order
> +       * take the locks that need to be taken (exclusive), 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 need to be locked
>           otherwise and the target because mixing directory and non-directory is
> @@ -135,12 +135,13 @@ If no directory is its own ancestor, the scheme above is deadlock-free.
>  Proof:
>
>  There is a ranking on the locks, such that all primitives take
> -them in order of non-decreasing rank.  Namely,
> +them in order of increasing rank.  Namely,
>
>    * rank ->i_rwsem of non-directories on given filesystem in inode pointer
>      order.
> -  * put ->i_rwsem of all directories on a filesystem at the same rank,
> -    lower than ->i_rwsem of any non-directory on the same filesystem.
> +  * put ->i_rwsem of all directories on a filesystem lower than ->i_rwsem
> +    of any non-directory on the same filesystem and the internal ranking
> +    of directories are defined later.
>    * put ->s_vfs_rename_mutex at rank lower than that of any ->i_rwsem
>      on the same filesystem.
>    * among the locks on different filesystems use the relative
> @@ -149,92 +150,120 @@ them in order of non-decreasing rank.  Namely,
>  For example, if we have NFS filesystem caching on a local one, we have
>
>    1. ->s_vfs_rename_mutex of NFS filesystem
> -  2. ->i_rwsem of directories on that NFS filesystem, same rank for all
> +  2. ->i_rwsem of directories on that NFS filesystem, internal ranking
> +     defined later
>    3. ->i_rwsem of non-directories on that filesystem, in order of
>       increasing address of inode
>    4. ->s_vfs_rename_mutex of local filesystem
> -  5. ->i_rwsem of directories on the local filesystem, same rank for all
> +  5. ->i_rwsem of directories on the local filesystem, internal ranking
> +     defined later
>    6. ->i_rwsem of non-directories on local filesystem, in order of
>       increasing address of inode.
>
> -It's easy to verify that operations never take a lock with rank
> -lower than that of an already held lock.
> -
> -Suppose deadlocks are possible.  Consider the minimal deadlocked
> -set of threads.  It is a cycle of several threads, each blocked on a lock
> -held by the next thread in the cycle.
> -
> -Since the locking order is consistent with the ranking, all
> -contended locks in the minimal deadlock will be of the same rank,
> -i.e. they all will be ->i_rwsem of directories on the same filesystem.
> -Moreover, without loss of generality we can assume that all operations
> -are done directly to that filesystem and none of them has actually
> -reached the method call.
> -
> -In other words, we have a cycle of threads, T1,..., Tn,
> -and the same number of directories (D1,...,Dn) such that
> -
> -       T1 is blocked on D1 which is held by T2
> -
> -       T2 is blocked on D2 which is held by T3
> -
> -       ...
> -
> -       Tn is blocked on Dn which is held by T1.
> -
> -Each operation in the minimal cycle must have locked at least
> -one directory and blocked on attempt to lock another.  That leaves
> -only 3 possible operations: directory removal (locks parent, then
> -child), same-directory rename killing a subdirectory (ditto) and
> -cross-directory rename of some sort.
> -
> -There must be a cross-directory rename in the set; indeed,
> -if all operations had been of the "lock parent, then child" sort
> -we would have Dn a parent of D1, which is a parent of D2, which is
> -a parent of D3, ..., which is a parent of Dn.  Relationships couldn't
> -have changed since the moment directory locks had been acquired,
> -so they would all hold simultaneously at the deadlock time and
> -we would have a loop.
> -
> -Since all operations are on the same filesystem, there can't be
> -more than one cross-directory rename among them.  Without loss of
> -generality we can assume that T1 is the one doing a cross-directory
> -rename and everything else is of the "lock parent, then child" sort.
> -
> -In other words, we have a cross-directory rename that locked
> -Dn and blocked on attempt to lock D1, which is a parent of D2, which is
> -a parent of D3, ..., which is a parent of Dn.  Relationships between
> -D1,...,Dn all hold simultaneously at the deadlock time.  Moreover,
> -cross-directory rename does not get to locking any directories until it
> -has acquired filesystem lock and verified that directories involved have
> -a common ancestor, which guarantees that ancestry relationships between
> -all of them had been stable.
> -
> -Consider the order in which directories are locked by the
> -cross-directory rename; parents first, then possibly their children.
> -Dn and D1 would have to be among those, with Dn locked before D1.
> -Which pair could it be?
> -
> -It can't be the parents - indeed, since D1 is an ancestor of Dn,
> -it would be the first parent to be locked.  Therefore at least one of the
> -children must be involved and thus neither of them could be a descendent
> -of another - otherwise the operation would not have progressed past
> -locking the parents.
> -
> -It can't be a parent and its child; otherwise we would've had
> -a loop, since the parents are locked before the children, so the parent
> -would have to be a descendent of its child.
> -
> -It can't be a parent and a child of another parent either.
> -Otherwise the child of the parent in question would've been a descendent
> -of another child.
> -
> -That leaves only one possibility - namely, both Dn and D1 are
> -among the children, in some order.  But that is also impossible, since
> -neither of the children is a descendent of another.
> -
> -That concludes the proof, since the set of operations with the
> -properties requiered for a minimal deadlock can not exist.
> +.. _RefFS: https://ipads.se.sjtu.edu.cn/projects/reffs
> +
> +The ranking above is mostly static except the internal ranking of
> +directories. Below we will focus on internal ranking of directories on
> +the same filesystem and present a way to dynamically define ranks of
> +directories such that operations still take locks in order of
> +increasing rank, i.e., operations always take a lock with rank higher
> +than that of any already held locks. Note that the idea of dynamic
> +ranking has a formal basis and more details can be found in the
> +academia paper RefFS_ from OSDI 2024.
> +
> +
> +First, without considering cross-directory renames, a directory's rank
> +is defined as its distance from the root, i.e., root directory has rank
> +0 and each child directory's rank equals one plus its parent
> +directory's rank. Because no directory is its own ancestor, every
> +directory's rank can be uniquely determined starting from root to its
> +descendants. This ranking accounts for the (locks parent, then child)
> +nested locking in directory removal and same-directory rename.
> +
> +Then, consider the most complex case in a cross-directory rename, where the
> +most locks need to be acquired. The rename may perform the following
> +nested locking (only show lock statements for convenience). Here, dir1
> +and dir2 are the two parents and according to the locking rules, we
> +know dir2 is not ancestor to dir1. child1 and child2 are source and
> +target in a order according to the rules above and may not necessarily
> +correspond to the child of dir1 and dir2. Ignore things in angle
> +brackets for now. The locking order here can not be predetermined. For
> +instance, rename(/a/x,/b/y) and rename(/b/y,/a/x) would acquire /a and
> +/b in different order because neither parent is an ancestor of the
> +other and we lock the parent of source first. But the observation is
> +that after acquiring the filesystem lock, the precise locking order
> +becomes known. We introduce a ghost state edge to help define the
> +ranks (ghost state is a commonly used technique in verification which
> +has no influence on the program execution and its only role is to
> +assist the proof).
> +
> +  1. lock filesystem (->s_vfs_rename_mutex);
> +  2. lock dir1->i_rwsem; <set edge to (dir1, dir2)>
> +  3. lock dir2->i_rwsem; <set edge to (non-parent, child1) if child1 is
> +     a directory and non-parent is the one in dir1 and dir2 that is not
> +     parent to child1, otherwise, set edge to None>
> +  4. lock child1->i_rwsem; <set edge to (child1, child2) if chil1 and
> +     child2 are directories, otherwise, set edge to None>
> +  5. lock child2->i_rwsem; <set edge to None>
> +
> +The value of edge is either None or (inode1,inode2)
> +representing a possible lock dependency from inode1 to inode2, i.e.,
> +some thread may request lock of inode2 with lock of inode1 held. There
> +is only ever one edge value in a filesystem with edge set to None when
> +the s_vfs_rename_mutex is not held and edge set to a determined value
> +when s_vfs_rename_mutex is held. When edge is None, the ranking rules
> +are the same as before (root is 0 and child is one plus its parent).
> +When edge is (inode1, inode2), only the ranking rule for inode2
> +changes: inode2's rank is one plus the larger rank of inode2's parent
> +and inode1. Intuitively, a directory's rank is the longest distance
> +from root in the filesystem tree extended with edge.
> +
> +Now let's check the setting of edge in angle brackets in above code to
> +see how it ensures cross-directory rename takes locks in increasing
> +order in this case. After the lock statement at line 2, edge is set to
> +(dir1,dir2). Because dir2 is not ancestor to dir1, the rank of dir1
> +stays the same and the rank of dir2 (as well as every other
> +directories) can be uniquely determined. So at line 3, dir2's rank is
> +higher than dir1's. Before the code acquires child1 at line 4, it
> +already holds dir1 and dir2. We know one of dir1 and dir2 is parent to
> +child1, so we update edge to (the non-parent of child1, child1).
> +Because child1 cannot be ancestor to dir1 or dir2, every directory's
> +rank is still uniquely defined (for the same reason as above). So at
> +line 4, we ensure child1 has higher rank than dir1 and dir2. Similarly,
> +we update edge to (child1, child2) so that at line 5, child2 has higher
> +rank than held locks (dir1, dir2 and child1). We reset edge to None
> +whenever we have acquired all directory locks.
> +
> +In a less complex case of cross-directory rename, the code may execute
> +a prefix of above lines and we always reset edge to None after having
> +acquired all directory locks.
> +
> +The ranking of directories may change under following cases:
> +
> +  * directory removal/creation: a directory's rank is removed/added
> +  * rename: the effect of rename is either (1) moving a subtree under a
> +    node that is outside of the subtree (the directories in the subtree are
> +    all recalculated based on the node whose rank is not influenced)
> +    and potentially removing target (a directory's rank is removed) or
> +    (2) in case of RENAME_EXCHANGE, two subtrees exchange their
> +    positions. Because source is not a descendent of the target and
> +    target is not a descendent of source, the parents of the two
> +    subtrees cannot not be in these subtrees so the ranks of parents
> +    are not influenced. The two subtrees' ranks are recalculated based
> +    on their parents
> +  * edge updates in a cross-directory rename: setting edge to
> +    (inode1,inode2) or resetting edge to None modify the ranks of the
> +    inode2 subtree
> +
> +Initially in a filesystem, no directory is its own ancestor and
> +directories' ranks can be uniquely determined. All these changes
> +preserve the fact that at every time, the ranking of every directory
> +can be uniquely determined. Thus no directory is its own ancestor
> +(because if a directory is its own ancestor, its rank cannot be
> +determined) and we prove the premise. Plus the fact that operations
> +always take locks in order of increasing rank (can be easily verified).
> +We can conclude the locking rules above are deadlock-free.
> +
>
>  Note that the check for having a common ancestor in cross-directory
>  rename is crucial - without it a deadlock would be possible.  Indeed,
> @@ -247,33 +276,6 @@ distant ancestor.  Add a bunch of rmdir() attempts on all directories
>  in between (all of those would fail with -ENOTEMPTY, had they ever gotten
>  the locks) and voila - we have a deadlock.
>
> -Loop avoidance
> -==============
> -
> -These operations are guaranteed to avoid loop creation.  Indeed,
> -the only operation that could introduce loops is cross-directory rename.
> -Suppose after the operation there is a loop; since there hadn't been such
> -loops before the operation, at least on of the nodes in that loop must've
> -had its parent changed.  In other words, the loop must be passing through
> -the source or, in case of exchange, possibly the target.
> -
> -Since the operation has succeeded, neither source nor target could have
> -been ancestors of each other.  Therefore the chain of ancestors starting
> -in the parent of source could not have passed through the target and
> -vice versa.  On the other hand, the chain of ancestors of any node could
> -not have passed through the node itself, or we would've had a loop before
> -the operation.  But everything other than source and target has kept
> -the parent after the operation, so the operation does not change the
> -chains of ancestors of (ex-)parents of source and target.  In particular,
> -those chains must end after a finite number of steps.
> -
> -Now consider the loop created by the operation.  It passes through either
> -source or target; the next node in the loop would be the ex-parent of
> -target or source resp.  After that the loop would follow the chain of
> -ancestors of that parent.  But as we have just shown, that chain must
> -end after a finite number of steps, which means that it can't be a part
> -of any loop.  Q.E.D.
> -
>  While this locking scheme works for arbitrary DAGs, it relies on
>  ability to check that directory is a descendent of another object.  Current
>  implementation assumes that directory graph is a tree.  This assumption is
> --
> 2.30.1 (Apple Git-130)
>





[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