Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH 0/2] HID: bpf: close device teardown races
@ 2026-11-02 11:58 Yifan Zhang
  2026-11-02 11:58 ` [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction Yifan Zhang
  2026-11-02 11:58 ` [PATCH 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
  0 siblings, 2 replies; 6+ messages in thread
From: Yifan Zhang @ 2026-11-02 11:58 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel

HID-BPF can race device destruction in two paths.  A hardware request can
outlive transport-private state, and a struct_ops registration can resume
after destruction has cleaned the device's SRCU state.

Serialize HID-BPF hardware operations with the destruction transition, then
reject registrations that acquire prog_list_lock after that transition.

Yifan Zhang (2):
  HID: bpf: serialize hardware operations with device destruction
  HID: bpf: reject struct_ops registration during device destruction

 drivers/hid/bpf/hid_bpf_dispatch.c   | 36 +++++++++++++++++++---------
 drivers/hid/bpf/hid_bpf_struct_ops.c |  5 ++++
 2 files changed, 30 insertions(+), 11 deletions(-)

-- 
2.39.5 (Apple Git-154)

^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction
  2026-11-02 11:58 [PATCH 0/2] HID: bpf: close device teardown races Yifan Zhang
@ 2026-11-02 11:58 ` Yifan Zhang
  2026-11-02 12:15   ` sashiko-bot
  2026-11-02 16:21   ` Alexei Starovoitov
  2026-11-02 11:58 ` [PATCH 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
  1 sibling, 2 replies; 6+ messages in thread
From: Yifan Zhang @ 2026-11-02 11:58 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel

A HID-BPF syscall context pins struct hid_device, but it does not pin the
transport-private data referenced by the low-level driver.  A hardware
request can therefore sleep in the UHID report-wait path while closing the
UHID file destroys the HID device, wakes the waiter, and frees struct
uhid_device.  The awakened request then accesses the freed object.

KASAN reports:

  BUG: KASAN: use-after-free in __uhid_report_queue_and_wait
  Read of size 1
  uhid_hid_raw_request
  __hid_hw_raw_request
  hid_bpf_hw_request
  bpf_prog_test_run_syscall

Hold prog_list_lock across HID-BPF hardware operations.  Device destruction
sets destroyed while holding the same lock, so it waits for an operation
that has already started.  Later operations return -ENODEV before reaching
the low-level driver.  Apply the same lifetime rule to output reports.

The reproducer completed 100 close-versus-request trials without a KASAN
report with this change.

Fixes: 91a7f802d185 ("HID: bpf: introduce hid_hw_request()")
Closes: https://lore.kernel.org/linux-input/179317460423.87432.1451672332362541157@stu.pku.edu.cn/
Signed-off-by: Yifan Zhang <yfzhang23@stu.pku.edu.cn>
---
 drivers/hid/bpf/hid_bpf_dispatch.c | 36 +++++++++++++++++++++---------
 1 file changed, 25 insertions(+), 11 deletions(-)

diff --git a/drivers/hid/bpf/hid_bpf_dispatch.c b/drivers/hid/bpf/hid_bpf_dispatch.c
index 6b467c74601cb..4fea42dd2f392 100644
--- a/drivers/hid/bpf/hid_bpf_dispatch.c
+++ b/drivers/hid/bpf/hid_bpf_dispatch.c
@@ -444,14 +444,20 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
 	if (!dma_data)
 		return -ENOMEM;
 
-	ret = hid_ops->hid_hw_raw_request(ctx->hid,
-					      dma_data[0],
-					      dma_data,
-					      size,
-					      rtype,
-					      reqtype,
-					      (u64)(long)ctx,
-					      true); /* prevent infinite recursions */
+	mutex_lock(&ctx->hid->bpf.prog_list_lock);
+	if (READ_ONCE(ctx->hid->bpf.destroyed)) {
+		ret = -ENODEV;
+	} else {
+		ret = hid_ops->hid_hw_raw_request(ctx->hid,
+						  dma_data[0],
+						  dma_data,
+						  size,
+						  rtype,
+						  reqtype,
+						  (u64)(long)ctx,
+						  true); /* prevent infinite recursions */
+	}
+	mutex_unlock(&ctx->hid->bpf.prog_list_lock);
 
 	if (ret > (ssize_t)size)
 		ret = size;
@@ -492,7 +498,13 @@ hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
 	if (!dma_data)
 		return -ENOMEM;
 
-	ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size, (u64)(long)ctx, true);
+	mutex_lock(&ctx->hid->bpf.prog_list_lock);
+	if (READ_ONCE(ctx->hid->bpf.destroyed))
+		ret = -ENODEV;
+	else
+		ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size,
+						    (u64)(long)ctx, true);
+	mutex_unlock(&ctx->hid->bpf.prog_list_lock);
 
 	kfree(dma_data);
 	return ret;
