summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiklos Szeredi <miklos@szeredi.hu>2013-09-05 14:39:11 +0200
committerAl Viro <viro@zeniv.linux.org.uk>2013-09-05 16:23:50 -0400
commiteed810076685c77dc9a8c5c3593e641c93caed1c (patch)
tree5aba507073b0a7e29dfa0739272dfeda16eb5e29
parent848ac114e847af3f1f9141c90a39ebe79bdb13b3 (diff)
vfs: check unlinked ancestors before mount
We check submounts before doing d_drop() on a non-empty directory dentry in NFS (have_submounts()), but we do not exclude a racing mount. Nor do we prevent mounts to be added to the disconnected subtree using relative paths after the d_drop(). This patch fixes these issues by checking for unlinked (unhashed, non-root) ancestors before proceeding with the mount. This is done with rename seqlock taken for write and with ->d_lock grabbed on each ancestor in turn, including our dentry itself. This ensures that the only one of check_submounts_and_drop() or has_unlinked_ancestor() can succeed. Signed-off-by: Miklos Szeredi <miklos@szeredi.hu> Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
-rw-r--r--fs/dcache.c33
-rw-r--r--fs/internal.h1
-rw-r--r--fs/namespace.c11
3 files changed, 39 insertions, 6 deletions
diff --git a/fs/dcache.c b/fs/dcache.c
index ce5a7e6d84c..761e31bacbc 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -1184,6 +1184,39 @@ int have_submounts(struct dentry *parent)
EXPORT_SYMBOL(have_submounts);
/*
+ * Called by mount code to set a mountpoint and check if the mountpoint is
+ * reachable (e.g. NFS can unhash a directory dentry and then the complete
+ * subtree can become unreachable).
+ *
+ * Only one of check_submounts_and_drop() and d_set_mounted() must succeed. For
+ * this reason take rename_lock and d_lock on dentry and ancestors.
+ */
+int d_set_mounted(struct dentry *dentry)
+{
+ struct dentry *p;
+ int ret = -ENOENT;
+ write_seqlock(&rename_lock);
+ for (p = dentry->d_parent; !IS_ROOT(p); p = p->d_parent) {
+ /* Need exclusion wrt. check_submounts_and_drop() */
+ spin_lock(&p->d_lock);
+ if (unlikely(d_unhashed(p))) {
+ spin_unlock(&p->d_lock);
+ goto out;
+ }
+ spin_unlock(&p->d_lock);
+ }
+ spin_lock(&dentry->d_lock);
+ if (!d_unlinked(dentry)) {
+ dentry->d_flags |= DCACHE_MOUNTED;
+ ret = 0;
+ }
+ spin_unlock(&dentry->d_lock);
+out:
+ write_sequnlock(&rename_lock);
+ return ret;
+}
+
+/*
* Search the dentry child list of the specified parent,
* and move any unused dentries to the end of the unused
* list for prune_dcache(). We descend to the next level
diff --git a/fs/internal.h b/fs/internal.h
index 7c5f01cf619..d2089379552 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -126,6 +126,7 @@ extern int invalidate_inodes(struct super_block *, bool);
* dcache.c
*/
extern struct dentry *__d_alloc(struct super_block *, const struct qstr *);
+extern int d_set_mounted(struct dentry *dentry);
/*
* read_write.c
diff --git a/fs/namespace.c b/fs/namespace.c
index ad8ea9bc251..5997887cc64 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -611,6 +611,7 @@ static struct mountpoint *new_mountpoint(struct dentry *dentry)
{
struct list_head *chain = mountpoint_hashtable + hash(NULL, dentry);
struct mountpoint *mp;
+ int ret;
list_for_each_entry(mp, chain, m_hash) {
if (mp->m_dentry == dentry) {
@@ -626,14 +627,12 @@ static struct mountpoint *new_mountpoint(struct dentry *dentry)
if (!mp)
return ERR_PTR(-ENOMEM);
- spin_lock(&dentry->d_lock);
- if (d_unlinked(dentry)) {
- spin_unlock(&dentry->d_lock);
+ ret = d_set_mounted(dentry);
+ if (ret) {
kfree(mp);
- return ERR_PTR(-ENOENT);
+ return ERR_PTR(ret);
}
- dentry->d_flags |= DCACHE_MOUNTED;
- spin_unlock(&dentry->d_lock);
+
mp->m_dentry = dentry;
mp->m_count = 1;
list_add(&mp->m_hash, chain);