Re: [PATCH v4 1/3] rpmb: core: Guard frame requests and teardown with mutex
From: Bean Huo
Date: Sun Sep 13 2026 - 13:48:13 EST
On Sun, 2026-09-13 at 11:36 +0800, Stanley Jhu wrote:
> rpmb_route_frames() has no serialisation. That has two independent
> consequences.
>
> Concurrent requests on one rpmb_dev corrupt each other. Two kthreads on
> different CPUs issuing RPMB_GET_WRITE_COUNTER against the same UFS RPMB
> region, 20000 iterations each:
>
> thread 0 done: 20000 iterations, mismatches=10889, errors=0
> thread 1 done: 20000 iterations, mismatches=6141, errors=0
> race complete: MISMATCH=17030 ERRORS=0
>
> A mismatch is a response whose echoed nonce belongs to the other thread,
> so 43% of requests returned somebody else's frame. The errors count is
> transport failures, so nothing failed and the corruption is silent to
> the caller. An authenticated RPMB operation is not one command. The UFS
> provider issues SECURITY PROTOCOL OUT carrying the request, then
> SECURITY PROTOCOL IN to collect the response, with a result read request
> in between for write-type operations. JESD220F 12.4.7 states that any
> request other than a result read overwrites the result register of the
> region, so a request landing in the middle of another initiator's
> sequence destroys its response. The same chapter states that a region
> processes one authenticated operation at a time, so the rpmb_dev is the
> granularity the device itself assumes.
>
> A request can also still be in flight when the provider tears down. The
> core reaches the provider through rdev->dev.parent, and the provider is
> free to release that device as soon as rpmb_dev_unregister() returns.
> Reference counting on the rpmb_dev does not prevent this:
> rpmb_dev_unregister() calls device_del(), which drops the reference that
> device_add() took on the parent, so the parent can be freed while the
> rpmb_dev is still alive and still routable. Unbinding a UFS host while a
> consumer holds an rpmb_dev reference and keeps issuing requests:
>
> BUG: KASAN: slab-use-after-free in ufs_rpmb_route_frames+0x328/0x420
> Read of size 8 at addr fff00000c833bce8 by task rpmb_hold/100
> Call trace:
> ufs_rpmb_route_frames+0x328/0x420
> rpmb_route_frames+0x64/0xd0
> Freed by task 1:
> kfree+0x2b8/0x5c4
> ufs_rpmb_device_release+0x3c/0x60
> device_release+0xa0/0x1fc
> device_unregister+0x20/0x38
> ufs_rpmb_remove+0x130/0x230
> ufshcd_remove+0x54/0x22c
>
> Both measurements needed patches 2/3 and 3/3 of this series applied,
> because UFS RPMB registration fails on mainline. The stable tag is for
> eMMC, which registers today. mmc_route_rpmb_frames() packs the whole
> sequence into one block request, so eMMC is not exposed to the
> interleaving above, but it does have the teardown window:
> mmc_blk_remove() reaches rpmb_dev_unregister() through
> mmc_blk_remove_parts() near its start,while the queue that
> mmc_route_rpmb_frames() submits to is only torn down at the end by
> mmc_blk_remove_req(), whose comment notes that it is freeing the queue
> that stops new requests being accepted. The eMMC window is from source
> reading; I have not reproduced it.
>
> The kerneldoc change is part of the fix. Calling rpmb_dev_unregister()
> from a release callback cannot work, because the child rpmb_dev holds a
> reference on its parent, so the parent's release callback never runs.
> The UFS provider does exactly that today, inert only because its
> registration fails; patch 2/3 moves the call to the remove path.
>
> Add a mutex and a dead flag to struct rpmb_dev. rpmb_route_frames()
> holds the mutex across the whole sequence and returns -ENODEV once the
> flag is set. rpmb_dev_unregister() sets the flag under the mutex before
> device_del(), so it cannot return while a request is inside the
> provider. Closing the teardown window requires excluding unregistration
> for the whole duration of a request, which is the same exclusion that
> serialises two requests, so one mutex covers both.
>
> On the same test with this patch applied, MISMATCH=0 and the unbind is
> clean.
>
Hi Stanley,
The fix looks right to me, the commit message is too long, it is easier to read
the code changes than the message.
> Fixes: 1e9046e3a154 ("rpmb: add Replay Protected Memory Block (RPMB)
> subsystem")
> Cc: stable@xxxxxxxxxxxxxxx
> Signed-off-by: Stanley Jhu <stanleyjhu@xxxxxxxxxx>
> ---
> drivers/misc/rpmb-core.c | 34 +++++++++++++++++++++++++++++-----
> include/linux/rpmb.h | 6 ++++++
> 2 files changed, 35 insertions(+), 5 deletions(-)
>
> diff --git a/drivers/misc/rpmb-core.c b/drivers/misc/rpmb-core.c
> index ecf14acf230a..bbc3c404ad6f 100644
> --- a/drivers/misc/rpmb-core.c
> +++ b/drivers/misc/rpmb-core.c
> @@ -45,16 +45,28 @@ EXPORT_SYMBOL_GPL(rpmb_dev_put);
> * @rsp: rpmb response frames
> * @rsp_len: length of rpmb response frames in bytes
> *
> + * Context: Might sleep.
> + *
> * Returns: < 0 on failure
> */
> int rpmb_route_frames(struct rpmb_dev *rdev, u8 *req,
> unsigned int req_len, u8 *rsp, unsigned int rsp_len)
> {
> - if (!req || !req_len || !rsp || !rsp_len)
> + int ret;
> +
> + if (!rdev || !req || !req_len || !rsp || !rsp_len)
> return -EINVAL;
>
> - return rdev->descr.route_frames(rdev->dev.parent, req, req_len,
> - rsp, rsp_len);
> + mutex_lock(&rdev->lock);
> + if (rdev->dead) {
> + I (&rdev->lock);
> + return -ENODEV;
> + }
> +
> + ret = rdev->descr.route_frames(rdev->dev.parent, req, req_len,
> + rsp, rsp_len);
> + mutex_unlock(&rdev->lock);
> + return ret;
> }
> EXPORT_SYMBOL_GPL(rpmb_route_frames);
>
> @@ -62,6 +74,7 @@ static void rpmb_dev_release(struct device *dev)
> {
> struct rpmb_dev *rdev = to_rpmb_dev(dev);
>
> + mutex_destroy(&rdev->lock);
> ida_free(&rpmb_ida, rdev->id);
> kfree(rdev->descr.dev_id);
> kfree(rdev);
> @@ -123,8 +136,9 @@ EXPORT_SYMBOL_GPL(rpmb_interface_unregister);
> * rpmb_dev_unregister() - unregister RPMB partition from the RPMB subsystem
> * @rdev: the rpmb device to unregister
> *
> - * This function should be called from the release function of the
> - * underlying device used when the RPMB device was registered.
> + * This function should be called from the remove or unbind callback of the
> + * underlying device used when the RPMB device was registered, never from
> + * a device release callback.
> *
> * Returns: < 0 on failure
> */
> @@ -133,6 +147,14 @@ int rpmb_dev_unregister(struct rpmb_dev *rdev)
> if (!rdev)
> return -EINVAL;
>
> + mutex_lock(&rdev->lock);
> + if (rdev->dead) {
> + mutex_unlock(&rdev->lock);
> + return 0;
> + }
> + rdev->dead = true;
> + mutex_unlock(&rdev->lock);
> +
> device_del(&rdev->dev);
>
> rpmb_dev_put(rdev);
> @@ -164,6 +186,7 @@ struct rpmb_dev *rpmb_dev_register(struct device *dev,
> rdev = kzalloc_obj(*rdev);
> if (!rdev)
> return ERR_PTR(-ENOMEM);
> + mutex_init(&rdev->lock);
> rdev->descr = *descr;
> rdev->descr.dev_id = kmemdup(descr->dev_id, descr->dev_id_len,
> GFP_KERNEL);
> @@ -194,6 +217,7 @@ struct rpmb_dev *rpmb_dev_register(struct device *dev,
> err_free_dev_id:
> kfree(rdev->descr.dev_id);
> err_free_rdev:
> + mutex_destroy(&rdev->lock);
> kfree(rdev);
> return ERR_PTR(ret);
> }
> diff --git a/include/linux/rpmb.h b/include/linux/rpmb.h
> index ed3f8e431eff..814ac3e69337 100644
> --- a/include/linux/rpmb.h
> +++ b/include/linux/rpmb.h
> @@ -7,6 +7,7 @@
> #define __RPMB_H__
>
> #include <linux/device.h>
> +#include <linux/mutex.h>
> #include <linux/types.h>
>
> /**
> @@ -48,15 +49,20 @@ struct rpmb_descr {
> * struct rpmb_dev - device which can support RPMB partition
> *
> * @dev : device
> + * @lock : protects in-flight operations against teardown
> * @id : device_id
> * @list_node : linked list node
> * @descr : RPMB description
> + * @dead : set to true when device is unregistered
> */
> struct rpmb_dev {
> struct device dev;
> + /* Protects in-flight operations against teardown */
> + struct mutex lock;
This lock also makes route_frames calls run one at a time, not only protect
against teardown.
> int id;
> struct list_head list_node;
> struct rpmb_descr descr;
> + bool dead;
> };
>
> #define to_rpmb_dev(x) container_of((x), struct rpmb_dev, dev)
Both are nits, feel free to add:
Reviewed-by: Bean Huo <beanhuo@xxxxxxxxxx>
Kind regards,
Bean