@@ -641,8 +653,10 @@ void hid_bpf_destroy_device(struct hid_device *hdev)
 	if (!hdev)
 		return;
 
-	/* mark the device as destroyed in bpf so we don't reattach it */
-	hdev->bpf.destroyed = true;
+	/* Block new BPF hardware operations before transport teardown. */
+	mutex_lock(&hdev->bpf.prog_list_lock);
+	WRITE_ONCE(hdev->bpf.destroyed, true);
+	mutex_unlock(&hdev->bpf.prog_list_lock);
 
 	__hid_bpf_ops_destroy_device(hdev);
 
-- 
2.39.5 (Apple Git-154)

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/2] HID: bpf: reject struct_ops registration during device destruction
  2026-11-02 11:58 [PATCH 0/2] HID: bpf: close device teardown races Yifan Zhang
  2026-11-02 11:58 ` [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction Yifan Zhang
@ 2026-11-02 11:58 ` Yifan Zhang
  2026-11-02 12:13   ` sashiko-bot
  1 sibling, 1 reply; 6+ messages in thread
From: Yifan Zhang @ 2026-11-02 11:58 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Greg Kroah-Hartman, Alexei Starovoitov, bpf, linux-kernel

hid_bpf_reg() can obtain a device reference before hid_destroy_device()
removes the device from the bus, then block on prog_list_lock while HID-BPF
destruction cleans the device's SRCU state.  The registration path does not
check the destroyed flag after taking the lock, so it can publish a new
struct_ops entry and call synchronize_srcu() on the cleaned SRCU object.

KASAN reports:

  BUG: KASAN: slab-use-after-free in check_init_srcu_struct
  Read of size 8
  check_init_srcu_struct
  synchronize_srcu
  hid_bpf_reg
  bpf_struct_ops_link_create

Check destroyed while holding prog_list_lock.  The preceding teardown
serialization change sets the flag while holding the same lock, so either a
registration completes before destruction starts or it fails with -ENODEV
without accessing SRCU.

The reproducer no longer triggers a KASAN report with this change.

Fixes: ebc0d8093e8c ("HID: bpf: implement HID-BPF through bpf_struct_ops")
Closes: https://lore.kernel.org/linux-input/179317460937.87455.1799017093083467190@stu.pku.edu.cn/
Signed-off-by: Yifan Zhang <yfzhang23@stu.pku.edu.cn>
---
 drivers/hid/bpf/hid_bpf_struct_ops.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/drivers/hid/bpf/hid_bpf_struct_ops.c b/drivers/hid/bpf/hid_bpf_struct_ops.c
index c90b68956cb3b..4c433f419c85e 100644
--- a/drivers/hid/bpf/hid_bpf_struct_ops.c
+++ b/drivers/hid/bpf/hid_bpf_struct_ops.c
@@ -201,6 +201,11 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
 
 	mutex_lock(&hdev->bpf.prog_list_lock);
 
+	if (READ_ONCE(hdev->bpf.destroyed)) {
+		err = -ENODEV;
+		goto out_unlock;
+	}
+
 	count = list_count_nodes(&hdev->bpf.prog_list);
 	if (count >= HID_BPF_MAX_PROGS_PER_DEV) {
 		err = -E2BIG;
-- 
2.39.5 (Apple Git-154)

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/2] HID: bpf: reject struct_ops registration during device destruction
  2026-11-02 11:58 ` [PATCH 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
@ 2026-11-02 12:13   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-11-02 12:13 UTC (permalink / raw)
  To: Yifan Zhang, linux-input; +Cc: bpf, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] HID: bpf: dangling ops->hdev pointer on error path in hid_bpf_reg()
- [High] HID: bpf: deadlock on prog_list_lock and srcu in hid_bpf_hw_request()
--

commit 318c71f825ad280448e4c11859ac62903f968c18
Author: Yifan Zhang <yfzhang23@stu.pku.edu.cn>

