diff options
| author | Nicolai Stange <nicstange@gmail.com> | 2017-10-30 19:15:48 -0400 |
|---|---|---|
| committer | Greg Kroah-Hartman <gregkh@linuxfoundation.org> | 2017-11-07 14:25:02 -0500 |
| commit | e9117a5a4bf65d8e99f060d356a04d27a60b436d (patch) | |
| tree | bd7ad76ed4c2f538f13ab38afb0dda9478ad044e /fs/debugfs | |
| parent | 7c8d469877b16d2c1cecf101a0abb7b218db85bc (diff) | |
debugfs: implement per-file removal protection
Since commit 49d200deaa68 ("debugfs: prevent access to removed files'
private data"), accesses to a file's private data are protected from
concurrent removal by covering all file_operations with a SRCU read section
and sychronizing with those before returning from debugfs_remove() by means
of synchronize_srcu().
As pointed out by Johannes Berg, there are debugfs files with forever
blocking file_operations. Their corresponding SRCU read side sections would
block any debugfs_remove() forever as well, even unrelated ones. This
results in a livelock. Because a remover can't cancel any indefinite
blocking within foreign files, this is a problem.
Resolve this by introducing support for more granular protection on a
per-file basis.
This is implemented by introducing an 'active_users' refcount_t to the
per-file struct debugfs_fsdata state. At file creation time, it is set to
one and a debugfs_remove() will drop that initial reference. The new
debugfs_file_get() and debugfs_file_put(), intended to be used in place of
former debugfs_use_file_start() and debugfs_use_file_finish(), increment
and decrement it respectively. Once the count drops to zero,
debugfs_file_put() will signal a completion which is possibly being waited
for from debugfs_remove().
Thus, as long as there is a debugfs_file_get() not yet matched by a
corresponding debugfs_file_put() around, debugfs_remove() will block.
Actual users of debugfs_use_file_start() and -finish() will get converted
to the new debugfs_file_get() and debugfs_file_put() by followup patches.
Fixes: 49d200deaa68 ("debugfs: prevent access to removed files' private data")
Reported-by: Johannes Berg <johannes@sipsolutions.net>
Signed-off-by: Nicolai Stange <nicstange@gmail.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Diffstat (limited to 'fs/debugfs')
| -rw-r--r-- | fs/debugfs/file.c | 48 | ||||
| -rw-r--r-- | fs/debugfs/inode.c | 29 | ||||
| -rw-r--r-- | fs/debugfs/internal.h | 2 |
3 files changed, 73 insertions, 6 deletions
diff --git a/fs/debugfs/file.c b/fs/debugfs/file.c index b6f5ddab66bf..6644bfdea2f8 100644 --- a/fs/debugfs/file.c +++ b/fs/debugfs/file.c | |||
| @@ -109,6 +109,54 @@ const struct file_operations *debugfs_real_fops(const struct file *filp) | |||
| 109 | } | 109 | } |
| 110 | EXPORT_SYMBOL_GPL(debugfs_real_fops); | 110 | EXPORT_SYMBOL_GPL(debugfs_real_fops); |
| 111 | 111 | ||
| 112 | /** | ||
| 113 | * debugfs_file_get - mark the beginning of file data access | ||
| 114 | * @dentry: the dentry object whose data is being accessed. | ||
| 115 | * | ||
| 116 | * Up to a matching call to debugfs_file_put(), any successive call | ||
| 117 | * into the file removing functions debugfs_remove() and | ||
| 118 | * debugfs_remove_recursive() will block. Since associated private | ||
| 119 | * file data may only get freed after a successful return of any of | ||
| 120 | * the removal functions, you may safely access it after a successful | ||
| 121 | * call to debugfs_file_get() without worrying about lifetime issues. | ||
| 122 | * | ||
| 123 | * If -%EIO is returned, the file has already been removed and thus, | ||
| 124 | * it is not safe to access any of its data. If, on the other hand, | ||
| 125 | * it is allowed to access the file data, zero is returned. | ||
| 126 | */ | ||
| 127 | int debugfs_file_get(struct dentry *dentry) | ||
| 128 | { | ||
| 129 | struct debugfs_fsdata *fsd = dentry->d_fsdata; | ||
| 130 | |||
| 131 | /* Avoid starvation of removers. */ | ||
| 132 | if (d_unlinked(dentry)) | ||
| 133 | return -EIO; | ||
| 134 | |||
| 135 | if (!refcount_inc_not_zero(&fsd->active_users)) | ||
| 136 | return -EIO; | ||
| 137 | |||
| 138 | return 0; | ||
| 139 | } | ||
| 140 | EXPORT_SYMBOL_GPL(debugfs_file_get); | ||
| 141 | |||
| 142 | /** | ||
| 143 | * debugfs_file_put - mark the end of file data access | ||
| 144 | * @dentry: the dentry object formerly passed to | ||
| 145 | * debugfs_file_get(). | ||
| 146 | * | ||
| 147 | * Allow any ongoing concurrent call into debugfs_remove() or | ||
| 148 | * debugfs_remove_recursive() blocked by a former call to | ||
| 149 | * debugfs_file_get() to proceed and return to its caller. | ||
| 150 | */ | ||
| 151 | void debugfs_file_put(struct dentry *dentry) | ||
| 152 | { | ||
| 153 | struct debugfs_fsdata *fsd = dentry->d_fsdata; | ||
| 154 | |||
| 155 | if (refcount_dec_and_test(&fsd->active_users)) | ||
| 156 | complete(&fsd->active_users_drained); | ||
| 157 | } | ||
| 158 | EXPORT_SYMBOL_GPL(debugfs_file_put); | ||
| 159 | |||
| 112 | static int open_proxy_open(struct inode *inode, struct file *filp) | 160 | static int open_proxy_open(struct inode *inode, struct file *filp) |
| 113 | { | 161 | { |
| 114 | const struct dentry *dentry = F_DENTRY(filp); | 162 | const struct dentry *dentry = F_DENTRY(filp); |
diff --git a/fs/debugfs/inode.c b/fs/debugfs/inode.c index a9c3d3e9af39..6449eb935540 100644 --- a/fs/debugfs/inode.c +++ b/fs/debugfs/inode.c | |||
| @@ -374,6 +374,7 @@ static struct dentry *__debugfs_create_file(const char *name, umode_t mode, | |||
| 374 | 374 | ||
| 375 | inode->i_fop = proxy_fops; | 375 | inode->i_fop = proxy_fops; |
| 376 | fsd->real_fops = real_fops; | 376 | fsd->real_fops = real_fops; |
| 377 | refcount_set(&fsd->active_users, 1); | ||
| 377 | dentry->d_fsdata = fsd; | 378 | dentry->d_fsdata = fsd; |
| 378 | 379 | ||
| 379 | d_instantiate(dentry, inode); | 380 | d_instantiate(dentry, inode); |
| @@ -631,18 +632,34 @@ struct dentry *debugfs_create_symlink(const char *name, struct dentry *parent, | |||
| 631 | } | 632 | } |
| 632 | EXPORT_SYMBOL_GPL(debugfs_create_symlink); | 633 | EXPORT_SYMBOL_GPL(debugfs_create_symlink); |
| 633 | 634 | ||
| 635 | static void __debugfs_remove_file(struct dentry *dentry, struct dentry *parent) | ||
| 636 | { | ||
| 637 | struct debugfs_fsdata *fsd; | ||
| 638 | |||
| 639 | simple_unlink(d_inode(parent), dentry); | ||
| 640 | d_delete(dentry); | ||
| 641 | fsd = dentry->d_fsdata; | ||
| 642 | init_completion(&fsd->active_users_drained); | ||
| 643 | if (!refcount_dec_and_test(&fsd->active_users)) | ||
| 644 | wait_for_completion(&fsd->active_users_drained); | ||
| 645 | } | ||
| 646 | |||
| 634 | static int __debugfs_remove(struct dentry *dentry, struct dentry *parent) | 647 | static int __debugfs_remove(struct dentry *dentry, struct dentry *parent) |
| 635 | { | 648 | { |
| 636 | int ret = 0; | 649 | int ret = 0; |
| 637 | 650 | ||
| 638 | if (simple_positive(dentry)) { | 651 | if (simple_positive(dentry)) { |
| 639 | dget(dentry); | 652 | dget(dentry); |
| 640 | if (d_is_dir(dentry)) | 653 | if (!d_is_reg(dentry)) { |
| 641 | ret = simple_rmdir(d_inode(parent), dentry); | 654 | if (d_is_dir(dentry)) |
| 642 | else | 655 | ret = simple_rmdir(d_inode(parent), dentry); |
| 643 | simple_unlink(d_inode(parent), dentry); | 656 | else |
| 644 | if (!ret) | 657 | simple_unlink(d_inode(parent), dentry); |
| 645 | d_delete(dentry); | 658 | if (!ret) |
| 659 | d_delete(dentry); | ||
| 660 | } else { | ||
| 661 | __debugfs_remove_file(dentry, parent); | ||
| 662 | } | ||
| 646 | dput(dentry); | 663 | dput(dentry); |
| 647 | } | 664 | } |
| 648 | return ret; | 665 | return ret; |
diff --git a/fs/debugfs/internal.h b/fs/debugfs/internal.h index 512601eed3ce..0eea99432840 100644 --- a/fs/debugfs/internal.h +++ b/fs/debugfs/internal.h | |||
| @@ -21,6 +21,8 @@ extern const struct file_operations debugfs_full_proxy_file_operations; | |||
| 21 | 21 | ||
| 22 | struct debugfs_fsdata { | 22 | struct debugfs_fsdata { |
| 23 | const struct file_operations *real_fops; | 23 | const struct file_operations *real_fops; |
| 24 | refcount_t active_users; | ||
| 25 | struct completion active_users_drained; | ||
| 24 | }; | 26 | }; |
| 25 | 27 | ||
| 26 | #endif /* _DEBUGFS_INTERNAL_H_ */ | 28 | #endif /* _DEBUGFS_INTERNAL_H_ */ |
