Re: [PATCH v2 03/10] blktrace: fix debugfs use after free

From: Christoph Hellwig
Date: Wed Apr 22 2020 - 03:27:38 EST


On Sun, Apr 19, 2020 at 07:45:22PM +0000, Luis Chamberlain wrote:
> On commit 6ac93117ab00 ("blktrace: use existing disk debugfs directory")
> merged on v4.12 Omar fixed the original blktrace code for request-based
> drivers (multiqueue). This however left in place a possible crash, if you
> happen to abuse blktrace in a way it was not intended, and even more so
> with our current asynchronous request_queue removal.
>
> Namely, if you loop adding a device, setup the blktrace with BLKTRACESETUP,
> forget to BLKTRACETEARDOWN, and then just remove the device you end up
> with a panic:

FYI, I find all this backtrace garbage not hepful at all. It requires
me to scroll for so long that I've forgot what was written above by
the time I'm past it.

> This splat happens to be very similar to the one reported via
> kernel.org korg#205713, only that korg#205713 was for v4.19.83
> and the above now includes the simple_recursive_removal() introduced
> via commit a3d1e7eb5abe ("simple_recursive_removal(): kernel-side rm
> -rf for ramfs-style filesystems") merged on v5.6.
>
> korg#205713 then was used to create CVE-2019-19770 and claims that
> the bug is in a use-after-free in the debugfs core code. The
> implications of this being a generic UAF on debugfs would be
> much more severe, as it would imply parent dentries can sometimes
> not be positive, which we hold by design is just not possible.
>
> Below is the splat explained with a bit more details, explaining
> what is happening in userspace, kernel, and a print of the CPU on,
> which the code runs on:
>
> load loopback module
> [ 13.603371] == blk_mq_debugfs_register(12) start
> [ 13.604040] == blk_mq_debugfs_register(12) q->debugfs_dir created

Same for this.. I think the real valuable changelog only stars below
this 'trace'.

> The root cause to this issue is that debugfs_lookup() can find a
> previous incarnation's dir of the same name which is about to get
> removed from a not yet schedule work. When that happens, the the files
> are taken underneath the nose of the blktrace, and when it comes time to
> cleanup, these dentries are long gone because of a scheduled removal.
>
> This issue is happening because of two reasons:
>
> 1) The request_queue is currently removed asynchronously as of commit
> dc9edc44de6c ("block: Fix a blk_exit_rl() regression") merged on
> v4.12, this allows races with userspace which were not possible
> before unless as removal of a block device used to happen
> synchronously with its request_queue. One could however still
> parallelize blksetup calls while one loops on device addition and
> removal.
>
> 2) There are no errors checks when we create the debugfs directory,
> be it on init or for blktrace. The concept of when the directory
> *should* really exist is further complicated by the fact that
> we have asynchronous request_queue removal. And, we have no
> real sanity checks to ensure we don't re-create the queue debugfs
> directory.
>
> We can fix the UAF by using a debugfs directory which moving forward
> will always be accessible if debugfs is enabled, this way, its allocated
> and avaialble always for both request-based block drivers or
> make_request drivers (multiqueue) block drivers.
>
> We also put sanity checks in place to ensure that if the directory is
> found with debugfs_lookup() it is the dentry we expect. When doing a
> blktrace against a parition, we will always be creating a temporary
> debugfs directory, so ensure that only exists once as well to avoid
> issues against concurrent blktrace runs.
>
> Lastly, since we are now always creating the needed request_queue
> debugfs directory upon init, we can also take the initiative to
> proactively check against errors. We currently do not check for
> errors on add_disk() and friends, but we shouldn't make the issue
> any worse.
>
> This also simplifies the code considerably, with the only penalty now
> being that we're always creating the request queue debugfs directory for
> the request-based block device drivers.
>
> The UAF then is not a core debugfs issue, but instead a complex misuse
> of debugfs, and this issue can only be triggered if you are root.
>
> This issue can be reproduced with break-blktrace [2] using:
>
> break-blktrace -c 10 -d -s
>
> This patch fixes this issue. Note that there is also another
> respective UAF but from the ioctl path [3], this should also fix
> that issue.
>
> This patch then also disputes the severity of CVE-2019-19770 as
> this issue is only possible by being root and using blktrace.
>
> It is not a core debugfs issue.
>
> [0] https://bugzilla.kernel.org/show_bug.cgi?id=205713
> [1] https://nvd.nist.gov/vuln/detail/CVE-2019-19770
> [2] https://github.com/mcgrof/break-blktrace
> [3] https://lore.kernel.org/lkml/000000000000ec635b059f752700@xxxxxxxxxx/

> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

This looks like an unrelated change.

> +
> #include <linux/kernel.h>
> #include <linux/blkdev.h>
> #include <linux/debugfs.h>
> @@ -13,3 +16,30 @@ void blk_debugfs_register(void)
> {
> blk_debugfs_root = debugfs_create_dir("block", NULL);
> }
> +
> +int __must_check blk_queue_debugfs_register(struct request_queue *q)

__must_check for a function with a single caller looks silly.