HID: bpf: reject struct_ops registration during device destruction

This series serializes HID-BPF hardware requests and output reports against
device teardown using prog_list_lock and the destroyed flag, and rejects new
struct_ops registrations once device destruction has started.

> diff --git a/drivers/hid/bpf/hid_bpf_dispatch.c b/drivers/hid/bpf/hid_bpf_dispatch.c
> index 6b467c74601cb..4fea42dd2f392 100644
> --- a/drivers/hid/bpf/hid_bpf_dispatch.c
> +++ b/drivers/hid/bpf/hid_bpf_dispatch.c
> @@ -444,14 +444,20 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
>  	if (!dma_data)
>  		return -ENOMEM;
>  
> -	ret = hid_ops->hid_hw_raw_request(ctx->hid,
> -					      dma_data[0],
> -					      dma_data,
> -					      size,
> -					      rtype,
> -					      reqtype,
> -					      (u64)(long)ctx,
> -					      true); /* prevent infinite recursions */
> +	mutex_lock(&ctx->hid->bpf.prog_list_lock);
> +	if (READ_ONCE(ctx->hid->bpf.destroyed)) {
> +		ret = -ENODEV;
> +	} else {
> +		ret = hid_ops->hid_hw_raw_request(ctx->hid,
> +						  dma_data[0],
> +						  dma_data,
> +						  size,
> +						  rtype,
> +						  reqtype,
> +						  (u64)(long)ctx,
> +						  true); /* prevent infinite recursions */
> +	}
> +	mutex_unlock(&ctx->hid->bpf.prog_list_lock);
>  
>  	if (ret > (ssize_t)size)
>  		ret = size;
> @@ -492,7 +498,13 @@ hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
>  	if (!dma_data)
>  		return -ENOMEM;
>  
> -	ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size, (u64)(long)ctx, true);
> +	mutex_lock(&ctx->hid->bpf.prog_list_lock);
> +	if (READ_ONCE(ctx->hid->bpf.destroyed))
> +		ret = -ENODEV;
> +	else
> +		ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size,
> +						    (u64)(long)ctx, true);
> +	mutex_unlock(&ctx->hid->bpf.prog_list_lock);

[Severity: High]
Can taking prog_list_lock in hid_bpf_hw_request() and
hid_bpf_hw_output_report() deadlock against hdev->bpf.srcu or against itself?

In drivers/hid/bpf/hid_bpf_struct_ops.c, hid_bpf_reg() and hid_bpf_unreg()
acquire prog_list_lock and then wait in synchronize_srcu():

drivers/hid/bpf/hid_bpf_struct_ops.c:hid_bpf_reg() {
	mutex_lock(&hdev->bpf.prog_list_lock);
	...
	synchronize_srcu(&hdev->bpf.srcu);
}

Meanwhile, in drivers/hid/bpf/hid_bpf_dispatch.c,
dispatch_hid_bpf_raw_requests() and dispatch_hid_bpf_output_report() hold
srcu_read_lock(&hdev->bpf.srcu) while invoking sleepable struct_ops programs
(e->hid_hw_request and e->hid_hw_output_report):

drivers/hid/bpf/hid_bpf_dispatch.c:dispatch_hid_bpf_raw_requests() {
	idx = srcu_read_lock(&hdev->bpf.srcu);
	list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
				 srcu_read_lock_held(&hdev->bpf.srcu)) {
		if (!e->hid_hw_request)
			continue;

		ret = e->hid_hw_request(&ctx_kern.ctx, reportnum, rtype, reqtype, source);
		...
	}
	srcu_read_unlock(&hdev->bpf.srcu, idx);
}

Those sleepable callbacks are permitted to call the kfuncs
hid_bpf_hw_request() and hid_bpf_hw_output_report() (as done in
tools/testing/selftests/hid/progs/hid.c). With prog_list_lock taken inside
those kfuncs, a task running in dispatch_hid_bpf_raw_requests() or
dispatch_hid_bpf_output_report() will lock srcu -> prog_list_lock, while a
concurrent hid_bpf_reg() or hid_bpf_unreg() locks prog_list_lock -> srcu,
causing an ABBA deadlock.

