diff options
author | NeilBrown <neilb@suse.de> | 2006-10-04 05:16:16 -0400 |
---|---|---|
committer | Linus Torvalds <torvalds@g5.osdl.org> | 2006-10-04 10:55:21 -0400 |
commit | 21c0d8fdd95024ffa708a938099148b8f1078d46 (patch) | |
tree | e812554e916f1fb7d9410640235fb7954d2a25b7 /fs | |
parent | 44c556000a31e8079cfbb9a42a7edb93ca6b589a (diff) |
[PATCH] knfsd: close a race-opportunity in d_splice_alias
There is a possible race in d_splice_alias. Though __d_find_alias(inode, 1)
will only return a dentry with DCACHE_DISCONNECTED set, it is possible for it
to get cleared before the BUG_ON, and it is is not possible to lock against
that.
There are a couple of problems here. Firstly, the code doesn't match the
comment. The comment describes a 'disconnected' dentry as being IS_ROOT as
well as DCACHE_DISCONNECTED, however there is not testing of IS_ROOT anythere.
A dentry is marked DCACHE_DISCONNECTED when allocated with d_alloc_anon, and
remains DCACHE_DISCONNECTED while a path is built up towards the root. So a
dentry can have a valid name and a valid parent and even grandparent, but will
still be DCACHE_DISCONNECTED until a path to the root is created. Once the
path to the root is complete, everything in the path gets DCACHE_DISCONNECTED
cleared. So the fact that DCACHE_DISCONNECTED isn't enough to say that a
dentry is free to be spliced in with a given name. This can only be allowed
if the dentry does not yet have a name, so the IS_ROOT test is needed too.
However even adding that test to __d_find_alias isn't enough. As
d_splice_alias drops dcache_lock before calling d_move to perform the splice,
it could race with another thread calling d_splice_alias to splice the inode
in with a different name in a different part of the tree (in the case where a
file has hard links). So that splicing code is only really safe for
directories (as we know that directories only have one link). For
directories, the caller of d_splice_alias will be holding i_mutex on the
(unique) parent so there is no room for a race.
A consequence of this is that a non-directory will never benefit from being
spliced into a pre-exisiting dentry, but that isn't a problem. It is
perfectly OK for a non-directory to have multiple dentries, some anonymous,
some not. And the comment for d_splice_alias says that it only happens for
directories anyway.
Signed-off-by: Neil Brown <neilb@suse.de>
Cc: Christoph Hellwig <hch@lst.de>
Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Dipankar Sarma <dipankar@in.ibm.com>
Signed-off-by: Andrew Morton <akpm@osdl.org>
Signed-off-by: Linus Torvalds <torvalds@osdl.org>
Diffstat (limited to 'fs')
-rw-r--r-- | fs/dcache.c | 9 |
1 files changed, 5 insertions, 4 deletions
diff --git a/fs/dcache.c b/fs/dcache.c index fc2faa44f8d1..2355bddad8de 100644 --- a/fs/dcache.c +++ b/fs/dcache.c | |||
@@ -291,9 +291,9 @@ struct dentry * dget_locked(struct dentry *dentry) | |||
291 | * it can be unhashed only if it has no children, or if it is the root | 291 | * it can be unhashed only if it has no children, or if it is the root |
292 | * of a filesystem. | 292 | * of a filesystem. |
293 | * | 293 | * |
294 | * If the inode has a DCACHE_DISCONNECTED alias, then prefer | 294 | * If the inode has an IS_ROOT, DCACHE_DISCONNECTED alias, then prefer |
295 | * any other hashed alias over that one unless @want_discon is set, | 295 | * any other hashed alias over that one unless @want_discon is set, |
296 | * in which case only return a DCACHE_DISCONNECTED alias. | 296 | * in which case only return an IS_ROOT, DCACHE_DISCONNECTED alias. |
297 | */ | 297 | */ |
298 | 298 | ||
299 | static struct dentry * __d_find_alias(struct inode *inode, int want_discon) | 299 | static struct dentry * __d_find_alias(struct inode *inode, int want_discon) |
@@ -309,7 +309,8 @@ static struct dentry * __d_find_alias(struct inode *inode, int want_discon) | |||
309 | prefetch(next); | 309 | prefetch(next); |
310 | alias = list_entry(tmp, struct dentry, d_alias); | 310 | alias = list_entry(tmp, struct dentry, d_alias); |
311 | if (S_ISDIR(inode->i_mode) || !d_unhashed(alias)) { | 311 | if (S_ISDIR(inode->i_mode) || !d_unhashed(alias)) { |
312 | if (alias->d_flags & DCACHE_DISCONNECTED) | 312 | if (IS_ROOT(alias) && |
313 | (alias->d_flags & DCACHE_DISCONNECTED)) | ||
313 | discon_alias = alias; | 314 | discon_alias = alias; |
314 | else if (!want_discon) { | 315 | else if (!want_discon) { |
315 | __dget_locked(alias); | 316 | __dget_locked(alias); |
@@ -1004,7 +1005,7 @@ struct dentry *d_splice_alias(struct inode *inode, struct dentry *dentry) | |||
1004 | { | 1005 | { |
1005 | struct dentry *new = NULL; | 1006 | struct dentry *new = NULL; |
1006 | 1007 | ||
1007 | if (inode) { | 1008 | if (inode && S_ISDIR(inode->i_mode)) { |
1008 | spin_lock(&dcache_lock); | 1009 | spin_lock(&dcache_lock); |
1009 | new = __d_find_alias(inode, 1); | 1010 | new = __d_find_alias(inode, 1); |
1010 | if (new) { | 1011 | if (new) { |