> +{
> + struct dentry *dir = NULL;
> +
> + /* This can happen if we have a bug in the lower layers */
> + dir = debugfs_lookup(kobject_name(q->kobj.parent), blk_debugfs_root);
> + if (dir) {
> + pr_warn("%s: registering request_queue debugfs directory twice is not allowed\n",
> + kobject_name(q->kobj.parent));
> + dput(dir);
> + return -EALREADY;
> + }

I don't see why we need this check. If it is valueable enough we
should have a debugfs_create_dir_exclusive or so that retunrns an error
for an exsting directory, instead of reimplementing it in the caller in
a racy way. But I'm not really sure we need it to start with.

> +
> + q->debugfs_dir = debugfs_create_dir(kobject_name(q->kobj.parent),
> + blk_debugfs_root);
> + if (!q->debugfs_dir)
> + return -ENOMEM;
> +
> + return 0;
> +}
> +
> +void blk_queue_debugfs_unregister(struct request_queue *q)
> +{
> + debugfs_remove_recursive(q->debugfs_dir);
> + q->debugfs_dir = NULL;
> +}

Which to me suggests we can just fold these two into the callers,
with an IS_ENABLED for the creation case given that we check for errors
and the stub will always return an error.

> debugfs_create_files(q->debugfs_dir, q, blk_mq_debugfs_queue_attrs);
>
> /*
> @@ -856,9 +853,7 @@ void blk_mq_debugfs_register(struct request_queue *q)
>
> void blk_mq_debugfs_unregister(struct request_queue *q)
> {
> - debugfs_remove_recursive(q->debugfs_dir);
> q->sched_debugfs_dir = NULL;
> - q->debugfs_dir = NULL;
> }

This function is weird - the sched dir gets removed by the
debugfs_remove_recursive, so just leaving a function that clears
a pointer is rather odd. In fact I don't think we need to clear
either sched_debugfs_dir or debugfs_dir anywhere.

>
> @@ -975,6 +976,14 @@ int blk_register_queue(struct gendisk *disk)
> goto unlock;
> }
>
> + ret = blk_queue_debugfs_register(q);
> + if (ret) {
> + blk_trace_remove_sysfs(dev);
> + kobject_del(&q->kobj);
> + kobject_put(&dev->kobj);
> + goto unlock;
> + }
> +

Please use a goto label to consolidate the common cleanup code.

Also I think these generic debugfs changes probably should be separate
to the blktrace changes.

> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> #include <linux/kernel.h>
> #include <linux/blkdev.h>
> #include <linux/blktrace_api.h>
> @@ -311,7 +314,15 @@ static void blk_trace_free(struct blk_trace *bt)
> debugfs_remove(bt->msg_file);
> debugfs_remove(bt->dropped_file);
> relay_close(bt->rchan);
> - debugfs_remove(bt->dir);
> + /*
> + * backing_dir is set when we use the request_queue debugfs directory.
> + * Otherwise we are using a temporary directory created only for the
> + * purpose of blktrace.
> + */
> + if (bt->backing_dir)
> + dput(bt->backing_dir);
> + else
> + debugfs_remove(bt->dir);
> free_percpu(bt->sequence);
> free_percpu(bt->msg_data);
> kfree(bt);
> @@ -468,16 +479,89 @@ static void blk_trace_setup_lba(struct blk_trace *bt,
> }
> }
>
> +static bool blk_trace_target_disk(const char *target, const char *diskname)
> +{
> + if (strlen(target) != strlen(diskname))
> + return false;
> +
> + if (!strncmp(target, diskname,
> + min_t(size_t, strlen(target), strlen(diskname))))
> + return true;
> +
> + return false;
> +}
> +
> static struct dentry *blk_trace_debugfs_dir(struct blk_user_trace_setup *buts,
> + struct request_queue *q,
> struct blk_trace *bt)
> {
> struct dentry *dir = NULL;
>
> + /* This can only happen if we have a bug on our lower layers */
> + if (!q->kobj.parent) {
> + pr_warn("%s: request_queue parent is gone\n", buts->name);
> + return NULL;
> + }

Why is this not simply a WARN_ON_ONCE()?

> + /*
> + * From a sysfs kobject perspective, the request_queue sits on top of
> + * the gendisk, which has the name of the disk. We always create a
> + * debugfs directory upon init for this gendisk kobject, so we re-use
> + * that if blktrace is going to be done for it.
> + */

-EPARSE.

> + if (blk_trace_target_disk(buts->name, kobject_name(q->kobj.parent))) {
> + if (!q->debugfs_dir) {
> + pr_warn("%s: expected request_queue debugfs_dir is not set\n",
> + buts->name);
> + return NULL;
> + }
> + /*
> + * debugfs_lookup() is used to ensure the directory is not
> + * taken from underneath us. We must dput() it later once
> + * done with it within blktrace.
> + */
> + dir = debugfs_lookup(buts->name, blk_debugfs_root);
> + if (!dir) {
> + pr_warn("%s: expected request_queue debugfs_dir dentry is gone\n",
> + buts->name);
> + return NULL;
> + }
> + /*
> + * This is a reaffirmation that debugfs_lookup() shall always
> + * return the same dentry if it was already set.
> + */
> + if (dir != q->debugfs_dir) {
> + dput(dir);
> + pr_warn("%s: expected dentry dir != q->debugfs_dir\n",
> + buts->name);
> + return NULL;
> + }
> + bt->backing_dir = q->debugfs_dir;
> + return bt->backing_dir;
> + }

Even with the gigantic commit log I don't get the point of this
code. It looks rather sketchy and I can't find a rationale for it.