Additionally, because prog_list_lock is held across
hid_ops->hid_hw_raw_request() and hid_ops->hid_hw_output_report(), which invoke
dispatch_hid_bpf_raw_requests() and dispatch_hid_bpf_output_report() on the
same task, if an attached sleepable struct_ops program allocates a fresh
context via hid_bpf_allocate_context() (where from_bpf is false) and calls
hid_bpf_hw_request() or hid_bpf_hw_output_report() on the same device, the task
will attempt to acquire prog_list_lock a second time and self-deadlock.

[ ... ]

> diff --git a/drivers/hid/bpf/hid_bpf_struct_ops.c b/drivers/hid/bpf/hid_bpf_struct_ops.c
> index c90b68956cb3b..4c433f419c85e 100644
> --- a/drivers/hid/bpf/hid_bpf_struct_ops.c
> +++ b/drivers/hid/bpf/hid_bpf_struct_ops.c
> @@ -201,6 +201,11 @@ static int hid_bpf_reg(void *kdata, struct bpf_link *link)
>  
>  	mutex_lock(&hdev->bpf.prog_list_lock);
>  
> +	if (READ_ONCE(hdev->bpf.destroyed)) {
> +		err = -ENODEV;
> +		goto out_unlock;
> +	}

[Severity: Critical]
Does jumping to out_unlock on error in hid_bpf_reg() leave a dangling pointer
in ops->hdev after hid_put_device(hdev) is called?

In drivers/hid/bpf/hid_bpf_struct_ops.c, hid_bpf_reg() assigns ops->hdev = hdev
before acquiring prog_list_lock, and the error path under out_unlock drops the
hdev reference without clearing ops->hdev:

drivers/hid/bpf/hid_bpf_struct_ops.c:hid_bpf_reg() {
	hdev = hid_get_device(ops->hid_id);
	if (IS_ERR(hdev))
		return PTR_ERR(hdev);

	ops->hdev = hdev;

	mutex_lock(&hdev->bpf.prog_list_lock);

	if (READ_ONCE(hdev->bpf.destroyed)) {
		err = -ENODEV;
		goto out_unlock;
	}
	...
out_unlock:
	mutex_unlock(&hdev->bpf.prog_list_lock);

	if (err) {
		if (hdev->bpf.rdesc_ops == ops)
			hdev->bpf.rdesc_ops = NULL;
		hid_put_device(hdev);
	}
}

When bpf_struct_ops_link_create() fails in st_ops->reg(), the struct_ops map's
kdata is not reset, so leaving ops->hdev non-NULL causes any subsequent attach
attempt on the map to fail at if (ops->hdev) return -EINVAL.

More importantly, if the map is already attached via an active bpf_link while
hid_destroy_device(hdev) runs, hid_bpf_destroy_device() calls
__hid_bpf_ops_destroy_device(hdev) (which clears ops->hdev = NULL and drops the
active link's hdev reference) before hid_remove_device(hdev) removes the device
from hid_bus_type. If a second bpf_struct_ops_link_create() on the same map
races in this window, it passes if (ops->hdev), gets a temporary reference via
hid_get_device(), sets ops->hdev = hdev, hits the new hdev->bpf.destroyed
check, and drops the temporary reference via hid_put_device(hdev) while leaving
ops->hdev pointing to hdev. Once hid_destroy_device() frees hdev, closing the
first bpf_link calls hid_bpf_unreg(), which sees ops->hdev != NULL and
dereferences the freed hdev.

Should ops->hdev be cleared in the if (err) block (or assigned only after the
error checks succeed)?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/179362071770.29269.16022650793604369465@stu.pku.edu.cn?part=2

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction
  2026-11-02 11:58 ` [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction Yifan Zhang
@ 2026-11-02 12:15   ` sashiko-bot
  2026-11-02 16:21   ` Alexei Starovoitov
  1 sibling, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-11-02 12:15 UTC (permalink / raw)
  To: Yifan Zhang, linux-input; +Cc: bpf, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] HID: bpf: report UAF in __hid_bpf_hw_check_params() during device teardown
- [High] HID: bpf: ABBA deadlock between prog_list_lock and srcu in hw request kfuncs
- [High] HID: bpf: recursive prog_list_lock deadlock in hid_bpf_hw_request()
- [High] HID: bpf: lock inversion between prog_list_lock and driver_input_lock
--

commit 49a737ce63da80cb915d4530a8a28ce52af759cc
Author: Yifan Zhang <yfzhang23@stu.pku.edu.cn>

HID: bpf: serialize hardware operations with device destruction

This commit serializes HID-BPF hardware requests and output reports with
device destruction by holding prog_list_lock and checking the destroyed
flag across low-level driver calls in hid_bpf_hw_request() and
hid_bpf_hw_output_report().

> diff --git a/drivers/hid/bpf/hid_bpf_dispatch.c b/drivers/hid/bpf/hid_bpf_dispatch.c
> index 6b467c74601cb..4fea42dd2f392 100644
> --- a/drivers/hid/bpf/hid_bpf_dispatch.c
> +++ b/drivers/hid/bpf/hid_bpf_dispatch.c
> @@ -444,14 +444,20 @@ hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz,
>  	if (!dma_data)
>  		return -ENOMEM;
>  
> -	ret = hid_ops->hid_hw_raw_request(ctx->hid,
> -					      dma_data[0],
> -					      dma_data,
> -					      size,
> -					      rtype,
> -					      reqtype,
> -					      (u64)(long)ctx,
> -					      true); /* prevent infinite recursions */
> +	mutex_lock(&ctx->hid->bpf.prog_list_lock);
> +	if (READ_ONCE(ctx->hid->bpf.destroyed)) {
> +		ret = -ENODEV;
> +	} else {
> +		ret = hid_ops->hid_hw_raw_request(ctx->hid,
> +						  dma_data[0],
> +						  dma_data,
> +						  size,
> +						  rtype,
> +						  reqtype,
> +						  (u64)(long)ctx,
> +						  true); /* prevent infinite recursions */
> +	}
> +	mutex_unlock(&ctx->hid->bpf.prog_list_lock);

[Severity: High]
Can acquiring ctx->hid->bpf.prog_list_lock here in hid_bpf_hw_request()
(and below in hid_bpf_hw_output_report()) in
drivers/hid/bpf/hid_bpf_dispatch.c cause an ABBA deadlock with
hdev->bpf.srcu?

In drivers/hid/bpf/hid_bpf_struct_ops.c, both hid_bpf_reg() and
hid_bpf_unreg() hold prog_list_lock while waiting on synchronize_srcu():

drivers/hid/bpf/hid_bpf_struct_ops.c:hid_bpf_unreg() {
	...
	mutex_lock(&hdev->bpf.prog_list_lock);
	...
	list_del_rcu(&ops->list);
	synchronize_srcu(&hdev->bpf.srcu);
	...
	mutex_unlock(&hdev->bpf.prog_list_lock);
}

When hid_bpf_hw_request() or hid_bpf_hw_output_report() is invoked from a
syscall BPF program, it also takes prog_list_lock and then calls
__hid_hw_raw_request() or __hid_hw_output_report(), which enters
dispatch_hid_bpf_raw_requests() or dispatch_hid_bpf_output_report() and
takes srcu_read_lock(&hdev->bpf.srcu). Both paths establish the lock order
prog_list_lock -> hdev->bpf.srcu.

However, hid_bpf_hw_request() and hid_bpf_hw_output_report() are also
registered in hid_bpf_kfunc_ids for BPF_PROG_TYPE_STRUCT_OPS, so sleepable
struct_ops callbacks (e->hid_hw_request and e->hid_hw_output_report) can
call them directly. When a non-BPF caller such as hidraw invokes
__hid_hw_raw_request(..., from_bpf=false) or
__hid_hw_output_report(..., from_bpf=false),
dispatch_hid_bpf_raw_requests() or dispatch_hid_bpf_output_report() holds
srcu_read_lock(&hdev->bpf.srcu) across the callback:

drivers/hid/bpf/hid_bpf_dispatch.c:dispatch_hid_bpf_raw_requests() {
	...
	idx = srcu_read_lock(&hdev->bpf.srcu);
	list_for_each_entry_srcu(e, &hdev->bpf.prog_list, list,
				 srcu_read_lock_held(&hdev->bpf.srcu)) {
		if (!e->hid_hw_request)
			continue;

		ret = e->hid_hw_request(&ctx_kern.ctx, reportnum, rtype, reqtype, source);
		if (ret)
			goto out;
	}
	ret = 0;

out:
	srcu_read_unlock(&hdev->bpf.srcu, idx);
	return ret;
}

When that struct_ops callback calls hid_bpf_hw_request() or
hid_bpf_hw_output_report(), it acquires prog_list_lock while holding
srcu_read_lock(&hdev->bpf.srcu), inverting the lock order to
hdev->bpf.srcu -> prog_list_lock and deadlocking if another thread is in
hid_bpf_reg() or hid_bpf_unreg().

[Severity: High]
Could holding ctx->hid->bpf.prog_list_lock across
hid_ops->hid_hw_raw_request() in hid_bpf_hw_request() (and across
hid_ops->hid_hw_output_report() in hid_bpf_hw_output_report()) also lead
to a recursive mutex self-deadlock on the same task?

When hid_ops->hid_hw_raw_request(..., true) or
hid_ops->hid_hw_output_report(..., true) runs with prog_list_lock held,
__hid_hw_raw_request() and __hid_hw_output_report() still invoke
dispatch_hid_bpf_raw_requests() and dispatch_hid_bpf_output_report(),
which call any attached sleepable struct_ops e->hid_hw_request or
e->hid_hw_output_report programs on that hid_device.

The recursion check at the start of hid_bpf_hw_request() and
hid_bpf_hw_output_report() only inspects ctx_kern->from_bpf on the passed
context:

drivers/hid/bpf/hid_bpf_dispatch.c:hid_bpf_hw_request() {
	ctx_kern = container_of(ctx, struct hid_bpf_ctx_kern, ctx);

	if (ctx_kern->from_bpf)
		return -EDEADLOCK;
	...
}

Because hid_bpf_allocate_context() is exposed in hid_bpf_kfunc_ids to
sleepable struct_ops programs and returns a fresh context with
from_bpf == false, wouldn't a struct_ops program that calls
hid_bpf_allocate_context(hctx->hid->id) and passes the new context to
hid_bpf_hw_request() or hid_bpf_hw_output_report() bypass the
ctx_kern->from_bpf check and try to lock ctx->hid->bpf.prog_list_lock a
second time?

[Severity: High]
Does holding ctx->hid->bpf.prog_list_lock across
hid_ops->hid_hw_raw_request() and hid_ops->hid_hw_output_report() in
drivers/hid/bpf/hid_bpf_dispatch.c introduce a lock inversion with
hdev->driver_input_lock?

In drivers/hid/hid-core.c, hid_device_probe() and hid_device_remove()
acquire hdev->driver_input_lock before calling __hid_device_probe() (which
runs call_hid_bpf_rdesc_fixup() and hdrv->probe()) and hdrv->remove(). If
a sleepable struct_ops callback (such as hid_rdesc_fixup, hid_hw_request,
or hid_hw_output_report) invoked under driver_input_lock calls
hid_bpf_hw_request() or hid_bpf_hw_output_report(), it acquires
prog_list_lock while holding driver_input_lock
(driver_input_lock -> prog_list_lock).

Conversely, while hid_bpf_hw_request() or hid_bpf_hw_output_report() holds
prog_list_lock across hid_ops->hid_hw_raw_request() or
hid_ops->hid_hw_output_report(), an attached sleepable struct_ops callback
e->hid_hw_request or e->hid_hw_output_report can call
hid_bpf_input_report(), which is exposed in hid_bpf_kfunc_ids and acquires
driver_input_lock before checking ctx_kern->from_bpf:

drivers/hid/bpf/hid_bpf_dispatch.c:hid_bpf_input_report() {
	...
	ret = down_interruptible(&ctx->hid->driver_input_lock);
	if (ret)
		return ret;
	...
}

Wouldn't that establish the reverse ordering
prog_list_lock -> driver_input_lock?

>  
>  	if (ret > (ssize_t)size)
>  		ret = size;
> @@ -492,7 +498,13 @@ hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)
>  	if (!dma_data)
>  		return -ENOMEM;
>  
> -	ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size, (u64)(long)ctx, true);
> +	mutex_lock(&ctx->hid->bpf.prog_list_lock);
> +	if (READ_ONCE(ctx->hid->bpf.destroyed))
> +		ret = -ENODEV;
> +	else
> +		ret = hid_ops->hid_hw_output_report(ctx->hid, dma_data, size,
> +						    (u64)(long)ctx, true);
> +	mutex_unlock(&ctx->hid->bpf.prog_list_lock);
>  
>  	kfree(dma_data);
>  	return ret;
> @@ -641,8 +653,10 @@ void hid_bpf_destroy_device(struct hid_device *hdev)
>  	if (!hdev)
>  		return;
>  
> -	/* mark the device as destroyed in bpf so we don't reattach it */
> -	hdev->bpf.destroyed = true;
> +	/* Block new BPF hardware operations before transport teardown. */
> +	mutex_lock(&hdev->bpf.prog_list_lock);
> +	WRITE_ONCE(hdev->bpf.destroyed, true);
> +	mutex_unlock(&hdev->bpf.prog_list_lock);

[Severity: Critical]
Is checking ctx->hid->bpf.destroyed after __hid_bpf_hw_check_params() in
hid_bpf_hw_request() and hid_bpf_hw_output_report() sufficient to prevent
a use-after-free on struct hid_report during device teardown?

In drivers/hid/bpf/hid_bpf_dispatch.c, both hid_bpf_hw_request() and
hid_bpf_hw_output_report() call __hid_bpf_hw_check_params() before
acquiring ctx->hid->bpf.prog_list_lock and before checking
ctx->hid->bpf.destroyed:

drivers/hid/bpf/hid_bpf_dispatch.c:hid_bpf_hw_request() {
	...
	/* check arguments */
	ret = __hid_bpf_hw_check_params(ctx, buf, &size, rtype, true);
	if (ret)
		return ret;
	...
	mutex_lock(&ctx->hid->bpf.prog_list_lock);
	if (READ_ONCE(ctx->hid->bpf.destroyed)) {
	...
}

__hid_bpf_hw_check_params() looks up and dereferences struct hid_report
from ctx->hid->report_enum:

drivers/hid/bpf/hid_bpf_dispatch.c:__hid_bpf_hw_check_params() {
	...
	report_enum = ctx->hid->report_enum + rtype;
	report = hid_ops->hid_get_report(report_enum, buf);
	if (!report)
		return -EINVAL;

	report_len = hid_report_len(report);
	...
}

During hid_destroy_device() in drivers/hid/hid-core.c,
hid_bpf_destroy_device() sets hdev->bpf.destroyed = true and unlocks
prog_list_lock, and then hid_remove_device() -> device_del() ->
hid_device_remove() -> hid_close_report() frees each report in
report_enum->report_id_hash[] before zeroing report_enum:

drivers/hid/hid-core.c:hid_close_report() {
	...
	for (i = 0; i < HID_REPORT_TYPES; i++) {
		struct hid_report_enum *report_enum = device->report_enum + i;

		for (j = 0; j < HID_MAX_IDS; j++) {
			struct hid_report *report = report_enum->report_id_hash[j];
			if (report)
				hid_free_report(report);
		}
		memset(report_enum, 0, sizeof(*report_enum));
	}
}

Because a syscall BPF context allocated by hid_bpf_allocate_context()
only pins struct hid_device via get_device(&hdev->dev), can a concurrent
call to hid_bpf_hw_request() or hid_bpf_hw_output_report() race with
hid_close_report() inside __hid_bpf_hw_check_params() and dereference a
freed struct hid_report before reaching the prog_list_lock and destroyed
check?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/179362071770.29269.16022650793604369465@stu.pku.edu.cn?part=1

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction
  2026-11-02 11:58 ` [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction Yifan Zhang
  2026-11-02 12:15   ` sashiko-bot
@ 2026-11-02 16:21   ` Alexei Starovoitov
  1 sibling, 0 replies; 6+ messages in thread
From: Alexei Starovoitov @ 2026-11-02 16:21 UTC (permalink / raw)
  To: Yifan Zhang, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Greg Kroah-Hartman, bpf, linux-kernel

On Mon, Nov 02, 2026 at 03:58 AM Yifan Zhang <yfzhang23@stu.pku.edu.cn> wrote:
> Hold prog_list_lock across HID-BPF hardware operations.  Device destruction
> sets destroyed while holding the same lock, so it waits for an operation
> that has already started.  Later operations return -ENODEV before reaching
> the low-level driver.  Apply the same lifetime rule to output reports.

sashiko is right. This will deadlock.

pw-bot: cr

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-11-02 16:21 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 11:58 [PATCH 0/2] HID: bpf: close device teardown races Yifan Zhang
2026-11-02 11:58 ` [PATCH 1/2] HID: bpf: serialize hardware operations with device destruction Yifan Zhang
2026-11-02 12:15   ` sashiko-bot
2026-11-02 16:21   ` Alexei Starovoitov
2026-11-02 11:58 ` [PATCH 2/2] HID: bpf: reject struct_ops registration during " Yifan Zhang
2026-11-02 12:13   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox