Skip to contentSkip to conversation

accel/qda: Qualcomm DSP Accelerator driver

PATCH NAK Ekansh Gupta 7 participants 68 messages Nov 1 – Nov 11

Subsystems (7)
lore
Share & tools
lore
#0 lore raw

v4 cover accel/qda: Qualcomm DSP Accelerator driver

This series adds QDA, a DRM accel driver for the Hexagon DSPs on Qualcomm SoCs. It is meant to replace drivers/misc/fastrpc.c. It speaks the same FastRPC protocol to the same DSP firmware over the same RPMsg channel, and it binds the same device-tree nodes. It adds no new DT binding or compatible.

Existing user space keeps working. The series also adds a separate module, qda_fastrpc_shim, which serves the full /dev/fastrpc-* ioctl interface on top of QDA. A distribution can therefore switch from CONFIG_QCOM_FASTRPC to CONFIG_DRM_ACCEL_QDA without breaking FastRPC users.

Why a new driver
================

drivers/misc/fastrpc.c predates the accel subsystem. As a miscdevice it implements its own buffer tracking, buffer sharing and per-process state, all of which DRM already provides as common, well-reviewed code. It keeps buffers in driver-private per-file lists. It accepts raw DMA-BUF fds, user pointers and inline data as invocation arguments. It keys state on the calling process. Each of these has needed driver-specific fixes over time, and none of the code is shared with any other driver.

Converting fastrpc.c in place into an accel driver was discussed on v3. The conclusion was to build the core cleanly on accel, and to keep the historical interface in a separate shim module that can later be deprecated and dropped.

What moving to accel gives us
=============================

Shared infrastructure instead of driver-private code
- GEM provides buffer lifetime, reference counting and per-file handle tables, in place of driver-private lists.
- DMA-BUF import goes through PRIME, the path used across DRM, rather than a FastRPC-specific fd lookup.
- Device registration, file handling and unplug follow the accel/DRM model. DSP-specific code is limited to the FastRPC protocol and the DSP memory model.
- The driver is maintained under accel and reviewed against its conventions, alongside the other compute accelerators.

Stronger isolation and a smaller attack surface
- Every buffer argument is a (GEM handle, offset, length) triplet, checked against the buffer size. The accel UAPI accepts no user pointers, raw fds or inline payloads, so the kernel never resolves arbitrary user addresses on the DSP's behalf.
- Each session owns one IOMMU context bank exclusively. Context banks are never shared, and a buffer that belongs to another session's context bank is rejected.
- No state is keyed on PID or TGID, so isolation follows the open file, not the process.
- Privileged operations (signed process creation, and Root, Sensors or Audio PD attach) require CAP_SYS_ADMIN. The check is made in one place and covers both the accel UAPI and the legacy interface.

More robust runtime behaviour
- Context IDs carry a sequence number, so a late or duplicate DSP response for a recycled ID cannot complete the wrong invocation.
- Waits are interruptible. An abandoned invocation keeps its buffers alive until the DSP has finished with them.
- When the DSP crashes or stops, pending invocations fail with -EPIPE and further ioctls fail with -ENODEV, instead of hanging or touching freed state.

A cleaner, documented UAPI
- The interface is documented in Documentation/accel/qda/qda.rst. It uses a consistent QDA_ namespace and rejects unknown query types and options, which leaves room to extend it without new ioctls.
- The handle/offset/length model carries no host addresses, which makes it a better basis for virtualised DSP devices than the legacy ioctls.

No flag day for users or distributions
- qda_fastrpc_shim keeps the existing /dev/fastrpc-* nodes and ioctls working on the same core, so current user space runs unmodified while it moves to the accel interface.
- Both interfaces share one implementation of the protocol, buffer and session logic, so a fix lands once and applies to both.
- Once fastrpc.c is removed, there is one FastRPC driver to maintain instead of two.

Backwards compatibility and the path to removing fastrpc.c
==========================================================

* qda_fastrpc_shim (CONFIG_DRM_ACCEL_QDA_FASTRPC_SHIM) binds to an auxiliary device that the core creates for each DSP domain. It registers the same /dev/fastrpc-<dsp> and /dev/fastrpc-<dsp>-secure nodes as drivers/misc/fastrpc.c, with the same secure/non-secure node rules, and translates the legacy ioctls onto QDA sessions. All protocol, buffer and session logic lives in the QDA core; the shim only handles the legacy ABI.

* The core is keyed on struct qda_session rather than struct drm_file, so a session can exist without a DRM file behind it. This removes the blocker reported on v3: there was no way to construct a drm_file from outside the DRM core.

* QDA and fastrpc bind the same compatibles and create the same device nodes, so they are mutually exclusive: QCOM_FASTRPC now depends on !DRM_ACCEL_QDA.

Shim coverage, against include/uapi/misc/fastrpc.h:

FASTRPC_IOCTL_ALLOC_DMA_BUFF supported
FASTRPC_IOCTL_FREE_DMA_BUFF -ENOTTY, as in fastrpc.c (buffers
are released by closing the fd)

  FASTRPC_IOCTL_INVOKE               supported
  FASTRPC_IOCTL_INIT_CREATE          supported (ELF image passed by fd)
  FASTRPC_IOCTL_INIT_ATTACH          supported
  FASTRPC_IOCTL_INIT_ATTACH_SNS      supported
  FASTRPC_IOCTL_INIT_CREATE_STATIC   supported (Audio PD)
  FASTRPC_IOCTL_MMAP / MUNMAP        supported
  FASTRPC_IOCTL_MEM_MAP / MEM_UNMAP  supported
  FASTRPC_IOCTL_GET_DSP_INFO         supported
  FASTRPC_IOCTL_SET_OPTION           supported (poll mode)

Known behavioural differences from fastrpc.c:
- INIT_CREATE with the ELF image given as a bare user pointer instead of an fd fails with -EOPNOTSUPP, as there is no buffer to resolve.
- INIT_CREATE_STATIC ignores memlen. The Audio PD remote heap size always comes from the device tree.
- Mappings are tracked per open file. MUNMAP with a length other than the mapped one is refused, and so is mapping the same fd twice with MEM_MAP. Mappings still outstanding when the file is closed are unmapped on the DSP before their buffers are released.

Proposed plan: merge QDA together with the shim, then remove drivers/misc/fastrpc.c, then deprecate the shim and eventually drop it once user space has moved to the accel interface.

Design overview
===============

* Context banks: each "qcom,fastrpc-compute-cb" child node becomes a platform device created with of_platform_device_create(), which gives it its own IOMMU domain. No driver binds to it, and no custom bus or iommu_buses[] change is needed. The devices are created before the accel device is registered and destroyed after it is unplugged.

* Session isolation: each session (accel file or legacy file) gets one context bank to itself on its first allocation or import. If none is free, the operation fails with -EBUSY. A buffer that resolves to another session's context bank, such as a PRIME export imported back on the same device, is rejected.

* Memory: buffers are DMA-coherent GEM objects allocated from the session's context bank. Imported DMA-BUFs are mapped into the same context bank and must be contiguous in its IOVA space, because the DSP firmware takes a single base address per buffer. The DSP manages its own virtual address space, so drm_gpuvm is not used.

* Per-SoC data: the stream-ID position in DSP addresses, the DMA address width and poll-mode support come from OF match data on the two existing compatibles, "qcom,kaanapali-fastrpc" and "qcom,fastrpc".

* Invocations: context IDs are allocated from an xarray and carry a sequence number. Waits are interruptible; an abandoned invocation stays alive until the DSP answers or the channel goes away.

* Privilege: the CAP_SYS_ADMIN check sits in the shared session code, so it applies equally to both interfaces.

* Audio PD remote heap: the reserved-memory region and "qcom,vmids" are parsed at probe, and the region is assigned to the DSP VMs with qcom_scm_assign_mem(). It is handed to the DSP only on the first successful Audio PD attach; later attaches, for example after a daemon restart, reuse it. It is returned to HLOS when the channel goes away.

* Capabilities: DSP capability attributes are fetched once per device through the DSP utilities handle and cached. If the DSP does not implement the API, the query fails with -EOPNOTSUPP.

* Poll mode: a session can opt in to polling a completion word for invocations of dynamically loaded modules in a user PD. If the DSP has not answered within 10 ms, the driver falls back to waiting for the RPMsg response.

Changes in v4
=============

v3 was mistakenly posted as "v2"; this posting is numbered v4 to keep the numbering consistent (Krzysztof).

Architecture
- Drop the qda-compute-cb custom bus and the iommu_buses[] patch. Context-bank devices are now plain platform devices created from the existing DT child nodes (Jason).
- Add in-kernel compatibility for the legacy /dev/fastrpc-* UABI as a separate module on the auxiliary bus, covering every legacy ioctl (Bjorn, Dmitry, Rob, Krzysztof).
- Bind the existing compatibles instead of proposing a new one, and make QCOM_FASTRPC and DRM_ACCEL_QDA mutually exclusive (Krzysztof, Bjorn).
- Explain the motivation, benefits, compatibility model and fastrpc.c removal plan in the cover letter (Bjorn, Krzysztof).

Memory and sessions
- Replace PID-based context-bank assignment with per-session assignment. Context banks are no longer shared (Christian, Rob, Dmitry).
- Size the context-bank registry from the DT count, and drop unused fields, NULL checks and stored counters (Dmitry).
- Reject buffers that resolve to another session's context bank, and check offset and length against the buffer size.
- Document the import contiguity requirement and the reason for it (Dmitry).

UAPI
- Buffer arguments are (handle, offset, length) only; the inline and DMA-BUF fd argument forms are gone (Dmitry).
- Use QDA_ prefixes throughout and document the scalars (sc) layout (Dmitry).
- DRM_IOCTL_QDA_QUERY rejects unknown query types; drop unneeded padding (Dmitry).
- Add DRM_IOCTL_QDA_REMOTE_MMAP and DRM_IOCTL_QDA_REMOTE_MUNMAP, with whole-buffer and attribute-based requests.
- Add DRM_IOCTL_QDA_REMOTE_SESSION_ATTACH for the Root, Sensors and Audio PDs, including the Audio PD remote heap (open item in v3).
- Add QDA_QUERY_DSP_CAPABILITIES to DRM_IOCTL_QDA_QUERY rather than a separate ioctl.
- Add DRM_IOCTL_QDA_SET_OPTION with QDA_SET_OPTION_POLL_MODE.
- Require CAP_SYS_ADMIN for signed process creation and PD attach (open item in v3).

Protocol
- Handle context-ID wraparound and stale responses with a sequence number (Dmitry).
- Make the response wait interruptible, and keep the invocation alive until the DSP is done with its buffers.
- Fail pending invocations cleanly when the DSP restarts.

Process and style
- Rewrite commit messages to explain why each change is needed rather than what it contains (Dmitry).
- Put the MAINTAINERS entry in the first patch and add each F: line together with the files it covers (Krzysztof).
- Use guard()/scoped_guard() for locking (Markus).
- Use the DRM driver debug category for debug messages.
- Add the missing newline at the end of drivers/accel/Makefile (Krzysztof).
- Reorganise the series so each patch adds one feature end to end, with the shim support for a feature following its accel UAPI patch.

Testing
=======

Tested on <PLATFORMS> with fastrpc_test from the open-source FastRPC user-space library, built with the open-source QAIC IDL compiler. The test was run both through the accel UAPI and, unmodified, through qda_fastrpc_shim:

fastrpc_test -d 3 -U 1 -t linux -a v68

User space
==========

- FastRPC library: https://github.com/qualcomm/fastrpc
(accel support: https://github.com/qualcomm/fastrpc/tree/accel/staging)
- QAIC compiler: https://github.com/qualcomm/QAIC

Patch organisation
==================

01      Documentation and MAINTAINERS
02      Driver skeleton and RPMsg binding; QCOM_FASTRPC depends on !QDA
03-04   Context-bank devices and per-session assignment
05      UAPI header and DSP name query
06-07   GEM buffer allocation and DMA-BUF import
08-09   Legacy ops table, auxiliary device and shim skeleton
10-14   Invocation: contexts, RPMsg send and receive, argument packing,
        invoke ioctl, invoke through the shim
15-16   DSP process creation and release, and through the shim
17-19   Remote mmap and munmap, and map/unmap through the shim
20-22   Root/Sensors PD attach, Audio PD attach with remote heap, and

attach through the shim
23-24 DSP capability query, and through the shim
25 Poll mode for invocation completion (accel UAPI and shim)

Parts of this patch were written with the help of an AI coding assistant (Claude, claude-sonnet-5) and reviewed by the author.

Previous versions
=================

v3 (posted as "v2"): https://lore.kernel.org/all/[email protected]/
v2 (posted as "v1"): https://lore.kernel.org/all/[email protected]/
v1 (RFC): https://lore.kernel.org/dri-devel/[email protected]/

---
Ekansh Gupta (25):
accel/qda: Add QDA driver documentation
accel/qda: Add initial QDA DRM accelerator driver accel/qda: Add compute context bank devices
accel/qda: Assign a context bank to each session accel/qda: Add UAPI header and DSP query ioctl accel/qda: Add buffer allocation for the DSP
accel/qda: Add DMA-BUF import
accel/qda: Add legacy ops base and auxiliary device accel/qda: Add legacy FastRPC shim skeleton
accel/qda: Add FastRPC invocation contexts
accel/qda: Add RPMsg send and response dispatch accel/qda: Add invocation argument validation and packing accel/qda: Add the invocation ioctl and teardown on unplug accel/qda: Add invocation through the legacy FastRPC shim accel/qda: Add DSP process creation and release accel/qda: Add DSP process creation and release to the legacy shim accel/qda: Add remote memory mapping to the DSP address space accel/qda: Add remote memory unmapping from the DSP address space accel/qda: Add remote memory map and unmap to the legacy shim accel/qda: Add remote attach to root and sensors protection domains accel/qda: Add Audio protection domain attach and its remote heap accel/qda: Add remote session attach to the legacy shim accel/qda: Add DSP capability query
accel/qda: Add legacy shim support for DSP capability query accel/qda: Add poll mode for invocation completion

 Documentation/accel/index.rst          |    1 +
 Documentation/accel/qda/index.rst      |   13 +
 Documentation/accel/qda/qda.rst        |  222 +++++
 MAINTAINERS                            |   11 +
 drivers/accel/Kconfig                  |    1 +
 drivers/accel/Makefile                 |    3 +-
 drivers/accel/qda/Kconfig              |   45 +
 drivers/accel/qda/Makefile             |   21 +
 drivers/accel/qda/qda_cb.c             |  129 +++
 drivers/accel/qda/qda_cb.h             |   29 +
 drivers/accel/qda/qda_drv.c            |  229 ++++++
 drivers/accel/qda/qda_drv.h            |  187 +++++
 drivers/accel/qda/qda_fastrpc.c        | 1417 ++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h        |  452 ++++++++++
 drivers/accel/qda/qda_fastrpc_shim.c   |  745 +++++++++++++++++
 drivers/accel/qda/qda_gem.c            |  146 ++++
 drivers/accel/qda/qda_gem.h            |   53 ++
 drivers/accel/qda/qda_ioctl.c          |  460 +++++++++++
 drivers/accel/qda/qda_ioctl.h          |   33 +
 drivers/accel/qda/qda_legacy_ops.c     |  212 +++++
 drivers/accel/qda/qda_legacy_ops.h     |   68 ++
 drivers/accel/qda/qda_memory_dma.c     |   85 ++
 drivers/accel/qda/qda_memory_dma.h     |   22 +
 drivers/accel/qda/qda_memory_manager.c |  213 +++++
 drivers/accel/qda/qda_memory_manager.h |   77 ++
 drivers/accel/qda/qda_prime.c          |  217 +++++
 drivers/accel/qda/qda_prime.h          |   29 +
 drivers/accel/qda/qda_rpmsg.c          |  322 ++++++++
 drivers/accel/qda/qda_rpmsg.h          |   26 +
 drivers/accel/qda/qda_soc.c            |   66 ++
 drivers/accel/qda/qda_soc.h            |   61 ++
 drivers/misc/Kconfig                   |    1 +
 include/uapi/drm/qda_accel.h           |  282 +++++++
 33 files changed, 5877 insertions(+), 1 deletion(-)

---
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
change-id: 20261102-v4-e8feb321263f

Best regards,

Signature · 1 line
Ekansh Gupta <[email protected]>
#1 lore raw

PATCH v4 1/25 accel/qda: Add QDA driver documentation

The QDA driver is intended to replace drivers/misc/fastrpc.c with a DRM accel driver. Its design differs from the existing driver in ways that reviewers need to see before reading the code: buffers are only ever passed as GEM handles, each open file owns an IOMMU context bank, and the legacy /dev/fastrpc-* interface is provided by a separate compatibility module instead of by the core driver.

Add the documentation at the start of the series so that the following patches can be reviewed against it. It describes the driver as the series completes it, and so covers ioctls and modules that later patches add.

The document also points to the open source user space, the FastRPC library and the QAIC compiler, used to exercise the driver.

---
 Documentation/accel/index.rst     |   1 +
 Documentation/accel/qda/index.rst |  13 +++
 Documentation/accel/qda/qda.rst   | 222 ++++++++++++++++++++++++++++++++++++++
 MAINTAINERS                       |   9 ++
 4 files changed, 245 insertions(+)
Documentation/accel/index.rst +1 -0
index cbc7d4c3876a..5901ea7f784c 100644
@@ -10,4 +10,5 @@ Compute Accelerators
    introduction
    amdxdna/index
    qaic/index
+   qda/index
    rocket/index
Documentation/accel/qda/index.rst new file +13 -0
new file mode 100644
index 000000000000..013400cf9c25
@@ -0,0 +1,13 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+==================================
+accel/qda Qualcomm DSP Accelerator
+==================================
+
+The QDA driver provides a DRM accel based interface for Qualcomm DSP offload.
+It uses the FastRPC protocol and integrates with DRM and GEM infrastructure
+for device and buffer management.
+
+.. toctree::
+
+   qda
Documentation/accel/qda/qda.rst new file +222 -0

222 lines not on this page · raw message

MAINTAINERS +9 -0
index aaf2825ac945..6803c60cbc64 100644
@@ -22973,6 +22973,15 @@ S:	Supported
 F:	Documentation/devicetree/bindings/crypto/qcom-qce.yaml
 F:	drivers/crypto/qce/
 
+QUALCOMM DSP ACCELERATOR (QDA) DRIVER
+M:	Ekansh Gupta <[email protected]>
+M:	Srinivas Kandagatla <[email protected]>
+L:	[email protected]
+L:	[email protected]
+S:	Supported
+T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F:	Documentation/accel/qda/
+
 QUALCOMM EMAC GIGABIT ETHERNET DRIVER
 M:	Timur Tabi <[email protected]>
 L:	[email protected]
Signature · 1 line
2.34.1
#2 lore raw

Re: [PATCH v4 01/25] accel/qda: Add QDA driver documentation

Quoted 1 line · Ekansh Gupta

Hi Ekansh,

The document reads well. I only have a few wording and consistency nits:

Quoted 1 line

+The driver binds to the device-tree nodes described by

The kernel documentation spells it "devicetree" (no hyphen). The same applies to the other "device-tree" occurrences in the file.

Quoted 3 lines

+4. **Context banks** (``qda_cb``): creates a platform device for each
...
+ They are created before the accel device is registered

"They" is ambiguous here (the platform devices or the context banks). Consider naming the subject.

Thanks,
Daniel

#3 lore raw

Re: [PATCH v4 01/25] accel/qda: Add QDA driver documentation

Quoted 12 lines · Daniel Pereira
Quoted 1 line · Ekansh Gupta

Hi Ekansh,

The document reads well. I only have a few wording and consistency nits:

Quoted 1 line

+The driver binds to the device-tree nodes described by

The kernel documentation spells it "devicetree" (no hyphen). The same applies to the other "device-tree" occurrences in the file.

Quoted 6 lines
Quoted 3 lines

+4. **Context banks** (``qda_cb``): creates a platform device for each
...
+ They are created before the accel device is registered

"They" is ambiguous here (the platform devices or the context banks). Consider naming the subject.

Thanks, will fix these.

Quoted 3 lines

Thanks,
Daniel

#4 lore raw

PATCH v4 2/25 accel/qda: Add initial QDA DRM accelerator driver

In reply to Ekansh Gupta

Qualcomm SoCs offload work to Hexagon DSPs today through drivers/misc/fastrpc.c, a misc character device driver written before the DRM accel subsystem existed. QDA is a new driver for the same hardware, built around GEM and DMA-BUF instead of the ad hoc buffer tracking the legacy driver has to do on its own, and exposed through /dev/accel/accelN like other compute accelerators.

This patch lays the skeleton: a drm_driver with DRIVER_COMPUTE_ACCEL, per-session state that later patches share between the native UAPI and the legacy compatibility shim, and an rpmsg binding that brings the device up when the DSP's RPMsg channel appears. The DSP domain name comes from the "label" property already used on this node, since that is how the platform identifies which Hexagon instance a given channel talks to.

Both compatibles the binding defines are matched, so the driver probes on every platform the legacy driver supports. The glymur, hawi, maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback compatible and so are matched by that entry. The DSP parameters that differ between these platforms - the stream ID position in a DSP-visible address, the DMA address width, and whether the firmware supports invoke completion polling - are added as match data together with the code that uses them, since the values are only meaningful next to it.

The rpmsg binding targets the same compatibles that drivers/misc/fastrpc.c binds to, because both drivers talk to the same physical DSP behind the same RPMsg channel. Two rpmsg_drivers cannot share a device, and driver_register() refuses two drivers with the same name on one bus, so drivers/misc/Kconfig now makes QCOM_FASTRPC depend on !DRM_ACCEL_QDA: a given kernel build contains one or the other. The dependency is expressed on the legacy symbol so that enabling QDA is what turns fastrpc off, rather than the reverse.

---
 MAINTAINERS                   |  1 +
 drivers/accel/Kconfig         |  1 +
 drivers/accel/Makefile        |  3 +-
 drivers/accel/qda/Kconfig     | 27 +++++++++++++
 drivers/accel/qda/Makefile    | 10 +++++
 drivers/accel/qda/qda_drv.c   | 84 +++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_drv.h   | 65 +++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_rpmsg.c | 91 +++++++++++++++++++++++++++++++++++++++++++
 drivers/misc/Kconfig          |  1 +
 9 files changed, 282 insertions(+), 1 deletion(-)
MAINTAINERS +1 -0
index 6803c60cbc64..77687a35ac13 100644
@@ -22981,6 +22981,7 @@ L:	[email protected]
 S:	Supported
 T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
 F:	Documentation/accel/qda/
+F:	drivers/accel/qda/
 
 QUALCOMM EMAC GIGABIT ETHERNET DRIVER
 M:	Timur Tabi <[email protected]>
drivers/accel/Kconfig +1 -0
index bdf48ccafcf2..74ac0f71bc9d 100644
@@ -29,6 +29,7 @@ source "drivers/accel/ethosu/Kconfig"
 source "drivers/accel/habanalabs/Kconfig"
 source "drivers/accel/ivpu/Kconfig"
 source "drivers/accel/qaic/Kconfig"
+source "drivers/accel/qda/Kconfig"
 source "drivers/accel/rocket/Kconfig"
 
 endif
drivers/accel/Makefile +2 -1
index 1d3a7251b950..17686a592900 100644
@@ -5,4 +5,5 @@ obj-$(CONFIG_DRM_ACCEL_ARM_ETHOSU)	+= ethosu/
 obj-$(CONFIG_DRM_ACCEL_HABANALABS)	+= habanalabs/
 obj-$(CONFIG_DRM_ACCEL_IVPU)		+= ivpu/
 obj-$(CONFIG_DRM_ACCEL_QAIC)		+= qaic/
-obj-$(CONFIG_DRM_ACCEL_ROCKET)		+= rocket/
\ No newline at end of file
+obj-$(CONFIG_DRM_ACCEL_QDA)		+= qda/
+obj-$(CONFIG_DRM_ACCEL_ROCKET)		+= rocket/
drivers/accel/qda/Kconfig new file +27 -0
new file mode 100644
index 000000000000..e2f3f01016c6
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Qualcomm DSP accelerator driver
+#
+
+config DRM_ACCEL_QDA
+	tristate "Qualcomm DSP accelerator"
+	depends on DRM_ACCEL
+	depends on ARCH_QCOM || COMPILE_TEST
+	depends on RPMSG
+	help
+	  Enables the DRM-based accelerator driver for Qualcomm's Hexagon DSPs.
+	  This driver provides a standardized interface for offloading computational
+	  tasks to the DSP, including audio processing, sensor offload, computer
+	  vision, and AI inference workloads.
+
+	  The driver supports all DSP domains (ADSP, CDSP, SDSP, GDSP) and
+	  implements the FastRPC protocol for communication between the application
+	  processor and DSP. It integrates with the Linux kernel's Compute
+	  Accelerators subsystem (drivers/accel/) and is intended to replace the
+	  FastRPC driver in drivers/misc/. Both drivers bind the same device tree
+	  nodes, so only one of them can be built.
+
+	  If unsure, say N.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called qda.
drivers/accel/qda/Makefile new file +10 -0
new file mode 100644
index 000000000000..dbe809067a8b
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for Qualcomm DSP accelerator driver
+#
+
+obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
+
+qda-y := \
+	qda_drv.o \
+	qda_rpmsg.o
drivers/accel/qda/qda_drv.c new file +84 -0

84 lines not on this page · raw message

drivers/accel/qda/qda_drv.h new file +65 -0

65 lines not on this page · raw message

drivers/accel/qda/qda_rpmsg.c new file +91 -0

91 lines not on this page · raw message

drivers/misc/Kconfig +1 -0
index c40e89cc5724..14b7bbaadcaa 100644
@@ -269,6 +269,7 @@ config QCOM_FASTRPC
 	tristate "Qualcomm FastRPC"
 	depends on ARCH_QCOM || COMPILE_TEST
 	depends on RPMSG
+	depends on !DRM_ACCEL_QDA
 	select DMA_SHARED_BUFFER
 	select QCOM_SCM
 	help
Signature · 1 line
2.34.1
#5 lore raw

Re: [PATCH v4 02/25] accel/qda: Add initial QDA DRM accelerator driver

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] accel/qda: !DRM_ACCEL_QDA in Kconfig allows building both drivers as modules --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] accel/qda: !DRM_ACCEL_QDA in Kconfig allows building both drivers as modules --

commit 546c9c847d7dbca90dfe6d1ba0861765d71753de
Author: Ekansh Gupta <[email protected]>

accel/qda: Add initial QDA DRM accelerator driver

This commit adds the initial DRM compute accelerator skeleton driver for Qualcomm Hexagon DSPs (qda), binding to the same RPMsg device tree compatibles as the legacy fastrpc driver. It also adds a Kconfig dependency on QCOM_FASTRPC intended to make the two drivers mutually exclusive.

drivers/misc/Kconfig config QCOM_FASTRPC · line 272

Quoted 9 lines
drivers/misc/Kconfig +1 -0
index c40e89cc57243..14b7bbaadcaa0 100644
@@ -269,6 +269,7 @@ config QCOM_FASTRPC
 	tristate "Qualcomm FastRPC"
 	depends on ARCH_QCOM || COMPILE_TEST
 	depends on RPMSG
+	depends on !DRM_ACCEL_QDA

[Severity: Medium]
As reported by Sashiko on v1 [1], since DRM_ACCEL_QDA in drivers/accel/qda/Kconfig and QCOM_FASTRPC in drivers/misc/Kconfig are both tristate symbols, does "depends on !DRM_ACCEL_QDA" still allow both drivers to be built together as modules?

In Kconfig tristate logic (EXPR_NOT in scripts/kconfig/expr.h), when DRM_ACCEL_QDA=m, !DRM_ACCEL_QDA evaluates to m rather than n. That only prevents QCOM_FASTRPC=y when DRM_ACCEL_QDA=m, while still allowing QCOM_FASTRPC=m and DRM_ACCEL_QDA=m at the same time. Both modules then export the same OF device table entries in qda_rpmsg_of_match and race to bind to the same RPMsg devices.

Should this dependency instead be written as:

depends on DRM_ACCEL_QDA=n

so that QCOM_FASTRPC is disabled whenever DRM_ACCEL_QDA is enabled as either built-in or a module?

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#6 lore raw

Re: [PATCH v4 02/25] accel/qda: Add initial QDA DRM accelerator driver

In reply to Ekansh Gupta

drivers/accel/qda/qda_rpmsg.c line 14

Quoted 8 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c new file +8 -0
+
+#include "qda_drv.h"
+
+/*
+ * Both compatibles the binding defines are matched here. The glymur, hawi,
+ * maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback
+ * compatible and so are matched by that entry. Per-SoC DSP parameters are
+ * attached as match data by a later patch, once there is something that

So you add empty or non-working code. No, add these once you actually have complete and working table.

Quoted 2 lines

+ * consumes them.
+ */

Drop comment, redundant. Referencing future patches makes no sense in source code, because there is no "patch" when looking at the code.

drivers/accel/qda/qda_rpmsg.c line 20

Quoted 4 lines
drivers/accel/qda/qda_rpmsg.c new file +4 -0
+static const struct of_device_id qda_rpmsg_of_match[] = {
+	{ .compatible = "qcom,kaanapali-fastrpc" },
+	{ .compatible = "qcom,fastrpc" },
+	{ }

Best regards,
Krzysztof

#7 lore raw

Re: [PATCH v4 02/25] accel/qda: Add initial QDA DRM accelerator driver

Quoted 12 lines · Krzysztof Kozlowski

drivers/accel/qda/qda_rpmsg.c line 14

Quoted 8 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c new file +8 -0
+
+#include "qda_drv.h"
+
+/*
+ * Both compatibles the binding defines are matched here. The glymur, hawi,
+ * maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback
+ * compatible and so are matched by that entry. Per-SoC DSP parameters are
+ * attached as match data by a later patch, once there is something that

So you add empty or non-working code. No, add these once you actually have complete and working table.

agreed, will add this once there is a complete table.

Quoted 6 lines
Quoted 3 lines

+ * consumes them.
+ */

Drop comment, redundant. Referencing future patches makes no sense in source code, because there is no "patch" when looking at the code.

ack.

Quoted 11 lines

drivers/accel/qda/qda_rpmsg.c line 20

Quoted 5 lines
drivers/accel/qda/qda_rpmsg.c new file +4 -0
+static const struct of_device_id qda_rpmsg_of_match[] = {
+	{ .compatible = "qcom,kaanapali-fastrpc" },
+	{ .compatible = "qcom,fastrpc" },
+	{ }

Best regards,
Krzysztof

#8 lore raw

PATCH v4 3/25 accel/qda: Add compute context bank devices

In reply to Ekansh Gupta

The DSP does not access memory through a single IOMMU domain. Its device tree node describes a set of context banks, one "qcom,fastrpc-compute-cb" child node each, and every context bank has its own SMMU address space. Later patches give each session a context bank of its own so that sessions cannot reach each other's buffers, but that needs a struct device per context bank to allocate from.

Create a platform device for each context bank node when the rpmsg channel probes, and destroy them again when the channel goes away or probe fails. Registering the device is what gives it an IOMMU group and domain: the IOMMU core probes it from the platform bus notifier when it is added, so no driver needs to bind to it. The devices are created before the DRM device is registered, so user space can never reach a context bank that does not exist yet. They are destroyed after drm_dev_unplug(), which makes sure no ioctl is still running against them.

The width of the address space the DSP can reach differs between SoCs and DSPs: the CDSP on Kaanapali and the platforms that use it as their fallback compatible can address 34 bits, everything else 32. Carry this in struct qda_soc_data, selected through the OF match data of the two compatibles the binding defines, and apply it to each context bank with dma_set_mask(). All CDSP instances (cdsp, cdsp1, ...) share the CDSP width, which is why the DSP name is matched by prefix. The remaining per-SoC parameters are added together with the code that uses them.

---
 drivers/accel/qda/Makefile    |  4 ++-
 drivers/accel/qda/qda_cb.c    | 76 +++++++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_cb.h    | 29 +++++++++++++++++
 drivers/accel/qda/qda_drv.c   |  2 ++
 drivers/accel/qda/qda_drv.h   |  6 ++++
 drivers/accel/qda/qda_rpmsg.c | 25 ++++++++++----
 drivers/accel/qda/qda_soc.c   | 31 ++++++++++++++++++
 drivers/accel/qda/qda_soc.h   | 30 +++++++++++++++++
 8 files changed, 196 insertions(+), 7 deletions(-)
drivers/accel/qda/Makefile +3 -1
index dbe809067a8b..4f2f236086a4 100644
@@ -6,5 +6,7 @@
 obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
 
 qda-y := \
+	qda_cb.o \
 	qda_drv.o \
-	qda_rpmsg.o
+	qda_rpmsg.o \
+	qda_soc.o
drivers/accel/qda/qda_cb.c new file +76 -0

76 lines not on this page · raw message

drivers/accel/qda/qda_cb.h new file +29 -0
new file mode 100644
index 000000000000..070dc1de67a0
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_CB_H__
+#define __QDA_CB_H__
+
+#include <linux/list.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include "qda_drv.h"
+
+/**
+ * struct qda_cb_dev - Bookkeeping for one compute context bank device
+ */
+struct qda_cb_dev {
+	/** @node: Entry in &qda_dev.cb_devs */
+	struct list_head node;
+	/** @pdev: Platform device created for the context bank node */
+	struct platform_device *pdev;
+};
+
+/* Create a device for each context bank child node of @parent_node */
+int qda_cb_populate(struct qda_dev *qdev, struct device_node *parent_node);
+/* Destroy every context bank device created by qda_cb_populate() */
+void qda_cb_unpopulate(struct qda_dev *qdev);
+
+#endif /* __QDA_CB_H__ */
drivers/accel/qda/qda_drv.c +2 -0
index f4430332ff6e..6ecb6a966f68 100644
@@ -65,6 +65,8 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 	if (IS_ERR(qdev))
 		return ERR_CAST(qdev);
 
+	INIT_LIST_HEAD(&qdev->cb_devs);
+
 	return qdev;
 }
 
drivers/accel/qda/qda_drv.h +6 -0
index dd3345b6a5ad..737232f7027f 100644
@@ -7,11 +7,13 @@
 #define __QDA_DRV_H__
 
 #include <linux/device.h>
+#include <linux/list.h>
 #include <linux/rpmsg.h>
 #include <linux/types.h>
 #include <drm/drm_device.h>
 #include <drm/drm_drv.h>
 #include <drm/drm_file.h>
+#include "qda_soc.h"
 
 /* Driver identification */
 #define QDA_DRIVER_NAME "qda"
@@ -41,6 +43,10 @@ struct qda_dev {
 	struct rpmsg_device *rpdev;
 	/** @dev: Underlying Linux device */
 	struct device *dev;
+	/** @cb_devs: Compute context-bank (CB) child devices */
+	struct list_head cb_devs;
+	/** @soc_data: Per-SoC DSP integration parameters */
+	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
 	const char *dsp_name;
 };
drivers/accel/qda/qda_rpmsg.c +19 -6
index 6bd977e8d68a..78b5f31974c1 100644
@@ -5,18 +5,18 @@
 #include <linux/rpmsg.h>
 #include <drm/drm_print.h>
 
+#include "qda_cb.h"
 #include "qda_drv.h"
+#include "qda_soc.h"
 
 /*
  * Both compatibles the binding defines are matched here. The glymur, hawi,
  * maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback
- * compatible and so are matched by that entry. Per-SoC DSP parameters are
- * attached as match data by a later patch, once there is something that
- * consumes them.
+ * compatible and so are matched by that entry.
  */
 static const struct of_device_id qda_rpmsg_of_match[] = {
-	{ .compatible = "qcom,kaanapali-fastrpc" },
-	{ .compatible = "qcom,fastrpc" },
+	{ .compatible = "qcom,kaanapali-fastrpc", .data = &kaanapali_soc_data },
+	{ .compatible = "qcom,fastrpc", .data = &default_soc_data },
 	{ }
 };
 MODULE_DEVICE_TABLE(of, qda_rpmsg_of_match);
@@ -58,6 +58,7 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 	 */
 	drm_dev_unplug(&qdev->drm_dev);
 	qdev->rpdev = NULL;
+	qda_cb_unpopulate(qdev);
 }
 
 static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
@@ -69,13 +70,25 @@ static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
 	if (IS_ERR(qdev))
 		return PTR_ERR(qdev);
 
+	qdev->soc_data = of_device_get_match_data(&rpdev->dev);
+	if (!qdev->soc_data)
+		return -ENODEV;
+
 	ret = of_property_read_string(rpdev->dev.of_node, "label", &qdev->dsp_name);
 	if (ret) {
 		dev_err(&rpdev->dev, "Missing 'label' property in DT node: %d\n", ret);
 		return ret;
 	}
 
-	return qda_register_device(qdev);
+	ret = qda_cb_populate(qdev, rpdev->dev.of_node);
+	if (ret)
+		return ret;
+
+	ret = qda_register_device(qdev);
+	if (ret)
+		qda_cb_unpopulate(qdev);
+
+	return ret;
 }
 
 static struct rpmsg_driver qda_rpmsg_driver = {
drivers/accel/qda/qda_soc.c new file +31 -0
new file mode 100644
index 000000000000..323b620b8adb
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+#include <linux/string.h>
+#include "qda_drv.h"
+#include "qda_soc.h"
+
+const struct qda_soc_data kaanapali_soc_data = {
+	.dma_addr_bits_cdsp = 34,
+	.dma_addr_bits_default = 32,
+};
+
+const struct qda_soc_data default_soc_data = {
+	.dma_addr_bits_cdsp = 32,
+	.dma_addr_bits_default = 32,
+};
+
+/**
+ * qda_soc_dma_addr_bits() - DMA address width of a device's context banks
+ * @qdev: QDA device
+ *
+ * All CDSP instances (cdsp, cdsp1, ...) share the CDSP width.
+ *
+ * Return: Number of DMA address bits the DSP can address on @qdev
+ */
+u32 qda_soc_dma_addr_bits(const struct qda_dev *qdev)
+{
+	if (!strncmp(qdev->dsp_name, "cdsp", 4))
+		return qdev->soc_data->dma_addr_bits_cdsp;
+
+	return qdev->soc_data->dma_addr_bits_default;
+}
drivers/accel/qda/qda_soc.h new file +30 -0
new file mode 100644
index 000000000000..b052964f11ae
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_SOC_H__
+#define __QDA_SOC_H__
+
+#include <linux/types.h>
+
+struct qda_dev;
+
+/**
+ * struct qda_soc_data - Per-SoC DSP integration parameters
+ *
+ * Selected by OF match on the fastrpc node; see qda_rpmsg.c.
+ */
+struct qda_soc_data {
+	/** @dma_addr_bits_cdsp: DMA address width of CDSP context banks */
+	u32 dma_addr_bits_cdsp;
+	/** @dma_addr_bits_default: DMA address width of all other context banks */
+	u32 dma_addr_bits_default;
+};
+
+extern const struct qda_soc_data kaanapali_soc_data;
+extern const struct qda_soc_data default_soc_data;
+
+u32 qda_soc_dma_addr_bits(const struct qda_dev *qdev);
+
+#endif /* __QDA_SOC_H__ */
Signature · 1 line
2.34.1
#9 lore raw

Re: [PATCH v4 03/25] accel/qda: Add compute context bank devices

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] accel/qda: missing IOMMU probe and domain ownership in qda_create_cb_device() - [Medium]…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] accel/qda: missing IOMMU probe and domain ownership in qda_create_cb_device()
- [Medium] accel/qda: qda_create_cb_device() leaves coherent_dma_mask at 32 bits --

commit 3c742ed6e8932c7c7fe69686db2d02383880697a
Author: Ekansh Gupta <[email protected]>

accel/qda: Add compute context bank devices

This commit creates a platform device for each qcom,fastrpc-compute-cb child node during rpmsg probe and configures its DMA address mask based on per-SoC data.

Quoted 19 lines
drivers/accel/qda/qda_cb.c new file +13 -0
new file mode 100644
index 0000000000000..afb0bb71a1f44
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+#include <linux/dma-mapping.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/slab.h>
+#include <drm/drm_print.h>
+#include "qda_cb.h"
+#include "qda_drv.h"
+#include "qda_soc.h"
+
+static int qda_create_cb_device(struct qda_dev *qdev, struct device_node *cb_node)
+{

drivers/accel/qda/qda_cb.c line 23

Quoted 10 lines
drivers/accel/qda/qda_cb.c new file +10 -0
+	struct qda_cb_dev *entry;
+	struct platform_device *pdev;
+	u32 dma_bits;
+	int ret;
+
+	pdev = of_platform_device_create(cb_node, NULL, qdev->dev);
+	if (!pdev) {
+		drm_err(&qdev->drm_dev, "Failed to create CB device for %pOFn\n", cb_node);
+		return -ENODEV;
+	}

[Severity: High]
Can qda_create_cb_device() succeed here even when the context bank device has not been attached to an IOMMU domain or claimed for kernel DMA API use?

Because the iommus property lives on the qcom,fastrpc-compute-cb child nodes rather than the parent fastrpc rpmsg node, fw_devlink only creates a DL_FLAG_SYNC_STATE_ONLY proxy link on rpdev->dev, which device_links_check_suppliers() ignores when qda_rpmsg_probe() runs.

When of_platform_device_create() calls of_device_add() -> device_add(), bus_notify(dev, BUS_NOTIFY_ADD_DEVICE) ignores the return value of iommu_bus_notifier(), and iommu_init_device() ignores the return value of dev->bus->dma_configure(dev):

drivers/iommu/iommu.c:iommu_init_device() {
	if (!dev->iommu->fwspec && !dev->driver && dev->bus->dma_configure) {
		mutex_unlock(&iommu_probe_device_lock);
		dev->bus->dma_configure(dev);
		mutex_lock(&iommu_probe_device_lock);
		/* If another instance finished the job for us, skip it */
		if (!dev->iommu || dev->iommu_group)
			return -ENODEV;
	}
}

If the SMMU supplier has not probed yet, of_iommu_configure() returns -EPROBE_DEFER, causing of_dma_configure_id() to return early before arch_setup_dma_ops() sets dev->dma_coherent:

drivers/of/device.c:of_dma_configure_id() {
	ret = of_iommu_configure(dev, np, id);
	if (ret == -EPROBE_DEFER) {
		/* Don't touch range map if it wasn't set from a valid dma-ranges */
		if (set_map)
			dev->dma_range_map = NULL;
		kfree(map);
		return -EPROBE_DEFER;
	}
}

In the platform bus model, DMA and IOMMU configuration errors (such as -EPROBE_DEFER or SMMU stream mapping failures) are only propagated when a driver binds in really_probe() -> platform_dma_configure(). Since no platform driver binds to pdev, of_platform_device_create() still returns a valid pdev with pdev->dev.iommu_group == NULL, allowing the DRM device to be registered while subsequent session DMA allocations fall back to unisolated dma-direct physical mappings.

Additionally, when platform_dma_configure() is invoked from iommu_init_device() with drv == NULL, it returns before calling iommu_device_use_default_domain():

drivers/base/platform.c:platform_dma_configure() {
	/* @dev->driver may not be valid when we're called from the IOMMU layer */
	if (ret || !drv || to_platform_driver(drv)->driver_managed_dma)
		return ret;

	ret = iommu_device_use_default_domain(dev);
}

This leaves group->owner_cnt at 0, so kernel DMA API ownership of the context bank's IOMMU group is never claimed (unlike drivers/misc/fastrpc.c, which binds a platform driver to qcom,fastrpc-compute-cb devices). Should a component/sub-driver bind to these context bank platform devices, or should qda_create_cb_device() explicitly configure DMA and claim default domain ownership while propagating errors such as -EPROBE_DEFER?

drivers/accel/qda/qda_cb.c line 26

Quoted 3 lines
drivers/accel/qda/qda_cb.c new file +3 -0
+
+	dma_bits = qda_soc_dma_addr_bits(qdev);
+	ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(dma_bits));

[Severity: Medium]
Should qda_create_cb_device() call dma_set_mask_and_coherent() instead of dma_set_mask() here?

When of_platform_device_create() allocates the device through of_platform_device_create_pdata() -> of_device_alloc() -> platform_device_alloc() -> setup_pdev_dma_masks(), pdev->dev.dma_mask is pointed at pdev->platform_dma_mask while pdev->dev.coherent_dma_mask is initialized separately to DMA_BIT_MASK(32):

drivers/base/platform.c:setup_pdev_dma_masks() {
	if (!pdev->dev.coherent_dma_mask)
		pdev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
	if (!pdev->dev.dma_mask) {
		pdev->platform_dma_mask = DMA_BIT_MASK(32);
		pdev->dev.dma_mask = &pdev->platform_dma_mask;
	}
}

Because dev->dev.dma_mask is already non-NULL when of_platform_device_create_pdata() checks if (!dev->dev.dma_mask), dev->dev.dma_mask and dev->dev.coherent_dma_mask remain separate fields.

Calling dma_set_mask() only updates *pdev->dev.dma_mask (pdev->platform_dma_mask) to 34 bits on Kaanapali CDSP and leaves pdev->dev.coherent_dma_mask at DMA_BIT_MASK(32). When later patches allocate context bank buffers via dma_alloc_coherent() (in commit "accel/qda: Add buffer allocation for the DSP"), the IOMMU DMA allocator passes dev->coherent_dma_mask as the upper IOVA limit to iommu_dma_alloc_iova(), restricting coherent allocations on 34-bit CDSP context banks to 32-bit IOVAs.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#10 lore raw

Re: [PATCH v4 03/25] accel/qda: Add compute context bank devices

In reply to Ekansh Gupta
Quoted 8 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c +0 -0
index 6bd977e8d68a..78b5f31974c1 100644
@@ -5,18 +5,18 @@
 #include <linux/rpmsg.h>
 #include <drm/drm_print.h>
 

drivers/accel/qda/qda_rpmsg.c line 15

Quoted 10 lines
drivers/accel/qda/qda_rpmsg.c +2 -3
+#include "qda_cb.h"
 #include "qda_drv.h"
+#include "qda_soc.h"
 
 /*
  * Both compatibles the binding defines are matched here. The glymur, hawi,
  * maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback
- * compatible and so are matched by that entry. Per-SoC DSP parameters are
- * attached as match data by a later patch, once there is something that
- * consumes them.

You just added this in previous commit. Why adding invalid documentation, which is immediately removed?

drivers/accel/qda/qda_rpmsg.c line 15

Quoted 1 line
drivers/accel/qda/qda_rpmsg.c +1 -0
+ * compatible and so are matched by that entry.

Best regards,
Krzysztof

#11 lore raw

Re: [PATCH v4 03/25] accel/qda: Add compute context bank devices

Quoted 13 lines · Krzysztof Kozlowski
Quoted 12 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c +2 -0
index 6bd977e8d68a..78b5f31974c1 100644
@@ -5,18 +5,18 @@
 #include <linux/rpmsg.h>
 #include <drm/drm_print.h>
 
+#include "qda_cb.h"
 #include "qda_drv.h"
+#include "qda_soc.h"
 
Quoted 9 lines

drivers/accel/qda/qda_rpmsg.c line 15

Quoted 6 lines
drivers/accel/qda/qda_rpmsg.c +0 -3
 /*
  * Both compatibles the binding defines are matched here. The glymur, hawi,
  * maili and nord nodes name "qcom,kaanapali-fastrpc" as their fallback
- * compatible and so are matched by that entry. Per-SoC DSP parameters are
- * attached as match data by a later patch, once there is something that
- * consumes them.

You just added this in previous commit. Why adding invalid documentation, which is immediately removed?

I'll fix this in the next spin.

Quoted 7 lines

drivers/accel/qda/qda_rpmsg.c line 15

Quoted 2 lines
drivers/accel/qda/qda_rpmsg.c +1 -0
+ * compatible and so are matched by that entry.

Best regards,
Krzysztof

#12 lore raw

PATCH v4 4/25 accel/qda: Assign a context bank to each session

In reply to Ekansh Gupta

Buffers a session gives to the DSP have to be mapped into one of the DSP's context banks, and the DSP identifies them by an address in that context bank's address space. If two sessions allocated from the same context bank, each would be able to reach the other's buffers through addresses the DSP accepts, so a context bank must not be shared.

Keep the context banks of a device in a registry sized to the number of nodes found in the device tree, and hand each session one of them, exclusively, until the session is released. A session that asks when none is free gets -EBUSY rather than sharing. Nothing allocates from a context bank yet; that comes with the DMA backend.

A context bank is reference counted and held both by the registry and by the session that owns it. The rpmsg channel can go away while a file is still open, which destroys the context bank devices, so the session keeps the object alive until it closes rather than leaving a dangling pointer behind.

qda_cb.c's cb_devs list and the new qda_memory_manager registry track the same set of context banks but for different reasons, and keeping them separate is deliberate. cb_devs exists to run the platform_device create/destroy dance against the device tree; nothing there knows about sessions. qda_memory_manager exists to answer a different question - which context bank, if any, is free right now - and is the only place that needs the per-session exclusivity rule from the second paragraph above. It is deliberately kept independent of GEM: the context bank registry has to be consulted (and a bank assigned) before any buffer exists to attach a GEM helper to, and the same registry will grow to hold each bank's IOMMU/DMA state once the DMA backend lands, which has no natural home on a GEM object either. Folding the two together would make qda_cb.c respond to session lifetime, or qda_memory_manager drive of_platform_device_create()/destroy(), neither of which is its job.

Note that this inherits a property of the existing fastrpc driver: a DSP has only a handful of context banks, so a process that opens the device repeatedly can take all of them and keep others off the DSP.

---
 drivers/accel/qda/Makefile             |   1 +
 drivers/accel/qda/qda_cb.c             |  45 ++++++++-
 drivers/accel/qda/qda_drv.c            |  30 +++++-
 drivers/accel/qda/qda_drv.h            |   7 ++
 drivers/accel/qda/qda_memory_manager.c | 176 +++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_memory_manager.h |  69 +++++++++++++
 drivers/accel/qda/qda_rpmsg.c          |  26 ++++-
 7 files changed, 349 insertions(+), 5 deletions(-)
drivers/accel/qda/Makefile +1 -0
index 4f2f236086a4..1ca991e198ed 100644
@@ -8,5 +8,6 @@ obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
 qda-y := \
 	qda_cb.o \
 	qda_drv.o \
+	qda_memory_manager.o \
 	qda_rpmsg.o \
 	qda_soc.o
drivers/accel/qda/qda_cb.c +44 -1

74 lines not on this page · raw message

drivers/accel/qda/qda_drv.c +28 -2
index 6ecb6a966f68..a99e23933f6b 100644
@@ -39,13 +39,19 @@ static int qda_open(struct drm_device *dev, struct drm_file *file)
 	return 0;
 }
 
-static void qda_postclose(struct drm_device *dev, struct drm_file *file)
+static void qda_session_free(struct qda_session *session)
 {
-	struct qda_session *session = file->driver_priv;
+	if (session->assigned_iommu_dev)
+		qda_memory_manager_release_device(session->assigned_iommu_dev);
 
 	kfree(session);
 }
 
+static void qda_postclose(struct drm_device *dev, struct drm_file *file)
+{
+	qda_session_free(file->driver_priv);
+}
+
 DEFINE_DRM_ACCEL_FOPS(qda_accel_fops);
 
 static const struct drm_driver qda_drm_driver = {
@@ -70,6 +76,26 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 	return qdev;
 }
 
+int qda_init_device(struct qda_dev *qdev, int num_cbs)
+{
+	int ret;
+
+	qdev->iommu_mgr = devm_kzalloc(qdev->dev, sizeof(*qdev->iommu_mgr), GFP_KERNEL);
+	if (!qdev->iommu_mgr)
+		return -ENOMEM;
+
+	ret = qda_memory_manager_init(qdev->iommu_mgr, num_cbs);
+	if (ret)
+		drm_err(&qdev->drm_dev, "Failed to initialize memory manager: %d\n", ret);
+
+	return ret;
+}
+
+void qda_deinit_device(struct qda_dev *qdev)
+{
+	qda_memory_manager_exit(qdev->iommu_mgr);
+}
+
 int qda_register_device(struct qda_dev *qdev)
 {
 	int ret;
drivers/accel/qda/qda_drv.h +7 -0
index 737232f7027f..c852d3b05310 100644
@@ -13,6 +13,7 @@
 #include <drm/drm_device.h>
 #include <drm/drm_drv.h>
 #include <drm/drm_file.h>
+#include "qda_memory_manager.h"
 #include "qda_soc.h"
 
 /* Driver identification */
@@ -28,6 +29,8 @@ struct qda_session {
 	struct qda_dev *qda_dev;
 	/** @file: The drm_file this session belongs to */
 	struct drm_file *file;
+	/** @assigned_iommu_dev: Context bank owned by this session, or NULL */
+	struct qda_iommu_device *assigned_iommu_dev;
 };
 
 /**
@@ -45,6 +48,8 @@ struct qda_dev {
 	struct device *dev;
 	/** @cb_devs: Compute context-bank (CB) child devices */
 	struct list_head cb_devs;
+	/** @iommu_mgr: Registry of this device's context banks */
+	struct qda_memory_manager *iommu_mgr;
 	/** @soc_data: Per-SoC DSP integration parameters */
 	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
@@ -66,6 +71,8 @@ static inline struct qda_dev *qda_dev_from_drm(struct drm_device *dev)
 struct qda_dev *qda_alloc_device(struct device *dev);
 
 /* Core device lifecycle */
+int qda_init_device(struct qda_dev *qdev, int num_cbs);
+void qda_deinit_device(struct qda_dev *qdev);
 int qda_register_device(struct qda_dev *qdev);
 
 #endif /* __QDA_DRV_H__ */
drivers/accel/qda/qda_memory_manager.c new file +176 -0

176 lines not on this page · raw message

drivers/accel/qda/qda_memory_manager.h new file +69 -0

69 lines not on this page · raw message

drivers/accel/qda/qda_rpmsg.c +24 -2
index 78b5f31974c1..2c9d79937b10 100644
@@ -59,6 +59,18 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 	drm_dev_unplug(&qdev->drm_dev);
 	qdev->rpdev = NULL;
 	qda_cb_unpopulate(qdev);
+	qda_deinit_device(qdev);
+}
+
+static int qda_count_cb_nodes(struct device_node *parent_node)
+{
+	int count = 0;
+
+	for_each_available_child_of_node_scoped(parent_node, child)
+		if (of_device_is_compatible(child, "qcom,fastrpc-compute-cb"))
+			count++;
+
+	return count;
 }
 
 static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
@@ -80,14 +92,24 @@ static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
 		return ret;
 	}
 
-	ret = qda_cb_populate(qdev, rpdev->dev.of_node);
+	ret = qda_init_device(qdev, qda_count_cb_nodes(rpdev->dev.of_node));
 	if (ret)
 		return ret;
 
+	ret = qda_cb_populate(qdev, rpdev->dev.of_node);
+	if (ret)
+		goto err_deinit;
+
 	ret = qda_register_device(qdev);
 	if (ret)
-		qda_cb_unpopulate(qdev);
+		goto err_unpopulate;
 
+	return 0;
+
+err_unpopulate:
+	qda_cb_unpopulate(qdev);
+err_deinit:
+	qda_deinit_device(qdev);
 	return ret;
 }
 
Signature · 1 line
2.34.1
#13 lore raw

Re: [PATCH v4 04/25] accel/qda: Assign a context bank to each session

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] accel/qda: dangling cb_dev pointer in struct qda_iommu_device after unpopulate - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] accel/qda: dangling cb_dev pointer in struct qda_iommu_device after unpopulate
- [High] accel/qda: devm lifetime mismatch and premature teardown of qdev->iommu_mgr --

commit 274f69c69f358c698c8f7dd37051492c7c060cc1
Author: Ekansh Gupta <[email protected]>

accel/qda: Assign a context bank to each session

This commit introduces a context bank registry (struct qda_memory_manager) to track a QDA device's compute context banks and exclusively assign one refcounted struct qda_iommu_device per open session.

Quoted 45 lines
drivers/accel/qda/qda_cb.c +35 -0
index afb0bb71a1f44..6f1cff1133f9d 100644
@@ -7,8 +7,44 @@
 #include <drm/drm_print.h>
 #include "qda_cb.h"
 #include "qda_drv.h"
+#include "qda_memory_manager.h"
 #include "qda_soc.h"
 
+static int qda_cb_setup_device(struct qda_dev *qdev, struct device *cb_dev)
+{
+	struct qda_iommu_device *iommu_dev;
+	int ret;
+
+	iommu_dev = kzalloc_obj(*iommu_dev);
+	if (!iommu_dev)
+		return -ENOMEM;
+
+	iommu_dev->dev = cb_dev;
+	iommu_dev->qdev = qdev;
+
+	ret = qda_memory_manager_register_device(qdev->iommu_mgr, iommu_dev);
+	if (ret) {
+		drm_err(&qdev->drm_dev, "Failed to register context bank: %d\n", ret);
+		kfree(iommu_dev);
+		return ret;
+	}
+
+	dev_set_drvdata(cb_dev, iommu_dev);
+
+	return 0;
+}
+
+static void qda_cb_teardown_device(struct device *cb_dev)
+{
+	struct qda_iommu_device *iommu_dev = dev_get_drvdata(cb_dev);
+
+	if (!iommu_dev)
+		return;
+
+	qda_memory_manager_unregister_device(iommu_dev->qdev->iommu_mgr, iommu_dev);
+	dev_set_drvdata(cb_dev, NULL);
+}

[ ... ]

drivers/accel/qda/qda_cb.c void qda_cb_unpopulate(struct qda_dev *qdev) · line 116

Quoted 6 lines
drivers/accel/qda/qda_cb.c +1 -0
@@ -70,6 +112,7 @@ void qda_cb_unpopulate(struct qda_dev *qdev)
 
 	list_for_each_entry_safe(entry, tmp, &qdev->cb_devs, node) {
 		list_del(&entry->node);
+		qda_cb_teardown_device(&entry->pdev->dev);
 		of_platform_device_destroy(&entry->pdev->dev, NULL);

[Severity: High]
Can this leave a dangling pointer in iommu_dev->dev when the rpmsg channel goes away while a session is still open?

In qda_cb_setup_device(), iommu_dev->dev stores the raw cb_dev pointer (&pdev->dev) without taking a reference via get_device(cb_dev), and iommu_device_free() frees struct qda_iommu_device without calling put_device().

When a session owns a context bank via qda_memory_manager_assign_device(), assign_available_device() takes a second reference on iommu_dev->ref so that struct qda_iommu_device stays alive until qda_postclose() -> qda_session_free() calls qda_memory_manager_release_device().

If qda_rpmsg_remove() runs while a DRM file is open:

qda_rpmsg_remove()

    qda_cb_unpopulate()
      qda_cb_teardown_device(&entry->pdev->dev)
        qda_memory_manager_unregister_device()
          kref_put(&iommu_dev->ref, iommu_device_free)  /* ref drops 2 -> 1 */
      of_platform_device_destroy(&entry->pdev->dev, NULL)

of_platform_device_destroy() unregisters and frees entry->pdev (and its embedded struct device), while session->assigned_iommu_dev remains alive with iommu_dev->dev still pointing to the freed struct device for the remainder of the open session's lifetime.

Should qda_cb_setup_device() and iommu_device_free() manage a device reference with get_device() and put_device(), or otherwise invalidate iommu_dev->dev under synchronization when the context bank is torn down?

Quoted 4 lines
drivers/accel/qda/qda_drv.c +0 -0
index 6ecb6a966f686..a99e23933f6ba 100644

[ ... ]

Quoted 13 lines
drivers/accel/qda/qda_drv.c +9 -0
@@ -70,6 +76,26 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 	return qdev;
 }
 
+int qda_init_device(struct qda_dev *qdev, int num_cbs)
+{
+	int ret;
+
+	qdev->iommu_mgr = devm_kzalloc(qdev->dev, sizeof(*qdev->iommu_mgr), GFP_KERNEL);
+	if (!qdev->iommu_mgr)
+		return -ENOMEM;
+
+	ret = qda_memory_manager_init(qdev->iommu_mgr, num_cbs);

drivers/accel/qda/qda_drv.c struct qda_dev *qda_alloc_device(struct device *dev) · line 97

Quoted 10 lines
drivers/accel/qda/qda_drv.c +10 -0
+	if (ret)
+		drm_err(&qdev->drm_dev, "Failed to initialize memory manager: %d\n", ret);
+
+	return ret;
+}
+
+void qda_deinit_device(struct qda_dev *qdev)
+{
+	qda_memory_manager_exit(qdev->iommu_mgr);
+}

[Severity: High]
Does allocating qdev->iommu_mgr with devm_kzalloc(qdev->dev, ...) and tearing it down in qda_rpmsg_remove() -> qda_deinit_device() mismatch the lifetime of struct qda_dev?

qda_alloc_device() allocates struct qda_dev with devm_drm_dev_alloc(), which keeps qdev alive until the last drm_device reference is dropped when all open drm_file descriptors are closed. In contrast, devm_kzalloc(qdev->dev, ...) ties qdev->iommu_mgr to the rpmsg_device driver binding.

When qda_rpmsg_remove() runs while a file is open:

qda_rpmsg_remove()

    drm_dev_unplug(&qdev->drm_dev)
    qda_cb_unpopulate(qdev)
    qda_deinit_device(qdev)
      qda_memory_manager_exit(qdev->iommu_mgr)
        kfree(mem_mgr->devices)
        mutex_destroy(&mem_mgr->assignment_lock)

When qda_rpmsg_remove() returns, devres releases qdev->iommu_mgr, leaving qdev->iommu_mgr as a non-NULL dangling pointer to freed memory and a destroyed mutex while qdev and open qda_session instances are still alive. Moreover, because drm_ioctl() only performs a point-in-time drm_dev_is_unplugged() check at entry before calling the ioctl handler, any in-flight operation that passed that check before drm_dev_unplug() and calls qda_memory_manager_assign_device(qdev->iommu_mgr, session) can race with qda_memory_manager_exit() and devres teardown.

Should struct qda_memory_manager be embedded in struct qda_dev (or managed via drmm) so its lifetime and mutex match qdev->drm_dev?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
#14 lore raw

Re: [PATCH v4 04/25] accel/qda: Assign a context bank to each session

In reply to Ekansh Gupta

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 69

Quoted 6 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c +6 -0
+
+static int qda_count_cb_nodes(struct device_node *parent_node)
+{
+	int count = 0;
+
+	for_each_available_child_of_node_scoped(parent_node, child)

I don't see a need for scoped loop.

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 74

Quoted 5 lines
drivers/accel/qda/qda_rpmsg.c +4 -0
+		if (of_device_is_compatible(child, "qcom,fastrpc-compute-cb"))
+			count++;
+
+	return count;
 }

Best regards,
Krzysztof

#15 lore raw

Re: [PATCH v4 04/25] accel/qda: Assign a context bank to each session

Quoted 9 lines · Krzysztof Kozlowski

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 69

Quoted 6 lines · Ekansh Gupta
drivers/accel/qda/qda_rpmsg.c +6 -0
+
+static int qda_count_cb_nodes(struct device_node *parent_node)
+{
+	int count = 0;
+
+	for_each_available_child_of_node_scoped(parent_node, child)

I don't see a need for scoped loop.

will switch to for_each_available_child_of_node()

Quoted 11 lines

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 74

Quoted 6 lines
drivers/accel/qda/qda_rpmsg.c +4 -0
+		if (of_device_is_compatible(child, "qcom,fastrpc-compute-cb"))
+			count++;
+
+	return count;
 }

Best regards,
Krzysztof

#16 lore raw

PATCH v4 5/25 accel/qda: Add UAPI header and DSP query ioctl

In reply to Ekansh Gupta

A system has one accel node per DSP domain, and which node is which is not something user space can tell from the node name: /dev/accel/accel0 may be the CDSP on one platform and the ADSP on another. An application that wants a particular DSP has to open the nodes and ask.

Add DRM_IOCTL_QDA_QUERY, which returns the name of the DSP behind a given node, taken from the "label" property of its device tree node. The ioctl takes a query_type selector so that later additions (DSP capabilities, attributes) do not each need an ioctl of their own, and is DRM_IOWR rather than DRM_IOR so that the selector reaches the kernel.

Unknown query types are rejected with -EINVAL rather than returning zeroes, so that user space can tell an unsupported query on an old kernel apart from a query that legitimately returned nothing.

---
 MAINTAINERS                   |  1 +
 drivers/accel/qda/Makefile    |  1 +
 drivers/accel/qda/qda_drv.c   |  8 ++++++++
 drivers/accel/qda/qda_ioctl.c | 30 ++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.h | 13 +++++++++++++
 include/uapi/drm/qda_accel.h  | 41 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 94 insertions(+)
MAINTAINERS +1 -0
index 77687a35ac13..1316e63c1151 100644
@@ -22982,6 +22982,7 @@ S:	Supported
 T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
 F:	Documentation/accel/qda/
 F:	drivers/accel/qda/
+F:	include/uapi/drm/qda_accel.h
 
 QUALCOMM EMAC GIGABIT ETHERNET DRIVER
 M:	Timur Tabi <[email protected]>
drivers/accel/qda/Makefile +1 -0
index 1ca991e198ed..402e7dbebbf0 100644
@@ -8,6 +8,7 @@ obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
 qda-y := \
 	qda_cb.o \
 	qda_drv.o \
+	qda_ioctl.o \
 	qda_memory_manager.o \
 	qda_rpmsg.o \
 	qda_soc.o
drivers/accel/qda/qda_drv.c +8 -0
index a99e23933f6b..e722135238f3 100644
@@ -8,8 +8,10 @@
 #include <drm/drm_gem.h>
 #include <drm/drm_ioctl.h>
 #include <drm/drm_print.h>
+#include <drm/qda_accel.h>
 
 #include "qda_drv.h"
+#include "qda_ioctl.h"
 
 static struct qda_session *qda_session_alloc(struct qda_dev *qdev)
 {
@@ -54,11 +56,17 @@ static void qda_postclose(struct drm_device *dev, struct drm_file *file)
 
 DEFINE_DRM_ACCEL_FOPS(qda_accel_fops);
 
+static const struct drm_ioctl_desc qda_ioctls[] = {
+	DRM_IOCTL_DEF_DRV(QDA_QUERY, qda_ioctl_query, 0),
+};
+
 static const struct drm_driver qda_drm_driver = {
 	.driver_features = DRIVER_COMPUTE_ACCEL,
 	.fops = &qda_accel_fops,
 	.open = qda_open,
 	.postclose = qda_postclose,
+	.ioctls = qda_ioctls,
+	.num_ioctls = ARRAY_SIZE(qda_ioctls),
 	.name = QDA_DRIVER_NAME,
 	.desc = "Qualcomm DSP Accelerator Driver",
 };
drivers/accel/qda/qda_ioctl.c new file +30 -0
new file mode 100644
index 000000000000..380c2526d9d2
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+#include <linux/string.h>
+#include <drm/drm_device.h>
+#include <drm/drm_file.h>
+#include <drm/qda_accel.h>
+#include "qda_drv.h"
+#include "qda_ioctl.h"
+
+/**
+ * qda_ioctl_query() - Query a property of the DSP
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_query)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(dev);
+	struct drm_qda_query *args = data;
+
+	switch (args->query_type) {
+	case QDA_QUERY_DSP_NAME:
+		strscpy(args->dsp_name, qdev->dsp_name, sizeof(args->dsp_name));
+		return 0;
+	default:
+		return -EINVAL;
+	}
+}
drivers/accel/qda/qda_ioctl.h new file +13 -0
new file mode 100644
index 000000000000..b8fd536a111f
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_IOCTL_H__
+#define __QDA_IOCTL_H__
+
+#include "qda_drv.h"
+
+int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv);
+
+#endif /* __QDA_IOCTL_H__ */
include/uapi/drm/qda_accel.h new file +41 -0

41 lines not on this page · raw message

Signature · 1 line
2.34.1
#17 lore raw

PATCH v4 6/25 accel/qda: Add buffer allocation for the DSP

In reply to Ekansh Gupta

Offloading work to the DSP means handing it buffers, and the DSP can only reach memory mapped into one of its context banks. Allocate those buffers as GEM objects so that user space gets the usual handle-based lifetime and the usual mmap path, and so that buffers can later be shared with other drivers through PRIME.

Buffers are allocated with dma_alloc_coherent() from the context bank the session owns, which is assigned on the session's first allocation. Coherent memory avoids explicit cache maintenance on every invocation, at the cost of uncached CPU access.

The DSP does not take a context bank identifier alongside an address. It derives the context bank from the upper bits of the address itself, so the stream ID of the bank is folded into every address handed to it, at a bit position that differs between SoCs. The DMA API must never see those bits, so they are stripped again before each free and mmap. This is also why the context bank devices are given a DMA mask covering only the lower bits: the allocator must not hand back an address that would overlap the stream ID field.

Add DRM_IOCTL_QDA_GEM_CREATE to allocate a buffer and DRM_IOCTL_QDA_GEM_MMAP_OFFSET to find the offset to mmap() it at. Nothing passes these buffers to the DSP yet; that comes with the invocation path.

---
 drivers/accel/qda/Makefile             |   2 +
 drivers/accel/qda/qda_cb.c             |  14 ++++-
 drivers/accel/qda/qda_drv.c            |   4 +-
 drivers/accel/qda/qda_gem.c            | 108 +++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_gem.h            |  44 ++++++++++++++
 drivers/accel/qda/qda_ioctl.c          |  45 ++++++++++++++
 drivers/accel/qda/qda_ioctl.h          |   2 +
 drivers/accel/qda/qda_memory_dma.c     |  72 ++++++++++++++++++++++
 drivers/accel/qda/qda_memory_dma.h     |  20 ++++++
 drivers/accel/qda/qda_memory_manager.c |  37 +++++++++++
 drivers/accel/qda/qda_memory_manager.h |   8 +++
 drivers/accel/qda/qda_soc.c            |   2 +
 drivers/accel/qda/qda_soc.h            |  28 +++++++++
 include/uapi/drm/qda_accel.h           |  38 +++++++++++-
 14 files changed, 420 insertions(+), 4 deletions(-)
drivers/accel/qda/Makefile +2 -0
index 402e7dbebbf0..2d3bcc3050eb 100644
@@ -8,7 +8,9 @@ obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
 qda-y := \
 	qda_cb.o \
 	qda_drv.o \
+	qda_gem.o \
 	qda_ioctl.o \
+	qda_memory_dma.o \
 	qda_memory_manager.o \
 	qda_rpmsg.o \
 	qda_soc.o
drivers/accel/qda/qda_cb.c +12 -2
index 6f1cff1133f9..033d1502282a 100644
@@ -10,7 +10,7 @@
 #include "qda_memory_manager.h"
 #include "qda_soc.h"
 
-static int qda_cb_setup_device(struct qda_dev *qdev, struct device *cb_dev)
+static int qda_cb_setup_device(struct qda_dev *qdev, struct device *cb_dev, u32 sid)
 {
 	struct qda_iommu_device *iommu_dev;
 	int ret;
@@ -21,6 +21,7 @@ static int qda_cb_setup_device(struct qda_dev *qdev, struct device *cb_dev)
 
 	iommu_dev->dev = cb_dev;
 	iommu_dev->qdev = qdev;
+	iommu_dev->sid = sid;
 
 	ret = qda_memory_manager_register_device(qdev->iommu_mgr, iommu_dev);
 	if (ret) {
@@ -50,8 +51,17 @@ static int qda_create_cb_device(struct qda_dev *qdev, struct device_node *cb_nod
 	struct qda_cb_dev *entry;
 	struct platform_device *pdev;
 	u32 dma_bits;
+	u32 sid = 0;
 	int ret;
 
+	/*
+	 * The stream ID identifies the context bank to the DSP. A node without
+	 * one is accepted so that buffers can still be allocated from it, but
+	 * the DSP will then see every such bank as stream ID 0.
+	 */
+	if (of_property_read_u32(cb_node, "reg", &sid))
+		drm_info(&qdev->drm_dev, "No stream ID in %pOFn, using 0\n", cb_node);
+
 	pdev = of_platform_device_create(cb_node, NULL, qdev->dev);
 	if (!pdev) {
 		drm_err(&qdev->drm_dev, "Failed to create CB device for %pOFn\n", cb_node);
@@ -66,7 +76,7 @@ static int qda_create_cb_device(struct qda_dev *qdev, struct device_node *cb_nod
 		goto err_destroy;
 	}
 
-	ret = qda_cb_setup_device(qdev, &pdev->dev);
+	ret = qda_cb_setup_device(qdev, &pdev->dev, sid);
 	if (ret)
 		goto err_destroy;
 
drivers/accel/qda/qda_drv.c +3 -1
index e722135238f3..37019ff38ecb 100644
@@ -58,10 +58,12 @@ DEFINE_DRM_ACCEL_FOPS(qda_accel_fops);
 
 static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_QUERY, qda_ioctl_query, 0),
+	DRM_IOCTL_DEF_DRV(QDA_GEM_CREATE, qda_ioctl_gem_create, 0),
+	DRM_IOCTL_DEF_DRV(QDA_GEM_MMAP_OFFSET, qda_ioctl_gem_mmap_offset, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
-	.driver_features = DRIVER_COMPUTE_ACCEL,
+	.driver_features = DRIVER_GEM | DRIVER_COMPUTE_ACCEL,
 	.fops = &qda_accel_fops,
 	.open = qda_open,
 	.postclose = qda_postclose,
drivers/accel/qda/qda_gem.c new file +108 -0

108 lines not on this page · raw message

drivers/accel/qda/qda_gem.h new file +44 -0

44 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.c +45 -0

56 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +2 -0
index b8fd536a111f..d1cbbfb6d965 100644
@@ -9,5 +9,7 @@
 #include "qda_drv.h"
 
 int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv);
 
 #endif /* __QDA_IOCTL_H__ */
drivers/accel/qda/qda_memory_dma.c new file +72 -0

72 lines not on this page · raw message

drivers/accel/qda/qda_memory_dma.h new file +20 -0
new file mode 100644
index 000000000000..f4d76b806df6
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_MEMORY_DMA_H__
+#define __QDA_MEMORY_DMA_H__
+
+#include <linux/mm_types.h>
+#include <linux/types.h>
+
+struct qda_gem_obj;
+struct qda_iommu_device;
+
+int qda_dma_alloc(struct qda_iommu_device *iommu_dev,
+		  struct qda_gem_obj *gem_obj, size_t size);
+void qda_dma_free(struct qda_gem_obj *gem_obj);
+int qda_dma_mmap(struct qda_gem_obj *gem_obj, struct vm_area_struct *vma);
+
+#endif /* __QDA_MEMORY_DMA_H__ */
drivers/accel/qda/qda_memory_manager.c +37 -0
index 87e8d3485e81..ccab85c17203 100644
@@ -5,6 +5,8 @@
 #include <linux/spinlock.h>
 #include <drm/drm_print.h>
 #include "qda_drv.h"
+#include "qda_gem.h"
+#include "qda_memory_dma.h"
 #include "qda_memory_manager.h"
 
 static void iommu_device_free(struct kref *ref)
@@ -86,6 +88,41 @@ void qda_memory_manager_release_device(struct qda_iommu_device *iommu_dev)
 	kref_put(&iommu_dev->ref, iommu_device_free);
 }
 
+/**
+ * qda_memory_manager_alloc() - Allocate a buffer for a session
+ * @mem_mgr: Memory manager of the device
+ * @gem_obj: GEM object to back with the allocation
+ * @session: Session the buffer belongs to
+ *
+ * Allocates from the context bank the session owns, assigning it one on its
+ * first allocation.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_memory_manager_alloc(struct qda_memory_manager *mem_mgr, struct qda_gem_obj *gem_obj,
+			     struct qda_session *session)
+{
+	int ret;
+
+	ret = qda_memory_manager_assign_device(mem_mgr, session);
+	if (ret)
+		return ret;
+
+	return qda_dma_alloc(session->assigned_iommu_dev, gem_obj, gem_obj->size);
+}
+
+/**
+ * qda_memory_manager_free() - Release a buffer
+ * @gem_obj: GEM object to free
+ *
+ * The buffer remembers the context bank it was allocated from, so no
+ * memory manager is needed here.
+ */
+void qda_memory_manager_free(struct qda_gem_obj *gem_obj)
+{
+	qda_dma_free(gem_obj);
+}
+
 /**
  * qda_memory_manager_register_device() - Add a context bank to the registry
  * @mem_mgr: Memory manager of the device
drivers/accel/qda/qda_memory_manager.h +8 -0
index 13354158674b..d2ca6682d58b 100644
@@ -13,6 +13,7 @@
 #include <linux/types.h>
 
 struct qda_dev;
+struct qda_gem_obj;
 struct qda_session;
 
 /**
@@ -31,6 +32,8 @@ struct qda_iommu_device {
 	struct device *dev;
 	/** @qdev: Back-pointer to the parent QDA device */
 	struct qda_dev *qdev;
+	/** @sid: Stream ID the DSP identifies this context bank by */
+	u32 sid;
 	/** @assigned_session: Session owning this device, or NULL if free */
 	struct qda_session *assigned_session;
 	/** @ref: Reference count controlling when the object is freed */
@@ -66,4 +69,9 @@ int qda_memory_manager_assign_device(struct qda_memory_manager *mem_mgr,
 				     struct qda_session *session);
 void qda_memory_manager_release_device(struct qda_iommu_device *iommu_dev);
 
+int qda_memory_manager_alloc(struct qda_memory_manager *mem_mgr,
+			     struct qda_gem_obj *gem_obj,
+			     struct qda_session *session);
+void qda_memory_manager_free(struct qda_gem_obj *gem_obj);
+
 #endif /* __QDA_MEMORY_MANAGER_H__ */
drivers/accel/qda/qda_soc.c +2 -0
index 323b620b8adb..fb316b74103d 100644
@@ -5,11 +5,13 @@
 #include "qda_soc.h"
 
 const struct qda_soc_data kaanapali_soc_data = {
+	.sid_pos = 56,
 	.dma_addr_bits_cdsp = 34,
 	.dma_addr_bits_default = 32,
 };
 
 const struct qda_soc_data default_soc_data = {
+	.sid_pos = 32,
 	.dma_addr_bits_cdsp = 32,
 	.dma_addr_bits_default = 32,
 };
drivers/accel/qda/qda_soc.h +28 -0
index b052964f11ae..fb5b786d0928 100644
@@ -6,6 +6,8 @@
 #ifndef __QDA_SOC_H__
 #define __QDA_SOC_H__
 
+#include <linux/bits.h>
+#include <linux/dma-mapping.h>
 #include <linux/types.h>
 
 struct qda_dev;
@@ -16,6 +18,8 @@ struct qda_dev;
  * Selected by OF match on the fastrpc node; see qda_rpmsg.c.
  */
 struct qda_soc_data {
+	/** @sid_pos: Bit position of the stream ID in DSP-visible addresses */
+	u32 sid_pos;
 	/** @dma_addr_bits_cdsp: DMA address width of CDSP context banks */
 	u32 dma_addr_bits_cdsp;
 	/** @dma_addr_bits_default: DMA address width of all other context banks */
@@ -27,4 +31,28 @@ extern const struct qda_soc_data default_soc_data;
 
 u32 qda_soc_dma_addr_bits(const struct qda_dev *qdev);
 
+/**
+ * qda_soc_sid_offset() - Stream ID bits to fold into a DSP-visible address
+ * @soc_data: SoC parameters of the device
+ * @sid: Stream ID of the context bank
+ *
+ * Return: @sid shifted to its SoC-specific position
+ */
+static inline u64 qda_soc_sid_offset(const struct qda_soc_data *soc_data, u32 sid)
+{
+	return (u64)sid << soc_data->sid_pos;
+}
+
+/**
+ * qda_soc_dma_addr() - Recover the DMA API address from a DSP-visible address
+ * @soc_data: SoC parameters of the device
+ * @iova: Address with the stream ID folded in
+ *
+ * Return: @iova with the stream ID bits cleared
+ */
+static inline dma_addr_t qda_soc_dma_addr(const struct qda_soc_data *soc_data, u64 iova)
+{
+	return iova & GENMASK_ULL(soc_data->sid_pos - 1, 0);
+}
+
 #endif /* __QDA_SOC_H__ */
include/uapi/drm/qda_accel.h +37 -1
index b9dc4787bd42..8e64480cdd82 100644
@@ -12,10 +12,16 @@
 extern "C" {
 #endif
 
-#define DRM_QDA_QUERY		0x00
+#define DRM_QDA_QUERY			0x00
+#define DRM_QDA_GEM_CREATE		0x01
+#define DRM_QDA_GEM_MMAP_OFFSET		0x02
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
+#define DRM_IOCTL_QDA_GEM_CREATE	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_GEM_CREATE, \
+					 struct drm_qda_gem_create)
+#define DRM_IOCTL_QDA_GEM_MMAP_OFFSET	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_GEM_MMAP_OFFSET, \
+					 struct drm_qda_gem_mmap_offset)
 
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
@@ -34,6 +40,36 @@ struct drm_qda_query {
 	__u8 dsp_name[16];
 };
 
+/**
+ * struct drm_qda_gem_create - Buffer allocation parameters
+ * @size: Size of the buffer to allocate in bytes (input)
+ * @handle: Handle of the new buffer (output)
+ * @pad: Must be zero
+ *
+ * Allocates a buffer the DSP can access and returns a handle to it. The
+ * handle identifies the buffer in later ioctls, and the buffer is released
+ * when the last handle to it is closed.
+ */
+struct drm_qda_gem_create {
+	__u64 size;
+	__u32 handle;
+	__u32 pad;
+};
+
+/**
+ * struct drm_qda_gem_mmap_offset - Buffer mmap offset query
+ * @offset: Offset to pass to mmap() (output)
+ * @handle: Handle of the buffer (input)
+ * @pad: Must be zero
+ *
+ * Returns the offset at which a buffer can be mmap()ed on the accel node.
+ */
+struct drm_qda_gem_mmap_offset {
+	__u64 offset;
+	__u32 handle;
+	__u32 pad;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#18 lore raw

Re: [PATCH v4 06/25] accel/qda: Add buffer allocation for the DSP

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider: - [Critical] accel/qda: UAF and context bank reuse when qda_gem_obj outlives qda_session - [High]…

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [Critical] accel/qda: UAF and context bank reuse when qda_gem_obj outlives qda_session
- [High] accel/qda: missing vm_ops in qda_gem_object_funcs leaks GEM buffers on mmap
- [High] accel/qda: UAF on iommu_dev->dev in qda_dma_free() after qda_cb_unpopulate()
- [High] accel/qda: missing drm_dev_enter() in qda_ioctl_gem_create() races with unplug
- [Medium] accel/qda: 32-bit dma_addr_t in qda_gem_obj truncates stream ID bits
- [Medium] accel/qda: 32-bit size_t truncation of args->size in qda_ioctl_gem_create()
- [Medium] accel/qda: coherent_dma_mask left at 32 bits in qda_create_cb_device() --

commit 28e778f1e5490a2de285e597396980427ca74098
Author: Ekansh Gupta <[email protected]>

accel/qda: Add buffer allocation for the DSP

This commit adds GEM buffer allocation and mmap offset ioctls for the QDA DSP accelerator driver, backing buffers with coherent DMA allocations from per-session IOMMU context banks and folding the context bank stream ID into DSP-visible addresses.

Quoted 6 lines
drivers/accel/qda/qda_gem.c new file +0 -0
new file mode 100644
index 0000000000000..864bac914e48d
@@ -0,0 +1,108 @@

[ ... ]

Quoted 11 lines
drivers/accel/qda/qda_gem.c new file +11 -0
+static int qda_gem_mmap_obj(struct drm_gem_object *gem_obj, struct vm_area_struct *vma)
+{
+	struct qda_gem_obj *qda_gem_obj = to_qda_gem_obj(gem_obj);
+	int ret;
+
+	/* The fake offset only identifies the object, it does not index into it */
+	vma->vm_pgoff = 0;
+
+	ret = qda_dma_mmap(qda_gem_obj, vma);
+	if (ret)
+		return ret;

drivers/accel/qda/qda_gem.c line 42

Quoted 10 lines
drivers/accel/qda/qda_gem.c new file +10 -0
+
+	vm_flags_set(vma, VM_DONTEXPAND | VM_DONTDUMP);
+
+	return 0;
+}
+
+static const struct drm_gem_object_funcs qda_gem_object_funcs = {
+	.free = qda_gem_free_object,
+	.mmap = qda_gem_mmap_obj,
+};

[Severity: High]
Does qda_gem_object_funcs leak every mmap()ed qda_gem_obj and its coherent DMA buffer?

When user space maps a buffer on the accel fd, the call chain is:

  drm_gem_mmap()
    drm_gem_mmap_obj()
      drm_gem_object_get(obj);
      vma->vm_ops = obj->funcs->vm_ops;
      obj->funcs->mmap(obj, vma); -> qda_gem_mmap_obj()

drm_gem_mmap_obj() takes a reference on the GEM object for the VMA and relies on vma->vm_ops->close (normally drm_gem_vm_close) to drop that reference when the VMA is unmapped, as well as vma->vm_ops->open (drm_gem_vm_open) to take additional references when the VMA is forked or split.

Because qda_gem_object_funcs leaves .vm_ops unset and neither qda_gem_mmap_obj() nor qda_dma_mmap() initializes vma->vm_ops, drm_gem_vm_close() is never called on munmap() or process exit, so the GEM object reference count never drops to zero and qda_gem_free_object() never runs.

Should qda_gem_object_funcs set .vm_ops to a vm_operations_struct wired to drm_gem_vm_open and drm_gem_vm_close?

Quoted 6 lines
drivers/accel/qda/qda_gem.h new file +0 -0
new file mode 100644
index 0000000000000..af3b9dfc75099
@@ -0,0 +1,44 @@

[ ... ]

drivers/accel/qda/qda_gem.h line 28

Quoted 9 lines
drivers/accel/qda/qda_gem.h new file +9 -0
+struct qda_gem_obj {
+	/** @base: DRM GEM object base; must be first member */
+	struct drm_gem_object base;
+	/** @iommu_dev: Context bank this buffer is mapped into */
+	struct qda_iommu_device *iommu_dev;
+	/** @virt: Kernel virtual address of the buffer */
+	void *virt;
+	/** @dma_addr: Address of the buffer as the DSP sees it */
+	dma_addr_t dma_addr;

[Severity: Medium]
Can storing the DSP-visible address in dma_addr_t truncate the folded stream ID bits when dma_addr_t is 32 bits wide?

In drivers/accel/qda/qda_soc.h, qda_soc_sid_offset() returns a u64 with the stream ID shifted to soc_data->sid_pos (32 on default_soc_data and 56 on kaanapali_soc_data), and qda_soc_dma_addr() takes a u64 iova.

However, struct qda_gem_obj declares dma_addr as dma_addr_t. On 32-bit builds without CONFIG_ARCH_DMA_ADDR_T_64BIT (which are permitted since DRM_ACCEL_QDA depends on ARCH_QCOM || COMPILE_TEST), assigning:

gem_obj->dma_addr = dma_addr +

                      qda_soc_sid_offset(iommu_dev->qdev->soc_data,
                                         iommu_dev->sid);

in qda_dma_alloc() truncates the upper 32 bits and discards the stream ID. Should struct qda_gem_obj::dma_addr be u64 instead of dma_addr_t?

Quoted 4 lines
drivers/accel/qda/qda_ioctl.c +0 -0
index 380c2526d9d2c..b11743ab791e7 100644

[ ... ]

drivers/accel/qda/qda_ioctl.c int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_pr · line 52

Quoted 11 lines
drivers/accel/qda/qda_ioctl.c +11 -0
+int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(dev);
+	struct drm_qda_gem_create *args = data;
+	struct drm_gem_object *gem_obj;
+
+	if (args->pad)
+		return -EINVAL;
+
+	gem_obj = qda_gem_create_object(dev, qdev->iommu_mgr, args->size,
+					file_priv->driver_priv);

[Severity: High]
Can qda_ioctl_gem_create() race with qda_rpmsg_remove() because it does not guard hardware/manager access with drm_dev_enter() and drm_dev_exit()?

In drivers/accel/qda/qda_rpmsg.c, qda_rpmsg_remove() runs:

  drm_dev_unplug(&qdev->drm_dev);
  qda_cb_unpopulate(qdev);
  qda_deinit_device(qdev);
    qda_memory_manager_exit(qdev->iommu_mgr);

drm_ioctl() only performs a point-in-time drm_dev_is_unplugged() check before invoking the driver ioctl callback; drm_dev_unplug() only waits for readers inside a drm_dev_enter() / drm_dev_exit() section.

If qda_rpmsg_remove() runs right after a thread enters qda_ioctl_gem_create(), drm_dev_unplug() returns immediately and qda_cb_unpopulate() and qda_memory_manager_exit() can destroy the context bank devices and free qdev->iommu_mgr->devices concurrently with qda_gem_create_object() -> qda_memory_manager_alloc() -> qda_dma_alloc().

[Severity: Medium]
Also in qda_ioctl_gem_create(), can passing args->size (__u64) to qda_gem_create_object() (which takes size_t) silently truncate sizes >= 4 GiB on 32-bit builds?

When args->size exceeds SIZE_MAX on a 32-bit kernel (for example 0x100001000ULL), the implicit conversion to size_t narrows the value to its lower 32 bits before PAGE_ALIGN(size) is checked in qda_gem_create_object(), allocating a smaller buffer than requested instead of returning -EINVAL.

Quoted 6 lines
drivers/accel/qda/qda_memory_dma.c new file +0 -0
new file mode 100644
index 0000000000000..0f4715a55b9de
@@ -0,0 +1,72 @@

[ ... ]

drivers/accel/qda/qda_memory_dma.c line 37

Quoted 9 lines
drivers/accel/qda/qda_memory_dma.c new file +9 -0
+int qda_dma_alloc(struct qda_iommu_device *iommu_dev,
+		  struct qda_gem_obj *gem_obj, size_t size)
+{
+	dma_addr_t dma_addr;
+	void *virt;
+
+	virt = dma_alloc_coherent(iommu_dev->dev, size, &dma_addr, GFP_KERNEL);
+	if (!virt)
+		return -ENOMEM;

[Severity: Medium]
Are coherent allocations in qda_dma_alloc() restricted to a 32-bit DMA mask even on 34-bit CDSP context banks such as kaanapali_soc_data?

In drivers/accel/qda/qda_cb.c, qda_create_cb_device() configures the context bank platform device with:

  dma_bits = qda_soc_dma_addr_bits(qdev);
  ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(dma_bits));

Because of_platform_device_create() initializes pdev->dev.dma_mask to &pdev->platform_dma_mask and pdev->dev.coherent_dma_mask to DMA_BIT_MASK(32), calling dma_set_mask() only updates *pdev->dev.dma_mask and leaves pdev->dev.coherent_dma_mask at 32 bits.

When qda_dma_alloc() calls dma_alloc_coherent(), the IOMMU DMA allocator bounds the IOVA using iommu_dev->dev->coherent_dma_mask rather than *iommu_dev->dev->dma_mask, so 34-bit CDSP context banks on Kaanapali (where dma_addr_bits_cdsp is 34) remain limited to 32-bit IOVAs. Should qda_create_cb_device() call dma_set_mask_and_coherent() instead?

Quoted 6 lines
drivers/accel/qda/qda_memory_dma.c new file +6 -0
+	gem_obj->virt = virt;
+	gem_obj->dma_addr = dma_addr +
+			    qda_soc_sid_offset(iommu_dev->qdev->soc_data, iommu_dev->sid);
+	gem_obj->iommu_dev = iommu_dev;
+
+	return 0;

drivers/accel/qda/qda_memory_dma.c line 54

Quoted 10 lines
drivers/accel/qda/qda_memory_dma.c new file +10 -0
+}
+
+/**
+ * qda_dma_free() - Release a buffer back to its context bank
+ * @gem_obj: GEM object to free
+ */
+void qda_dma_free(struct qda_gem_obj *gem_obj)
+{
+	dma_free_coherent(gem_obj->iommu_dev->dev, gem_obj->size,
+			  gem_obj->virt, qda_gem_dma_addr(gem_obj));

[Severity: Critical]
What happens if a qda_gem_obj outlives the drm_file session that created it?

A GEM object can stay alive after its drm_file is closed, for example if user space exports it to a dma-buf fd via DRM_IOCTL_PRIME_HANDLE_TO_FD (enabled by DRIVER_GEM) or keeps a VMA mapped after closing the accel fd.

When the drm_file closes, the teardown path runs:

  qda_postclose()
    qda_session_free()
      qda_memory_manager_release_device(session->assigned_iommu_dev)
        iommu_dev->assigned_session = NULL;
        kref_put(&iommu_dev->ref, iommu_device_free);

Because qda_dma_alloc() stores gem_obj->iommu_dev without holding a reference on iommu_dev or keeping the context bank reserved while buffers are still allocated from it:

1. Setting iommu_dev->assigned_session = NULL allows qda_memory_manager_assign_device() to hand the same context bank to a new session while the previous session's DMA buffer is still mapped into that context bank's IOMMU domain.

2. If the context bank is also unregistered via qda_cb_teardown_device() -> qda_memory_manager_unregister_device(), iommu_dev->ref drops to zero and iommu_device_free() frees iommu_dev, causing a use-after-free in qda_dma_free() (and qda_dma_mmap()) when it later dereferences gem_obj->iommu_dev->dev and gem_obj->iommu_dev->qdev->soc_data.

[Severity: High]
Similarly, in qda_dma_free(), can gem_obj->iommu_dev->dev be a dangling pointer if the RPMsg device is removed while a user process still holds an open session or GEM buffer?

In drivers/accel/qda/qda_cb.c, qda_cb_unpopulate() runs during qda_rpmsg_remove():

  qda_cb_teardown_device(&entry->pdev->dev);
  of_platform_device_destroy(&entry->pdev->dev, NULL);

While struct qda_iommu_device itself is refcounted via iommu_dev->ref so that it can survive qda_cb_teardown_device(), of_platform_device_destroy() immediately unregisters and frees entry->pdev (which is iommu_dev->dev). When the user process later closes its fd or frees the GEM handle, qda_gem_free_object() -> qda_memory_manager_free() -> qda_dma_free() passes the freed gem_obj->iommu_dev->dev to dma_free_coherent().

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
#19 lore raw

PATCH v4 7/25 accel/qda: Add DMA-BUF import

In reply to Ekansh Gupta

Buffers the DSP works on often originate elsewhere - a camera frame to run inference on, a video surface to post-process. Importing those directly avoids a copy, so wire up the DRM PRIME import path.

A buffer has to be mapped into the context bank of the session that imports it, not into the parent device's address space, or the DSP would not be able to reach it under the session's stream ID. The core PRIME helper takes the device to attach to, so pass the session's context bank. It does not pass the calling file down to the import callback, so the session is published on the device for the duration of the call.

The DSP is told where a buffer is by a single address, so only buffers that map as one contiguous range can be described to it. Whether an exporter or an IOMMU produces a single range is not guaranteed, so check rather than assume: handing over a multi-range buffer as its first range alone would leave the DSP reading and writing past the end of it into whatever follows in the context bank. Contiguous imports, such as from the CMA DMA-BUF heap, are accepted; the rest are rejected.

Imported buffers own no memory, they only hold a mapping of the exporter's, so they are released by handing the mapping back rather than freeing anything, and cannot be mmap()ed through this driver.

---
 drivers/accel/qda/Makefile         |   1 +
 drivers/accel/qda/qda_drv.c        |   6 ++
 drivers/accel/qda/qda_drv.h        |  10 ++
 drivers/accel/qda/qda_gem.c        |  42 ++++++++-
 drivers/accel/qda/qda_gem.h        |  11 ++-
 drivers/accel/qda/qda_memory_dma.c |  13 +++
 drivers/accel/qda/qda_memory_dma.h |   2 +
 drivers/accel/qda/qda_prime.c      | 184 +++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_prime.h      |  25 +++++
 9 files changed, 291 insertions(+), 3 deletions(-)
drivers/accel/qda/Makefile +1 -0
index 2d3bcc3050eb..01c0319da900 100644
@@ -12,5 +12,6 @@ qda-y := \
 	qda_ioctl.o \
 	qda_memory_dma.o \
 	qda_memory_manager.o \
+	qda_prime.o \
 	qda_rpmsg.o \
 	qda_soc.o
drivers/accel/qda/qda_drv.c +6 -0
index 37019ff38ecb..5e953d1f98bf 100644
@@ -12,6 +12,7 @@
 
 #include "qda_drv.h"
 #include "qda_ioctl.h"
+#include "qda_prime.h"
 
 static struct qda_session *qda_session_alloc(struct qda_dev *qdev)
 {
@@ -69,6 +70,9 @@ static const struct drm_driver qda_drm_driver = {
 	.postclose = qda_postclose,
 	.ioctls = qda_ioctls,
 	.num_ioctls = ARRAY_SIZE(qda_ioctls),
+	.gem_prime_import = qda_gem_prime_import,
+	.gem_prime_import_sg_table = qda_gem_prime_import_sg_table,
+	.prime_fd_to_handle = qda_prime_fd_to_handle,
 	.name = QDA_DRIVER_NAME,
 	.desc = "Qualcomm DSP Accelerator Driver",
 };
@@ -82,6 +86,7 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 		return ERR_CAST(qdev);
 
 	INIT_LIST_HEAD(&qdev->cb_devs);
+	mutex_init(&qdev->import_lock);
 
 	return qdev;
 }
@@ -104,6 +109,7 @@ int qda_init_device(struct qda_dev *qdev, int num_cbs)
 void qda_deinit_device(struct qda_dev *qdev)
 {
 	qda_memory_manager_exit(qdev->iommu_mgr);
+	mutex_destroy(&qdev->import_lock);
 }
 
 int qda_register_device(struct qda_dev *qdev)
drivers/accel/qda/qda_drv.h +10 -0
index c852d3b05310..31aad9057e28 100644
@@ -8,6 +8,7 @@
 
 #include <linux/device.h>
 #include <linux/list.h>
+#include <linux/mutex.h>
 #include <linux/rpmsg.h>
 #include <linux/types.h>
 #include <drm/drm_device.h>
@@ -50,6 +51,15 @@ struct qda_dev {
 	struct list_head cb_devs;
 	/** @iommu_mgr: Registry of this device's context banks */
 	struct qda_memory_manager *iommu_mgr;
+	/** @import_lock: Serialises PRIME imports, protects @importing_session */
+	struct mutex import_lock;
+	/**
+	 * @importing_session: Session currently importing a buffer. Set only
+	 * while @import_lock is held, to pass the session down to the
+	 * .gem_prime_import callback, which the DRM core does not hand the
+	 * calling file to.
+	 */
+	struct qda_session *importing_session;
 	/** @soc_data: Per-SoC DSP integration parameters */
 	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
drivers/accel/qda/qda_gem.c +40 -2

67 lines not on this page · raw message

drivers/accel/qda/qda_gem.h +10 -1
index af3b9dfc7509..1dab494197aa 100644
@@ -6,6 +6,7 @@
 #define __QDA_GEM_H__
 
 #include <linux/dma-mapping.h>
+#include <linux/scatterlist.h>
 #include <drm/drm_device.h>
 #include <drm/drm_file.h>
 #include <drm/drm_gem.h>
@@ -16,14 +17,20 @@ struct qda_session;
 
 /**
  * struct qda_gem_obj - A buffer the DSP can access
+ *
+ * Either allocated by this driver or imported from another one. Use
+ * drm_gem_is_imported() on @base to tell which; imported buffers have no
+ * @virt and own no memory, they only hold a mapping of someone else's.
  */
 struct qda_gem_obj {
 	/** @base: DRM GEM object base; must be first member */
 	struct drm_gem_object base;
 	/** @iommu_dev: Context bank this buffer is mapped into */
 	struct qda_iommu_device *iommu_dev;
-	/** @virt: Kernel virtual address of the buffer */
+	/** @virt: Kernel virtual address, NULL for imported buffers */
 	void *virt;
+	/** @sgt: Mapping of an imported buffer, NULL otherwise */
+	struct sg_table *sgt;
 	/** @dma_addr: Address of the buffer as the DSP sees it */
 	dma_addr_t dma_addr;
 	/** @size: Size of the buffer in bytes */
@@ -36,6 +43,8 @@ struct qda_gem_obj {
  */
 #define to_qda_gem_obj(gem_obj) container_of(gem_obj, struct qda_gem_obj, base)
 
+extern const struct drm_gem_object_funcs qda_gem_object_funcs;
+
 struct drm_gem_object *qda_gem_create_object(struct drm_device *drm_dev,
 					     struct qda_memory_manager *iommu_mgr,
 					     size_t size, struct qda_session *session);
drivers/accel/qda/qda_memory_dma.c +13 -0
index 0f4715a55b9d..e7c5d540c0c1 100644
@@ -70,3 +70,16 @@ int qda_dma_mmap(struct qda_gem_obj *gem_obj, struct vm_area_struct *vma)
 	return dma_mmap_coherent(gem_obj->iommu_dev->dev, vma, gem_obj->virt,
 				 qda_gem_dma_addr(gem_obj), gem_obj->size);
 }
+
+/**
+ * qda_dma_get_sgtable() - Describe a buffer's pages for an importer
+ * @gem_obj: GEM object to describe
+ * @sgt: Table to fill in; the caller frees it with sg_free_table()
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_dma_get_sgtable(struct qda_gem_obj *gem_obj, struct sg_table *sgt)
+{
+	return dma_get_sgtable(gem_obj->iommu_dev->dev, sgt, gem_obj->virt,
+			       qda_gem_dma_addr(gem_obj), gem_obj->size);
+}
drivers/accel/qda/qda_memory_dma.h +2 -0
index f4d76b806df6..f1688d7a57ea 100644
@@ -9,6 +9,7 @@
 #include <linux/mm_types.h>
 #include <linux/types.h>
 
+struct sg_table;
 struct qda_gem_obj;
 struct qda_iommu_device;
 
@@ -16,5 +17,6 @@ int qda_dma_alloc(struct qda_iommu_device *iommu_dev,
 		  struct qda_gem_obj *gem_obj, size_t size);
 void qda_dma_free(struct qda_gem_obj *gem_obj);
 int qda_dma_mmap(struct qda_gem_obj *gem_obj, struct vm_area_struct *vma);
+int qda_dma_get_sgtable(struct qda_gem_obj *gem_obj, struct sg_table *sgt);
 
 #endif /* __QDA_MEMORY_DMA_H__ */
drivers/accel/qda/qda_prime.c new file +184 -0

184 lines not on this page · raw message

drivers/accel/qda/qda_prime.h new file +25 -0
new file mode 100644
index 000000000000..eb2d8cbc8cd3
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_PRIME_H__
+#define __QDA_PRIME_H__
+
+#include <drm/drm_device.h>
+#include <drm/drm_file.h>
+#include <drm/drm_gem.h>
+
+struct dma_buf;
+struct dma_buf_attachment;
+struct sg_table;
+
+struct drm_gem_object *qda_gem_prime_import_sg_table(struct drm_device *drm_dev,
+						     struct dma_buf_attachment *attach,
+						     struct sg_table *sgt);
+struct drm_gem_object *qda_gem_prime_import(struct drm_device *drm_dev,
+					    struct dma_buf *dma_buf);
+int qda_prime_fd_to_handle(struct drm_device *drm_dev, struct drm_file *file_priv,
+			   int prime_fd, u32 *handle);
+
+#endif /* __QDA_PRIME_H__ */
Signature · 1 line
2.34.1
#20 lore raw

Re: [PATCH v4 07/25] accel/qda: Add DMA-BUF import

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider: - [High] accel/qda: unreferenced iommu_dev in qda_gem_obj causes UAF and CB reuse - [High] accel/qda: UAF…

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] accel/qda: unreferenced iommu_dev in qda_gem_obj causes UAF and CB reuse
- [High] accel/qda: UAF on CB device unplug in qda_gem_free_object() and import
- [High] accel/qda: missing vm_ops in qda_gem_object_funcs leaks GEM ref on mmap
- [Medium] accel/qda: 32-bit dma_addr_t truncates SID in qda_gem_prime_import_sg_table()
- [Low] accel/qda: unused gem_prime_import_sg_table hook and stale import comments
- [Low] accel/qda: qda_gem_prime_import_sg_table() allocates unused shmem file --

commit c21f00e936e2fb1a3bd269969e95dd625e477ff7
Author: Ekansh Gupta <[email protected]>

accel/qda: Add DMA-BUF import

This commit adds DMA-BUF PRIME import and sg_table export support to the QDA accelerator driver, mapping imported buffers into the importing session's IOMMU context bank and verifying that mappings are physically or IOVA contiguous.

Quoted 4 lines
drivers/accel/qda/qda_drv.c +0 -0
index 37019ff38ecbc..5e953d1f98bf4 100644

[ ... ]

drivers/accel/qda/qda_drv.c static const struct drm_driver qda_drm_driver = { · line 78

Quoted 10 lines
drivers/accel/qda/qda_drv.c +3 -0
@@ -69,6 +70,9 @@ static const struct drm_driver qda_drm_driver = {
 	.postclose = qda_postclose,
 	.ioctls = qda_ioctls,
 	.num_ioctls = ARRAY_SIZE(qda_ioctls),
+	.gem_prime_import = qda_gem_prime_import,
+	.gem_prime_import_sg_table = qda_gem_prime_import_sg_table,
+	.prime_fd_to_handle = qda_prime_fd_to_handle,
 	.name = QDA_DRIVER_NAME,
 	.desc = "Qualcomm DSP Accelerator Driver",
 };

[Severity: Low]
This isn't a bug, but is .gem_prime_import_sg_table in qda_drm_driver ever called by the DRM core?

In DRM PRIME, dev->driver->gem_prime_import_sg_table is only invoked by drm_gem_prime_import_dev(). However, qda_gem_prime_import() does not call drm_gem_prime_import_dev(); it open-codes dma_buf_attach() and dma_buf_map_attachment_unlocked() and calls qda_gem_prime_import_sg_table() directly.

The commit message ("The core PRIME helper takes the device to attach to, so pass the session's context bank") and the kernel-doc comment on qda_gem_prime_import_sg_table() ("Called by the DRM PRIME helpers once they have attached to the exporter...") also describe the drm_gem_prime_import_dev() flow rather than the open-coded path in qda_gem_prime_import(). Could qda_gem_prime_import() either call drm_gem_prime_import_dev(drm_dev, dma_buf, session->assigned_iommu_dev->dev) after handling the same-device check, or drop the unused hook and update the comments?

Quoted 4 lines
drivers/accel/qda/qda_gem.c +0 -0
index 864bac914e48d..a9ab8ccb78930 100644

[ ... ]

Quoted 22 lines
drivers/accel/qda/qda_gem.c +10 -1
@@ -24,6 +31,10 @@ static int qda_gem_mmap_obj(struct drm_gem_object *gem_obj, struct vm_area_struc
 	struct qda_gem_obj *qda_gem_obj = to_qda_gem_obj(gem_obj);
 	int ret;
 
+	/* Imported buffers have to be mapped through the exporter */
+	if (drm_gem_is_imported(gem_obj))
+		return -EINVAL;
+
 	/* The fake offset only identifies the object, it does not index into it */
 	vma->vm_pgoff = 0;
 
@@ -36,8 +47,35 @@ static int qda_gem_mmap_obj(struct drm_gem_object *gem_obj, struct vm_area_struc
 	return 0;
 }
 
-static const struct drm_gem_object_funcs qda_gem_object_funcs = {
+/*
+ * Lets another context bank map this buffer when a session imports the
+ * dma-buf this object was exported as: see qda_gem_prime_import().
+ */
+static struct sg_table *qda_gem_get_sg_table(struct drm_gem_object *gem_obj)
+{

[ ... ]

drivers/accel/qda/qda_gem.c static int qda_gem_mmap_obj(struct drm_gem_object *gem_obj, struct vm_area_struc · line 80

Quoted 8 lines
drivers/accel/qda/qda_gem.c +5 -0
+	return sgt;
+}
+
+const struct drm_gem_object_funcs qda_gem_object_funcs = {
 	.free = qda_gem_free_object,
+	.get_sg_table = qda_gem_get_sg_table,
 	.mmap = qda_gem_mmap_obj,
 };

[Severity: High]
Does mmap() on a QDA GEM object or a PRIME-exported QDA DMA-BUF leak the drm_gem_object reference and its coherent DMA buffer?

Both drm_gem_mmap_obj() (for DRM fd mmap) and drm_gem_dmabuf_mmap()->drm_gem_prime_mmap() (for PRIME-exported DMA-BUF mmap, now reachable because .get_sg_table is wired up) take a reference via drm_gem_object_get(obj), set vma->vm_ops = obj->funcs->vm_ops, and call obj->funcs->mmap(obj, vma).

Because qda_gem_object_funcs leaves .vm_ops unset and qda_gem_mmap_obj()->qda_dma_mmap()->dma_mmap_coherent() does not set vma->vm_ops either, vma->vm_ops remains NULL (and is replaced with dummy_vm_ops in mmap_region()). When the VMA is later unmapped, drm_gem_vm_close() is never called to drop the reference acquired at mmap time, so qda_gem_free_object() never runs and qda_gem_obj and its DMA memory are leaked.

Additionally, in the PRIME DMA-BUF mmap path, drm_gem_prime_mmap() adds drm_vma_node_start(&obj->vma_node) to vma->vm_pgoff before calling obj->funcs->mmap(). Resetting vma->vm_pgoff = 0 in qda_gem_mmap_obj() instead of subtracting drm_vma_node_start(&gem_obj->vma_node) discards any non-zero page offset passed to mmap() on the exported DMA-BUF fd.

Should qda_gem_object_funcs set .vm_ops to a vm_operations_struct with .open = drm_gem_vm_open and .close = drm_gem_vm_close, and adjust vma->vm_pgoff -= drm_vma_node_start(&gem_obj->vma_node) in qda_gem_mmap_obj()?

Quoted 4 lines
drivers/accel/qda/qda_gem.h +0 -0
index af3b9dfc75099..1dab494197aab 100644

[ ... ]

Quoted 11 lines
drivers/accel/qda/qda_gem.h +4 -0
@@ -16,14 +17,20 @@ struct qda_session;
 
 /**
  * struct qda_gem_obj - A buffer the DSP can access
+ *
+ * Either allocated by this driver or imported from another one. Use
+ * drm_gem_is_imported() on @base to tell which; imported buffers have no
+ * @virt and own no memory, they only hold a mapping of someone else's.
  */
 struct qda_gem_obj {
 	/** @base: DRM GEM object base; must be first member */

drivers/accel/qda/qda_gem.h struct qda_session; · line 35

Quoted 10 lines
drivers/accel/qda/qda_gem.h +3 -1
 	struct drm_gem_object base;
 	/** @iommu_dev: Context bank this buffer is mapped into */
 	struct qda_iommu_device *iommu_dev;
-	/** @virt: Kernel virtual address of the buffer */
+	/** @virt: Kernel virtual address, NULL for imported buffers */
 	void *virt;
+	/** @sgt: Mapping of an imported buffer, NULL otherwise */
+	struct sg_table *sgt;
 	/** @dma_addr: Address of the buffer as the DSP sees it */
 	dma_addr_t dma_addr;

[Severity: Medium]
Can qda_gem_obj->dma_addr truncate the stream ID offset on 32-bit CONFIG_COMPILE_TEST builds where dma_addr_t is 32 bits wide?

In qda_gem_prime_import_sg_table():

	qda_gem_obj->dma_addr = sg_dma_address(sgt->sgl) +
				qda_soc_sid_offset(qdev->soc_data, iommu_dev->sid);

qda_soc_sid_offset() returns a u64 with sid shifted left by 32 or 56 bits, and qda_soc_dma_addr() takes a u64 iova, but struct qda_gem_obj stores dma_addr as dma_addr_t. When !CONFIG_ARCH_DMA_ADDR_T_64BIT, storing the sum into dma_addr_t drops the upper 32 bits containing the SID. Should dma_addr in struct qda_gem_obj be u64?

Quoted 6 lines
drivers/accel/qda/qda_prime.c new file +0 -0
new file mode 100644
index 0000000000000..7a709e3000d8b
@@ -0,0 +1,184 @@

[ ... ]

Quoted 22 lines
drivers/accel/qda/qda_prime.c new file +22 -0
+struct drm_gem_object *qda_gem_prime_import_sg_table(struct drm_device *drm_dev,
+						     struct dma_buf_attachment *attach,
+						     struct sg_table *sgt)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(drm_dev);
+	size_t size = PAGE_ALIGN(attach->dmabuf->size);
+	struct qda_iommu_device *iommu_dev;
+	struct qda_gem_obj *qda_gem_obj;
+	int ret;
+
+	if (drm_prime_get_contiguous_size(sgt) < size) {
+		drm_dbg_driver(drm_dev, "Rejecting non-contiguous buffer of %zu bytes\n", size);
+		return ERR_PTR(-EINVAL);
+	}
+
+	/*
+	 * The buffer was mapped into the context bank of the session doing the
+	 * import, so that is the bank the DSP has to be told about.
+	 */
+	iommu_dev = dev_get_drvdata(attach->dev);
+	if (!iommu_dev)
+		return ERR_PTR(-ENODEV);

drivers/accel/qda/qda_prime.c line 65

Quoted 10 lines
drivers/accel/qda/qda_prime.c new file +10 -0
+
+	qda_gem_obj = kzalloc_obj(*qda_gem_obj);
+	if (!qda_gem_obj)
+		return ERR_PTR(-ENOMEM);
+
+	ret = drm_gem_object_init(drm_dev, &qda_gem_obj->base, size);
+	if (ret) {
+		kfree(qda_gem_obj);
+		return ERR_PTR(ret);
+	}

[Severity: Low]
Does an imported DMA-BUF GEM object need shmem backing via drm_gem_object_init()?

drm_gem_object_init() calls drm_gem_private_object_init() and then allocates a shmem file of size bytes via shmem_file_setup(). Since imported DMA-BUF objects are backed by the exporter's memory rather than shmem (and also if shmem_file_setup() fails, kfree(qda_gem_obj) is called without drm_gem_private_object_fini() to clean up _resv and gpuva.lock), should qda_gem_prime_import_sg_table() call drm_gem_private_object_init() instead?

Quoted 11 lines
drivers/accel/qda/qda_prime.c new file +11 -0
+
+	qda_gem_obj->base.funcs = &qda_gem_object_funcs;
+	qda_gem_obj->size = size;
+	qda_gem_obj->sgt = sgt;
+	qda_gem_obj->iommu_dev = iommu_dev;
+	qda_gem_obj->dma_addr = sg_dma_address(sgt->sgl) +
+				qda_soc_sid_offset(qdev->soc_data, iommu_dev->sid);
+
+	return &qda_gem_obj->base;
+}
+

[ ... ]

Quoted 17 lines
drivers/accel/qda/qda_prime.c new file +17 -0
+struct drm_gem_object *qda_gem_prime_import(struct drm_device *drm_dev,
+					    struct dma_buf *dma_buf)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(drm_dev);
+	struct qda_session *session = qdev->importing_session;
+	struct dma_buf_attachment *attach;
+	struct drm_gem_object *obj;
+	struct sg_table *sgt;
+	int ret;
+
+	if (!session || !session->assigned_iommu_dev)
+		return ERR_PTR(-EINVAL);
+
+	if (drm_gem_is_prime_exported_dma_buf(drm_dev, dma_buf)) {
+		/*
+		 * A buffer this driver exported. The DRM core would hand back
+		 * the original object, which stays in the context bank of the

drivers/accel/qda/qda_prime.c line 118

Quoted 10 lines
drivers/accel/qda/qda_prime.c new file +10 -0
+		 * session that allocated it, while the DSP of this session
+		 * translates addresses in its own bank. Share the object only
+		 * within a bank, and map it again for any other.
+		 */
+		obj = dma_buf->priv;
+		if (to_qda_gem_obj(obj)->iommu_dev == session->assigned_iommu_dev) {
+			drm_gem_object_get(obj);
+			return obj;
+		}
+	}

[Severity: High]
What happens if a session exports a QDA GEM object as a DMA-BUF (now enabled via qda_gem_get_sg_table()) and then closes its DRM fd while the exported dma_buf fd remains open?

When the exporting session closes its DRM fd:

qda_postclose()

    -> qda_session_free()
      -> qda_memory_manager_release_device(session->assigned_iommu_dev)

qda_memory_manager_release_device() clears iommu_dev->assigned_session = NULL and drops the session's kref on iommu_dev, even though the exported qda_gem_obj is still alive and holds an unreferenced pointer in qda_gem_obj->iommu_dev (and remains mapped in that context bank's SMMU domain).

This appears to allow two failure modes:

1. If a second session opens the device, qda_memory_manager_assign_device() can assign that same iommu_dev to the new session while the first session's exported buffer is still mapped in that context bank. If the second session then imports the dma_buf, to_qda_gem_obj(obj)->iommu_dev == session->assigned_iommu_dev in qda_gem_prime_import() evaluates to true and returns the original non-imported GEM object (where drm_gem_is_imported(obj) is false), allowing the second session to mmap() the buffer through QDA as well as sharing the SMMU context bank before the old buffer is freed.

2. If the RPMsg device is removed after the exporting session closed its DRM fd while the exported dma_buf is still held open:

qda_rpmsg_remove()

    -> qda_cb_unpopulate()
      -> qda_cb_teardown_device()
        -> qda_memory_manager_unregister_device()

drops the remaining kref on iommu_dev and frees iommu_dev via kfree(). Any subsequent import/attach calling qda_gem_get_sg_table()->qda_dma_get_sgtable() or final release calling qda_gem_free_object()->qda_memory_manager_free()->qda_dma_free() will then dereference the freed qda_gem_obj->iommu_dev pointer.

Should each qda_gem_obj hold a reference on iommu_dev (and keep the context bank from being reassigned until all GEM allocations/mappings in it have been released)?

Quoted 4 lines
drivers/accel/qda/qda_prime.c new file +4 -0
+
+	attach = dma_buf_attach(dma_buf, session->assigned_iommu_dev->dev);
+	if (IS_ERR(attach))
+		return ERR_CAST(attach);

[ ... ]

Quoted 9 lines
drivers/accel/qda/qda_prime.c new file +9 -0
+int qda_prime_fd_to_handle(struct drm_device *drm_dev, struct drm_file *file_priv,
+			   int prime_fd, u32 *handle)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(drm_dev);
+	struct qda_session *session = file_priv->driver_priv;
+	int ret;
+
+	ret = qda_memory_manager_assign_device(qdev->iommu_mgr, session);
+	if (ret)

drivers/accel/qda/qda_prime.c line 182

Quoted 10 lines
drivers/accel/qda/qda_prime.c new file +10 -0
+		return ret;
+
+	guard(mutex)(&qdev->import_lock);
+
+	qdev->importing_session = session;
+	ret = drm_gem_prime_fd_to_handle(drm_dev, file_priv, prime_fd, handle);
+	qdev->importing_session = NULL;
+
+	return ret;
+}

[Severity: High]
Can qda_prime_fd_to_handle() and qda_gem_free_object() race with or access freed context bank devices after qda_rpmsg_remove()?

In qda_rpmsg_remove():

  drm_dev_unplug(&qdev->drm_dev);
  qda_cb_unpopulate(qdev);
  qda_deinit_device(qdev);

First, drm_ioctl_kernel() only checks drm_dev_is_unplugged() at entry and does not hold a drm_dev_enter()/drm_dev_exit() critical section. Because qda_prime_fd_to_handle() does not guard its body with drm_dev_enter()/drm_dev_exit(), drm_dev_unplug() will not wait for an in-flight import to complete before qda_cb_unpopulate() destroys the context bank platform devices and qda_deinit_device() frees qdev->iommu_mgr->devices and destroys qdev->import_lock.

Second, in qda_gem_prime_import(), dma_buf_attach(dma_buf, session->assigned_iommu_dev->dev) attaches the DMA-BUF to the context bank child platform_device (&entry->pdev->dev) without holding a reference that keeps the device registered. If qda_rpmsg_remove() runs while a user process has an open session with imported (or allocated) GEM objects, qda_cb_unpopulate() calls of_platform_device_destroy(&entry->pdev->dev, NULL), which unregisters and frees entry->pdev. When the user process later closes its fd, qda_gem_free_object() calls:

drm_prime_gem_destroy(gem_obj, qda_gem_obj->sgt)

    -> dma_buf_unmap_attachment_unlocked()
      -> dma_unmap_sgtable(attach->dev, ...)

(or qda_dma_free() -> dma_free_coherent(gem_obj->iommu_dev->dev, ...)), dereferencing the unregistered and freed context bank struct device.

Should qda_prime_fd_to_handle() use drm_dev_enter()/drm_dev_exit(), and should the context bank device lifetime be tied to active GEM objects/attachments or managed via drmm so it outlives open file descriptors and GEM objects?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=7
#21 lore raw

PATCH v4 8/25 accel/qda: Add legacy ops base and auxiliary device

In reply to Ekansh Gupta

The /dev/fastrpc-* interface of the driver this one replaces is to be served by a separate compatibility module, so existing user space keeps working. That module needs two things from here: a device per DSP domain to bind to, and a way to use the same session and buffer handling as the accel UAPI rather than reimplementing it.

Create an auxiliary device per domain for it to bind, rather than keeping a private registry and a notifier callback. Domains appear as each one's rpmsg channel probes and the module may load at any point in that, which is exactly what the driver core already handles: it matches devices to the driver whichever order the two arrive in, and autoloads the module from the device's modalias. The module is optional, so failing to create the device is not fatal to the accel interface.

The device is destroyed explicitly on rpmsg remove rather than through devres, so that the legacy nodes are taken away before the DRM device is unplugged and nothing can open one during teardown.

Collect the entry points the module needs into one table exported only to it, so this driver does not depend on the module. Start with session create/destroy and DMA-BUF allocation/import; further ops arrive with the code that uses them.

A session the module creates has no DRM file behind it, so nothing else holds the DRM device. Take a reference when it is created, or the device could be freed on rpmsg remove while a legacy file is still open. For the same reason, buffers are exported and imported as DMA-BUFs without a GEM handle, and struct qda_session gains a ->file that is NULL for such sessions. Code that resolves handles can only be reached from the accel UAPI.

---
 drivers/accel/qda/Kconfig          |  1 +
 drivers/accel/qda/Makefile         |  1 +
 drivers/accel/qda/qda_drv.c        | 48 +++++++++++++++++++++--
 drivers/accel/qda/qda_drv.h        | 31 +++++++++++++--
 drivers/accel/qda/qda_legacy_ops.c | 80 ++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_legacy_ops.h | 41 +++++++++++++++++++
 drivers/accel/qda/qda_prime.c      | 33 ++++++++++++++++
 drivers/accel/qda/qda_prime.h      |  4 ++
 drivers/accel/qda/qda_rpmsg.c      |  3 ++
 9 files changed, 235 insertions(+), 7 deletions(-)
drivers/accel/qda/Kconfig +1 -0
index e2f3f01016c6..c174f9edf2d5 100644
@@ -8,6 +8,7 @@ config DRM_ACCEL_QDA
 	depends on DRM_ACCEL
 	depends on ARCH_QCOM || COMPILE_TEST
 	depends on RPMSG
+	select AUXILIARY_BUS
 	help
 	  Enables the DRM-based accelerator driver for Qualcomm's Hexagon DSPs.
 	  This driver provides a standardized interface for offloading computational
drivers/accel/qda/Makefile +1 -0
index 01c0319da900..a867a888c5f5 100644
@@ -10,6 +10,7 @@ qda-y := \
 	qda_drv.o \
 	qda_gem.o \
 	qda_ioctl.o \
+	qda_legacy_ops.o \
 	qda_memory_dma.o \
 	qda_memory_manager.o \
 	qda_prime.o \
drivers/accel/qda/qda_drv.c +44 -4

85 lines not on this page · raw message

drivers/accel/qda/qda_drv.h +28 -3
index 31aad9057e28..c04f19e5a91b 100644
@@ -6,6 +6,7 @@
 #ifndef __QDA_DRV_H__
 #define __QDA_DRV_H__
 
+#include <linux/auxiliary_bus.h>
 #include <linux/device.h>
 #include <linux/list.h>
 #include <linux/mutex.h>
@@ -20,15 +21,29 @@
 /* Driver identification */
 #define QDA_DRIVER_NAME "qda"
 
+/*
+ * Name of the auxiliary device the legacy FastRPC module binds to, one per
+ * DSP domain. Combined with this module's name it forms the match name
+ * "qda.fastrpc", which that module lists in its id_table.
+ */
+#define QDA_LEGACY_AUXDEV_NAME "fastrpc"
+
 /**
- * struct qda_session - Per-open-file private data
+ * struct qda_session - One user of a DSP
  *
- * Allocated in qda_open() and stored in drm_file::driver_priv.
+ * Created either by the accel UAPI, one per open DRM file, or by the legacy
+ * FastRPC compatibility shim, one per open /dev/fastrpc-* file with no DRM
+ * file behind it at all. The core is keyed on this rather than on struct
+ * drm_file so that it can serve both.
  */
 struct qda_session {
 	/** @qda_dev: Back-pointer to device structure */
 	struct qda_dev *qda_dev;
-	/** @file: The drm_file this session belongs to */
+	/**
+	 * @file: The DRM file this session belongs to, or NULL for a session
+	 * the legacy shim created. Anything that resolves a buffer handle
+	 * needs this, so those paths are only reachable from the accel UAPI.
+	 */
 	struct drm_file *file;
 	/** @assigned_iommu_dev: Context bank owned by this session, or NULL */
 	struct qda_iommu_device *assigned_iommu_dev;
@@ -64,6 +79,11 @@ struct qda_dev {
 	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
 	const char *dsp_name;
+	/**
+	 * @legacy_auxdev: Auxiliary device the legacy FastRPC module binds
+	 * to, or NULL if it could not be created
+	 */
+	struct auxiliary_device *legacy_auxdev;
 };
 
 /**
@@ -84,5 +104,10 @@ struct qda_dev *qda_alloc_device(struct device *dev);
 int qda_init_device(struct qda_dev *qdev, int num_cbs);
 void qda_deinit_device(struct qda_dev *qdev);
 int qda_register_device(struct qda_dev *qdev);
+void qda_unregister_device(struct qda_dev *qdev);
+
+/* Sessions, shared with the legacy shim */
+struct qda_session *qda_session_alloc(struct qda_dev *qdev);
+void qda_session_free(struct qda_session *session);
 
 #endif /* __QDA_DRV_H__ */
drivers/accel/qda/qda_legacy_ops.c new file +80 -0

80 lines not on this page · raw message

drivers/accel/qda/qda_legacy_ops.h new file +41 -0

41 lines not on this page · raw message

drivers/accel/qda/qda_prime.c +33 -0
index 7a709e3000d8..128d4ab2cf64 100644
@@ -181,4 +181,37 @@ int qda_prime_fd_to_handle(struct drm_device *drm_dev, struct drm_file *file_pri
 	return ret;
 }
 
+/**
+ * qda_prime_import_dmabuf() - Import a DMA-BUF without installing a handle
+ * @drm_dev: DRM device
+ * @session: Session to import into
+ * @dma_buf: Buffer to import
+ *
+ * As qda_prime_fd_to_handle(), but stopping at the GEM object: a session the
+ * legacy shim created has no DRM file to install a handle in. The caller owns
+ * the returned reference.
+ *
+ * Return: The new GEM object on success, ERR_PTR on failure
+ */
+struct drm_gem_object *qda_prime_import_dmabuf(struct drm_device *drm_dev,
+					       struct qda_session *session,
+					       struct dma_buf *dma_buf)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(drm_dev);
+	struct drm_gem_object *gem_obj;
+	int ret;
+
+	ret = qda_memory_manager_assign_device(qdev->iommu_mgr, session);
+	if (ret)
+		return ERR_PTR(ret);
+
+	guard(mutex)(&qdev->import_lock);
+
+	qdev->importing_session = session;
+	gem_obj = qda_gem_prime_import(drm_dev, dma_buf);
+	qdev->importing_session = NULL;
+
+	return gem_obj;
+}
+
 MODULE_IMPORT_NS("DMA_BUF");
drivers/accel/qda/qda_prime.h +4 -0
index eb2d8cbc8cd3..c5e2f7ffa882 100644
@@ -12,6 +12,7 @@
 
 struct dma_buf;
 struct dma_buf_attachment;
+struct qda_session;
 struct sg_table;
 
 struct drm_gem_object *qda_gem_prime_import_sg_table(struct drm_device *drm_dev,
@@ -21,5 +22,8 @@ struct drm_gem_object *qda_gem_prime_import(struct drm_device *drm_dev,
 					    struct dma_buf *dma_buf);
 int qda_prime_fd_to_handle(struct drm_device *drm_dev, struct drm_file *file_priv,
 			   int prime_fd, u32 *handle);
+struct drm_gem_object *qda_prime_import_dmabuf(struct drm_device *drm_dev,
+					       struct qda_session *session,
+					       struct dma_buf *dma_buf);
 
 #endif /* __QDA_PRIME_H__ */
drivers/accel/qda/qda_rpmsg.c +3 -0
index 2c9d79937b10..14034e250555 100644
@@ -50,6 +50,9 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 {
 	struct qda_dev *qdev = dev_get_drvdata(&rpdev->dev);
 
+	/* Take the legacy nodes away first, so nothing new opens during teardown */
+	qda_unregister_device(qdev);
+
 	/*
 	 * Unplug before clearing @rpdev: once there are ioctls that reach the
 	 * DSP, this is what stops a new one from starting and waits for the
Signature · 1 line
2.34.1
#22 lore raw

Re: [PATCH v4 08/25] accel/qda: Add legacy ops base and auxiliary device

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] accel/qda: use-after-free of iommu_dev when DMA-BUF outlives session - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] accel/qda: use-after-free of iommu_dev when DMA-BUF outlives session
- [High] accel/qda: use-after-free in legacy ops after qda_rpmsg_remove()
- [High] accel/qda: GEM object leak on mmap() due to missing vm_ops
- [Low] accel/qda: unnecessary export of qda_session_alloc() and qda_session_free() --

commit c10272995846f83dffe56b2be6fa2691d606459a
Author: Ekansh Gupta <[email protected]>

accel/qda: Add legacy ops base and auxiliary device

This commit creates an auxiliary device per DSP domain for the legacy FastRPC compatibility module to bind to and exports a table of operations (qda_legacy_ops_table) for session lifecycle and DMA-BUF allocation and import. Sessions created by the legacy module hold a drm_device reference and operate on DMA-BUFs and GEM objects directly without a drm_file.

Quoted 34 lines
drivers/accel/qda/qda_drv.c +10 -2
index 5e953d1f98bf4..21113deceaadb 100644
@@ -14,7 +15,13 @@
 #include "qda_ioctl.h"
 #include "qda_prime.h"
 
-static struct qda_session *qda_session_alloc(struct qda_dev *qdev)
+/**
+ * qda_session_alloc() - Create a session on a device
+ * @qdev: Device to create the session on
+ *
+ * Return: The new session, or ERR_PTR on failure
+ */
+struct qda_session *qda_session_alloc(struct qda_dev *qdev)
 {
 	struct qda_session *session;
 
@@ -26,6 +33,7 @@ static struct qda_session *qda_session_alloc(struct qda_dev *qdev)
 
 	return session;
 }
+EXPORT_SYMBOL_FOR_MODULES(qda_session_alloc, "qda_fastrpc_shim");
 
 static int qda_open(struct drm_device *dev, struct drm_file *file)
 {
@@ -42,13 +50,18 @@ static int qda_open(struct drm_device *dev, struct drm_file *file)
 	return 0;
 }
 
-static void qda_session_free(struct qda_session *session)
+/**
+ * qda_session_free() - Release a session and whatever it still holds

drivers/accel/qda/qda_drv.c static int qda_open(struct drm_device *dev, struct drm_file *file) · line 64

Quoted 10 lines
drivers/accel/qda/qda_drv.c +4 -0
+ * @session: Session to release
+ */
+void qda_session_free(struct qda_session *session)
 {
 	if (session->assigned_iommu_dev)
 		qda_memory_manager_release_device(session->assigned_iommu_dev);
 
 	kfree(session);
 }
+EXPORT_SYMBOL_FOR_MODULES(qda_session_free, "qda_fastrpc_shim");

[Severity: Low]
This isn't a bug, but why are qda_session_alloc() and qda_session_free() exported with EXPORT_SYMBOL_FOR_MODULES(..., "qda_fastrpc_shim") in drivers/accel/qda/qda_drv.c?

Since qda_legacy_ops.o is linked into the same qda module via qda-y in drivers/accel/qda/Makefile and exposes session creation and destruction through qda_legacy_ops_table (where legacy_session_create() and legacy_session_destroy() also manage the drm_dev reference), does qda_fastrpc_shim ever need to call the raw qda_session_alloc() and qda_session_free() helpers directly?

[ ... ]

Quoted 6 lines
drivers/accel/qda/qda_legacy_ops.c new file +0 -0
new file mode 100644
index 0000000000000..3566a10c64880
@@ -0,0 +1,80 @@

[ ... ]

Quoted 8 lines
drivers/accel/qda/qda_legacy_ops.c new file +8 -0
+static struct qda_session *legacy_session_create(struct qda_dev *qdev)
+{
+	struct qda_session *session;
+
+	session = qda_session_alloc(qdev);
+	if (!IS_ERR(session))
+		drm_dev_get(&qdev->drm_dev);
+

drivers/accel/qda/qda_legacy_ops.c line 38

Quoted 10 lines
drivers/accel/qda/qda_legacy_ops.c new file +10 -0
+	return session;
+}
+
+static void legacy_session_destroy(struct qda_session *session)
+{
+	struct drm_device *drm_dev = &session->qda_dev->drm_dev;
+
+	qda_session_free(session);
+	drm_dev_put(drm_dev);
+}

[Severity: High]
What happens if a DMA-BUF allocated by legacy_alloc_dmabuf() (or a GEM object imported by legacy_import_dmabuf()) outlives the session when legacy_session_destroy() is called in drivers/accel/qda/qda_legacy_ops.c? Note that on normal process exit, close_files() closes file descriptors in ascending order, so an earlier /dev/fastrpc-* fd will be closed before DMA-BUF fds allocated later from that session.

When legacy_alloc_dmabuf() creates a buffer and exports it via drm_gem_prime_export(), the resulting dma_buf holds a reference to gem_obj and qdev->drm_dev, and gem_obj->iommu_dev points to session->assigned_iommu_dev. However, neither qda_dma_alloc() nor qda_gem_prime_import_sg_table() takes a reference on iommu_dev->ref or on the session.

When legacy_session_destroy() runs while the exported DMA-BUF is still alive:

legacy_session_destroy()
-> qda_session_free()

       -> qda_memory_manager_release_device(session->assigned_iommu_dev)
            iommu_dev->assigned_session = NULL;
            kref_put(&iommu_dev->ref, iommu_device_free);

First, clearing iommu_dev->assigned_session allows assign_available_device() in drivers/accel/qda/qda_memory_manager.c to hand the same context bank to a new session while the old session's DMA-BUF remains allocated and mapped in that context bank's SMMU domain, and causes qda_gem_prime_import() (which checks to_qda_gem_obj(obj)->iommu_dev == session->assigned_iommu_dev) to treat the previous session's buffer as belonging to the new session.

Second, if qda_rpmsg_remove() subsequently runs and unregisters the context bank via qda_cb_unpopulate() -> qda_cb_teardown_device() -> qda_memory_manager_unregister_device(), iommu_dev->ref drops to zero and iommu_device_free() frees struct qda_iommu_device. When the DMA-BUF fd is later closed, drm_gem_dmabuf_release() -> qda_gem_free_object() -> qda_memory_manager_free() -> qda_dma_free() in drivers/accel/qda/qda_memory_dma.c dereferences the freed gem_obj->iommu_dev:

void qda_dma_free(struct qda_gem_obj *gem_obj)

{
	dma_free_coherent(gem_obj->iommu_dev->dev, gem_obj->size,
			  gem_obj->virt, qda_gem_dma_addr(gem_obj));

Can this lead to a use-after-free of gem_obj->iommu_dev as well as premature context bank reuse?

Quoted 14 lines
drivers/accel/qda/qda_legacy_ops.c new file +14 -0
+
+/*
+ * As the GEM_CREATE ioctl, but exporting a DMA-BUF rather than installing a
+ * handle: a session the shim created has no DRM file to hold handles, and the
+ * legacy interface hands buffers to user space as file descriptors anyway.
+ */
+static struct dma_buf *legacy_alloc_dmabuf(struct qda_session *session, u64 size)
+{
+	struct qda_dev *qdev = session->qda_dev;
+	struct drm_gem_object *gem_obj;
+	struct dma_buf *dmabuf;
+
+	gem_obj = qda_gem_create_object(&qdev->drm_dev, qdev->iommu_mgr, size, session);
+	if (IS_ERR(gem_obj))

drivers/accel/qda/qda_legacy_ops.c line 62

Quoted 10 lines
drivers/accel/qda/qda_legacy_ops.c new file +10 -0
+		return ERR_CAST(gem_obj);
+
+	/* O_RDWR so the fd can be mapped writable */
+	dmabuf = drm_gem_prime_export(gem_obj, O_RDWR);
+
+	/* The DMA-BUF took its own reference if the export succeeded */
+	drm_gem_object_put(gem_obj);
+
+	return dmabuf;
+}

[Severity: High]
Does mmap() on a DMA-BUF returned by legacy_alloc_dmabuf() leak gem_obj and its backing DMA allocation?

When user space maps the exported DMA-BUF fd, the call chain is:

dma_buf_mmap()
-> drm_gem_dmabuf_mmap()

       -> drm_gem_prime_mmap()

In drm_gem_prime_mmap():

	vma->vm_ops = obj->funcs->vm_ops;
	drm_gem_object_get(obj);
	ret = obj->funcs->mmap(obj, vma);

However, in drivers/accel/qda/qda_gem.c, qda_gem_object_funcs does not set .vm_ops:

const struct drm_gem_object_funcs qda_gem_object_funcs = {
.free = qda_gem_free_object, .get_sg_table = qda_gem_get_sg_table, .mmap = qda_gem_mmap_obj,
};

And qda_gem_mmap_obj() -> qda_dma_mmap() -> dma_mmap_coherent() does not set vma->vm_ops either. Because vma->vm_ops remains NULL instead of using drm_gem_vm_open and drm_gem_vm_close, unmapping the VMA never calls drm_gem_vm_close() to drop the reference taken by drm_gem_object_get(obj). Won't every mmap() call permanently leak the qda_gem_obj, its coherent DMA buffer, and the associated drm_device reference?

[ ... ]

Quoted 47 lines
drivers/accel/qda/qda_prime.c +33 -0
index 7a709e3000d8b..128d4ab2cf64f 100644
@@ -181,4 +181,37 @@ int qda_prime_fd_to_handle(struct drm_device *drm_dev, struct drm_file *file_pri
 	return ret;
 }
 
+/**
+ * qda_prime_import_dmabuf() - Import a DMA-BUF without installing a handle
+ * @drm_dev: DRM device
+ * @session: Session to import into
+ * @dma_buf: Buffer to import
+ *
+ * As qda_prime_fd_to_handle(), but stopping at the GEM object: a session the
+ * legacy shim created has no DRM file to install a handle in. The caller owns
+ * the returned reference.
+ *
+ * Return: The new GEM object on success, ERR_PTR on failure
+ */
+struct drm_gem_object *qda_prime_import_dmabuf(struct drm_device *drm_dev,
+					       struct qda_session *session,
+					       struct dma_buf *dma_buf)
+{
+	struct qda_dev *qdev = qda_dev_from_drm(drm_dev);
+	struct drm_gem_object *gem_obj;
+	int ret;
+
+	ret = qda_memory_manager_assign_device(qdev->iommu_mgr, session);
+	if (ret)
+		return ERR_PTR(ret);
+
+	guard(mutex)(&qdev->import_lock);
+
+	qdev->importing_session = session;
+	gem_obj = qda_gem_prime_import(drm_dev, dma_buf);
+	qdev->importing_session = NULL;
+
+	return gem_obj;
+}
+
 MODULE_IMPORT_NS("DMA_BUF");
drivers/accel/qda/qda_rpmsg.c +0 -0
index 2c9d79937b106..14034e250555d 100644

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 58

Quoted 9 lines
drivers/accel/qda/qda_rpmsg.c +3 -0
@@ -50,6 +50,9 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 {
 	struct qda_dev *qdev = dev_get_drvdata(&rpdev->dev);
 
+	/* Take the legacy nodes away first, so nothing new opens during teardown */
+	qda_unregister_device(qdev);
+
 	/*
 	 * Unplug before clearing @rpdev: once there are ioctls that reach the
 	 * DSP, this is what stops a new one from starting and waits for the

[Severity: High]
In legacy_session_create(), drm_dev_get(&qdev->drm_dev) is taken so that qdev survives qda_rpmsg_remove() while a legacy session remains open. However, can legacy_alloc_dmabuf(), legacy_import_dmabuf() (via qda_prime_import_dmabuf()), or later buffer cleanup trigger a use-after-free if they run concurrently with or after qda_rpmsg_remove()?

In drivers/accel/qda/qda_rpmsg.c, qda_rpmsg_remove() does:

	qda_unregister_device(qdev);
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_cb_unpopulate(qdev);
	qda_deinit_device(qdev);

1. In qda_init_device(), qdev->iommu_mgr is allocated with devm_kzalloc(qdev->dev, ...), which is managed by the rpmsg device rather than &qdev->drm_dev. When qda_rpmsg_remove() finishes, devres frees qdev->iommu_mgr even though qdev->drm_dev is still held by the open legacy session.

2. qda_deinit_device() frees iommu_mgr->devices and destroys both iommu_mgr->assignment_lock and qdev->import_lock, while qda_cb_unpopulate() calls of_platform_device_destroy() on each context bank child device (which qda_cb_setup_device() stored in iommu_dev->dev without taking a device reference).

3. Neither legacy_alloc_dmabuf() nor qda_prime_import_dmabuf() enters a drm_dev_enter() / drm_dev_exit() critical section, so drm_dev_unplug() does not synchronize with them. If legacy_alloc_dmabuf() or qda_prime_import_dmabuf() is called on an already-open legacy session during or after qda_rpmsg_remove(), won't it dereference the freed qdev->iommu_mgr, lock the destroyed assignment_lock and import_lock mutexes, and pass the destroyed iommu_dev->dev to dma_alloc_coherent() or dma_buf_attach()? And even if buffers were allocated before qda_rpmsg_remove() and only freed afterwards, won't qda_gem_free_object() pass the destroyed iommu_dev->dev to dma_free_coherent() or dma_buf_unmap_attachment_unlocked()?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
#23 lore raw

PATCH v4 9/25 accel/qda: Add legacy FastRPC shim skeleton

In reply to Ekansh Gupta

Existing FastRPC user space talks to /dev/fastrpc-<dsp> through the ioctl interface of the driver in drivers/misc/. Add a separate module that serves that interface on top of this driver, so such user space keeps working while it moves to the accel UAPI. It is separate so that the driver itself does not depend on it, and so it can be left out.

The module binds the auxiliary device the driver creates per DSP domain and gives that domain its misc node(s). As in the driver it replaces, a channel is secure unless its DT node has qcom,non-secure-domain. ADSP, MDSP and SDSP get a single node, secure or not according to the channel. CDSP and GDSP support unsigned PD offload, so they always get both a secure and a non-secure node, letting untrusted applications use the latter on a secure channel. Nothing distinguishes the two nodes yet; that is applied to the operations that need it as they are added.

Each open file gets a session from the driver, which also pins the DRM device for as long as the file stays open.

Only FASTRPC_IOCTL_ALLOC_DMA_BUFF is implemented here: it allocates a buffer and returns it as a DMA-BUF fd. FASTRPC_IOCTL_FREE_DMA_BUFF returns -ENOTTY, as in the driver this replaces; buffers are released by closing the fd. The fd is installed only once nothing else can fail.

---
 drivers/accel/qda/Kconfig            |  16 +++
 drivers/accel/qda/Makefile           |   2 +
 drivers/accel/qda/qda_fastrpc_shim.c | 268 +++++++++++++++++++++++++++++++++++
 3 files changed, 286 insertions(+)
drivers/accel/qda/Kconfig +16 -0
index c174f9edf2d5..c440191a54ff 100644
@@ -26,3 +26,19 @@ config DRM_ACCEL_QDA
 
 	  To compile this driver as a module, choose M here: the
 	  module will be called qda.
+
+config DRM_ACCEL_QDA_FASTRPC_SHIM
+	tristate "Qualcomm DSP accelerator legacy FastRPC compatibility shim"
+	depends on DRM_ACCEL_QDA
+	default DRM_ACCEL_QDA
+	help
+	  Exposes a /dev/fastrpc-<dsp> character device per DSP domain, serving
+	  the ioctl interface of the FastRPC driver in drivers/misc/ on top of
+	  the QDA driver, so that existing FastRPC user space keeps working.
+
+	  This is a compatibility layer for user space that has not moved to
+	  the native interface in include/uapi/drm/qda_accel.h; new users
+	  should use that instead.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called qda_fastrpc_shim.
drivers/accel/qda/Makefile +2 -0
index a867a888c5f5..0996e4ed5798 100644
@@ -16,3 +16,5 @@ qda-y := \
 	qda_prime.o \
 	qda_rpmsg.o \
 	qda_soc.o
+
+obj-$(CONFIG_DRM_ACCEL_QDA_FASTRPC_SHIM) += qda_fastrpc_shim.o
drivers/accel/qda/qda_fastrpc_shim.c new file +268 -0

268 lines not on this page · raw message

Signature · 1 line
2.34.1
#24 lore raw

Re: [PATCH v4 09/25] accel/qda: Add legacy FastRPC shim skeleton

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Critical] accel/qda: unplug race in qda_shim_ioctl() due to missing drm_dev_enter() - [Critical]…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] accel/qda: unplug race in qda_shim_ioctl() due to missing drm_dev_enter()
- [Critical] accel/qda: UAF and CB reuse when DMA-BUF outlives qda_shim_release()
- [High] accel/qda: GEM object leak on DMA-BUF mmap() due to NULL vm_ops
--

commit a13f2a6988b9b1d52b35c9e30bbc2ea3fc109ecb
Author: Ekansh Gupta <[email protected]>

accel/qda: Add legacy FastRPC shim skeleton

This commit adds a separate compatibility module that exposes /dev/fastrpc-* misc device nodes per DSP domain on top of the QDA driver and implements the FASTRPC_IOCTL_ALLOC_DMA_BUFF ioctl by exporting buffers as DMA-BUF file descriptors.

Quoted 5 lines
drivers/accel/qda/qda_fastrpc_shim.c new file +0 -0
new file mode 100644
index 0000000000000..25f5c242efde1

[ ... ]

drivers/accel/qda/qda_fastrpc_shim.c line 84

Quoted 9 lines
drivers/accel/qda/qda_fastrpc_shim.c new file +9 -0
+static int qda_shim_release(struct inode *inode, struct file *file)
+{
+	struct qda_shim_file *sf = file->private_data;
+
+	qda_legacy_ops_table.session_destroy(sf->session);
+	kfree(sf);
+
+	return 0;
+}

[Severity: Critical]
What happens if a DMA-BUF fd returned by qda_shim_alloc_dma_buf() remains open after the /dev/fastrpc-* file descriptor is closed (which also occurs on normal process exit when close_files() closes fds in ascending numerical order)?

When qda_shim_alloc_dma_buf() allocates a buffer via legacy_alloc_dmabuf() -> qda_gem_create_object() -> qda_memory_manager_alloc(), a context bank (struct qda_iommu_device) is assigned to sf->session->assigned_iommu_dev and qda_dma_alloc() stores a raw pointer to it in gem_obj->iommu_dev:

drivers/accel/qda/qda_memory_dma.c:qda_dma_alloc() {
	...
	gem_obj->virt = virt;
	gem_obj->dma_addr = dma_addr +
			    qda_soc_sid_offset(iommu_dev->qdev->soc_data, iommu_dev->sid);
	gem_obj->iommu_dev = iommu_dev;
	...
}

Closing the /dev/fastrpc-* fd calls qda_shim_release() -> legacy_session_destroy() -> qda_session_free() -> qda_memory_manager_release_device(session->assigned_iommu_dev), which clears iommu_dev->assigned_session = NULL and drops the session's kref on iommu_dev->ref while the exported DMA-BUF is still allocated and mapped in that context bank's SMMU domain.

Doesn't that allow another session to be assigned the same context bank while the buffer is still live, and also lead to a use-after-free of gem_obj->iommu_dev and gem_obj->iommu_dev->dev in qda_dma_free(), qda_dma_mmap(), or qda_dma_get_sgtable() if qda_rpmsg_remove() tears down the context bank devices before the DMA-BUF fd is closed?

Quoted 7 lines
drivers/accel/qda/qda_fastrpc_shim.c new file +7 -0
+static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp)
+{
+	struct fastrpc_alloc_dma_buf args;
+	struct dma_buf *dmabuf;
+	int fd;
+
+	if (copy_from_user(&args, argp, sizeof(args)))

drivers/accel/qda/qda_fastrpc_shim.c line 102

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c new file +10 -0
+		return -EFAULT;
+
+	/* The size is 64 bits wide on the wire but a size_t further down */
+	if (args.size > SIZE_MAX)
+		return -EINVAL;
+
+	/* args.flags is not used, as in drivers/misc/fastrpc.c */
+	dmabuf = qda_legacy_ops_table.alloc_dmabuf(sf->session, args.size);
+	if (IS_ERR(dmabuf))
+		return PTR_ERR(dmabuf);

[Severity: High]
Does mapping the DMA-BUF fd returned here leak the underlying drm_gem_object and coherent DMA buffer?

qda_shim_alloc_dma_buf() calls legacy_alloc_dmabuf(), which creates a GEM object with qda_gem_object_funcs and exports it via drm_gem_prime_export():

drivers/accel/qda/qda_gem.c:
const struct drm_gem_object_funcs qda_gem_object_funcs = {
.free = qda_gem_free_object, .get_sg_table = qda_gem_get_sg_table, .mmap = qda_gem_mmap_obj,
};

When user space calls mmap() on the returned DMA-BUF fd, drm_gem_dmabuf_mmap() -> drm_gem_prime_mmap() sets
vma->vm_ops = obj->funcs->vm_ops, calls drm_gem_object_get(obj), and invokes obj->funcs->mmap(obj, vma). Because qda_gem_object_funcs leaves .vm_ops NULL (instead of setting drm_gem_vm_open and drm_gem_vm_close) and qda_gem_mmap_obj() does not set vma->vm_ops either, nothing drops the GEM object reference when the VMA is unmapped.

[ ... ]

drivers/accel/qda/qda_fastrpc_shim.c line 151

Quoted 9 lines
drivers/accel/qda/qda_fastrpc_shim.c new file +9 -0
+static long qda_shim_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+	struct qda_shim_file *sf = file->private_data;
+
+	if (drm_dev_is_unplugged(&sf->session->qda_dev->drm_dev))
+		return -ENODEV;
+
+	return qda_shim_dispatch(sf, cmd, (void __user *)arg);
+}

[Severity: Critical]
Can qda_shim_ioctl() race with qda_rpmsg_remove() and access freed memory?

drm_dev_is_unplugged() enters the SRCU read-side critical section via drm_dev_enter() and immediately exits it via drm_dev_exit() before returning false. As a result, qda_shim_dispatch() -> qda_shim_alloc_dma_buf() -> legacy_alloc_dmabuf() -> qda_gem_create_object() executes without holding drm_dev_enter().

If qda_rpmsg_remove() runs concurrently:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_remove() {
	...
	qda_unregister_device(qdev);
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_cb_unpopulate(qdev);
	qda_deinit_device(qdev);
}

misc_deregister() in qda_unregister_device() only serializes against open() and does not wait for in-flight ioctls on already-open /dev/fastrpc-* files, and drm_dev_unplug() returns without waiting for qda_shim_alloc_dma_buf() because the SRCU read lock was already dropped.

qda_cb_unpopulate() and qda_deinit_device() then destroy the context bank platform devices, free qdev->iommu_mgr->devices, and destroy qdev->iommu_mgr->assignment_lock, and devres frees qdev->iommu_mgr (which was allocated with devm_kzalloc(qdev->dev, ...) in qda_init_device()) while the concurrent qda_shim_alloc_dma_buf() thread is still accessing qdev->iommu_mgr and allocating from the context bank device.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=9
#25 lore raw

PATCH v4 10/25 accel/qda: Add FastRPC invocation contexts

In reply to Ekansh Gupta

Running something on the DSP means sending it a message and waiting for the reply. Add the bookkeeping for an invocation that has been sent and not yet answered, before there is anything to send.

The DSP identifies an invocation by a context value it echoes back in its response, so each invocation gets an entry in a per-device lookup table and the entry's index is placed in that value. The index is only 8 bits wide and is reused as soon as its invocation is freed, so a late or duplicate response could otherwise name an ID that now belongs to a different invocation and complete it early, with its caller then reading a buffer the DSP is still writing. To tell the two apart, each context also carries a sequence number that does not repeat in practice, for the response handler to check.

An invocation is removed from the table only when its last reference is dropped, never when its caller gives up waiting, so an ID cannot be reused while the old invocation can still be found and completed. The number of invocations in flight on one device is bounded by the size of the ID space, which also bounds what they can allocate.

---
 drivers/accel/qda/Makefile      |  1 +
 drivers/accel/qda/qda_drv.c     |  3 ++
 drivers/accel/qda/qda_drv.h     |  6 ++++
 drivers/accel/qda/qda_fastrpc.c | 63 +++++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h | 54 +++++++++++++++++++++++++++++++++++
 5 files changed, 127 insertions(+)
drivers/accel/qda/Makefile +1 -0
index 0996e4ed5798..aee69cd7d150 100644
@@ -8,6 +8,7 @@ obj-$(CONFIG_DRM_ACCEL_QDA)	:= qda.o
 qda-y := \
 	qda_cb.o \
 	qda_drv.o \
+	qda_fastrpc.o \
 	qda_gem.o \
 	qda_ioctl.o \
 	qda_legacy_ops.o \
drivers/accel/qda/qda_drv.c +3 -0
index 21113deceaad..bdfb23c3c0b1 100644
@@ -100,6 +100,8 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 
 	INIT_LIST_HEAD(&qdev->cb_devs);
 	mutex_init(&qdev->import_lock);
+	xa_init_flags(&qdev->ctx_xa, XA_FLAGS_ALLOC1);
+	atomic64_set(&qdev->ctx_seq, 0);
 
 	return qdev;
 }
@@ -122,6 +124,7 @@ int qda_init_device(struct qda_dev *qdev, int num_cbs)
 void qda_deinit_device(struct qda_dev *qdev)
 {
 	qda_memory_manager_exit(qdev->iommu_mgr);
+	xa_destroy(&qdev->ctx_xa);
 	mutex_destroy(&qdev->import_lock);
 }
 
drivers/accel/qda/qda_drv.h +6 -0
index c04f19e5a91b..b7cbb1667adb 100644
@@ -6,12 +6,14 @@
 #ifndef __QDA_DRV_H__
 #define __QDA_DRV_H__
 
+#include <linux/atomic.h>
 #include <linux/auxiliary_bus.h>
 #include <linux/device.h>
 #include <linux/list.h>
 #include <linux/mutex.h>
 #include <linux/rpmsg.h>
 #include <linux/types.h>
+#include <linux/xarray.h>
 #include <drm/drm_device.h>
 #include <drm/drm_drv.h>
 #include <drm/drm_file.h>
@@ -75,6 +77,10 @@ struct qda_dev {
 	 * calling file to.
 	 */
 	struct qda_session *importing_session;
+	/** @ctx_xa: Invocations in flight, keyed by context ID */
+	struct xarray ctx_xa;
+	/** @ctx_seq: Distinguishes invocations that reuse a context ID */
+	atomic64_t ctx_seq;
 	/** @soc_data: Per-SoC DSP integration parameters */
 	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
drivers/accel/qda/qda_fastrpc.c new file +63 -0

63 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h new file +54 -0

54 lines not on this page · raw message

Signature · 1 line
2.34.1
#26 lore raw

Re: [PATCH v4 10/25] accel/qda: Add FastRPC invocation contexts

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] accel/qda: premature publication of ctx in qda_fastrpc_context_alloc() - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] accel/qda: premature publication of ctx in qda_fastrpc_context_alloc()
- [High] accel/qda: premature ctx_xa destruction in qda_deinit_device()
--

commit 0b392f877740b474d80d7f4392edfd3f880836d4
Author: Ekansh Gupta <[email protected]>

accel/qda: Add FastRPC invocation contexts

This commit adds FastRPC invocation context allocation and reference-counted cleanup, tracking in-flight invocations per device in an XArray with an 8-bit context ID and a 48-bit sequence number.

Quoted 14 lines
drivers/accel/qda/qda_drv.c +2 -0
index 21113deceaadb..bdfb23c3c0b1d 100644
@@ -100,6 +100,8 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 
 	INIT_LIST_HEAD(&qdev->cb_devs);
 	mutex_init(&qdev->import_lock);
+	xa_init_flags(&qdev->ctx_xa, XA_FLAGS_ALLOC1);
+	atomic64_set(&qdev->ctx_seq, 0);
 
 	return qdev;
 }

drivers/accel/qda/qda_drv.c int qda_init_device(struct qda_dev *qdev, int num_cbs) · line 129

Quoted 6 lines
drivers/accel/qda/qda_drv.c +1 -0
@@ -122,6 +124,7 @@ int qda_init_device(struct qda_dev *qdev, int num_cbs)
 void qda_deinit_device(struct qda_dev *qdev)
 {
 	qda_memory_manager_exit(qdev->iommu_mgr);
+	xa_destroy(&qdev->ctx_xa);
 	mutex_destroy(&qdev->import_lock);
 }

[Severity: High]
Can calling xa_destroy(&qdev->ctx_xa) in qda_deinit_device() in drivers/accel/qda/qda_drv.c race with outstanding invocation contexts that have not yet dropped their final reference?

In qda_fastrpc_context_alloc() in drivers/accel/qda/qda_fastrpc.c, each struct qda_fastrpc_invoke_ctx is inserted into qdev->ctx_xa with an initial caller reference (refcount = 1) and stores a raw pointer ctx->qdev = qdev without holding a reference on qdev->drm_dev. By design, the entry is only erased from qdev->ctx_xa in qda_fastrpc_context_free() when the last reference is released.

When invocations are wired up in the subsequent commits ("accel/qda: Add RPMsg send and response dispatch" and "accel/qda: Add the invocation ioctl and teardown on unplug"), qda_rpmsg_send_msg() takes a second reference for the response path and the caller sleeps in qda_rpmsg_wait_for_rsp() outside any drm_dev_enter() critical section.

If the rpmsg device is removed while callers are waiting (for example, on a DSP crash or remoteproc restart), qda_rpmsg_remove() runs drm_dev_unplug(), qda_fastrpc_flush_pending(qdev), and then immediately qda_deinit_device(qdev). Because qda_fastrpc_flush_pending() only wakes the waiters and drops the response reference, the callers still hold their reference (and if qda_rpmsg_cb() already ran and queued ctx->put_work, ctx is still in qdev->ctx_xa while put_work is pending, causing qda_fastrpc_flush_pending() to drop a reference that put_work will also drop).

When qda_deinit_device() immediately calls xa_destroy(&qdev->ctx_xa), the internal XArray nodes are freed while live contexts remain in the table. When those woken callers or pending put_work items later run kref_put(&ctx->refcount, qda_fastrpc_context_free), won't qda_fastrpc_context_free() call xa_erase(&ctx->qdev->ctx_xa, ...) on an already-destroyed XArray and dereference ctx->qdev after device teardown?

Quoted 56 lines
drivers/accel/qda/qda_fastrpc.c new file +50 -0
new file mode 100644
index 0000000000000..14d9da8c20b3f
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+#include <linux/bitfield.h>
+#include <linux/slab.h>
+#include "qda_drv.h"
+#include "qda_fastrpc.h"
+
+/**
+ * qda_fastrpc_context_free() - Release an invocation
+ * @ref: Reference counter embedded in the invocation
+ *
+ * Runs when the last reference goes. The invocation leaves the lookup table
+ * here and not earlier, so that its ID cannot be handed to a new invocation
+ * while this one can still be found and completed.
+ */
+void qda_fastrpc_context_free(struct kref *ref)
+{
+	struct qda_fastrpc_invoke_ctx *ctx =
+		container_of(ref, struct qda_fastrpc_invoke_ctx, refcount);
+
+	xa_erase(&ctx->qdev->ctx_xa, FIELD_GET(QDA_FASTRPC_CTXID_MASK, ctx->ctxid));
+	kfree(ctx);
+}
+
+/**
+ * qda_fastrpc_context_alloc() - Allocate an invocation and give it an ID
+ * @qdev: Device the invocation is issued on
+ *
+ * The caller holds the initial reference and drops it with
+ * qda_fastrpc_context_free().
+ *
+ * Return: The new invocation, or ERR_PTR on failure
+ */
+struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev)
+{
+	struct qda_fastrpc_invoke_ctx *ctx;
+	u32 id;
+	int ret;
+
+	ctx = kzalloc_obj(*ctx);
+	if (!ctx)
+		return ERR_PTR(-ENOMEM);
+
+	init_completion(&ctx->work);
+	kref_init(&ctx->refcount);
+	ctx->qdev = qdev;
+	/* Anything but a status the DSP could send, until it answers */
+	ctx->retval = -1;
+
+	/* ID 0 is never used, so a zeroed context in a response matches nothing */

drivers/accel/qda/qda_fastrpc.c line 60

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c new file +10 -0
+	ret = xa_alloc(&qdev->ctx_xa, &id, ctx, XA_LIMIT(1, QDA_FASTRPC_CTXID_MAX),
+		       GFP_KERNEL);
+	if (ret) {
+		kfree(ctx);
+		return ERR_PTR(ret);
+	}
+
+	ctx->ctxid = FIELD_PREP(QDA_FASTRPC_CTXID_MASK, id) |
+		     FIELD_PREP(QDA_FASTRPC_CTXID_SEQ_MASK,
+				(u64)atomic64_inc_return(&qdev->ctx_seq));

[Severity: High]
In qda_fastrpc_context_alloc() in drivers/accel/qda/qda_fastrpc.c, does calling xa_alloc(&qdev->ctx_xa, &id, ctx, ...) publish ctx into qdev->ctx_xa and drop xa_lock before ctx->ctxid is initialized?

Because the 8-bit context IDs (1..255) are reused as soon as a prior invocation is freed, a concurrent lookup on qdev->ctx_xa for that recycled id (such as qda_rpmsg_cb() handling a late, duplicate, or malformed DSP response, or qda_fastrpc_flush_pending() during device unplug) can observe ctx while ctx->ctxid is still 0 or is being written without locking:

1. If a late or malformed response carries sequence number 0 in bits [63:16] for slot id, FIELD_GET(QDA_FASTRPC_CTXID_SEQ_MASK, ctx->ctxid) evaluates to 0 and matches the uninitialized ctx->ctxid, completing ctx->work early and scheduling ctx->put_work to drop a reference that qda_rpmsg_send_msg() has not yet taken.

2. If qda_fastrpc_context_free() runs while ctx->ctxid is still 0, xa_erase(&ctx->qdev->ctx_xa, FIELD_GET(QDA_FASTRPC_CTXID_MASK, ctx->ctxid)) erases index 0 instead of index id and frees ctx, leaving a dangling pointer in qdev->ctx_xa[id].

Should qda_fastrpc_context_alloc() either hold xa_lock(&qdev->ctx_xa) across __xa_alloc() and the ctx->ctxid assignment, or reserve the slot with NULL first and store ctx only after ctx->ctxid is initialized?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=10
#27 lore raw

PATCH v4 11/25 accel/qda: Add RPMsg send and response dispatch

In reply to Ekansh Gupta

An invocation reaches the DSP as an rpmsg message, and the DSP answers with a second message naming the invocation. Add both directions, along with the wire message itself and the kernel-side bookkeeping that is carried alongside it.

Sending takes a reference on the invocation for the response handler, so the invocation stays alive even if its caller stops waiting for the answer. If the send fails, that reference is dropped again here, because no response will come to drop it.

A response is data the DSP wrote, so the handler checks that the whole message is there before reading any of it, then looks the invocation up by the context ID the DSP echoed. Context IDs are narrow and get reused, so a late or duplicate response can name an ID that now belongs to a newer invocation. The handler compares the sequence number as well and drops a response that does not match, rather than completing the wrong invocation early.

The handler runs in atomic context, and dropping the last reference frees the context's DMA buffers, so that reference is dropped from a worker instead.

---
 drivers/accel/qda/qda_fastrpc.c |   9 ++++
 drivers/accel/qda/qda_fastrpc.h |  44 +++++++++++++++++
 drivers/accel/qda/qda_rpmsg.c   | 107 ++++++++++++++++++++++++++++++++++++++--
 drivers/accel/qda/qda_rpmsg.h   |  26 ++++++++++
 4 files changed, 182 insertions(+), 4 deletions(-)
drivers/accel/qda/qda_fastrpc.c +9 -0
index 14d9da8c20b3..ec37a38e9215 100644
@@ -22,6 +22,14 @@ void qda_fastrpc_context_free(struct kref *ref)
 	kfree(ctx);
 }
 
+static void qda_fastrpc_context_put_wq(struct work_struct *work)
+{
+	struct qda_fastrpc_invoke_ctx *ctx =
+		container_of(work, struct qda_fastrpc_invoke_ctx, put_work);
+
+	kref_put(&ctx->refcount, qda_fastrpc_context_free);
+}
+
 /**
  * qda_fastrpc_context_alloc() - Allocate an invocation and give it an ID
  * @qdev: Device the invocation is issued on
@@ -42,6 +50,7 @@ struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev)
 		return ERR_PTR(-ENOMEM);
 
 	init_completion(&ctx->work);
+	INIT_WORK(&ctx->put_work, qda_fastrpc_context_put_wq);
 	kref_init(&ctx->refcount);
 	ctx->qdev = qdev;
 	/* Anything but a status the DSP could send, until it answers */
drivers/accel/qda/qda_fastrpc.h +44 -0

62 lines not on this page · raw message

drivers/accel/qda/qda_rpmsg.c +103 -4

129 lines not on this page · raw message

drivers/accel/qda/qda_rpmsg.h new file +26 -0
new file mode 100644
index 000000000000..a70f4a80808d
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#ifndef __QDA_RPMSG_H__
+#define __QDA_RPMSG_H__
+
+#include "qda_drv.h"
+#include "qda_fastrpc.h"
+
+/**
+ * struct qda_invoke_rsp - Response structure for FastRPC invocations
+ */
+struct qda_invoke_rsp {
+	/** @ctx: Invoke caller context for matching request/response */
+	u64 ctx;
+	/** @retval: Return value from the remote invocation */
+	int retval;
+};
+
+/* RPMsg transport layer functions */
+int qda_rpmsg_send_msg(struct qda_dev *qdev, struct qda_msg *msg);
+int qda_rpmsg_wait_for_rsp(struct qda_fastrpc_invoke_ctx *ctx);
+
+#endif /* __QDA_RPMSG_H__ */
Signature · 1 line
2.34.1
#28 lore raw

Re: [PATCH v4 11/25] accel/qda: Add RPMsg send and response dispatch

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] accel/qda: use-after-free in qda_rpmsg_cb() due to unlocked xa_load() - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] accel/qda: use-after-free in qda_rpmsg_cb() due to unlocked xa_load()
- [High] accel/qda: duplicate response refcount underflow in qda_rpmsg_cb()
- [High] accel/qda: unflushed put_work and in-flight contexts in qda_rpmsg_remove()
- [High] accel/qda: missing PD exit sentinel filtering in qda_rpmsg_cb()
--

commit 66df96a5b81c60ce1864c2d544af7d04014ec8e1
Author: Ekansh Gupta <[email protected]>

accel/qda: Add RPMsg send and response dispatch

This commit adds RPMsg message transmission and response callback handling for FastRPC invocations, along with the wire message definitions and workqueue reference cleanup.

Quoted 4 lines
drivers/accel/qda/qda_rpmsg.c +0 -0
index 14034e250555d..43b56ab3f0128 100644

[ ... ]

Quoted 16 lines
drivers/accel/qda/qda_rpmsg.c +16 -0
+static int qda_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len,
+			void *priv, u32 src)
+{
+	struct qda_dev *qdev = dev_get_drvdata(&rpdev->dev);
+	struct qda_invoke_rsp *resp_msg = data;
+	struct qda_fastrpc_invoke_ctx *ctx;
+	int ret = 0, idx;
+
+	if (!qdev)
+		return -ENODEV;
+
+	if (!drm_dev_enter(&qdev->drm_dev, &idx))
+		return -ENODEV;
+
+	if (len < sizeof(*resp_msg)) {
+		ret = -EINVAL;

drivers/accel/qda/qda_rpmsg.c static struct qda_dev *qda_rpmsg_alloc_and_init_qdev(struct rpmsg_device *rpdev) · line 118

Quoted 10 lines
drivers/accel/qda/qda_rpmsg.c +10 -0
+		goto out_exit;
+	}
+
+	ctx = xa_load(&qdev->ctx_xa, FIELD_GET(QDA_FASTRPC_CTXID_MASK, resp_msg->ctx));
+	if (!ctx) {
+		drm_dbg_driver(&qdev->drm_dev, "No context matches response 0x%llx\n",
+			       resp_msg->ctx);
+		ret = -ENOENT;
+		goto out_exit;
+	}

[Severity: High]
When a protection domain exits on the DSP, the firmware sends a PD exit notification carrying the sentinel value 0xABCDABCD in resp_msg->ctx (which drivers/misc/fastrpc.c filters out via FASTRPC_DSP_PD_NOTIFY_CTX in fastrpc_rpmsg_callback()).

Under the QDA context ID layout, FIELD_GET(QDA_FASTRPC_CTXID_MASK, 0xABCDABCD) evaluates to 0xCD (205) and FIELD_GET(QDA_FASTRPC_CTXID_SEQ_MASK, 0xABCDABCD) evaluates to 0xABCD. If a PD exit notification arrives while context slot 0xCD is occupied by an invocation at sequence number 0xABCD, won't qda_rpmsg_cb() mistakenly match it, overwrite ctx->retval, wake the caller early via complete(&ctx->work) while the DSP is still accessing its DMA buffers, and schedule ctx->put_work?

Additionally, when slot 0xCD is empty, returning -ENOENT here instead of 0 causes SMD-backed RPMsg channels (qcom_smd_channel_recv_single() in drivers/rpmsg/qcom_smd.c) to stop advancing the RX FIFO tail. Should qda_rpmsg_cb() filter out 0xABCDABCD early and return 0 when dropping unmatched responses?

drivers/accel/qda/qda_rpmsg.c static struct qda_dev *qda_rpmsg_alloc_and_init_qdev(struct rpmsg_device *rpdev) · line 130

Quoted 12 lines
drivers/accel/qda/qda_rpmsg.c +12 -0
+
+	/*
+	 * Context IDs are only 8 bits wide and are recycled once a context is
+	 * freed, so a late or duplicate response may name an ID that now belongs
+	 * to a newer context. The sequence number tells the two apart.
+	 */
+	if (FIELD_GET(QDA_FASTRPC_CTXID_SEQ_MASK, resp_msg->ctx) !=
+	    FIELD_GET(QDA_FASTRPC_CTXID_SEQ_MASK, ctx->ctxid)) {
+		drm_dbg_driver(&qdev->drm_dev, "Dropping stale response 0x%llx\n",
+			       resp_msg->ctx);
+		goto out_exit;
+	}

[Severity: High]
Can this unlocked xa_load() race with context teardown and cause a use-after-free in qda_rpmsg_cb()?

When the final reference on ctx drops to zero on another CPU (for example, when qda_rpmsg_send_msg() fails and drops the callback reference followed by the caller dropping its reference, or when an invocation finishes), qda_fastrpc_context_free() removes ctx from qdev->ctx_xa and immediately frees it without an RCU grace period:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_free() {
	struct qda_fastrpc_invoke_ctx *ctx =
		container_of(ref, struct qda_fastrpc_invoke_ctx, refcount);

	xa_erase(&ctx->qdev->ctx_xa, FIELD_GET(QDA_FASTRPC_CTXID_MASK, ctx->ctxid));
	kfree(ctx);
}

Because xa_load(&qdev->ctx_xa, ...) only holds rcu_read_lock() internally during the lookup and returns the raw ctx pointer without holding qdev->ctx_xa.xa_lock or pinning ctx->refcount via kref_get_unless_zero(), if a late or duplicate response arrives in qda_rpmsg_cb() just before xa_erase() and kfree(ctx), qda_rpmsg_cb() will dereference freed memory at ctx->ctxid, write ctx->retval, and call complete(&ctx->work) and schedule_work(&ctx->put_work) on the freed object.

Similarly, in qda_fastrpc_context_alloc(), xa_alloc() publishes ctx into qdev->ctx_xa before ctx->ctxid is initialized:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_alloc() {
	...
	ret = xa_alloc(&qdev->ctx_xa, &id, ctx, XA_LIMIT(1, QDA_FASTRPC_CTXID_MAX),
		       GFP_KERNEL);
	if (ret) {
		kfree(ctx);
		return ERR_PTR(ret);
	}

	ctx->ctxid = FIELD_PREP(QDA_FASTRPC_CTXID_MASK, id) |
		     FIELD_PREP(QDA_FASTRPC_CTXID_SEQ_MASK,
				(u64)atomic64_inc_return(&qdev->ctx_seq));
}

which briefly exposes ctx with ctx->ctxid == 0 to concurrent lookups in qda_rpmsg_cb().

drivers/accel/qda/qda_rpmsg.c static struct qda_dev *qda_rpmsg_alloc_and_init_qdev(struct rpmsg_device *rpdev) · line 140

Quoted 10 lines
drivers/accel/qda/qda_rpmsg.c +10 -0
+
+	ctx->retval = resp_msg->retval;
+	complete(&ctx->work);
+
+	/*
+	 * Release the reference taken by qda_rpmsg_send_msg(). This callback
+	 * runs in atomic context, and if it drops the last reference the
+	 * context's DMA buffers are freed, which must not happen here.
+	 */
+	schedule_work(&ctx->put_work);

[Severity: High]
What prevents a duplicate or unsolicited DSP response from scheduling ctx->put_work without a corresponding callback reference and underflowing ctx->refcount?

In qda_fastrpc_context_alloc(), ctx is inserted into qdev->ctx_xa with refcount = 1 (owned by the caller), and qda_rpmsg_send_msg() takes a single additional reference via kref_get(&ctx->refcount) for the response handler. When a valid response arrives in qda_rpmsg_cb(), ctx is neither removed from qdev->ctx_xa nor atomically marked as already answered; it stays in qdev->ctx_xa until qda_fastrpc_context_free() runs at refcount == 0.

Once the first put_work begins executing in qda_fastrpc_context_put_wq() and drops refcount from 2 to 1 while the caller still holds its reference, a duplicate response from the DSP carrying the same ctx value (matching both the 8-bit ID and the sequence number) will match ctx in qda_rpmsg_cb() a second time and call schedule_work(&ctx->put_work) again. That second execution of qda_fastrpc_context_put_wq() drops the caller's reference, freeing ctx while the caller is still using it and triggering a refcount underflow when the caller drops its own reference.

Likewise, because ctx is in qdev->ctx_xa before qda_rpmsg_send_msg() is called (and remains in qdev->ctx_xa after rpmsg_send() fails and drops the callback reference), an unsolicited response matching ctx->ctxid before send or after a failed send will also schedule ctx->put_work without a matching callback reference.

drivers/accel/qda/qda_rpmsg.c static struct qda_dev *qda_rpmsg_alloc_and_init_qdev(struct rpmsg_device *rpdev) · line 148

Quoted 8 lines
drivers/accel/qda/qda_rpmsg.c +7 -0
+
+out_exit:
+	drm_dev_exit(idx);
+
+	return ret;
+}
+
 static void qda_rpmsg_remove(struct rpmsg_device *rpdev)

[Severity: High]
If the RPMsg device is removed while ctx->put_work is queued or while invocations are in flight, can qda_fastrpc_context_put_wq() access qdev->ctx_xa after qda_deinit_device() has destroyed it?

In qda_rpmsg_cb(), schedule_work(&ctx->put_work) queues qda_fastrpc_context_put_wq() on system_percpu_wq and immediately exits the drm_dev_enter() critical section via drm_dev_exit(idx). When qda_rpmsg_remove() runs:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_remove() {
	...
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_cb_unpopulate(qdev);
	qda_deinit_device(qdev);
}

drm_dev_unplug() only waits for active drm_dev_enter() sections; it does not flush or wait for pending ctx->put_work items before qda_deinit_device(qdev) destroys qdev->ctx_xa via xa_destroy(&qdev->ctx_xa) (and if all DRM files are closed, devm frees qdev when qda_rpmsg_remove() returns). When the queued qda_fastrpc_context_put_wq() worker subsequently runs, qda_fastrpc_context_free() calls xa_erase(&ctx->qdev->ctx_xa, ...) on the destroyed XArray and freed qdev.

In addition, if the RPMsg device is removed in this commit while invocations sent via qda_rpmsg_send_msg() are waiting for a response, qda_rpmsg_remove() does not complete ctx->work to wake waiters in qda_rpmsg_wait_for_rsp() or drop the callback references on entries remaining in qdev->ctx_xa before xa_destroy(). Although a later commit in the series, commit 9e89a77ec2ad ("accel/qda: Add the invocation ioctl and teardown on unplug"), adds qda_fastrpc_flush_pending(qdev) to qda_rpmsg_remove() to wake and put remaining entries in ctx_xa, it still does not flush pending ctx->put_work items that were already scheduled by qda_rpmsg_cb().

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=11
#29 lore raw

PATCH v4 12/25 accel/qda: Add invocation argument validation and packing

In reply to Ekansh Gupta

The DSP reads one buffer per invocation describing where every argument is. Build that buffer from the argument descriptors user space supplies.

A descriptor names a buffer by handle and a region of it by offset and length. Handles are resolved in the calling session's own table, and the buffer they name must be mapped in the context bank assigned to that session. The table alone is not enough: a buffer exported with DRM_IOCTL_PRIME_HANDLE_TO_FD and imported back on the same device resolves to the original object, still mapped in the exporting session's bank, and the DSP derives the bank from the address it is given. Without the second check one session could read and write in another's address space.

For a buffer argument the region is also checked against the buffer's real size. A region described as larger than its buffer would otherwise let the DSP read and write past the end of it, into whatever else is mapped in the same context bank.

The DSP is given the exact address of each argument and the page-aligned region containing it, since it takes the offset within the page from the low bits of the address.

The payload is cleared before use, so nothing left from a previous allocation is visible to the DSP, and a write barrier separates filling it in from telling the DSP about it.

The same buffer carries a list the DSP writes into to say which buffers it has finished with, so releasing those is added here alongside the code that decides where the list lives. That memory is writable by the DSP, so its contents are not trusted: the walk is bounded by the region sized for it, stops at the first empty entry, and resolves each entry in the calling session's own handle table, so a bad value can at worst name a buffer that session already owns. It is only read once a barrier has made the DSP's writes visible.

Buffers resolved for an invocation are held until it is freed, so an error part-way through releases what was already resolved.

An invocation keeps a reference on the session that issued it. A caller that stops waiting for the DSP leaves the invocation behind, because the DSP still owns the payload buffer, and that buffer was allocated from the session's context bank. Holding the session keeps the bank assigned for as long as the DSP may still write into it, and keeps the handle table around for the list above.

Payload buffers are kept per session rather than freed, and reused by a later invocation needing one no larger, since allocating and tearing down a DMA-coherent, IOMMU-mapped buffer for every invocation is wasteful for a workload that invokes the same handle repeatedly. The number kept is bounded, so a session cannot pin an unbounded number of buffers, and the rest are freed with the session.

---
 drivers/accel/qda/qda_drv.c        |  49 +++-
 drivers/accel/qda/qda_drv.h        |  23 +-
 drivers/accel/qda/qda_fastrpc.c    | 482 +++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h    | 111 +++++++++
 drivers/accel/qda/qda_legacy_ops.c |   2 +-
 include/uapi/drm/qda_accel.h       |  36 +++
 6 files changed, 695 insertions(+), 8 deletions(-)
drivers/accel/qda/qda_drv.c +43 -6
index bdfb23c3c0b1..99bd37fca438 100644
@@ -12,6 +12,7 @@
 #include <drm/qda_accel.h>
 
 #include "qda_drv.h"
+#include "qda_fastrpc.h"
 #include "qda_ioctl.h"
 #include "qda_prime.h"
 
@@ -19,6 +20,8 @@
  * qda_session_alloc() - Create a session on a device
  * @qdev: Device to create the session on
  *
+ * The caller holds the initial reference and drops it with qda_session_put().
+ *
  * Return: The new session, or ERR_PTR on failure
  */
 struct qda_session *qda_session_alloc(struct qda_dev *qdev)
@@ -30,6 +33,14 @@ struct qda_session *qda_session_alloc(struct qda_dev *qdev)
 		return ERR_PTR(-ENOMEM);
 
 	session->qda_dev = qdev;
+	/*
+	 * Wraps after 2^31 sessions on one device, which no real workload
+	 * reaches: a session lasts as long as the file it was opened on.
+	 */
+	session->remote_session_id = atomic_inc_return(&qdev->session_id_seq);
+	INIT_LIST_HEAD(&session->msg_buf_pool);
+	mutex_init(&session->msg_buf_pool_lock);
+	kref_init(&session->ref);
 
 	return session;
 }
@@ -50,22 +61,47 @@ static int qda_open(struct drm_device *dev, struct drm_file *file)
 	return 0;
 }
 
-/**
- * qda_session_free() - Release a session and whatever it still holds
- * @session: Session to release
+/*
+ * Runs once the owner and every invocation of the session are gone, so the
+ * context bank is held for as long as the DSP may still write into a buffer
+ * allocated from it.
  */
-void qda_session_free(struct qda_session *session)
+static void qda_session_release(struct kref *ref)
 {
+	struct qda_session *session = container_of(ref, struct qda_session, ref);
+
+	qda_session_msg_buf_pool_destroy(session);
+
 	if (session->assigned_iommu_dev)
 		qda_memory_manager_release_device(session->assigned_iommu_dev);
 
+	mutex_destroy(&session->msg_buf_pool_lock);
 	kfree(session);
 }
-EXPORT_SYMBOL_FOR_MODULES(qda_session_free, "qda_fastrpc_shim");
+
+/**
+ * qda_session_get() - Take a reference on a session
+ * @session: Session to reference
+ */
+void qda_session_get(struct qda_session *session)
+{
+	kref_get(&session->ref);
+}
+EXPORT_SYMBOL_FOR_MODULES(qda_session_get, "qda_fastrpc_shim");
+
+/**
+ * qda_session_put() - Drop a reference on a session
+ * @session: Session to release
+ */
+void qda_session_put(struct qda_session *session)
+{
+	kref_put(&session->ref, qda_session_release);
+}
+EXPORT_SYMBOL_FOR_MODULES(qda_session_put, "qda_fastrpc_shim");
 
 static void qda_postclose(struct drm_device *dev, struct drm_file *file)
 {
-	qda_session_free(file->driver_priv);
+	qda_session_put(file->driver_priv);
 }
 
 DEFINE_DRM_ACCEL_FOPS(qda_accel_fops);
@@ -102,6 +138,7 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 	mutex_init(&qdev->import_lock);
 	xa_init_flags(&qdev->ctx_xa, XA_FLAGS_ALLOC1);
 	atomic64_set(&qdev->ctx_seq, 0);
+	atomic_set(&qdev->session_id_seq, 0);
 
 	return qdev;
 }
drivers/accel/qda/qda_drv.h +22 -1
index b7cbb1667adb..80a8569b311a 100644
@@ -9,6 +9,7 @@
 #include <linux/atomic.h>
 #include <linux/auxiliary_bus.h>
 #include <linux/device.h>
+#include <linux/kref.h>
 #include <linux/list.h>
 #include <linux/mutex.h>
 #include <linux/rpmsg.h>
@@ -37,6 +38,10 @@
  * FastRPC compatibility shim, one per open /dev/fastrpc-* file with no DRM
  * file behind it at all. The core is keyed on this rather than on struct
  * drm_file so that it can serve both.
+ *
+ * Reference counted because an invocation outlives the caller that issued it
+ * when the DSP has not answered yet, and keeps using the session it belongs
+ * to after the file it was opened on is closed.
  */
 struct qda_session {
 	/** @qda_dev: Back-pointer to device structure */
@@ -49,6 +54,19 @@ struct qda_session {
 	struct drm_file *file;
 	/** @assigned_iommu_dev: Context bank owned by this session, or NULL */
 	struct qda_iommu_device *assigned_iommu_dev;
+	/** @remote_session_id: Identifies this session to the DSP */
+	int remote_session_id;
+	/**
+	 * @msg_buf_pool: Invoke header buffers this session is done with,
+	 * kept for its later invocations to reuse.
+	 */
+	struct list_head msg_buf_pool;
+	/** @msg_buf_pool_count: Number of entries in @msg_buf_pool */
+	int msg_buf_pool_count;
+	/** @msg_buf_pool_lock: Protects @msg_buf_pool and @msg_buf_pool_count */
+	struct mutex msg_buf_pool_lock;
+	/** @ref: Dropped by the owner of the session and by every invocation */
+	struct kref ref;
 };
 
 /**
@@ -81,6 +99,8 @@ struct qda_dev {
 	struct xarray ctx_xa;
 	/** @ctx_seq: Distinguishes invocations that reuse a context ID */
 	atomic64_t ctx_seq;
+	/** @session_id_seq: Hands out the session IDs the DSP is told */
+	atomic_t session_id_seq;
 	/** @soc_data: Per-SoC DSP integration parameters */
 	const struct qda_soc_data *soc_data;
 	/** @dsp_name: Name of the DSP domain (e.g. "cdsp", "adsp") */
@@ -114,6 +134,7 @@ void qda_unregister_device(struct qda_dev *qdev);
 
 /* Sessions, shared with the legacy shim */
 struct qda_session *qda_session_alloc(struct qda_dev *qdev);
-void qda_session_free(struct qda_session *session);
+void qda_session_get(struct qda_session *session);
+void qda_session_put(struct qda_session *session);
 
 #endif /* __QDA_DRV_H__ */
drivers/accel/qda/qda_fastrpc.c +482 -0

507 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +111 -0

146 lines not on this page · raw message

drivers/accel/qda/qda_legacy_ops.c +1 -1
index 3566a10c6488..9639ea84b200 100644
@@ -33,7 +33,7 @@ static void legacy_session_destroy(struct qda_session *session)
 {
 	struct drm_device *drm_dev = &session->qda_dev->drm_dev;
 
-	qda_session_free(session);
+	qda_session_put(session);
 	drm_dev_put(drm_dev);
 }
 
include/uapi/drm/qda_accel.h +36 -0
index 8e64480cdd82..6d81f0ef47b6 100644
@@ -70,6 +70,42 @@ struct drm_qda_gem_mmap_offset {
 	__u32 pad;
 };
 
+/**
+ * struct drm_qda_fastrpc_invoke_args - FastRPC invocation argument descriptor
+ * @offset: Byte offset within the buffer at which the argument starts
+ * @length: Length of the argument data in bytes
+ * @handle: GEM handle of the buffer holding the argument
+ * @attr: Argument attributes and flags
+ *
+ * Describes a single argument of a FastRPC invocation. Every argument lives
+ * in a buffer, named by the handle it was created or imported under, and
+ * @offset and @length must describe a region within that buffer. Userspace
+ * imports any DMA-BUF fd to a GEM handle before passing it here; the driver
+ * never accepts DMA-BUF fds directly.
+ */
+struct drm_qda_fastrpc_invoke_args {
+	__u64 offset;
+	__u64 length;
+	__u32 handle;
+	__u32 attr;
+};
+
+/**
+ * struct drm_qda_invoke_args - Dynamic FastRPC invocation parameters
+ * @handle: Remote handle to invoke on the DSP
+ * @sc: Scalars value encoding the remote method and the argument counts
+ * @args: User-space pointer to an array of struct
+ *        drm_qda_fastrpc_invoke_args entries, one per argument @sc describes
+ *
+ * Runs a method on the DSP. Results are written directly into the argument
+ * buffers, so there is nothing to read back from this structure.
+ */
+struct drm_qda_invoke_args {
+	__u32 handle;
+	__u32 sc;
+	__u64 args;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#30 lore raw

Re: [PATCH v4 12/25] accel/qda: Add invocation argument validation and packing

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: - [High] accel/qda: union corruption on zero handle in process_dma_handle() - [High] accel/qda: missing…

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] accel/qda: union corruption on zero handle in process_dma_handle()
- [High] accel/qda: missing bounds and overflow checks in process_dma_handle()
- [High] accel/qda: dangling session->file pointer after qda_postclose()
- [High] accel/qda: premature drm_dev_put() while qda_session remains alive
- [Medium] accel/qda: unvalidated attr field in qda_fastrpc_prepare_args_invoke() --

commit 0212e9880ce32dca1d81b275fcea22de7515a72a
Author: Ekansh Gupta <[email protected]>

accel/qda: Add invocation argument validation and packing

This commit adds FastRPC invocation argument validation, message payload packing, and per-session message buffer pooling for the QDA driver. It also converts struct qda_session to be reference-counted so in-flight invocations can outlive the open file and clean up DSP-released GEM handles.

Quoted 4 lines
drivers/accel/qda/qda_drv.c +0 -0
index bdfb23c3c0b1d..99bd37fca4384 100644

[ ... ]

Quoted 1 line
drivers/accel/qda/qda_drv.c +0 -0
@@ -50,22 +61,47 @@ static int qda_open(struct drm_device *dev, struct drm_file *file)

[ ... ]

drivers/accel/qda/qda_drv.c static int qda_open(struct drm_device *dev, struct drm_file *file) · line 105

Quoted 5 lines

static void qda_postclose(struct drm_device *dev, struct drm_file *file) {

drivers/accel/qda/qda_drv.c +1 -1
-	qda_session_free(file->driver_priv);
+	qda_session_put(file->driver_priv);
 }

[Severity: High]
When an invocation outlives the file it was issued on, does session->file become a dangling pointer to a freed struct drm_file after qda_postclose() runs?

In qda_open(), session->file is set to the raw struct drm_file pointer, and qda_fastrpc_prepare_args() takes a reference on ctx->session so that the session can outlive file closure if the caller stops waiting for the DSP.

However, when the DRM file is closed, drm_file_free() destroys file->object_idr via drm_gem_release(), calls qda_postclose(), and then frees struct drm_file with kfree(file):

drm_file_free()
drm_gem_release(dev, file) dev->driver->postclose(dev, file) -> qda_postclose()

    qda_session_put(file->driver_priv)

kfree(file)

Because qda_postclose() only drops the file owner's session reference without clearing session->file or synchronizing with in-flight invocations, any session kept alive by an invocation retains a non-NULL session->file pointer to freed memory. When the DSP response later triggers qda_fastrpc_cleanup_handlelist():

void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx)

{
	...
	for (i = 0; i < QDA_FASTRPC_MAX_HANDLELIST; i++) {
		if (!handlelist[i])
			break;
		if (ctx->session->file)
			drm_gem_handle_delete(ctx->session->file, (u32)handlelist[i]);
	}
}

checking ctx->session->file and passing it to drm_gem_handle_delete() will dereference the freed struct drm_file and its destroyed handle table.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
index ec37a38e92158..2971f2f9461fc 100644

[ ... ]

Quoted 1 line
drivers/accel/qda/qda_fastrpc.c +0 -0
@@ -70,3 +283,272 @@ struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev)

[ ... ]

drivers/accel/qda/qda_fastrpc.c struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev) · line 352

Quoted 12 lines
drivers/accel/qda/qda_fastrpc.c +12 -0
+static int process_dma_handle(struct qda_fastrpc_invoke_ctx *ctx, int i,
+			      union qda_remote_arg *rpra,
+			      struct qda_phy_page *pages)
+{
+	struct drm_gem_object *gem_obj;
+	int err;
+
+	if (!ctx->args[i].handle) {
+		rpra[i].buf.addr = ctx->args[i].offset;
+		rpra[i].buf.len = ctx->args[i].length;
+		return 0;
+	}

[Severity: High]
What prevents user space from bypassing handle validation in process_dma_handle() by passing handle == 0 with a non-zero offset for a DMA-handle argument?

In qda_fastrpc_get_args(), arguments at indices ctx->nbufs <= i < ctx->nscalars are DMA-handle arguments, which the DSP interprets as struct qda_remote_dmahandle (rpra[i].dma). Because union qda_remote_arg overlays struct qda_remote_buf with struct qda_remote_dmahandle:

union qda_remote_arg {
	struct qda_remote_buf buf;
	struct qda_remote_dmahandle dma;
};

writing the 64-bit ctx->args[i].offset into rpra[i].buf.addr when ctx->args[i].handle is 0 populates rpra[i].dma.handle with the low 32 bits of ctx->args[i].offset and rpra[i].dma.offset with the high 32 bits, while rpra[i].buf.len overwrites rpra[i].dma.len.

In addition, qda_fastrpc_get_args() sets list[i].num = ctx->args[i].length ? 1 : 0 before calling process_dma_handle(), so a zero handle with a non-zero length leaves list[i].num set to 1 while pages[i] remains unpopulated. Checking !ctx->args[i].handle before ctx->gem_objs[i] also skips any pre-populated ctx->gem_objs[i] entry when handle is 0.

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+
+	if (ctx->gem_objs[i]) {
+		gem_obj = ctx->gem_objs[i];
+	} else {
+		err = get_gem_obj_from_handle(ctx, ctx->args[i].handle, &gem_obj);
+		if (err)
+			return err;
+
+		ctx->gem_objs[i] = gem_obj;
+	}

drivers/accel/qda/qda_fastrpc.c struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev) · line 372

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+
+	setup_pages_from_gem_obj(to_qda_gem_obj(gem_obj), &pages[i]);
+
+	/* The DSP-facing offset and length fields are 32-bit by wire format */
+	rpra[i].dma.handle = ctx->args[i].handle;
+	rpra[i].dma.len = (u32)ctx->args[i].length;
+	rpra[i].dma.offset = (u32)ctx->args[i].offset;
+
+	return 0;
+}

[Severity: High]
Should process_dma_handle() validate ctx->args[i].offset and ctx->args[i].length against the GEM object's size and U32_MAX before casting them to u32?

In process_msg_buffer(), the user-supplied offset and length are checked against qda_gem_obj->size so that a descriptor cannot reference memory past the end of its buffer in the session's context bank:

	if (ctx->args[i].offset > qda_gem_obj->size ||
	    len > qda_gem_obj->size - ctx->args[i].offset) {
		...
		return -EINVAL;
	}

In process_dma_handle(), however, ctx->args[i].length and ctx->args[i].offset are cast directly to u32 and written into rpra[i].dma.len and rpra[i].dma.offset without checking that the region lies within to_qda_gem_obj(gem_obj)->size or fits within 32 bits. Can user space use a DMA-handle argument to pass a truncated or out-of-bounds region to the DSP?

[ ... ]

Quoted 17 lines
drivers/accel/qda/qda_fastrpc.c +17 -0
+static int qda_fastrpc_prepare_args_invoke(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
+{
+	struct drm_qda_invoke_args *invoke_args = argp;
+	struct drm_qda_fastrpc_invoke_args *args;
+	u32 nscalars;
+
+	ctx->handle = invoke_args->handle;
+	ctx->sc = invoke_args->sc;
+
+	nscalars = QDA_REMOTE_SCALARS_LENGTH(ctx->sc);
+	if (!nscalars)
+		return 0;
+
+	args = kzalloc_objs(*args, nscalars);
+	if (!args)
+		return -ENOMEM;
+

drivers/accel/qda/qda_fastrpc.c struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev) · line 517

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+	if (copy_from_user(args, u64_to_user_ptr(invoke_args->args),
+			   nscalars * sizeof(*args))) {
+		kfree(args);
+		return -EFAULT;
+	}
+
+	ctx->args = args;
+
+	return 0;
+}

[Severity: Medium]
Should qda_fastrpc_prepare_args_invoke() reject non-zero values in args[i].attr with -EINVAL?

struct drm_qda_fastrpc_invoke_args defines __u32 attr in the UAPI header, with no valid attribute flags currently defined, and nothing in qda_fastrpc_prepare_args_invoke() or qda_fastrpc_get_args() checks that args[i].attr is zero. If user space passes uninitialized non-zero values in attr today, won't that prevent future kernel versions from introducing flag bits in attr without breaking existing user space?

drivers/accel/qda/qda_legacy_ops.c static void legacy_session_destroy(struct qda_session *session) · line 38

Quoted 12 lines
drivers/accel/qda/qda_legacy_ops.c +1 -1
index 3566a10c64880..9639ea84b2007 100644
@@ -33,7 +33,7 @@ static void legacy_session_destroy(struct qda_session *session)
 {
 	struct drm_device *drm_dev = &session->qda_dev->drm_dev;
 
-	qda_session_free(session);
+	qda_session_put(session);
 	drm_dev_put(drm_dev);
 }

[Severity: High]
Can dropping the drm_device reference before the last reference to struct qda_session is released cause a use-after-free of struct qda_dev?

With this change, struct qda_session is reference-counted and can outlive file closure (both via legacy_session_destroy() and via drm_release() -> qda_postclose()) when an in-flight invocation or its deferred ctx->put_work still holds a reference on ctx->session.

However, legacy_session_destroy() still calls drm_dev_put(drm_dev) immediately after qda_session_put(session), and struct qda_session does not hold its own drm_dev_get() reference until qda_session_release(). If the file is closed while an invocation is still in flight and the underlying rpmsg device is unbound before ctx->put_work completes, the final drm_device reference can be dropped and struct qda_dev freed before qda_fastrpc_context_free() and qda_session_release() run:

qda_fastrpc_context_free()
xa_erase(&ctx->qdev->ctx_xa, ...) qda_session_msg_buf_put(ctx->session, &ctx->msg_gem_obj->base) qda_session_put(ctx->session) -> qda_session_release()

    qda_session_msg_buf_pool_destroy(session)
      drm_gem_object_put(entry->gem_obj)

At that point, accessing ctx->qdev->ctx_xa and freeing the session's GEM objects will dereference the already-freed struct qda_dev. Should the drm_device reference be held across the lifetime of struct qda_session and released in qda_session_release() instead?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=12
#31 lore raw

PATCH v4 13/25 accel/qda: Add the invocation ioctl and teardown on unplug

In reply to Ekansh Gupta

Tie the invocation pieces together behind an ioctl: allocate an invocation, take its arguments from user space, build the payload, send it to the DSP, wait, and act on the answer.

The wait is interruptible, so a DSP that never answers does not leave the caller unkillable. An interrupted caller drops its own reference and returns, but the invocation lives on, because the DSP still owns the payload buffer and may write into it. It is freed once the DSP answers or the transport goes away.

When the remote processor goes away, invocations may be waiting for answers that will never come. Those waiters are failed with -EPIPE and woken so they can release the buffers they hold. The flush runs after the device is unplugged, so the response handler is drained and the flush is the only thing completing invocations.

The flush drops the reference the send path took for the response. An invocation that has not been sent yet, or whose response has already been handled, does not hold that reference, so the flush can drop one that is not its own. Both windows are narrow but open exactly when the DSP crashes.

---
 drivers/accel/qda/qda_drv.c     |  1 +
 drivers/accel/qda/qda_fastrpc.c | 67 +++++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h |  2 ++
 drivers/accel/qda/qda_ioctl.c   | 36 ++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.h   |  1 +
 drivers/accel/qda/qda_rpmsg.c   |  1 +
 include/uapi/drm/qda_accel.h    |  3 ++
 7 files changed, 111 insertions(+)
drivers/accel/qda/qda_drv.c +1 -0
index 99bd37fca438..7d44342d3f67 100644
@@ -110,6 +110,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_QUERY, qda_ioctl_query, 0),
 	DRM_IOCTL_DEF_DRV(QDA_GEM_CREATE, qda_ioctl_gem_create, 0),
 	DRM_IOCTL_DEF_DRV(QDA_GEM_MMAP_OFFSET, qda_ioctl_gem_mmap_offset, 0),
+	DRM_IOCTL_DEF_DRV(QDA_REMOTE_INVOKE, qda_ioctl_invoke, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_fastrpc.c +67 -0

79 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +2 -0
index 8440efc21b77..61f80c64e6d2 100644
@@ -205,5 +205,7 @@ void qda_session_msg_buf_pool_destroy(struct qda_session *session);
 int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx,
 			     struct qda_session *session, void *argp);
 int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg *msg);
+int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ctx *ctx);
+void qda_fastrpc_flush_pending(struct qda_dev *qdev);
 
 #endif /* __QDA_FASTRPC_H__ */
drivers/accel/qda/qda_ioctl.c +36 -0
index b11743ab791e..e9f4151e344a 100644
@@ -6,6 +6,7 @@
 #include <drm/drm_gem.h>
 #include <drm/qda_accel.h>
 #include "qda_drv.h"
+#include "qda_fastrpc.h"
 #include "qda_gem.h"
 #include "qda_ioctl.h"
 
@@ -73,3 +74,38 @@ int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil
 
 	return drm_gem_dumb_map_offset(file_priv, dev, args->handle, &args->offset);
 }
+
+static int qda_fastrpc_invoke(void *data, struct qda_session *session)
+{
+	struct qda_dev *qdev = session->qda_dev;
+	struct qda_fastrpc_invoke_ctx *ctx;
+	int err;
+
+	ctx = qda_fastrpc_context_alloc(qdev);
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
+
+	err = qda_fastrpc_prepare_args(ctx, session, data);
+	if (err)
+		goto out_put;
+
+	err = qda_fastrpc_invoke_common(qdev, ctx);
+
+out_put:
+	kref_put(&ctx->refcount, qda_fastrpc_context_free);
+
+	return err;
+}
+
+/**
+ * qda_ioctl_invoke() - Run a method on the DSP
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_invoke_args)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	return qda_fastrpc_invoke(data, file_priv->driver_priv);
+}
drivers/accel/qda/qda_ioctl.h +1 -0
index d1cbbfb6d965..3bb9cfd98370 100644
@@ -11,5 +11,6 @@
 int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv);
 
 #endif /* __QDA_IOCTL_H__ */
drivers/accel/qda/qda_rpmsg.c +1 -0
index 43b56ab3f012..7e58df3eb2e1 100644
@@ -160,6 +160,7 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 	 */
 	drm_dev_unplug(&qdev->drm_dev);
 	qdev->rpdev = NULL;
+	qda_fastrpc_flush_pending(qdev);
 	qda_cb_unpopulate(qdev);
 	qda_deinit_device(qdev);
 }
include/uapi/drm/qda_accel.h +3 -0
index 6d81f0ef47b6..c21bc5b2446c 100644
@@ -15,6 +15,7 @@ extern "C" {
 #define DRM_QDA_QUERY			0x00
 #define DRM_QDA_GEM_CREATE		0x01
 #define DRM_QDA_GEM_MMAP_OFFSET		0x02
+#define DRM_QDA_REMOTE_INVOKE		0x03
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -22,6 +23,8 @@ extern "C" {
 					 struct drm_qda_gem_create)
 #define DRM_IOCTL_QDA_GEM_MMAP_OFFSET	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_GEM_MMAP_OFFSET, \
 					 struct drm_qda_gem_mmap_offset)
+#define DRM_IOCTL_QDA_REMOTE_INVOKE	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_INVOKE, \
+					 struct drm_qda_invoke_args)
 
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
Signature · 1 line
2.34.1
#32 lore raw

Re: [PATCH v4 13/25] accel/qda: Add the invocation ioctl and teardown on unplug

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: - [Critical] accel/qda: unconditional kref_put() in qda_fastrpc_flush_pending() - [Critical] accel/qda…

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] accel/qda: unconditional kref_put() in qda_fastrpc_flush_pending()
- [Critical] accel/qda: premature teardown in qda_rpmsg_remove() while ctxs active
- [Critical] accel/qda: missing init handle check in qda_fastrpc_prepare_args_invoke()
- [Critical] accel/qda: missing bounds check and union misuse in process_dma_handle()
- [Medium] accel/qda: handle list leak on DSP error in qda_fastrpc_invoke_common() --

commit 9e89a77ec2ad7f5231cb9a9e8fb15165b01e374a
Author: Ekansh Gupta <[email protected]>

accel/qda: Add the invocation ioctl and teardown on unplug

This commit adds the DRM_IOCTL_QDA_REMOTE_INVOKE ioctl to allocate an invocation context, prepare arguments, pack and send the payload over RPMsg, and wait for the DSP response. It also adds qda_fastrpc_flush_pending() to fail and wake pending invocations when the RPMsg device is removed.

Quoted 29 lines
drivers/accel/qda/qda_fastrpc.c +14 -0
index 2971f2f9461fc..3a1959e2c53ee 100644
@@ -10,6 +10,7 @@
 #include "qda_drv.h"
 #include "qda_fastrpc.h"
 #include "qda_gem.h"
+#include "qda_rpmsg.h"
 
 /*
  * The caller owns the returned reference and must drop it with
@@ -484,6 +485,72 @@ int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg *
 	return 0;
 }
 
+/**
+ * qda_fastrpc_flush_pending() - Fail every invocation still in flight
+ * @qdev: Device whose invocations should be failed
+ *
+ * The DSP has gone away and will never answer, so stand in for the response
+ * handler: wake each waiter with an error and drop the reference the send
+ * path took for that handler. Each waiter then tears its invocation down,
+ * releasing the buffers it holds.
+ *
+ * Must run after drm_dev_unplug() has drained the response handler, so this
+ * is the only path completing an invocation.
+ */
+void qda_fastrpc_flush_pending(struct qda_dev *qdev)

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * · line 510

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+{
+	struct qda_fastrpc_invoke_ctx *ctx;
+	unsigned long idx;
+
+	xa_for_each(&qdev->ctx_xa, idx, ctx) {
+		ctx->retval = -EPIPE;
+		complete(&ctx->work);
+		kref_put(&ctx->refcount, qda_fastrpc_context_free);
+	}
+}

[Severity: Critical]
Can qda_fastrpc_flush_pending() drop a reference that was never taken (or was already queued to be dropped), causing a reference count underflow and use-after-free of struct qda_fastrpc_invoke_ctx?

In qda_fastrpc_invoke(), qda_fastrpc_context_alloc() initializes ctx->refcount to 1 and immediately inserts ctx into qdev->ctx_xa before qda_fastrpc_prepare_args(), qda_fastrpc_invoke_pack(), and qda_rpmsg_send_msg() run:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_alloc() {
	...
	kref_init(&ctx->refcount);
	...
	ret = xa_alloc(&qdev->ctx_xa, &id, ctx, XA_LIMIT(1, QDA_FASTRPC_CTXID_MAX),
		       GFP_KERNEL);
}

Because drm_ioctl() only checks drm_dev_is_unplugged() at entry and does not hold a drm_dev_enter() read lock across qda_ioctl_invoke(), drm_dev_unplug() in qda_rpmsg_remove() does not wait for in-flight qda_fastrpc_invoke() calls. The second reference for the response handler is only acquired inside qda_rpmsg_send_msg() if drm_dev_enter() succeeds, qdev->rpdev is non-NULL, and rpmsg_send() succeeds:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_send_msg() {
	if (!drm_dev_enter(&qdev->drm_dev, &idx))
		return -ENODEV;

	if (!qdev->rpdev) {
		ret = -ENODEV;
		goto out_exit;
	}

	kref_get(&ctx->refcount);

	ret = rpmsg_send(qdev->rpdev->ept, &msg->fastrpc, sizeof(msg->fastrpc));
	if (ret) {
		drm_err_ratelimited(&qdev->drm_dev, "Failed to send rpmsg: %d\n", ret);
		kref_put(&ctx->refcount, qda_fastrpc_context_free);
	}
}

Similarly, if qda_rpmsg_cb() handled a response just before unplug, it schedules ctx->put_work to drop the response reference asynchronously while ctx remains in qdev->ctx_xa until qda_fastrpc_context_free() runs:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_cb() {
	...
	ctx->retval = resp_msg->retval;
	complete(&ctx->work);
	schedule_work(&ctx->put_work);
}

When qda_fastrpc_flush_pending() iterates qdev->ctx_xa unlocked and unconditionally calls kref_put(&ctx->refcount, qda_fastrpc_context_free), it drops the caller's or worker's reference in both windows, freeing ctx while qda_fastrpc_invoke() or qda_fastrpc_context_put_wq() is still using it. In addition, because xa_for_each() runs without locking or pinning ctx, a concurrent caller or put_work worker can free ctx right after xa_for_each() loads the pointer, causing qda_fastrpc_flush_pending() to write ctx->retval and call complete() and kref_put() on freed memory.

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+
+/**
+ * qda_fastrpc_invoke_common() - Shared tail of a FastRPC invocation
+ * @qdev: Device to invoke on
+ * @ctx: Invocation whose arguments are already prepared
+ *
+ * Packs the invocation into a message, sends it, waits for the DSP, and acts
+ * on the answer. Common to the accel UAPI and the legacy shim, once each has
+ * prepared ctx->args and ctx->gem_objs by its own means.
+ *

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * · line 530

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ctx *ctx)
+{
+	struct qda_msg msg = {};
+	int err;
+
+	err = qda_fastrpc_invoke_pack(ctx, &msg);
+	if (err)
+		return err;

[Severity: Critical]
When qda_fastrpc_invoke_common() calls qda_fastrpc_invoke_pack() -> qda_fastrpc_get_args() -> process_dma_handle() for handle arguments (i >= ctx->nbufs), can user space forge DSP DMA handle descriptors or trigger out-of-bounds DSP memory accesses?

In process_dma_handle():

drivers/accel/qda/qda_fastrpc.c:process_dma_handle() {
	if (!ctx->args[i].handle) {
		rpra[i].buf.addr = ctx->args[i].offset;
		rpra[i].buf.len = ctx->args[i].length;
		return 0;
	}
	...
	/* The DSP-facing offset and length fields are 32-bit by wire format */
	rpra[i].dma.handle = ctx->args[i].handle;
	rpra[i].dma.len = (u32)ctx->args[i].length;
	rpra[i].dma.offset = (u32)ctx->args[i].offset;
}

First, rpra[i] is a union qda_remote_arg, which the DSP interprets as struct qda_remote_dmahandle dma (s32 handle; u32 offset; u32 len;) for handle arguments (i >= ctx->nbufs). When ctx->args[i].handle is 0, writing the 64-bit ctx->args[i].offset into rpra[i].buf.addr aliases rpra[i].dma.handle with the lower 32 bits and rpra[i].dma.offset with the upper 32 bits, allowing user space to inject an arbitrary non-zero DSP handle and offset while bypassing get_gem_obj_from_handle() validation.

Second, when ctx->args[i].handle is non-zero, process_dma_handle() looks up gem_obj via get_gem_obj_from_handle(), but unlike process_msg_buffer(), it never verifies that ctx->args[i].offset and ctx->args[i].length fit within to_qda_gem_obj(gem_obj)->size (or within u32 before truncating them to rpra[i].dma.offset and rpra[i].dma.len).

Quoted 12 lines
drivers/accel/qda/qda_fastrpc.c +12 -0
+
+	err = qda_rpmsg_send_msg(qdev, &msg);
+	if (err)
+		return err;
+
+	err = qda_rpmsg_wait_for_rsp(ctx);
+	if (err)
+		return err;
+
+	/* Ensure the results written by the DSP are visible before reading */
+	dma_rmb();
+

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * · line 552

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+	err = ctx->retval;
+	if (err) {
+		drm_dbg_driver(&qdev->drm_dev, "DSP returned status %d\n", ctx->retval);
+		return err;
+	}
+
+	qda_fastrpc_cleanup_handlelist(ctx);
+
+	return 0;
+}

[Severity: Medium]
Does returning early when ctx->retval is non-zero in qda_fastrpc_invoke_common() leak the GEM handles listed in the DSP's response handle list?

When a remote DSP invocation fails with a non-zero status code, the DSP framework can still populate the handle release list in the payload buffer (as addressed in drivers/misc/fastrpc.c by commit 1c8093591d1e ("misc:
fastrpc: Clean buffers on remote invocation failures")). Returning before qda_fastrpc_cleanup_handlelist(ctx) leaves those GEM handles open in ctx->session->file.

[ ... ]

Quoted 22 lines
drivers/accel/qda/qda_ioctl.c +7 -0
index b11743ab791e7..e9f4151e344af 100644
@@ -6,6 +6,7 @@
 #include <drm/drm_gem.h>
 #include <drm/qda_accel.h>
 #include "qda_drv.h"
+#include "qda_fastrpc.h"
 #include "qda_gem.h"
 #include "qda_ioctl.h"
 
@@ -73,3 +74,38 @@ int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil
 
 	return drm_gem_dumb_map_offset(file_priv, dev, args->handle, &args->offset);
 }
+
+static int qda_fastrpc_invoke(void *data, struct qda_session *session)
+{
+	struct qda_dev *qdev = session->qda_dev;
+	struct qda_fastrpc_invoke_ctx *ctx;
+	int err;

drivers/accel/qda/qda_ioctl.c int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil · line 92

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+
+	ctx = qda_fastrpc_context_alloc(qdev);
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
+
+	err = qda_fastrpc_prepare_args(ctx, session, data);
+	if (err)
+		goto out_put;
+
+	err = qda_fastrpc_invoke_common(qdev, ctx);

[Severity: Critical]
Should qda_fastrpc_invoke() or qda_fastrpc_prepare_args_invoke() reject user-space invocations targeting handle 1 (QDA_FASTRPC_INIT_HANDLE)?

In qda_fastrpc_prepare_args() -> qda_fastrpc_prepare_args_invoke():

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_prepare_args_invoke() {
	...
	ctx->handle = invoke_args->handle;
	ctx->sc = invoke_args->sc;
}

In the FastRPC protocol, handle 1 is reserved for kernel-issued process and protection domain management calls. Unlike fastrpc_internal_invoke() in drivers/misc/fastrpc.c, which rejects user-space calls with handle == FASTRPC_INIT_HANDLE with -EPERM (CVE-2019-2308), DRM_IOCTL_QDA_REMOTE_INVOKE allows unprivileged user space to invoke handle 1 directly.

[ ... ]

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 166

Quoted 12 lines
drivers/accel/qda/qda_rpmsg.c +1 -0
index 43b56ab3f0128..7e58df3eb2e17 100644
@@ -160,6 +160,7 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 	 */
 	drm_dev_unplug(&qdev->drm_dev);
 	qdev->rpdev = NULL;
+	qda_fastrpc_flush_pending(qdev);
 	qda_cb_unpopulate(qdev);
 	qda_deinit_device(qdev);
 }

[Severity: Critical]
Does qda_rpmsg_remove() tear down qdev->cb_devs, qdev->iommu_mgr, and qdev->ctx_xa while woken invocations and pending ctx->put_work items are still using them?

When an invocation is waiting in qda_rpmsg_wait_for_rsp(), ctx->refcount is 2 (one held by the caller in qda_fastrpc_invoke() and one taken by qda_rpmsg_send_msg()). Calling qda_fastrpc_flush_pending() wakes the waiter and drops one reference, reducing ctx->refcount to 1, while ctx remains in qdev->ctx_xa until the woken caller thread (or a pending ctx->put_work worker) runs qda_fastrpc_context_free():

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_free() {
	...
	xa_erase(&ctx->qdev->ctx_xa, FIELD_GET(QDA_FASTRPC_CTXID_MASK, ctx->ctxid));
	...
	if (ctx->msg_gem_obj)
		qda_session_msg_buf_put(ctx->session, &ctx->msg_gem_obj->base);

	if (ctx->session)
		qda_session_put(ctx->session);
	...
}

Because qda_rpmsg_remove() immediately proceeds to qda_cb_unpopulate(qdev) and qda_deinit_device(qdev) without waiting for in-flight invocations or flushing pending ctx->put_work items:

1. qda_cb_unpopulate() destroys and frees the child context-bank platform devices via of_platform_device_destroy(&entry->pdev->dev, NULL) while GEM objects held by active invocations and sessions still reference &entry->pdev->dev via gem_obj->iommu_dev->dev, leading to a use-after-free in qda_dma_free() when dma_free_coherent() runs.

2. qda_deinit_device() calls qda_memory_manager_exit(qdev->iommu_mgr) and xa_destroy(&qdev->ctx_xa) while contexts are still present in qdev->ctx_xa; when woken threads or ctx->put_work workers subsequently execute qda_fastrpc_context_free(), they call xa_erase() on the destroyed XArray.

3. If an interrupted caller already returned and closed its DRM file while ctx->put_work is still queued (since neither qda_session nor qda_fastrpc_invoke_ctx holds a drm_dev_get() reference on qdev->drm_dev), finishing qda_rpmsg_remove() allows devm to release qdev before qda_fastrpc_context_put_wq() runs qda_fastrpc_context_free() and dereferences ctx->qdev.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=13
#33 lore raw

PATCH v4 14/25 accel/qda: Add invocation through the legacy FastRPC shim

In reply to Ekansh Gupta

Let applications written for the /dev/fastrpc-* interface run methods on the DSP, by converting their arguments into what the invocation code takes and handing them over.

A legacy argument is a user pointer, a length and a file descriptor for the buffer it lives in. The shim imports each descriptor into the session's context bank, so the DSP can reach the buffer, and passes the resulting objects along with the pointers and lengths. A session the shim created has no handle table, so these arguments cannot be named by handle the way the accel interface does; the invocation takes the already-resolved objects instead. Ownership of each object moves into the invocation, so the shim drops its references itself only if importing a later argument fails.

The pointer a legacy caller passes is an address in its own mapping of the buffer, and the interface predates GEM handles and cannot change, so the offset into the buffer is recovered from that mapping. Unlike handle-named arguments, these are not checked against the size of the buffer they name.

---
 drivers/accel/qda/qda_fastrpc.c      | 151 +++++++++++++++++++++++++++++++++--
 drivers/accel/qda/qda_fastrpc.h      |  19 +++++
 drivers/accel/qda/qda_fastrpc_shim.c |  87 ++++++++++++++++++++
 drivers/accel/qda/qda_legacy_ops.c   |   8 ++
 drivers/accel/qda/qda_legacy_ops.h   |   4 +
 5 files changed, 262 insertions(+), 7 deletions(-)
drivers/accel/qda/qda_fastrpc.c +144 -7
index 3a1959e2c53e..693dd6f035d2 100644
@@ -55,6 +55,22 @@ static void setup_pages_from_gem_obj(struct qda_gem_obj *qda_gem_obj,
 	pages->size = qda_gem_obj->size;
 }
 
+static int calculate_vma_offset(u64 user_ptr, u64 *offset)
+{
+	struct vm_area_struct *vma;
+	int ret = -EFAULT;
+
+	mmap_read_lock(current->mm);
+	vma = find_vma(current->mm, user_ptr);
+	if (vma && vma->vm_start <= user_ptr) {
+		*offset = (user_ptr & PAGE_MASK) - vma->vm_start;
+		ret = 0;
+	}
+	mmap_read_unlock(current->mm);
+
+	return ret;
+}
+
 static u64 calculate_page_aligned_size(u64 addr, u64 len)
 {
 	u64 pg_start = (addr & PAGE_MASK) >> PAGE_SHIFT;
@@ -286,23 +302,45 @@ struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev)
 }
 
 /*
- * Resolve the address of an in/out buffer argument. The buffer is named by a
- * GEM handle, looked up in the calling session's own handle table, so a
- * session cannot name another's buffer. The offset and length come from user
- * space and are checked against the buffer's real size: a region described as
- * larger than its buffer would let the DSP read and write past the end of it,
- * into whatever else is mapped in the same context bank.
+ * Resolve the address of an in/out buffer argument. The buffer is either a
+ * GEM object the legacy shim already resolved into ctx->gem_objs[i], or a GEM
+ * handle looked up in the calling session's own handle table. For a handle,
+ * the offset and length come from user space and are checked against the
+ * buffer's real size: a region described as larger than its buffer would let
+ * the DSP read and write past the end of it, into whatever else is mapped in
+ * the same context bank.
  */
 static int process_msg_buffer(struct qda_fastrpc_invoke_ctx *ctx, int i,
 			      union qda_remote_arg *rpra,
 			      struct qda_phy_page *pages)
 {
 	u64 len = ctx->args[i].length;
-	struct qda_gem_obj *qda_gem_obj;
 	struct drm_gem_object *gem_obj;
+	struct qda_gem_obj *qda_gem_obj;
 	u64 unaligned_addr;
+	u64 vma_offset = 0;
 	int err;
 
+	if (ctx->gem_objs[i]) {
+		/*
+		 * The legacy ABI predates GEM handles, so the offset field
+		 * holds an address in the caller's own mapping of the buffer,
+		 * or 0 if it has none, meaning the start of the buffer.
+		 */
+		qda_gem_obj = to_qda_gem_obj(ctx->gem_objs[i]);
+		if (ctx->args[i].offset) {
+			err = calculate_vma_offset(ctx->args[i].offset, &vma_offset);
+			if (err)
+				return err;
+		}
+
+		rpra[i].buf.addr = ctx->args[i].offset;
+		pages[i].addr = qda_gem_obj->dma_addr + vma_offset;
+		pages[i].size = calculate_page_aligned_size(ctx->args[i].offset, len);
+
+		return 0;
+	}
+
 	if (!ctx->args[i].handle)
 		return -EINVAL;
 
@@ -551,6 +589,105 @@ int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ct
 	return 0;
 }
 
+/**
+ * qda_fastrpc_invoke_legacy() - Perform a dynamic invocation on behalf of the legacy shim
+ * @qdev: Device to invoke on
+ * @session: Legacy session (no backing drm_file)
+ * @handle: Remote handle to invoke on the DSP
+ * @sc: FastRPC scalars value encoding the number of in/out buffers
+ * @legacy_args: Kernel-side argument array, one entry per QDA_REMOTE_SCALARS_LENGTH(sc)
+ *
+ * Unlike the accel UAPI path, the argument descriptors and any backing GEM
+ * objects already live in kernel memory: the shim has already resolved
+ * every fd-backed argument via its own import path, since a legacy session
+ * has no GEM handle table to resolve a handle-based argument against.
+ * Ownership of each @legacy_args[i].obj reference transfers to this function,
+ * on failure as well as on success.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_fastrpc_invoke_legacy(struct qda_dev *qdev, struct qda_session *session,
+			      u32 handle, u32 sc, struct qda_legacy_invoke_arg *legacy_args)
+{
+	struct qda_fastrpc_invoke_ctx *ctx;
+	struct drm_qda_fastrpc_invoke_args *args;
+	u32 nscalars;
+	int err;
+	int i;
+
+	nscalars = QDA_REMOTE_SCALARS_LENGTH(sc);
+
+	/*
+	 * An fd naming a buffer another session exported resolves to the
+	 * original object, still mapped in that session's context bank, so
+	 * the same check as for a handle applies.
+	 */
+	for (i = 0; i < nscalars; ++i) {
+		if (legacy_args[i].obj &&
+		    to_qda_gem_obj(legacy_args[i].obj)->iommu_dev != session->assigned_iommu_dev) {
+			err = -EACCES;
+			goto err_put_legacy_objs;
+		}
+	}
+
+	ctx = qda_fastrpc_context_alloc(qdev);
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		goto err_put_legacy_objs;
+	}
+
+	/* Dropped by qda_fastrpc_context_free(), as for the accel UAPI path */
+	qda_session_get(session);
+	ctx->session = session;
+	ctx->handle = handle;
+	ctx->sc = sc;
+
+	ctx->nbufs = QDA_REMOTE_SCALARS_INBUFS(sc) + QDA_REMOTE_SCALARS_OUTBUFS(sc);
+	if (nscalars) {
+		args = kzalloc_objs(*args, nscalars);
+		if (!args) {
+			err = -ENOMEM;
+			goto err_put_legacy_objs_ctx;
+		}
+
+		ctx->gem_objs = kzalloc_objs(*ctx->gem_objs, nscalars);
+		if (!ctx->gem_objs) {
+			kfree(args);
+			err = -ENOMEM;
+			goto err_put_legacy_objs_ctx;
+		}
+
+		for (i = 0; i < nscalars; ++i) {
+			args[i].offset = legacy_args[i].ptr;
+			args[i].length = legacy_args[i].length;
+			/* process_dma_handle() skips handle args whose handle is 0 */
+			args[i].handle = (i >= ctx->nbufs && legacy_args[i].obj) ?
+					 (u32)legacy_args[i].fd : 0;
+			ctx->gem_objs[i] = legacy_args[i].obj;
+		}
+
+		ctx->args = args;
+	}
+
+	ctx->nscalars = nscalars;
+
+	err = qda_fastrpc_invoke_common(qdev, ctx);
+
+	kref_put(&ctx->refcount, qda_fastrpc_context_free);
+
+	return err;
+
+err_put_legacy_objs_ctx:
+	kref_put(&ctx->refcount, qda_fastrpc_context_free);
+err_put_legacy_objs:
+	/* The context never took the objects, so release them here */
+	for (i = 0; i < nscalars; ++i)
+		if (legacy_args[i].obj)
+			drm_gem_object_put(legacy_args[i].obj);
+
+	return err;
+}
+
 /*
  * INVOKE_DYNAMIC: the argument descriptors are supplied by user space, which
  * is also responsible for having imported every buffer to a GEM handle.
drivers/accel/qda/qda_fastrpc.h +19 -0
index 61f80c64e6d2..3243bd1065c6 100644
@@ -198,6 +198,23 @@ struct qda_fastrpc_invoke_ctx {
 	struct work_struct put_work;
 };
 
+/**
+ * struct qda_legacy_invoke_arg - Kernel-side invoke argument for the legacy shim
+ *
+ * Built by the FastRPC compatibility shim from a legacy fastrpc_invoke_args
+ * entry; @obj is NULL for an argument with no backing fd.
+ */
+struct qda_legacy_invoke_arg {
+	/** @ptr: Argument pointer (user virtual address) */
+	u64 ptr;
+	/** @length: Length of the argument data in bytes */
+	u64 length;
+	/** @obj: Pre-resolved GEM object for an fd-backed argument, or NULL */
+	struct drm_gem_object *obj;
+	/** @fd: Legacy fd, forwarded as the DSP-visible handle for handle arguments */
+	s32 fd;
+};
+
 struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev);
 void qda_fastrpc_context_free(struct kref *ref);
 void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx);
@@ -207,5 +224,7 @@ int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx,
 int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg *msg);
 int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ctx *ctx);
 void qda_fastrpc_flush_pending(struct qda_dev *qdev);
+int qda_fastrpc_invoke_legacy(struct qda_dev *qdev, struct qda_session *session,
+			      u32 handle, u32 sc, struct qda_legacy_invoke_arg *legacy_args);
 
 #endif /* __QDA_FASTRPC_H__ */
drivers/accel/qda/qda_fastrpc_shim.c +87 -0

112 lines not on this page · raw message

drivers/accel/qda/qda_legacy_ops.c +8 -0
index 9639ea84b200..74ee4995350e 100644
@@ -8,6 +8,7 @@
 #include <drm/drm_gem.h>
 #include <drm/drm_prime.h>
 #include "qda_drv.h"
+#include "qda_fastrpc.h"
 #include "qda_gem.h"
 #include "qda_legacy_ops.h"
 #include "qda_prime.h"
@@ -71,10 +72,17 @@ static struct drm_gem_object *legacy_import_dmabuf(struct qda_session *session,
 	return qda_prime_import_dmabuf(&session->qda_dev->drm_dev, session, dmabuf);
 }
 
+static int legacy_invoke(struct qda_session *session, u32 handle, u32 sc,
+			 struct qda_legacy_invoke_arg *args)
+{
+	return qda_fastrpc_invoke_legacy(session->qda_dev, session, handle, sc, args);
+}
+
 const struct qda_legacy_ops qda_legacy_ops_table = {
 	.session_create = legacy_session_create,
 	.session_destroy = legacy_session_destroy,
 	.alloc_dmabuf = legacy_alloc_dmabuf,
 	.import_dmabuf = legacy_import_dmabuf,
+	.invoke = legacy_invoke,
 };
 EXPORT_SYMBOL_FOR_MODULES(qda_legacy_ops_table, "qda_fastrpc_shim");
drivers/accel/qda/qda_legacy_ops.h +4 -0
index 76b80dc87a4b..ba1ce3c0a22b 100644
@@ -11,6 +11,7 @@
 struct dma_buf;
 struct drm_gem_object;
 struct qda_dev;
+struct qda_legacy_invoke_arg;
 struct qda_session;
 
 /**
@@ -34,6 +35,9 @@ struct qda_legacy_ops {
 	/** @import_dmabuf: Map a DMA-BUF into a session's context bank */
 	struct drm_gem_object *(*import_dmabuf)(struct qda_session *session,
 						struct dma_buf *dmabuf);
+	/** @invoke: Run a method on the DSP; takes ownership of each @args[i].obj */
+	int (*invoke)(struct qda_session *session, u32 handle, u32 sc,
+		      struct qda_legacy_invoke_arg *args);
 };
 
 extern const struct qda_legacy_ops qda_legacy_ops_table;
Signature · 1 line
2.34.1
#34 lore raw

Re: [PATCH v4 14/25] accel/qda: Add invocation through the legacy FastRPC shim

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: - [Critical] accel/qda: missing VMA and size checks in process_msg_buffer() - [High] accel/qda: broken fd…

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] accel/qda: missing VMA and size checks in process_msg_buffer()
- [High] accel/qda: broken fd and handle semantics in legacy invoke path
- [High] accel/qda: premature unmap of legacy DMA handle args in qda_fastrpc_context_free()
- [High] accel/qda: premature ctx_xa publication in qda_fastrpc_invoke_legacy()
- [High] accel/qda: race with unplug teardown in qda_shim_invoke()
--

commit db6b9ba28a99a54082c1f760f47bf540c628d123
Author: Ekansh Gupta <[email protected]>

accel/qda: Add invocation through the legacy FastRPC shim

This commit adds FASTRPC_IOCTL_INVOKE support to the legacy FastRPC compatibility shim by importing each fd-backed argument into the session's context bank and passing the resolved GEM objects into qda_fastrpc_invoke_legacy(). Buffer offsets for legacy arguments are derived from the caller's VMA mapping in process_msg_buffer().

Quoted 66 lines
drivers/accel/qda/qda_fastrpc.c +34 -7
index 3a1959e2c53ee..693dd6f035d2a 100644
@@ -55,6 +55,22 @@ static void setup_pages_from_gem_obj(struct qda_gem_obj *qda_gem_obj,
 	pages->size = qda_gem_obj->size;
 }
 
+static int calculate_vma_offset(u64 user_ptr, u64 *offset)
+{
+	struct vm_area_struct *vma;
+	int ret = -EFAULT;
+
+	mmap_read_lock(current->mm);
+	vma = find_vma(current->mm, user_ptr);
+	if (vma && vma->vm_start <= user_ptr) {
+		*offset = (user_ptr & PAGE_MASK) - vma->vm_start;
+		ret = 0;
+	}
+	mmap_read_unlock(current->mm);
+
+	return ret;
+}
+
 static u64 calculate_page_aligned_size(u64 addr, u64 len)
 {
 	u64 pg_start = (addr & PAGE_MASK) >> PAGE_SHIFT;
@@ -286,23 +302,45 @@ struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev)
 }
 
 /*
- * Resolve the address of an in/out buffer argument. The buffer is named by a
- * GEM handle, looked up in the calling session's own handle table, so a
- * session cannot name another's buffer. The offset and length come from user
- * space and are checked against the buffer's real size: a region described as
- * larger than its buffer would let the DSP read and write past the end of it,
- * into whatever else is mapped in the same context bank.
+ * Resolve the address of an in/out buffer argument. The buffer is either a
+ * GEM object the legacy shim already resolved into ctx->gem_objs[i], or a GEM
+ * handle looked up in the calling session's own handle table. For a handle,
+ * the offset and length come from user space and are checked against the
+ * buffer's real size: a region described as larger than its buffer would let
+ * the DSP read and write past the end of it, into whatever else is mapped in
+ * the same context bank.
  */
 static int process_msg_buffer(struct qda_fastrpc_invoke_ctx *ctx, int i,
 			      union qda_remote_arg *rpra,
 			      struct qda_phy_page *pages)
 {
 	u64 len = ctx->args[i].length;
-	struct qda_gem_obj *qda_gem_obj;
 	struct drm_gem_object *gem_obj;
+	struct qda_gem_obj *qda_gem_obj;
 	u64 unaligned_addr;
+	u64 vma_offset = 0;
 	int err;
 
+	if (ctx->gem_objs[i]) {
+		/*
+		 * The legacy ABI predates GEM handles, so the offset field
+		 * holds an address in the caller's own mapping of the buffer,
+		 * or 0 if it has none, meaning the start of the buffer.
+		 */
+		qda_gem_obj = to_qda_gem_obj(ctx->gem_objs[i]);
+		if (ctx->args[i].offset) {
+			err = calculate_vma_offset(ctx->args[i].offset, &vma_offset);

drivers/accel/qda/qda_fastrpc.c struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev) · line 342

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+			if (err)
+				return err;
+		}
+
+		rpra[i].buf.addr = ctx->args[i].offset;
+		pages[i].addr = qda_gem_obj->dma_addr + vma_offset;
+		pages[i].size = calculate_page_aligned_size(ctx->args[i].offset, len);
+
+		return 0;
+	}

[Severity: Critical]
Could this legacy buffer path in process_msg_buffer() in drivers/accel/qda/qda_fastrpc.c allow out-of-bounds DSP DMA access or Stream ID spoofing across context banks?

When qda_shim_invoke() -> qda_fastrpc_invoke_legacy() -> qda_fastrpc_invoke_common() -> qda_fastrpc_invoke_pack() -> qda_fastrpc_get_args() -> process_msg_buffer() resolves a buffer with ctx->gem_objs[i] set, calculate_vma_offset() looks up any VMA in current->mm containing user_ptr and computes:

    *offset = (user_ptr & PAGE_MASK) - vma->vm_start;

without verifying that the VMA backs the imported DMA-BUF for ctx->gem_objs[i] or accounting for vma->vm_pgoff.

Unlike the GEM handle path below it, which checks:

    if (ctx->args[i].offset > qda_gem_obj->size ||
        len > qda_gem_obj->size - ctx->args[i].offset)

and unlike fastrpc_map_attach() in drivers/misc/fastrpc.c, which checks len > map->size, the legacy branch in process_msg_buffer() does not check vma_offset, the in-page offset (ctx->args[i].offset & ~PAGE_MASK), or len against qda_gem_obj->size.

As a result, passing a length or VMA offset larger than qda_gem_obj->size sets pages[i].addr and pages[i].size to a range extending past the end of qda_gem_obj into adjacent mappings in the session's IOMMU context bank, such as ctx->msg_gem_obj. In addition, because qda_gem_obj->dma_addr encodes the session's Stream ID at soc_data->sid_pos (bit 32 on default_soc_data), passing a user_ptr inside a large anonymous VMA so that vma_offset >= (1ULL << 32) causes qda_gem_obj->dma_addr + vma_offset to carry into the Stream ID bits and target another session's context bank.

Finally, because ctx->args[i].offset and len are unvalidated 64-bit values on this path, addr + len - 1 and (pg_end - pg_start + 1) * PAGE_SIZE in calculate_page_aligned_size() can overflow 64-bit arithmetic and wrap pages[i].size to a small value or zero.

Should calculate_vma_offset() and process_msg_buffer() verify that the offset and length stay within qda_gem_obj->size without overflowing?

[ ... ]

Quoted 70 lines
drivers/accel/qda/qda_fastrpc.c +66 -0
@@ -551,6 +589,105 @@ int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ct
 	return 0;
 }
 
+/**
+ * qda_fastrpc_invoke_legacy() - Perform a dynamic invocation on behalf of the legacy shim
+ * @qdev: Device to invoke on
+ * @session: Legacy session (no backing drm_file)
+ * @handle: Remote handle to invoke on the DSP
+ * @sc: FastRPC scalars value encoding the number of in/out buffers
+ * @legacy_args: Kernel-side argument array, one entry per QDA_REMOTE_SCALARS_LENGTH(sc)
+ *
+ * Unlike the accel UAPI path, the argument descriptors and any backing GEM
+ * objects already live in kernel memory: the shim has already resolved
+ * every fd-backed argument via its own import path, since a legacy session
+ * has no GEM handle table to resolve a handle-based argument against.
+ * Ownership of each @legacy_args[i].obj reference transfers to this function,
+ * on failure as well as on success.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_fastrpc_invoke_legacy(struct qda_dev *qdev, struct qda_session *session,
+			      u32 handle, u32 sc, struct qda_legacy_invoke_arg *legacy_args)
+{
+	struct qda_fastrpc_invoke_ctx *ctx;
+	struct drm_qda_fastrpc_invoke_args *args;
+	u32 nscalars;
+	int err;
+	int i;
+
+	nscalars = QDA_REMOTE_SCALARS_LENGTH(sc);
+
+	/*
+	 * An fd naming a buffer another session exported resolves to the
+	 * original object, still mapped in that session's context bank, so
+	 * the same check as for a handle applies.
+	 */
+	for (i = 0; i < nscalars; ++i) {
+		if (legacy_args[i].obj &&
+		    to_qda_gem_obj(legacy_args[i].obj)->iommu_dev != session->assigned_iommu_dev) {
+			err = -EACCES;
+			goto err_put_legacy_objs;
+		}
+	}
+
+	ctx = qda_fastrpc_context_alloc(qdev);
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		goto err_put_legacy_objs;
+	}
+
+	/* Dropped by qda_fastrpc_context_free(), as for the accel UAPI path */
+	qda_session_get(session);
+	ctx->session = session;
+	ctx->handle = handle;
+	ctx->sc = sc;
+
+	ctx->nbufs = QDA_REMOTE_SCALARS_INBUFS(sc) + QDA_REMOTE_SCALARS_OUTBUFS(sc);
+	if (nscalars) {
+		args = kzalloc_objs(*args, nscalars);
+		if (!args) {
+			err = -ENOMEM;
+			goto err_put_legacy_objs_ctx;
+		}
+
+		ctx->gem_objs = kzalloc_objs(*ctx->gem_objs, nscalars);
+		if (!ctx->gem_objs) {
+			kfree(args);
+			err = -ENOMEM;
+			goto err_put_legacy_objs_ctx;

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ct · line 667

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+		}
+
+		for (i = 0; i < nscalars; ++i) {
+			args[i].offset = legacy_args[i].ptr;
+			args[i].length = legacy_args[i].length;
+			/* process_dma_handle() skips handle args whose handle is 0 */
+			args[i].handle = (i >= ctx->nbufs && legacy_args[i].obj) ?
+					 (u32)legacy_args[i].fd : 0;
+			ctx->gem_objs[i] = legacy_args[i].obj;
+		}

[Severity: High]
Won't moving the sole reference to legacy_args[i].obj into ctx->gem_objs[i] for DMA handle arguments (i >= ctx->nbufs) in qda_fastrpc_invoke_legacy() cause them to be unmapped from the SMMU context bank as soon as the invocation finishes?

In qda_shim_invoke(), shim_import_fd() imports each fd via qda_prime_import_dmabuf(), returning a drm_gem_object with a single reference for any external DMA-BUF (or once user space closes its fd). When the invocation finishes, qda_fastrpc_context_free() drops every entry in ctx->gem_objs:

    if (ctx->gem_objs) {
        for (i = 0; i < ctx->nscalars; ++i) {
            if (ctx->gem_objs[i])
                drm_gem_object_put(ctx->gem_objs[i]);
        }
        kfree(ctx->gem_objs);
    }

Unlike drivers/misc/fastrpc.c (where fastrpc_create_maps() retains handle mappings on fl->maps across invocations until the DSP returns their fd in fdlist) and unlike the accel UAPI path (where ctx->session->file holds a GEM handle reference until qda_fastrpc_cleanup_handlelist() deletes it), qda_fastrpc_context_free() drops the last reference on DMA handle objects immediately on return, unmapping them while the DSP still holds their IOVA. Moreover, qda_fastrpc_cleanup_handlelist() checks if (ctx->session->file), which is always NULL for legacy sessions.

How are DMA handle mappings meant to stay alive across invocations and be released via handlelist for legacy sessions?

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_common(struct qda_dev *qdev, struct qda_fastrpc_invoke_ct · line 676

Quoted 9 lines
drivers/accel/qda/qda_fastrpc.c +9 -0
+
+		ctx->args = args;
+	}
+
+	ctx->nscalars = nscalars;
+
+	err = qda_fastrpc_invoke_common(qdev, ctx);
+
+	kref_put(&ctx->refcount, qda_fastrpc_context_free);

[Severity: High]
Can a concurrent call to qda_rpmsg_remove() -> qda_fastrpc_flush_pending() drop the only reference on ctx and cause a use-after-free and double put in qda_fastrpc_invoke_legacy()?

In qda_fastrpc_invoke_legacy(), qda_fastrpc_context_alloc() initializes ctx->refcount to 1 and immediately inserts ctx into qdev->ctx_xa via xa_alloc(), before ctx->session, ctx->args, and ctx->gem_objs are populated and before qda_rpmsg_send_msg() takes the second reference for the response path.

Because qda_shim_ioctl() only performs a lockless drm_dev_is_unplugged() check rather than holding a drm_dev_enter() section across the ioctl, qda_rpmsg_remove() can run concurrently and call qda_fastrpc_flush_pending():

    xa_for_each(&qdev->ctx_xa, idx, ctx) {
        ctx->retval = -EPIPE;
        complete(&ctx->work);
        kref_put(&ctx->refcount, qda_fastrpc_context_free);
    }

If qda_fastrpc_flush_pending() runs before qda_rpmsg_send_msg() acquires the second reference (or after qda_rpmsg_send_msg() fails with -ENODEV), it drops ctx->refcount from 1 to 0 and frees ctx while qda_fastrpc_invoke_legacy() is still using it, followed by a second kref_put() on the freed ctx when qda_fastrpc_invoke_legacy() returns.

[ ... ]

Quoted 4 lines
drivers/accel/qda/qda_fastrpc_shim.c +0 -0
index 25f5c242efde1..03fce990ccdb9 100644

[ ... ]

Quoted 65 lines
drivers/accel/qda/qda_fastrpc_shim.c +54 -0
@@ -50,6 +52,25 @@ struct qda_shim_dev {
 
 static const struct file_operations qda_shim_fops;
 
+/* A negative @fd means the argument has no buffer, which is not an error */
+static struct drm_gem_object *shim_import_fd(struct qda_session *session, s32 fd)
+{
+	struct drm_gem_object *obj;
+	struct dma_buf *dmabuf;
+
+	if (fd < 0)
+		return NULL;
+
+	dmabuf = dma_buf_get(fd);
+	if (IS_ERR(dmabuf))
+		return ERR_CAST(dmabuf);
+
+	obj = qda_legacy_ops_table.import_dmabuf(session, dmabuf);
+	dma_buf_put(dmabuf);
+
+	return obj;
+}
+
 static int qda_shim_open(struct inode *inode, struct file *file)
 {
 	struct qda_shim_node *snode = container_of(file->private_data, struct qda_shim_node,
@@ -120,11 +141,77 @@ static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp)
 	return 0;
 }
 
+static long qda_shim_invoke(struct qda_shim_file *sf, void __user *argp)
+{
+	struct qda_legacy_invoke_arg *largs;
+	struct fastrpc_invoke_args *uargs;
+	struct fastrpc_invoke args;
+	u32 nargs;
+	int err;
+	int i;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	nargs = QDA_REMOTE_SCALARS_LENGTH(args.sc);
+	if (!nargs)
+		return qda_legacy_ops_table.invoke(sf->session, args.handle, args.sc, NULL);
+
+	uargs = kcalloc(nargs, sizeof(*uargs), GFP_KERNEL);
+	if (!uargs)
+		return -ENOMEM;
+
+	if (copy_from_user(uargs, u64_to_user_ptr(args.args), nargs * sizeof(*uargs))) {
+		kfree(uargs);
+		return -EFAULT;
+	}
+
+	largs = kcalloc(nargs, sizeof(*largs), GFP_KERNEL);
+	if (!largs) {
+		kfree(uargs);
+		return -ENOMEM;
+	}
+
+	for (i = 0; i < nargs; ++i) {
+		struct drm_gem_object *obj;
+
+		largs[i].ptr = uargs[i].ptr;

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp) · line 188

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+		largs[i].length = uargs[i].length;
+		largs[i].fd = uargs[i].fd;
+
+		obj = shim_import_fd(sf->session, uargs[i].fd);
+		if (IS_ERR(obj)) {
+			err = PTR_ERR(obj);
+			goto err_put_objs;
+		}
+		largs[i].obj = obj;
+	}

[Severity: High]
Does the fd handling across shim_import_fd(), qda_fastrpc_invoke_legacy(), process_msg_buffer(), and process_dma_handle() break several valid legacy FASTRPC_IOCTL_INVOKE argument patterns?

1. In drivers/misc/fastrpc.c, fastrpc_create_maps() skips importing arguments when fd == 0, fd == -1, or length == 0. Here, qda_shim_invoke() calls shim_import_fd() even when uargs[i].length == 0, and shim_import_fd() only returns NULL for fd < 0. If user space passes a zero-initialized descriptor (fd == 0, length == 0) or fd == 0 for an unbacked argument, shim_import_fd() calls dma_buf_get(0) (typically stdin) and fails the ioctl with -EINVAL or -EBADF.

2. Conversely, if a DMA handle argument (i >= ctx->nbufs) genuinely uses fd == 0 and shim_import_fd() imports it into legacy_args[i].obj, qda_fastrpc_invoke_legacy() sets:

       args[i].handle = (i >= ctx->nbufs && legacy_args[i].obj) ?
                        (u32)legacy_args[i].fd : 0;

which evaluates to 0. In process_dma_handle(), the check for !ctx->args[i].handle runs before checking ctx->gem_objs[i], so fd 0 is treated as having no buffer and setup_pages_from_gem_obj() is skipped.

3. For a buffer argument (i < ctx->nbufs) with fd < 0 and length > 0, shim_import_fd() returns NULL ("not an error"), leaving ctx->gem_objs[i] == NULL and ctx->args[i].handle == 0. When qda_fastrpc_get_args() calls process_msg_buffer(), both ctx->gem_objs[i] and ctx->args[i].handle are 0, so process_msg_buffer() immediately returns -EINVAL.

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp) · line 193

Quoted 5 lines
drivers/accel/qda/qda_fastrpc_shim.c +5 -0
+
+	kfree(uargs);
+
+	/* Takes ownership of every largs[i].obj, whether or not it succeeds */
+	err = qda_legacy_ops_table.invoke(sf->session, args.handle, args.sc, largs);

[Severity: High]
What prevents qda_rpmsg_remove() from tearing down the device while qda_shim_invoke() is importing buffers and building the invocation in drivers/accel/qda/qda_fastrpc_shim.c?

In qda_shim_ioctl(), the unplug check is a lockless call to drm_dev_is_unplugged(&sf->session->qda_dev->drm_dev), and drm_dev_enter() is not acquired until qda_rpmsg_send_msg().

If qda_rpmsg_remove() runs right after qda_shim_ioctl() passes drm_dev_is_unplugged(), drm_dev_unplug() completes immediately without waiting for in-flight shim ioctls, and qda_rpmsg_remove() proceeds to run:

    qda_fastrpc_flush_pending(qdev);
    qda_cb_unpopulate(qdev);
    qda_deinit_device(qdev);

This destroys the context bank platform devices (of_platform_device_destroy), frees qdev->iommu_mgr->devices, and destroys assignment_lock, qdev->ctx_xa, and qdev->import_lock while qda_shim_invoke() -> shim_import_fd() -> qda_prime_import_dmabuf() and qda_fastrpc_invoke_legacy() are concurrently accessing them.

Should qda_shim_ioctl() guard the dispatch call with drm_dev_enter() and drm_dev_exit() so that drm_dev_unplug() waits for in-flight legacy ioctls to finish before tearing down the device?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=14
#35 lore raw

PATCH v4 15/25 accel/qda: Add DSP process creation and release

In reply to Ekansh Gupta

An application needs a process on the DSP to run its own code, and that process has to be torn down when the application goes away. Add the requests that create and release one, and the ioctl that creates it.

Creating a process hands the DSP an ELF image, which the caller names by buffer handle, together with a block of memory the DSP loads the image into. That memory is allocated per session and kept until the process is released, since the DSP uses it for as long as the process runs. A process created again on the same session replaces the first, so the memory held for the previous one is dropped first.

A signed process runs with the DSP's trust in the signature, so creating one requires CAP_SYS_ADMIN; only an unsigned process may be created without it.

Releasing is sent when a session closes, if the device is still there to receive it. Once the DSP has torn the process down, the memory held for it is freed.

Every request after a successful create is directed at the new protection domain, so the session remembers which one it is in and the message built for a request carries it. Requests addressed to the initialisation handle carry session ID 0, since the session has no process yet to identify.

---
 drivers/accel/qda/qda_drv.c     |  16 +++-
 drivers/accel/qda/qda_drv.h     |  11 +++
 drivers/accel/qda/qda_fastrpc.c | 202 ++++++++++++++++++++++++++++++++++++++--
 drivers/accel/qda/qda_fastrpc.h |  48 +++++++++-
 drivers/accel/qda/qda_ioctl.c   |  59 +++++++++++-
 drivers/accel/qda/qda_ioctl.h   |   1 +
 include/uapi/drm/qda_accel.h    |  28 ++++++
 7 files changed, 354 insertions(+), 11 deletions(-)
drivers/accel/qda/qda_drv.c +15 -1
index 7d44342d3f67..983c6a3dcfb4 100644
@@ -13,6 +13,7 @@
 
 #include "qda_drv.h"
 #include "qda_fastrpc.h"
+#include "qda_gem.h"
 #include "qda_ioctl.h"
 #include "qda_prime.h"
 
@@ -72,6 +73,9 @@ static void qda_session_release(struct kref *ref)
 
 	qda_session_msg_buf_pool_destroy(session);
 
+	if (session->init_mem_gem_obj)
+		drm_gem_object_put(&session->init_mem_gem_obj->base);
+
 	if (session->assigned_iommu_dev)
 		qda_memory_manager_release_device(session->assigned_iommu_dev);
 
@@ -101,7 +105,16 @@ EXPORT_SYMBOL_FOR_MODULES(qda_session_put, "qda_fastrpc_shim");
 
 static void qda_postclose(struct drm_device *dev, struct drm_file *file)
 {
-	qda_session_put(file->driver_priv);
+	struct qda_session *session = file->driver_priv;
+	int idx;
+
+	/* Only a session that created a process has one to release */
+	if (READ_ONCE(session->init_mem_gem_obj) && drm_dev_enter(dev, &idx)) {
+		qda_release_dsp_process(session->qda_dev, session);
+		drm_dev_exit(idx);
+	}
+
+	qda_session_put(session);
 }
 
 DEFINE_DRM_ACCEL_FOPS(qda_accel_fops);
@@ -111,6 +124,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_GEM_CREATE, qda_ioctl_gem_create, 0),
 	DRM_IOCTL_DEF_DRV(QDA_GEM_MMAP_OFFSET, qda_ioctl_gem_mmap_offset, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_INVOKE, qda_ioctl_invoke, 0),
+	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_CREATE, qda_ioctl_init_create, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_drv.h +11 -0
index 80a8569b311a..2f98fd83fdda 100644
@@ -56,6 +56,14 @@ struct qda_session {
 	struct qda_iommu_device *assigned_iommu_dev;
 	/** @remote_session_id: Identifies this session to the DSP */
 	int remote_session_id;
+	/** @pd: Protection domain this session's requests are directed at */
+	int pd;
+	/**
+	 * @init_mem_gem_obj: Memory the DSP loaded this session's process
+	 * into, held for as long as the process runs. Swapped with xchg(), as
+	 * ioctls on one file can run concurrently.
+	 */
+	struct qda_gem_obj *init_mem_gem_obj;
 	/**
 	 * @msg_buf_pool: Invoke header buffers this session is done with,
 	 * kept for its later invocations to reuse.
@@ -137,4 +145,7 @@ struct qda_session *qda_session_alloc(struct qda_dev *qdev);
 void qda_session_get(struct qda_session *session);
 void qda_session_put(struct qda_session *session);
 
+/* DSP process management */
+int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session);
+
 #endif /* __QDA_DRV_H__ */
drivers/accel/qda/qda_fastrpc.c +195 -7

262 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +47 -1

79 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.c +57 -2

86 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +1 -0
index 3bb9cfd98370..da67f863c03c 100644
@@ -12,5 +12,6 @@ int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_pr
 int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 
 #endif /* __QDA_IOCTL_H__ */
include/uapi/drm/qda_accel.h +28 -0
index c21bc5b2446c..75e67c5f5923 100644
@@ -16,6 +16,7 @@ extern "C" {
 #define DRM_QDA_GEM_CREATE		0x01
 #define DRM_QDA_GEM_MMAP_OFFSET		0x02
 #define DRM_QDA_REMOTE_INVOKE		0x03
+#define DRM_QDA_REMOTE_SESSION_CREATE	0x04
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -25,6 +26,9 @@ extern "C" {
 					 struct drm_qda_gem_mmap_offset)
 #define DRM_IOCTL_QDA_REMOTE_INVOKE	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_INVOKE, \
 					 struct drm_qda_invoke_args)
+#define DRM_IOCTL_QDA_REMOTE_SESSION_CREATE					\
+	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_SESSION_CREATE,		\
+		 struct drm_qda_init_create)
 
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
@@ -109,6 +113,30 @@ struct drm_qda_invoke_args {
 	__u64 args;
 };
 
+/* Process attribute flags for drm_qda_init_create.attrs */
+#define QDA_INIT_ATTR_UNSIGNED_MODULE	(1U << 3)
+
+/**
+ * struct drm_qda_init_create - DSP process creation parameters
+ * @filelen: Length of the ELF image in bytes
+ * @filehandle: Handle of the buffer holding the ELF image
+ * @attrs: Process attribute flags
+ * @siglen: Length of the signature data in bytes
+ * @file: Reserved, must be zero
+ *
+ * Creates a process on the DSP for this file to run in. Without
+ * QDA_INIT_ATTR_UNSIGNED_MODULE in @attrs the process is created signed, which
+ * requires CAP_SYS_ADMIN. Creating a process again on the same file replaces
+ * the first.
+ */
+struct drm_qda_init_create {
+	__u32 filelen;
+	__u32 filehandle;
+	__u32 attrs;
+	__u32 siglen;
+	__u64 file;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#36 lore raw

Re: [PATCH v4 15/25] accel/qda: Add DSP process creation and release

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: - [Critical] accel/qda: DMA UAF of init_mem_gem_obj on signal in qda_fastrpc_invoke() - [Critical]…

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] accel/qda: DMA UAF of init_mem_gem_obj on signal in qda_fastrpc_invoke()
- [Critical] accel/qda: race in qda_session_set_init_mem() frees active process memory
- [Critical] accel/qda: unprivileged userspace can invoke QDA_FASTRPC_INIT_HANDLE
- [High] accel/qda: deadlock in qda_postclose() holding drm_dev_enter() across RPC
- [High] accel/qda: default session->pd of QDA_ROOT_PD allows unattached root PD invoke --

commit 981b12f498982e2e454ee8c3d214b24f64472c31
Author: Ekansh Gupta <[email protected]>

accel/qda: Add DSP process creation and release

This commit adds FastRPC requests and the DRM_IOCTL_QDA_REMOTE_SESSION_CREATE ioctl to create a DSP process on a session and release it when the session closes. It allocates per-session memory for the DSP process image and tracks the session's protection domain for subsequent requests.

Quoted 4 lines
drivers/accel/qda/qda_drv.c +0 -0
index 7d44342d3f67b..983c6a3dcfb45 100644

[ ... ]

drivers/accel/qda/qda_drv.c EXPORT_SYMBOL_FOR_MODULES(qda_session_put, "qda_fastrpc_shim"); · line 115

Quoted 13 lines
drivers/accel/qda/qda_drv.c +8 -1
@@ -101,7 +105,16 @@ EXPORT_SYMBOL_FOR_MODULES(qda_session_put, "qda_fastrpc_shim");
 
 static void qda_postclose(struct drm_device *dev, struct drm_file *file)
 {
-	qda_session_put(file->driver_priv);
+	struct qda_session *session = file->driver_priv;
+	int idx;
+
+	/* Only a session that created a process has one to release */
+	if (READ_ONCE(session->init_mem_gem_obj) && drm_dev_enter(dev, &idx)) {
+		qda_release_dsp_process(session->qda_dev, session);
+		drm_dev_exit(idx);
+	}

[Severity: High]
Can holding drm_dev_enter() across qda_release_dsp_process() in qda_postclose() deadlock with qda_rpmsg_remove() if the DSP crashes or the RPMsg transport disconnects while the release request is in flight?

qda_release_dsp_process() calls qda_fastrpc_invoke() -> qda_fastrpc_invoke_common(), which sends the QDA_FASTRPC_RMID_INIT_RELEASE message (inside qda_rpmsg_send_msg(), which already uses its own short drm_dev_enter()/drm_dev_exit() section around rpmsg_send()) and then blocks in qda_rpmsg_wait_for_rsp() via wait_for_completion_interruptible(&ctx->work).

If the RPMsg device is removed before the DSP replies, qda_rpmsg_remove() runs:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_remove() {
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_fastrpc_flush_pending(qdev);
	...
}

drm_dev_unplug() calls synchronize_srcu(&drm_unplug_srcu) and waits for qda_postclose() to call drm_dev_exit(idx), while qda_postclose() is blocked in qda_rpmsg_wait_for_rsp() waiting for qda_fastrpc_flush_pending() to complete ctx->work (and when qda_postclose() runs during process exit after exit_signals() or from the delayed_fput workqueue, signals cannot interrupt the wait).

Should qda_postclose() check !drm_dev_is_unplugged(dev) instead of holding drm_dev_enter() across the blocking RPC wait?

[ ... ]

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
index 693dd6f035d2a..4672405ea4b79 100644

[ ... ]

Quoted 7 lines
drivers/accel/qda/qda_fastrpc.c +2 -1
@@ -511,9 +572,14 @@ int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg *
 	/* Ensure the payload is visible to the DSP before the doorbell */
 	dma_wmb();
 
-	msg->fastrpc.remote_session_id = ctx->session->remote_session_id;
+	/* The session has no process yet to identify while one is being created */
+	if (ctx->handle == QDA_FASTRPC_INIT_HANDLE)

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * · line 585

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +5 -1
+		msg->fastrpc.remote_session_id = 0;
+	else
+		msg->fastrpc.remote_session_id = ctx->session->remote_session_id;
+
 	msg->fastrpc.tid = current->pid;
-	msg->fastrpc.ctx = ctx->ctxid | QDA_USER_PD;
+	msg->fastrpc.ctx = ctx->ctxid | ctx->pd;
 	msg->fastrpc.handle = ctx->handle;
 	msg->fastrpc.sc = ctx->sc;
 	msg->fastrpc.addr = msg->phys;

[Severity: Critical]
What prevents unprivileged user space from invoking QDA_FASTRPC_INIT_HANDLE directly via DRM_IOCTL_QDA_REMOTE_INVOKE or the legacy invoke path?

In qda_fastrpc_invoke_pack(), any invocation with ctx->handle == QDA_FASTRPC_INIT_HANDLE is formatted with msg->fastrpc.remote_session_id = 0. However, neither qda_fastrpc_prepare_args_invoke() nor qda_fastrpc_invoke_legacy() rejects user-supplied requests where handle == QDA_FASTRPC_INIT_HANDLE:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_prepare_args_invoke() {
	ctx->handle = invoke_args->handle;
	ctx->sc = invoke_args->sc;
	...
}
drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_invoke_legacy() {
	...
	ctx->type = QDA_FASTRPC_RMID_INVOKE_DYNAMIC;
	ctx->legacy = true;
	ctx->pd = READ_ONCE(session->pd);
	ctx->handle = handle;
	ctx->sc = sc;
	...
}

Doesn't this allow an unprivileged caller to forge kernel initialisation requests to QDA_FASTRPC_INIT_HANDLE with remote_session_id = 0—for example, invoking QDA_FASTRPC_RMID_INIT_CREATE / QDA_FASTRPC_RMID_INIT_CREATE_ATTR for a signed process without CAP_SYS_ADMIN, supplying arbitrary physical page descriptors in argument 3, or sending QDA_FASTRPC_RMID_INIT_RELEASE with another session's remote_session_id?

[ ... ]

Quoted 13 lines
drivers/accel/qda/qda_fastrpc.c +7 -2
@@ -740,8 +913,23 @@ int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx,
 	 */
 	qda_session_get(session);
 	ctx->session = session;
-
-	err = qda_fastrpc_prepare_args_invoke(ctx, argp);
+	ctx->pd = READ_ONCE(session->pd);
+
+	switch (ctx->type) {
+	case QDA_FASTRPC_RMID_INIT_RELEASE:
+		err = qda_fastrpc_prepare_args_release_process(ctx);
+		break;
+	case QDA_FASTRPC_RMID_INIT_CREATE:

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx, · line 932

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+		/* The new process is in the user PD, and so is the request creating it */
+		ctx->pd = QDA_USER_PD;
+		err = qda_fastrpc_prepare_args_init_create(ctx, argp);
+		break;
+	case QDA_FASTRPC_RMID_INVOKE_DYNAMIC:
+		err = qda_fastrpc_prepare_args_invoke(ctx, argp);
+		break;
+	default:
+		return -EINVAL;
+	}

[Severity: High]
Because qda_session_alloc() zero-initializes struct qda_session, session->pd starts as 0 (QDA_ROOT_PD).

If user space calls DRM_IOCTL_QDA_REMOTE_INVOKE (via qda_ioctl_invoke() -> qda_fastrpc_prepare_args()) or FASTRPC_IOCTL_INVOKE (via qda_fastrpc_invoke_legacy()) before creating a DSP process on the session, ctx->pd is set to READ_ONCE(session->pd) == QDA_ROOT_PD, and qda_fastrpc_invoke_pack() packs QDA_ROOT_PD into msg->fastrpc.ctx.

Can an unprivileged caller use this to direct invocations at QDA_ROOT_PD before any protection domain has been created or attached?

[ ... ]

Quoted 13 lines
drivers/accel/qda/qda_ioctl.c +4 -1
index e9f4151e344af..f51ad0bc9e9cc 100644
@@ -75,7 +75,22 @@ int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil
 	return drm_gem_dumb_map_offset(file_priv, dev, args->handle, &args->offset);
 }
 
-static int qda_fastrpc_invoke(void *data, struct qda_session *session)
+/*
+ * A process created again replaces the first, so the memory held for the
+ * previous one is dropped. Swapped atomically, as ioctls on one file can run
+ * concurrently.

drivers/accel/qda/qda_ioctl.c int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil · line 91

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+ */
+static void qda_session_set_init_mem(struct qda_session *session, struct qda_gem_obj *mem)
+{
+	struct qda_gem_obj *old;
+
+	drm_gem_object_get(&mem->base);
+	old = xchg(&session->init_mem_gem_obj, mem);
+	if (old)
+		drm_gem_object_put(&old->base);
+}

[Severity: Critical]
As reported by Sashiko on v1 [1], is an atomic xchg() in qda_session_set_init_mem() sufficient when two threads call DRM_IOCTL_QDA_REMOTE_SESSION_CREATE concurrently on the same file?

In qda_fastrpc_invoke(), qda_fastrpc_invoke_common() and qda_session_set_init_mem() run without a per-session lock serializing process creation. If Thread A (with memA) and Thread B (with memB) issue QDA_FASTRPC_RMID_INIT_CREATE concurrently:

1. The DSP processes Thread A first and Thread B second, so the active DSP process is replaced and runs in memB.
2. On the CPU, Thread B runs qda_session_set_init_mem(session, memB) before Thread A runs qda_session_set_init_mem(session, memA).
3. Thread A swaps session->init_mem_gem_obj to memA and calls drm_gem_object_put(&memB->base).
4. When Thread B drops ctx->refcount at out_put, qda_fastrpc_context_free() drops the last reference to memB, unmapping and freeing memB while the DSP process is actively running in it.

Should process creation on a session be serialized with a mutex (or rejected if a process already exists)?

Quoted 22 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+
+static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
 {
 	struct qda_dev *qdev = session->qda_dev;
 	struct qda_fastrpc_invoke_ctx *ctx;
@@ -85,11 +100,26 @@ static int qda_fastrpc_invoke(void *data, struct qda_session *session)
 	if (IS_ERR(ctx))
 		return PTR_ERR(ctx);
 
+	ctx->type = type;
+
 	err = qda_fastrpc_prepare_args(ctx, session, data);
 	if (err)
 		goto out_put;
 
 	err = qda_fastrpc_invoke_common(qdev, ctx);
+	if (err)
+		goto out_put;
+
+	/*
+	 * The session only changes once the DSP has accepted the request, so a
+	 * failed create leaves the previous process, if any, as it was. The

drivers/accel/qda/qda_ioctl.c static int qda_fastrpc_invoke(void *data, struct qda_session *session) · line 125

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +7 -0
+	 * memory is freed with the session, which the invocation keeps alive
+	 * until it is done, so a release needs no reference of its own.
+	 */
+	if (type == QDA_FASTRPC_RMID_INIT_CREATE) {
+		qda_session_set_init_mem(session, ctx->init_mem_gem_obj);
+		WRITE_ONCE(session->pd, QDA_USER_PD);
+	}
 
 out_put:
 	kref_put(&ctx->refcount, qda_fastrpc_context_free);

[Severity: Critical]
What happens if a thread calling DRM_IOCTL_QDA_REMOTE_SESSION_CREATE is interrupted by a signal while qda_fastrpc_invoke_common() is waiting in qda_rpmsg_wait_for_rsp()?

After qda_rpmsg_send_msg() has sent the QDA_FASTRPC_RMID_INIT_CREATE request to the DSP, a signal causes wait_for_completion_interruptible(&ctx->work) to return -ERESTARTSYS. Here in qda_fastrpc_invoke(), the error check jumps to out_put without calling qda_session_set_init_mem(session, ctx->init_mem_gem_obj).

The DSP still processes the in-flight create request, loads the ELF image into ctx->init_mem_gem_obj, starts the process, and replies. When qda_rpmsg_cb() receives the reply and runs qda_fastrpc_context_put_wq() -> qda_fastrpc_context_free():

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_free() {
	...
	if (ctx->init_mem_gem_obj)
		drm_gem_object_put(&ctx->init_mem_gem_obj->base);
	...
}

Because qda_session_set_init_mem() never took a reference for the session, qda_fastrpc_context_free() drops the only reference to ctx->init_mem_gem_obj and frees the DMA buffer while the DSP process is running in it. In addition, session->init_mem_gem_obj remains NULL, so qda_postclose() later skips qda_release_dsp_process() and qda_session_release() releases session->assigned_iommu_dev while the orphaned DSP process is still using that context bank.

Could ctx->init_mem_gem_obj be retained by the session whenever an INIT_CREATE request has been sent to the DSP (or transferred when the DSP replies with status 0)?

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=15
#37 lore raw

PATCH v4 16/25 accel/qda: Add DSP process creation and release to the legacy shim

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Applications written for the /dev/fastrpc-* interface create their DSP process with an ioctl that names the ELF image by file descriptor. Serve it by importing the descriptor into the session's context bank and creating the process with the shared creation code, which takes the already-imported buffer in place of a handle. Closing the file releases the process, as closing a DRM file does.

The old interface offers a secure and a non-secure node per domain. Mirror its rule: on a secure channel, a client that opened the non-secure node may only create an unsigned process, and only on a domain that supports them. This sits in front of the shared check that a signed process needs CAP_SYS_ADMIN, which applies to both interfaces.

An ELF image passed as a bare pointer rather than a descriptor is not supported, as there is no buffer to resolve for it.

---
 drivers/accel/qda/qda_fastrpc.c      | 35 ++++++++++++++++++--
 drivers/accel/qda/qda_fastrpc.h      | 17 ++++++++++
 drivers/accel/qda/qda_fastrpc_shim.c | 64 +++++++++++++++++++++++++++++++++---
 drivers/accel/qda/qda_ioctl.c        | 22 ++++++++++++-
 drivers/accel/qda/qda_ioctl.h        |  3 ++
 drivers/accel/qda/qda_legacy_ops.c   | 39 ++++++++++++++++++++++
 drivers/accel/qda/qda_legacy_ops.h   |  5 +++
 7 files changed, 176 insertions(+), 9 deletions(-)
drivers/accel/qda/qda_fastrpc.c +32 -3
index 4672405ea4b7..35ce60f3de2e 100644
@@ -757,6 +757,27 @@ int qda_fastrpc_invoke_legacy(struct qda_dev *qdev, struct qda_session *session,
 	return err;
 }
 
+/*
+ * Use the ELF buffer the caller imported itself as argument 2. It is checked
+ * as a buffer named by handle would be: it must be mapped in this session's
+ * context bank, and the image must fit in it, as the DSP would otherwise read
+ * past the end of the buffer.
+ */
+static int qda_fastrpc_use_elf_obj(struct qda_fastrpc_invoke_ctx *ctx,
+				   struct drm_gem_object *obj, u32 filelen)
+{
+	if (to_qda_gem_obj(obj)->iommu_dev != ctx->session->assigned_iommu_dev)
+		return -EACCES;
+
+	if (filelen > to_qda_gem_obj(obj)->size)
+		return -EINVAL;
+
+	drm_gem_object_get(obj);
+	ctx->gem_objs[2] = obj;
+
+	return 0;
+}
+
 /*
  * INIT_RELEASE packed layout:
  *   [0] remote_session_id	sizeof(int)	IN
@@ -796,7 +817,8 @@ static int qda_fastrpc_prepare_args_release_process(struct qda_fastrpc_invoke_ct
  */
 static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
 {
-	struct drm_qda_init_create *init = argp;
+	struct qda_init_create_args *create = argp;
+	struct drm_qda_init_create *init = &create->init;
 	struct qda_fastrpc_create_process_inbuf *inbuf;
 	struct qda_dev *qdev = ctx->qdev;
 	struct drm_gem_object *gem_obj;
@@ -853,12 +875,19 @@ static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c
 	assign_kernel_gem_arg(ctx, 1, kernel_gem, gv + off_comm, inbuf->namelen);
 	/* The image starts at the beginning of the buffer the caller named */
 	ctx->args[2].length = init->filelen;
-	ctx->args[2].handle = init->filehandle;
+	if (create->elf_obj) {
+		err = qda_fastrpc_use_elf_obj(ctx, create->elf_obj, init->filelen);
+		if (err)
+			goto out_put;
+	} else {
+		ctx->args[2].handle = init->filehandle;
+	}
 	assign_kernel_gem_arg(ctx, 3, kernel_gem, gv + off_pages, sizeof(struct qda_phy_page));
 
+out_put:
 	drm_gem_object_put(&kernel_gem->base);
 
-	return 0;
+	return err;
 }
 
 /*
drivers/accel/qda/qda_fastrpc.h +17 -0
index eaebc4e99676..a5bd7661c26c 100644
@@ -261,6 +261,23 @@ struct qda_legacy_invoke_arg {
 	s32 fd;
 };
 
+/**
+ * struct qda_init_create_args - Kernel-side arguments for creating a DSP process
+ *
+ * Wraps the UAPI arguments so that a caller with no handle table, such as the
+ * legacy shim, can pass the ELF image as an already imported buffer.
+ */
+struct qda_init_create_args {
+	/** @init: Creation parameters; @init.file must be zero */
+	struct drm_qda_init_create init;
+	/**
+	 * @elf_obj: Buffer holding the ELF image, mapped in the session's
+	 * context bank, or NULL if @init.filehandle names it. Borrowed: the
+	 * caller keeps its reference for the length of the call.
+	 */
+	struct drm_gem_object *elf_obj;
+};
+
 struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev);
 void qda_fastrpc_context_free(struct kref *ref);
 void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx);
drivers/accel/qda/qda_fastrpc_shim.c +59 -5

136 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.c +21 -1
index f51ad0bc9e9c..99b8a2517726 100644
@@ -137,7 +137,27 @@ static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
  */
 int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv)
 {
-	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE, data, file_priv->driver_priv);
+	struct qda_init_create_args create = {
+		.init = *(struct drm_qda_init_create *)data,
+	};
+
+	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE, &create, file_priv->driver_priv);
+}
+
+/**
+ * qda_session_init_create() - Create a DSP process from an imported ELF buffer
+ * @session: Session to create the process for
+ * @create: Creation parameters; @create->init.filehandle and @create->init.file
+ *          must be zero
+ *
+ * For sessions with no handle table to name the ELF buffer by, such as those
+ * of the legacy shim.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create)
+{
+	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE, create, session);
 }
 
 /**
drivers/accel/qda/qda_ioctl.h +3 -0
index da67f863c03c..b2c7bd25b138 100644
@@ -8,10 +8,13 @@
 
 #include "qda_drv.h"
 
+struct qda_init_create_args;
+
 int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 
 #endif /* __QDA_IOCTL_H__ */
drivers/accel/qda/qda_legacy_ops.c +39 -0
index 74ee4995350e..2b3b3f4fb246 100644
@@ -10,6 +10,7 @@
 #include "qda_drv.h"
 #include "qda_fastrpc.h"
 #include "qda_gem.h"
+#include "qda_ioctl.h"
 #include "qda_legacy_ops.h"
 #include "qda_prime.h"
 
@@ -38,6 +39,42 @@ static void legacy_session_destroy(struct qda_session *session)
 	drm_dev_put(drm_dev);
 }
 
+/*
+ * The ELF image of a legacy caller is named by file descriptor, so it arrives
+ * already imported. An image passed as a bare pointer has no buffer to
+ * resolve, so it is only accepted when there is none, i.e. an empty image.
+ */
+static int legacy_init_create(struct qda_session *session, struct drm_gem_object *elf_obj,
+			      u32 filelen, u32 attrs, u32 siglen)
+{
+	struct qda_init_create_args create = {
+		.init = {
+			.filelen = filelen,
+			.attrs = attrs,
+			.siglen = siglen,
+		},
+		.elf_obj = elf_obj,
+	};
+
+	if (!elf_obj && filelen)
+		return -EOPNOTSUPP;
+
+	return qda_session_init_create(session, &create);
+}
+
+static int legacy_release(struct qda_session *session)
+{
+	/* Only a session that created a process has one to release */
+	if (!READ_ONCE(session->init_mem_gem_obj))
+		return 0;
+
+	/* Unplug check only; see qda_shim_ioctl() for why not drm_dev_enter() */
+	if (drm_dev_is_unplugged(&session->qda_dev->drm_dev))
+		return -ENODEV;
+
+	return qda_release_dsp_process(session->qda_dev, session);
+}
+
 /*
  * As the GEM_CREATE ioctl, but exporting a DMA-BUF rather than installing a
  * handle: a session the shim created has no DRM file to hold handles, and the
@@ -81,6 +118,8 @@ static int legacy_invoke(struct qda_session *session, u32 handle, u32 sc,
 const struct qda_legacy_ops qda_legacy_ops_table = {
 	.session_create = legacy_session_create,
 	.session_destroy = legacy_session_destroy,
+	.init_create = legacy_init_create,
+	.release = legacy_release,
 	.alloc_dmabuf = legacy_alloc_dmabuf,
 	.import_dmabuf = legacy_import_dmabuf,
 	.invoke = legacy_invoke,
drivers/accel/qda/qda_legacy_ops.h +5 -0
index ba1ce3c0a22b..05bd1f806c9a 100644
@@ -30,6 +30,11 @@ struct qda_legacy_ops {
 	struct qda_session *(*session_create)(struct qda_dev *qdev);
 	/** @session_destroy: Close a session */
 	void (*session_destroy)(struct qda_session *session);
+	/** @init_create: Create a DSP process from an imported ELF image */
+	int (*init_create)(struct qda_session *session, struct drm_gem_object *elf_obj,
+			   u32 filelen, u32 attrs, u32 siglen);
+	/** @release: Release the session's DSP process, if it has one */
+	int (*release)(struct qda_session *session);
 	/** @alloc_dmabuf: Allocate a buffer and export it as a DMA-BUF */
 	struct dma_buf *(*alloc_dmabuf)(struct qda_session *session, u64 size);
 	/** @import_dmabuf: Map a DMA-BUF into a session's context bank */
Signature · 1 line
2.34.1
#38 lore raw

Re: [PATCH v4 16/25] accel/qda: Add DSP process creation and release to the legacy shim

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] accel/qda: TOCTOU race and UAF between legacy_release() and qda_rpmsg_remove() - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] accel/qda: TOCTOU race and UAF between legacy_release() and qda_rpmsg_remove()
- [High] accel/qda: missing QDA_FASTRPC_INIT_HANDLE check on user invoke paths
- [Medium] accel/qda: qda_shim_init_create() imports args.filefd when filelen or filefd is 0 --

commit cd276e39d68d699c859df87ba50e8e071d334588
Author: Ekansh Gupta <[email protected]>

accel/qda: Add DSP process creation and release to the legacy shim

This commit adds DSP process creation (FASTRPC_IOCTL_INIT_CREATE) and release upon file close to the legacy /dev/fastrpc-* compatibility shim, enforcing secure/non-secure node rules and importing the ELF image file descriptor into the session's context bank.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc_shim.c +0 -0
index 03fce990ccdb9..4a2ba20ee01ac 100644

[ ... ]

Quoted 18 lines
drivers/accel/qda/qda_fastrpc_shim.c +14 -0
@@ -141,6 +154,43 @@ static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp)
 	return 0;
 }
 
+/*
+ * As in drivers/misc/fastrpc.c: on a secure channel, a client that came in
+ * through the non-secure node may only create an unsigned PD, and only on a
+ * domain that supports unsigned PD offload.
+ */
+static bool qda_shim_session_rejected(struct qda_shim_file *sf, bool unsigned_pd)
+{
+	if (!sf->secure_node && sf->secure_channel)
+		return !sf->unsigned_support || !unsigned_pd;
+
+	return false;
+}
+
+static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp)

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp) · line 180

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+{
+	struct drm_gem_object *elf_obj = NULL;
+	struct fastrpc_init_create args;
+	int err;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	if (qda_shim_session_rejected(sf, args.attrs & FASTRPC_MODE_UNSIGNED_MODULE))
+		return -ECONNREFUSED;

[Severity: High]
Can user space bypass both qda_shim_session_rejected() here and the CAP_SYS_ADMIN check in qda_fastrpc_prepare_args_init_create() by sending a QDA_FASTRPC_INIT_HANDLE (0x1) message through FASTRPC_IOCTL_INVOKE or DRM_IOCTL_QDA_REMOTE_INVOKE?

In drivers/misc/fastrpc.c, fastrpc_internal_invoke() rejects user-space invocations targeting FASTRPC_INIT_HANDLE with -EPERM when !kernel. Here, however, neither qda_shim_invoke() -> legacy_invoke() -> qda_fastrpc_invoke_legacy() nor qda_ioctl_invoke() -> qda_fastrpc_prepare_args_invoke() blocks handle == QDA_FASTRPC_INIT_HANDLE.

Even more concerning, qda_fastrpc_invoke_pack() checks ctx->handle rather than ctx->type:

    if (ctx->handle == QDA_FASTRPC_INIT_HANDLE)
        msg->fastrpc.remote_session_id = 0;
    else
        msg->fastrpc.remote_session_id = ctx->session->remote_session_id;

As a result, an unprivileged client on a non-secure node can call FASTRPC_IOCTL_INIT_CREATE with FASTRPC_MODE_UNSIGNED_MODULE to transition session->pd to QDA_USER_PD (or leave session->pd at QDA_ROOT_PD) and then issue FASTRPC_IOCTL_INVOKE with handle = QDA_FASTRPC_INIT_HANDLE and sc encoding QDA_FASTRPC_RMID_INIT_CREATE, QDA_FASTRPC_RMID_INIT_CREATE_ATTR, or QDA_FASTRPC_RMID_INIT_RELEASE using user-controlled buffers. Should QDA_FASTRPC_INIT_HANDLE be rejected on the user-space invoke paths?

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_alloc_dma_buf(struct qda_shim_file *sf, void __user *argp) · line 184

Quoted 4 lines
drivers/accel/qda/qda_fastrpc_shim.c +4 -0
+
+	elf_obj = shim_import_fd(sf->session, args.filefd);
+	if (IS_ERR(elf_obj))
+		return PTR_ERR(elf_obj);

[Severity: Medium]
What happens in qda_shim_init_create() when user space passes a zero-initialized args.filefd (0) for an empty image (args.filelen == 0) or for a bare-pointer image (args.file != 0, args.filelen > 0), or when args.filelen is 0 with a non-negative args.filefd?

In drivers/misc/fastrpc.c, fastrpc_init_create_process() only imports init.filefd when both init.filelen and init.filefd are non-zero:

    if (init.filelen && init.filefd) {
        ...
    }

And in drivers/accel/qda/qda_legacy_ops.c, legacy_init_create() is written to expect elf_obj == NULL when no descriptor-backed image is used:

    if (!elf_obj && filelen)
        return -EOPNOTSUPP;

However, shim_import_fd() only treats negative file descriptors as absent:

    if (fd < 0)
        return NULL;

    dmabuf = dma_buf_get(fd);

If a caller zero-initializes struct fastrpc_init_create so args.filefd is 0, shim_import_fd(sf->session, 0) calls dma_buf_get(0) on standard input, failing with -EINVAL or -EBADF instead of allowing an empty image or returning -EOPNOTSUPP for a bare pointer (or silently importing fd 0 if stdin happens to be a valid DMA-BUF). Should qda_shim_init_create() only call shim_import_fd() when args.filelen && args.filefd (or should shim_import_fd() treat fd <= 0 as absent)?

Quoted 4 lines
drivers/accel/qda/qda_legacy_ops.c +0 -0
index 74ee4995350e2..2b3b3f4fb246b 100644

[ ... ]

Quoted 3 lines
drivers/accel/qda/qda_legacy_ops.c +0 -0
@@ -38,6 +39,42 @@ static void legacy_session_destroy(struct qda_session *session)
 	drm_dev_put(drm_dev);
 }

[ ... ]

drivers/accel/qda/qda_legacy_ops.c static void legacy_session_destroy(struct qda_session *session) · line 76

Quoted 12 lines
drivers/accel/qda/qda_legacy_ops.c +12 -0
+static int legacy_release(struct qda_session *session)
+{
+	/* Only a session that created a process has one to release */
+	if (!READ_ONCE(session->init_mem_gem_obj))
+		return 0;
+
+	/* Unplug check only; see qda_shim_ioctl() for why not drm_dev_enter() */
+	if (drm_dev_is_unplugged(&session->qda_dev->drm_dev))
+		return -ENODEV;
+
+	return qda_release_dsp_process(session->qda_dev, session);
+}

[Severity: High]
Does using drm_dev_is_unplugged() instead of holding a critical section across qda_release_dsp_process() expose a race and use-after-free with qda_rpmsg_remove()?

In drivers/accel/qda/qda_rpmsg.c, qda_rpmsg_remove() relies on drm_dev_unplug() to wait for active callers to finish before flushing pending contexts and tearing down device resources:

    drm_dev_unplug(&qdev->drm_dev);
    qdev->rpdev = NULL;
    qda_fastrpc_flush_pending(qdev);
    qda_cb_unpopulate(qdev);
    qda_deinit_device(qdev);

Because drm_dev_is_unplugged() immediately drops its SRCU read lock via drm_dev_exit(), qda_rpmsg_remove() can run concurrently right after drm_dev_is_unplugged() returns false:

1. In qda_release_dsp_process() -> qda_fastrpc_invoke(), qda_fastrpc_context_alloc(qdev) allocates ctx with refcount == 1 and stores it in qdev->ctx_xa.
2. Concurrently, qda_rpmsg_remove() calls drm_dev_unplug(&qdev->drm_dev). When qda_fastrpc_invoke_common() calls qda_rpmsg_send_msg(), drm_dev_enter() now fails with -ENODEV, so the second reference on ctx for the response callback is never taken.
3. Meanwhile, qda_fastrpc_flush_pending(qdev) in qda_rpmsg_remove() iterates qdev->ctx_xa and calls kref_put(&ctx->refcount, qda_fastrpc_context_free), dropping the caller's sole reference and freeing ctx while qda_fastrpc_invoke() is still using it and about to call kref_put(&ctx->refcount, qda_fastrpc_context_free) a second time at out_put.
4. Furthermore, qda_rpmsg_remove() proceeds to qda_cb_unpopulate(qdev) and qda_deinit_device(qdev) (destroying the context-bank platform devices, qdev->iommu_mgr, and qdev->ctx_xa) while qda_release_dsp_process() is still allocating from qdev->ctx_xa and qdev->iommu_mgr.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=16
#39 lore raw

Re: [PATCH v4 16/25] accel/qda: Add DSP process creation and release to the legacy shim

In reply to Ekansh Gupta

drivers/accel/qda/qda_fastrpc_shim.c static int qda_shim_probe(struct auxiliary_device *auxdev, · line 297

Quoted 11 lines · Ekansh Gupta
drivers/accel/qda/qda_fastrpc_shim.c +1 -2
 	snode->sdev = sdev;
+	snode->secure = secure;
 	snode->miscdev.minor = MISC_DYNAMIC_MINOR;
 	snode->miscdev.name = snode->name;
 	snode->miscdev.fops = &qda_shim_fops;
@@ -293,8 +346,6 @@ static int qda_shim_probe(struct auxiliary_device *auxdev,
 			  const struct auxiliary_device_id *id)
 {
 	struct qda_dev *qdev = dev_get_platdata(&auxdev->dev);
-	bool secure_channel = !of_property_read_bool(qdev->dev->of_node,
-						     "qcom,non-secure-domain");

You just added these lines two patches earlier.

You are adding COMPLETELY new driver, so adding completely new code and then removing it is confusing. It means you added something which was not needed.

Best regards,
Krzysztof

#40 lore raw

Re: [PATCH v4 16/25] accel/qda: Add DSP process creation and release to the legacy shim

Quoted 15 lines · Krzysztof Kozlowski

drivers/accel/qda/qda_fastrpc_shim.c static int qda_shim_probe(struct auxiliary_device *auxdev, · line 297

Quoted 11 lines · Ekansh Gupta
drivers/accel/qda/qda_fastrpc_shim.c +1 -2
 	snode->sdev = sdev;
+	snode->secure = secure;
 	snode->miscdev.minor = MISC_DYNAMIC_MINOR;
 	snode->miscdev.name = snode->name;
 	snode->miscdev.fops = &qda_shim_fops;
@@ -293,8 +346,6 @@ static int qda_shim_probe(struct auxiliary_device *auxdev,
 			  const struct auxiliary_device_id *id)
 {
 	struct qda_dev *qdev = dev_get_platdata(&auxdev->dev);
-	bool secure_channel = !of_property_read_bool(qdev->dev->of_node,
-						     "qcom,non-secure-domain");

You just added these lines two patches earlier.

Quoted 3 lines

You are adding COMPLETELY new driver, so adding completely new code and then removing it is confusing. It means you added something which was not needed.

I'll restructure this properly.

Quoted 4 lines

Best regards,
Krzysztof

#41 lore raw

PATCH v4 17/25 accel/qda: Add remote memory mapping to the DSP address space

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

The DSP can only touch memory it has been told about, so user space needs a way to make a buffer visible to it ahead of time rather than only per call.

Add an ioctl that maps a buffer, named by handle, into the DSP's address space of the calling session and returns the address the DSP will use. There are two flavours, selected by the request: a plain mapping of the whole buffer, optionally at an address the caller suggests, and a mapping of a region of it with caller-chosen attributes and a buffer identifier the DSP tracks.

The length of the mapping is checked against the size of the buffer, so a mapping cannot be described as larger than what backs it. Buffers are looked up in the calling session's own handle table.

---
 drivers/accel/qda/qda_drv.c     |   1 +
 drivers/accel/qda/qda_fastrpc.c | 190 +++++++++++++++++++++++++++++++++++++++-
 drivers/accel/qda/qda_fastrpc.h |  51 +++++++++++
 drivers/accel/qda/qda_ioctl.c   |  25 ++++++
 drivers/accel/qda/qda_ioctl.h   |   1 +
 include/uapi/drm/qda_accel.h    |  36 ++++++++
 6 files changed, 303 insertions(+), 1 deletion(-)
drivers/accel/qda/qda_drv.c +1 -0
index 983c6a3dcfb4..7f19380701d9 100644
@@ -125,6 +125,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_GEM_MMAP_OFFSET, qda_ioctl_gem_mmap_offset, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_INVOKE, qda_ioctl_invoke, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_CREATE, qda_ioctl_init_create, 0),
+	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MMAP, qda_ioctl_mmap, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_fastrpc.c +189 -1

232 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +51 -0

77 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.c +25 -0
index 99b8a2517726..cd3cea72f50e 100644
@@ -110,6 +110,8 @@ static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
 	if (err)
 		goto out_put;
 
+	qda_fastrpc_return_result(ctx, data);
+
 	/*
 	 * The session only changes once the DSP has accepted the request, so a
 	 * failed create leaves the previous process, if any, as it was. The
@@ -172,6 +174,29 @@ int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session)
 	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_RELEASE, NULL, session);
 }
 
+/**
+ * qda_ioctl_mmap() - Map a buffer into the DSP's address space
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_mem_map)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct drm_qda_mem_map *map_req = data;
+
+	switch (map_req->request) {
+	case QDA_REQUEST_MMAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MMAP, data, file_priv->driver_priv);
+	case QDA_REQUEST_MEM_MAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MEM_MAP, data,
+					  file_priv->driver_priv);
+	default:
+		return -EINVAL;
+	}
+}
+
 /**
  * qda_ioctl_invoke() - Run a method on the DSP
  * @dev: DRM device structure
drivers/accel/qda/qda_ioctl.h +1 -0
index b2c7bd25b138..afb3ed0e132e 100644
@@ -15,6 +15,7 @@ int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *fi
 int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 
 #endif /* __QDA_IOCTL_H__ */
include/uapi/drm/qda_accel.h +36 -0
index 75e67c5f5923..bd2d0f4308b1 100644
@@ -17,6 +17,7 @@ extern "C" {
 #define DRM_QDA_GEM_MMAP_OFFSET		0x02
 #define DRM_QDA_REMOTE_INVOKE		0x03
 #define DRM_QDA_REMOTE_SESSION_CREATE	0x04
+#define DRM_QDA_REMOTE_MMAP		0x05
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -29,6 +30,12 @@ extern "C" {
 #define DRM_IOCTL_QDA_REMOTE_SESSION_CREATE					\
 	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_SESSION_CREATE,		\
 		 struct drm_qda_init_create)
+#define DRM_IOCTL_QDA_REMOTE_MMAP	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_MMAP, \
+					 struct drm_qda_mem_map)
+
+/* Request types for drm_qda_mem_map.request */
+#define QDA_REQUEST_MMAP	1	/* Map a whole buffer */
+#define QDA_REQUEST_MEM_MAP	2	/* Map a region of a buffer, with attributes */
 
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
@@ -137,6 +144,35 @@ struct drm_qda_init_create {
 	__u64 file;
 };
 
+/**
+ * struct drm_qda_mem_map - Parameters for mapping a buffer into the DSP
+ * @request: QDA_REQUEST_MMAP or QDA_REQUEST_MEM_MAP
+ * @flags: Mapping flags for the DSP (cache attributes, permissions)
+ * @handle: Handle of the buffer to map
+ * @attrs: Mapping attributes (QDA_REQUEST_MEM_MAP)
+ * @offset: Offset within the buffer (QDA_REQUEST_MEM_MAP), at most INT_MAX
+ * @dsp_handle: Identifier the DSP tracks the buffer by (QDA_REQUEST_MEM_MAP)
+ * @vaddrin: Address the DSP should map the buffer at, if it can (input)
+ * @size: Length of the region to map in bytes, or 0 for the whole buffer
+ * @vaddrout: Address the DSP mapped the buffer at (output)
+ *
+ * Makes a buffer visible to the DSP of this file ahead of the calls that use
+ * it. QDA_REQUEST_MMAP maps a whole buffer and uses @handle, @flags,
+ * @vaddrin, @size and @vaddrout. QDA_REQUEST_MEM_MAP maps a region of it with
+ * caller-chosen attributes and uses every field.
+ */
+struct drm_qda_mem_map {
+	__u32 request;
+	__u32 flags;
+	__u32 handle;
+	__u32 attrs;
+	__u32 offset;
+	__s32 dsp_handle;
+	__u64 vaddrin;
+	__u64 size;
+	__u64 vaddrout;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#42 lore raw

Re: [PATCH v4 17/25] accel/qda: Add remote memory mapping to the DSP address space

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: - [Critical] accel/qda: premature GEM object put in qda_fastrpc_context_free() on mmap - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] accel/qda: premature GEM object put in qda_fastrpc_context_free() on mmap
- [High] accel/qda: missing offset bounds check in setup_mmap_pages() for mem_map
- [High] accel/qda: uninitialized session PD and leaked mappings in qda_ioctl_mmap()
- [Medium] accel/qda: drm_qda_mem_map.attrs is ignored and unused fields are unvalidated
- [Medium] accel/qda: DSP mapping leak on signal interruption in qda_fastrpc_invoke() --

commit 395c018ba64267dfbd2bc4c753e6cc750574fd40
Author: Ekansh Gupta <[email protected]>

accel/qda: Add remote memory mapping to the DSP address space

This commit adds the DRM_IOCTL_QDA_REMOTE_MMAP ioctl to map a buffer handle into the remote processor's address space via QDA_FASTRPC_RMID_INIT_MMAP or QDA_FASTRPC_RMID_INIT_MEM_MAP and return the mapped address to user space.

Quoted 13 lines
drivers/accel/qda/qda_fastrpc.c +1 -0
index 35ce60f3de2ec..744d7317cbe11 100644
@@ -3,6 +3,7 @@
 #include <linux/bitfield.h>
 #include <linux/capability.h>
 #include <linux/dma-mapping.h>
+#include <linux/limits.h>
 #include <linux/mm.h>
 #include <linux/sched.h>
 #include <linux/slab.h>

drivers/accel/qda/qda_fastrpc.c void qda_fastrpc_context_free(struct kref *ref) · line 256

Quoted 5 lines
drivers/accel/qda/qda_fastrpc.c +2 -0
@@ -251,6 +252,9 @@ void qda_fastrpc_context_free(struct kref *ref)
 	if (ctx->init_mem_gem_obj)
 		drm_gem_object_put(&ctx->init_mem_gem_obj->base);
 
+	if (ctx->map_obj)
+		drm_gem_object_put(ctx->map_obj);

[Severity: Critical]
Could dropping ctx->map_obj here when the invocation context is freed cause a use-after-free while the mapping remains active on the remote processor?

In setup_mmap_pages(), get_gem_obj_from_handle() stores the buffer reference in ctx->map_obj and passes qda_gem_obj->dma_addr to the remote processor for QDA_FASTRPC_RMID_INIT_MMAP or QDA_FASTRPC_RMID_INIT_MEM_MAP.

However, as soon as the mapping call finishes in qda_fastrpc_invoke():

    qda_ioctl_mmap()
      -> qda_fastrpc_invoke()
           -> kref_put(&ctx->refcount, qda_fastrpc_context_free)
                -> drm_gem_object_put(ctx->map_obj)

the kernel drops its reference to ctx->map_obj before qda_ioctl_mmap() returns to user space, unlike ctx->init_mem_gem_obj which is transferred to the session via qda_session_set_init_mem().

If user space then closes the buffer handle with DRM_IOCTL_GEM_CLOSE while the remote mapping at vaddrout is still in place, qda_gem_free_object() will free the backing memory and release its address in session->assigned_iommu_dev. Because driver-allocated buffers (such as alloc_kernel_gem() buffers and ctx->msg_gem_obj) allocate from the same session->assigned_iommu_dev context bank, couldn't later ioctls reuse that address and allow remote code to access or corrupt in-flight control messages? Also, should margs->flags be validated in qda_fastrpc_prepare_args_map() before forwarding it to QDA_FASTRPC_RMID_INIT_MMAP?

[ ... ]

Quoted 19 lines
drivers/accel/qda/qda_fastrpc.c +15 -0
@@ -890,6 +930,148 @@ static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c
 	return err;
 }
 
+/*
+ * Find the buffer a mapping request names and describe it to the DSP. The
+ * buffer is held by the context until the request is done, so that closing its
+ * handle meanwhile cannot free memory the DSP is being told to map. The
+ * region must fit in the buffer, as the DSP would otherwise map past its end.
+ */
+static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct drm_qda_mem_map *margs,
+			    struct qda_phy_page *pages)
+{
+	struct qda_gem_obj *qda_gem_obj;
+	int err;
+
+	if (!margs->handle)
+		return -EINVAL;
+

drivers/accel/qda/qda_fastrpc.c static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c · line 957

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+	err = get_gem_obj_from_handle(ctx, margs->handle, &ctx->map_obj);
+	if (err)
+		return err;
+
+	qda_gem_obj = to_qda_gem_obj(ctx->map_obj);
+	if (margs->size > qda_gem_obj->size)
+		return -EINVAL;
+
+	pages->addr = qda_gem_obj->dma_addr;
+	pages->size = margs->size ? margs->size : qda_gem_obj->size;

[Severity: High]
Does setup_mmap_pages() need to check margs->offset against qda_gem_obj->size as well?

In qda_fastrpc_prepare_args_mem_map(), margs->offset is only checked against INT_MAX before being written to req->offset:

	if (margs->offset > INT_MAX)
		return -EINVAL;
	...
	req->offset = margs->offset;

And setup_mmap_pages() only verifies that margs->size <= qda_gem_obj->size, unlike process_msg_buffer() which checks both the offset and the remaining length:

	if (ctx->args[i].offset > qda_gem_obj->size ||
	    len > qda_gem_obj->size - ctx->args[i].offset) {

When DRM_IOCTL_QDA_REMOTE_MMAP is called with QDA_REQUEST_MEM_MAP and a non-zero margs->offset (for example with margs->size == 0, which sets pages->size to qda_gem_obj->size, or with margs->offset + pages->size exceeding qda_gem_obj->size), won't this allow mapping past the end of qda_gem_obj?

Quoted 3 lines
drivers/accel/qda/qda_fastrpc.c +3 -0
+
+	return 0;
+}

[ ... ]

Quoted 27 lines
drivers/accel/qda/qda_fastrpc.c +27 -0
+static int qda_fastrpc_prepare_args_mem_map(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
+{
+	struct drm_qda_mem_map *margs = argp;
+	struct qda_fastrpc_mem_map_req_msg *req;
+	struct qda_gem_obj *kernel_gem;
+	u8 *gv;
+	int err;
+
+	const size_t off_req = 0;
+	const size_t off_pages = off_req + sizeof(*req);
+	const size_t off_rsp = off_pages + sizeof(struct qda_phy_page);
+	const size_t total_size = off_rsp + sizeof(struct qda_fastrpc_map_rsp_msg);
+
+	/* The wire field is signed */
+	if (margs->offset > INT_MAX)
+		return -EINVAL;
+
+	ctx->sc = QDA_FASTRPC_SCALARS(QDA_FASTRPC_RMID_INIT_MEM_MAP, 3, 1);
+	ctx->handle = QDA_FASTRPC_INIT_HANDLE;
+
+	err = alloc_kernel_args(ctx, 4);
+	if (err)
+		return err;
+
+	kernel_gem = alloc_kernel_gem(ctx, total_size);
+	if (IS_ERR(kernel_gem))
+		return PTR_ERR(kernel_gem);

drivers/accel/qda/qda_fastrpc.c static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c · line 1057

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+	gv = kernel_gem->virt;
+
+	req = (struct qda_fastrpc_mem_map_req_msg *)(gv + off_req);
+	req->remote_session_id = ctx->session->remote_session_id;
+	req->handle = margs->dsp_handle;
+	req->offset = margs->offset;
+	req->flags = margs->flags;
+	req->vaddrin = margs->vaddrin;
+	req->num = sizeof(struct qda_phy_page);
+	req->data_len = 0;

[Severity: Medium]
Should margs->attrs be handled or rejected here, and should unused fields in struct drm_qda_mem_map be checked for zero in qda_fastrpc_prepare_args_map()?

The comment on struct drm_qda_mem_map in include/uapi/drm/qda_accel.h states:

* Makes a buffer visible to the DSP of this file ahead of the calls that use
* it. QDA_REQUEST_MMAP maps a whole buffer and uses @handle, @flags,
* @vaddrin, @size and @vaddrout. QDA_REQUEST_MEM_MAP maps a region of it with
* caller-chosen attributes and uses every field.

However, qda_fastrpc_prepare_args_mem_map() never reads or validates margs->attrs, and qda_fastrpc_prepare_args_map() does not reject non-zero values in margs->attrs, margs->offset, or margs->dsp_handle when request is QDA_REQUEST_MMAP.

[ ... ]

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx, · line 1143

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +6 -0
@@ -953,6 +1135,12 @@ int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx,
 		ctx->pd = QDA_USER_PD;
 		err = qda_fastrpc_prepare_args_init_create(ctx, argp);
 		break;
+	case QDA_FASTRPC_RMID_INIT_MMAP:
+		err = qda_fastrpc_prepare_args_map(ctx, argp);
+		break;
+	case QDA_FASTRPC_RMID_INIT_MEM_MAP:
+		err = qda_fastrpc_prepare_args_mem_map(ctx, argp);
+		break;

[Severity: High]
What happens if user space calls DRM_IOCTL_QDA_REMOTE_MMAP before creating a session process, or closes the file while mappings are still active?

When a file is opened in qda_open(), qda_session_alloc() zero-initializes struct qda_session, leaving session->pd as 0 (QDA_ROOT_PD) and session->init_mem_gem_obj as NULL until DRM_IOCTL_QDA_REMOTE_SESSION_CREATE completes. In qda_fastrpc_prepare_args(), ctx->pd is initialized from READ_ONCE(session->pd), so calling qda_ioctl_mmap() on a freshly opened file will send QDA_FASTRPC_RMID_INIT_MMAP or QDA_FASTRPC_RMID_INIT_MEM_MAP to QDA_ROOT_PD without an initialized session or a capability check.

In addition, when the DRM file is closed, qda_postclose() only calls qda_release_dsp_process() when session->init_mem_gem_obj is non-NULL:

	if (READ_ONCE(session->init_mem_gem_obj) && drm_dev_enter(dev, &idx)) {
		qda_release_dsp_process(session->qda_dev, session);
		drm_dev_exit(idx);
	}

and nothing tracks or unmaps active DRM_IOCTL_QDA_REMOTE_MMAP mappings before drm_gem_release() frees the GEM objects and qda_session_release() releases session->assigned_iommu_dev. Doesn't this leak the remote mappings on close?

drivers/accel/qda/qda_ioctl.c static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session) · line 113

Quoted 9 lines
drivers/accel/qda/qda_ioctl.c +1 -0
index 99b8a2517726b..cd3cea72f50e8 100644
@@ -110,6 +110,8 @@ static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
 	if (err)
 		goto out_put;
 
+	qda_fastrpc_return_result(ctx, data);

[Severity: Medium]
Can a signal interrupting qda_fastrpc_invoke_common() leak the remote mapping in the DSP address space?

After qda_rpmsg_send_msg() sends a QDA_FASTRPC_RMID_INIT_MMAP or QDA_FASTRPC_RMID_INIT_MEM_MAP request, qda_rpmsg_wait_for_rsp() waits in wait_for_completion_interruptible(&ctx->work).

If a signal arrives before the reply, qda_fastrpc_invoke_common() returns -ERESTARTSYS and qda_fastrpc_invoke() jumps to out_put, skipping qda_fastrpc_return_result(). When the DSP later finishes the mapping and writes rsp->vaddrout into ctx->gem_objs[idx], qda_rpmsg_cb() drops the last reference on ctx and frees the buffer without returning vaddrout to user space or unmapping the region on the DSP.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=17
#43 lore raw

Re: [PATCH v4 17/25] accel/qda: Add remote memory mapping to the DSP address space

In reply to Ekansh Gupta
Quoted 5 lines · Ekansh Gupta

From: Ekansh Gupta <[email protected]>

The DSP can only touch memory it has been told about, so user space needs a way to make a buffer visible to it ahead of time rather than only per call.

[...]

drivers/accel/qda/qda_fastrpc.h struct qda_fastrpc_create_process_inbuf { · line 181

Quoted 7 lines
drivers/accel/qda/qda_fastrpc.h +7 -0
+struct qda_fastrpc_mem_map_req_msg {
+	/** @remote_session_id: Session the mapping belongs to */
+	s32 remote_session_id;
+	/** @handle: Identifier the DSP tracks the buffer by */
+	s32 handle;
+	/** @offset: Offset within the buffer */
+	s32 offset;

I think this should be 64b too.

drivers/accel/qda/qda_fastrpc.h struct qda_fastrpc_create_process_inbuf { · line 189

Quoted 8 lines
drivers/accel/qda/qda_fastrpc.h +8 -0
+	/** @flags: Mapping flags (cache attributes, permissions) */
+	u32 flags;
+	/** @vaddrin: Address the DSP should map the buffer at, if it can */
+	u64 vaddrin;
+	/** @num: Size of the physical page descriptor in bytes */
+	s32 num;
+	/** @data_len: Length of additional inline data */
+	s32 data_len;

And same here (and in the corresponding uapi struct)

Konrad

#44 lore raw

PATCH v4 18/25 accel/qda: Add remote memory unmapping from the DSP address space

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

A buffer mapped for the DSP stays mapped until it is explicitly unmapped, so user space needs a way to take a mapping down once it is no longer needed.

Add an ioctl that removes a mapping, naming it by the address that map returned. As with map there are two flavours, selected by the request:
one matching the plain mapping and one matching the attribute-based mapping, which also carries the DSP-side buffer identifier.

The buffer itself is kept alive by the handle it was created or imported under, so unmapping does not touch it; the mapping and the buffer have independent lifetimes and user space is expected to keep the handle open for as long as the DSP uses the mapping.

---
 drivers/accel/qda/qda_drv.c     |  1 +
 drivers/accel/qda/qda_fastrpc.c | 73 +++++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h | 32 ++++++++++++++++++
 drivers/accel/qda/qda_ioctl.c   | 24 ++++++++++++++
 drivers/accel/qda/qda_ioctl.h   |  1 +
 include/uapi/drm/qda_accel.h    | 30 +++++++++++++++++
 6 files changed, 161 insertions(+)
drivers/accel/qda/qda_drv.c +1 -0
index 7f19380701d9..c5b41b869692 100644
@@ -126,6 +126,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_INVOKE, qda_ioctl_invoke, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_CREATE, qda_ioctl_init_create, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MMAP, qda_ioctl_mmap, 0),
+	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MUNMAP, qda_ioctl_munmap, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_fastrpc.c +73 -0

85 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +32 -0
index 1bd696baa95c..889b41dc20b0 100644
@@ -46,9 +46,11 @@ struct qda_session;
 /* Methods of the initialisation handle */
 #define QDA_FASTRPC_RMID_INIT_RELEASE		1
 #define QDA_FASTRPC_RMID_INIT_MMAP		4
+#define QDA_FASTRPC_RMID_INIT_MUNMAP		5
 #define QDA_FASTRPC_RMID_INIT_CREATE		6
 #define QDA_FASTRPC_RMID_INIT_CREATE_ATTR	7
 #define QDA_FASTRPC_RMID_INIT_MEM_MAP		10
+#define QDA_FASTRPC_RMID_INIT_MEM_UNMAP		11
 #define QDA_FASTRPC_RMID_INVOKE_DYNAMIC		0xFFFFFFFFU
 
 /* Handle the initialisation methods are addressed to */
@@ -197,6 +199,36 @@ struct qda_fastrpc_map_rsp_msg {
 	u64 vaddrout;
 };
 
+/**
+ * struct qda_fastrpc_munmap_req_msg - Request to remove a plain mapping
+ *
+ * Layout the DSP expects; do not reorder.
+ */
+struct qda_fastrpc_munmap_req_msg {
+	/** @remote_session_id: Session the mapping belongs to */
+	s32 remote_session_id;
+	/** @vaddr: Address the mapping was made at */
+	u64 vaddr;
+	/** @size: Length of the mapping in bytes */
+	u64 size;
+};
+
+/**
+ * struct qda_fastrpc_mem_unmap_req_msg - Request to remove an attribute-based mapping
+ *
+ * Layout the DSP expects; do not reorder.
+ */
+struct qda_fastrpc_mem_unmap_req_msg {
+	/** @remote_session_id: Session the mapping belongs to */
+	s32 remote_session_id;
+	/** @handle: Identifier the DSP tracks the buffer by */
+	s32 handle;
+	/** @vaddrin: Address the mapping was made at */
+	u64 vaddrin;
+	/** @len: Length of the mapping in bytes */
+	u64 len;
+};
+
 /**
  * struct qda_fastrpc_msg - FastRPC wire message for remote invocations
  *
drivers/accel/qda/qda_ioctl.c +24 -0
index cd3cea72f50e..17d1b488b4a9 100644
@@ -197,6 +197,30 @@ int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_pri
 	}
 }
 
+/**
+ * qda_ioctl_munmap() - Remove a buffer's mapping from the DSP's address space
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_mem_unmap)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct drm_qda_mem_unmap *unmap_req = data;
+
+	switch (unmap_req->request) {
+	case QDA_REQUEST_MUNMAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MUNMAP, data,
+					  file_priv->driver_priv);
+	case QDA_REQUEST_MEM_UNMAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MEM_UNMAP, data,
+					  file_priv->driver_priv);
+	default:
+		return -EINVAL;
+	}
+}
+
 /**
  * qda_ioctl_invoke() - Run a method on the DSP
  * @dev: DRM device structure
drivers/accel/qda/qda_ioctl.h +1 -0
index afb3ed0e132e..9c053349c3fa 100644
@@ -16,6 +16,7 @@ int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_fil
 int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 
 #endif /* __QDA_IOCTL_H__ */
include/uapi/drm/qda_accel.h +30 -0
index bd2d0f4308b1..43598ddef461 100644
@@ -18,6 +18,7 @@ extern "C" {
 #define DRM_QDA_REMOTE_INVOKE		0x03
 #define DRM_QDA_REMOTE_SESSION_CREATE	0x04
 #define DRM_QDA_REMOTE_MMAP		0x05
+#define DRM_QDA_REMOTE_MUNMAP		0x06
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -32,11 +33,17 @@ extern "C" {
 		 struct drm_qda_init_create)
 #define DRM_IOCTL_QDA_REMOTE_MMAP	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_MMAP, \
 					 struct drm_qda_mem_map)
+#define DRM_IOCTL_QDA_REMOTE_MUNMAP	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_MUNMAP, \
+					 struct drm_qda_mem_unmap)
 
 /* Request types for drm_qda_mem_map.request */
 #define QDA_REQUEST_MMAP	1	/* Map a whole buffer */
 #define QDA_REQUEST_MEM_MAP	2	/* Map a region of a buffer, with attributes */
 
+/* Request types for drm_qda_mem_unmap.request */
+#define QDA_REQUEST_MUNMAP	1
+#define QDA_REQUEST_MEM_UNMAP	2
+
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
 
@@ -173,6 +180,29 @@ struct drm_qda_mem_map {
 	__u64 vaddrout;
 };
 
+/**
+ * struct drm_qda_mem_unmap - Parameters for removing a DSP mapping
+ * @request: QDA_REQUEST_MUNMAP or QDA_REQUEST_MEM_UNMAP
+ * @dsp_handle: Identifier the DSP tracks the buffer by (QDA_REQUEST_MEM_UNMAP)
+ * @vaddr: Address the mapping was made at (QDA_REQUEST_MEM_UNMAP)
+ * @vaddrout: Address the mapping was made at (QDA_REQUEST_MUNMAP)
+ * @size: Length of the mapping in bytes
+ *
+ * Removes a mapping made with DRM_IOCTL_QDA_REMOTE_MMAP, named by the address
+ * that call returned in drm_qda_mem_map.vaddrout. The request must match the
+ * one the mapping was made with: QDA_REQUEST_MUNMAP uses @vaddrout and @size,
+ * QDA_REQUEST_MEM_UNMAP uses @dsp_handle, @vaddr and @size. The buffer is not
+ * affected, so its handle must stay open for as long as the DSP uses the
+ * mapping.
+ */
+struct drm_qda_mem_unmap {
+	__u32 request;
+	__s32 dsp_handle;
+	__u64 vaddr;
+	__u64 vaddrout;
+	__u64 size;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#45 lore raw

Re: [PATCH v4 18/25] accel/qda: Add remote memory unmapping from the DSP address space

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] accel/qda: missing GEM object reference for active DSP mappings - [High] accel/qda: refcount…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] accel/qda: missing GEM object reference for active DSP mappings
- [High] accel/qda: refcount underflow and UAF in qda_fastrpc_flush_pending()
- [Low] accel/qda: duplicate address fields and unchecked fields in qda_ioctl_munmap() --

commit 52a28fd51f1253e9ef94a6c86442ba00197bb35c
Author: Ekansh Gupta <[email protected]>

accel/qda: Add remote memory unmapping from the DSP address space

This commit adds the DRM_IOCTL_QDA_REMOTE_MUNMAP ioctl and FastRPC prepare handlers to allow user space to unmap plain and attribute-based buffer mappings from the DSP address space.

Quoted 64 lines
drivers/accel/qda/qda_fastrpc.c +56 -0
index 744d7317cbe11..dbd14cd07cb20 100644
@@ -1072,6 +1072,73 @@ static int qda_fastrpc_prepare_args_mem_map(struct qda_fastrpc_invoke_ctx *ctx,
 	return err;
 }
 
+/*
+ * INIT_MUNMAP packed layout:
+ *   [0] req	sizeof(qda_fastrpc_munmap_req_msg)	IN
+ */
+static int qda_fastrpc_prepare_args_munmap(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
+{
+	struct drm_qda_mem_unmap *uargs = argp;
+	struct qda_fastrpc_munmap_req_msg *req;
+	struct qda_gem_obj *kernel_gem;
+	int err;
+
+	ctx->sc = QDA_FASTRPC_SCALARS(QDA_FASTRPC_RMID_INIT_MUNMAP, 1, 0);
+	ctx->handle = QDA_FASTRPC_INIT_HANDLE;
+
+	err = alloc_kernel_args(ctx, 1);
+	if (err)
+		return err;
+
+	kernel_gem = alloc_kernel_gem(ctx, sizeof(*req));
+	if (IS_ERR(kernel_gem))
+		return PTR_ERR(kernel_gem);
+
+	req = kernel_gem->virt;
+	req->remote_session_id = ctx->session->remote_session_id;
+	req->vaddr = uargs->vaddrout;
+	req->size = uargs->size;
+
+	assign_kernel_gem_arg(ctx, 0, kernel_gem, req, sizeof(*req));
+	drm_gem_object_put(&kernel_gem->base);
+
+	return 0;
+}
+
+/*
+ * INIT_MEM_UNMAP packed layout:
+ *   [0] req	sizeof(qda_fastrpc_mem_unmap_req_msg)	IN
+ */
+static int qda_fastrpc_prepare_args_mem_unmap(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
+{
+	struct drm_qda_mem_unmap *uargs = argp;
+	struct qda_fastrpc_mem_unmap_req_msg *req;
+	struct qda_gem_obj *kernel_gem;
+	int err;
+
+	ctx->sc = QDA_FASTRPC_SCALARS(QDA_FASTRPC_RMID_INIT_MEM_UNMAP, 1, 0);
+	ctx->handle = QDA_FASTRPC_INIT_HANDLE;
+
+	err = alloc_kernel_args(ctx, 1);
+	if (err)
+		return err;
+
+	kernel_gem = alloc_kernel_gem(ctx, sizeof(*req));
+	if (IS_ERR(kernel_gem))
+		return PTR_ERR(kernel_gem);
+
+	req = kernel_gem->virt;

drivers/accel/qda/qda_fastrpc.c static int qda_fastrpc_prepare_args_mem_map(struct qda_fastrpc_invoke_ctx *ctx, · line 1140

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+	req->remote_session_id = ctx->session->remote_session_id;
+	req->handle = uargs->dsp_handle;
+	req->vaddrin = uargs->vaddr;
+	req->len = uargs->size;
+
+	assign_kernel_gem_arg(ctx, 0, kernel_gem, req, sizeof(*req));
+	drm_gem_object_put(&kernel_gem->base);
+
+	return 0;
+}

[Severity: High]
Can leaving the GEM object lifetime entirely to user space while a DSP mapping is active lead to DMA use-after-free?

During qda_ioctl_mmap(), setup_mmap_pages() acquires ctx->map_obj only for the duration of the MMAP RPC, and qda_fastrpc_context_free() drops it as soon as the ioctl completes:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_free() {
	...
	if (ctx->map_obj)
		drm_gem_object_put(ctx->map_obj);
	...
}

While the mapping remains live on the DSP between DRM_IOCTL_QDA_REMOTE_MMAP and DRM_IOCTL_QDA_REMOTE_MUNMAP, the kernel does not track the mapping on struct qda_session or hold a drm_gem_object reference.

If user space closes the GEM handle via DRM_IOCTL_GEM_CLOSE before calling DRM_IOCTL_QDA_REMOTE_MUNMAP (or if qda_rpmsg_wait_for_rsp() is interrupted by a signal after the map request was sent so user space never receives vaddrout), qda_gem_free_object() immediately frees the DMA buffer and its IOVA in session->assigned_iommu_dev while the DSP mapping is still active.

Because session->assigned_iommu_dev is also used by alloc_kernel_gem() and qda_fastrpc_invoke_pack() to allocate internal FastRPC request and payload buffers, can the freed IOVA be reused for kernel RPC messages while an unprivileged DSP user process still has vaddrout mapped to it?

Furthermore, even if user space keeps the handle open, when a process exits or is killed while a mapping is active, drm_file_free() runs drm_gem_release() before dev->driver->postclose() (qda_postclose()):

drivers/gpu/drm/drm_file.c:drm_file_free()
-> drm_gem_release()
-> qda_postclose()

       -> qda_release_dsp_process()

Doesn't this free and unmap all GEM buffers while the DSP process is still running and before qda_release_dsp_process() tears it down? Also, because active mappings are not tracked by the kernel, qda_fastrpc_prepare_args_munmap() and qda_fastrpc_prepare_args_mem_unmap() forward arbitrary user-supplied vaddr/vaddrout, size, and dsp_handle values to QDA_FASTRPC_INIT_HANDLE without verifying that they match an existing mapping.

Quoted 21 lines
drivers/accel/qda/qda_ioctl.c +13 -0
index cd3cea72f50e8..17d1b488b4a9b 100644
@@ -197,6 +197,30 @@ int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_pri
 	}
 }
 
+/**
+ * qda_ioctl_munmap() - Remove a buffer's mapping from the DSP's address space
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_mem_unmap)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct drm_qda_mem_unmap *unmap_req = data;
+
+	switch (unmap_req->request) {

drivers/accel/qda/qda_ioctl.c int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_pri · line 222

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+	case QDA_REQUEST_MUNMAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MUNMAP, data,
+					  file_priv->driver_priv);
+	case QDA_REQUEST_MEM_UNMAP:
+		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_MEM_UNMAP, data,
+					  file_priv->driver_priv);
+	default:
+		return -EINVAL;
+	}
+}

[Severity: High]
Can calling qda_fastrpc_invoke() from qda_ioctl_munmap() race with qda_fastrpc_flush_pending() during rpmsg teardown and cause a refcount underflow and use-after-free on struct qda_fastrpc_invoke_ctx?

In qda_fastrpc_invoke(), qda_fastrpc_context_alloc() initializes ctx->refcount to 1 and inserts ctx into qdev->ctx_xa before the message is prepared or sent:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_context_alloc() {
	...
	kref_init(&ctx->refcount);
	...
	ret = xa_alloc(&qdev->ctx_xa, &id, ctx, XA_LIMIT(1, QDA_FASTRPC_CTXID_MAX),
		       GFP_KERNEL);
	...
}

The second reference on ctx->refcount for the response handler is only taken later in qda_rpmsg_send_msg(), and is dropped immediately if rpmsg_send() fails or asynchronously via ctx->put_work after qda_rpmsg_cb() runs. However, ctx is only erased from qdev->ctx_xa inside qda_fastrpc_context_free() when the final reference is dropped.

If qda_rpmsg_remove() runs concurrently and calls
qda_fastrpc_flush_pending():

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_flush_pending() {
	...
	xa_for_each(&qdev->ctx_xa, idx, ctx) {
		ctx->retval = -EPIPE;
		complete(&ctx->work);
		kref_put(&ctx->refcount, qda_fastrpc_context_free);
	}
}

while an invocation in qda_fastrpc_invoke() is still in qda_fastrpc_prepare_args() or qda_fastrpc_invoke_pack(), or after qda_rpmsg_send_msg() failed (for example with -ENODEV at drm_dev_enter()), or after qda_rpmsg_cb() already queued ctx->put_work, won't qda_fastrpc_flush_pending() drop a reference that was never held by the pending response path, freeing ctx before the caller or worker drops its own reference?

Quoted 4 lines
include/uapi/drm/qda_accel.h +0 -0
index bd2d0f4308b10..43598ddef461d 100644

[ ... ]

Quoted 16 lines
include/uapi/drm/qda_accel.h +12 -0
@@ -173,6 +180,29 @@ struct drm_qda_mem_map {
 	__u64 vaddrout;
 };
 
+/**
+ * struct drm_qda_mem_unmap - Parameters for removing a DSP mapping
+ * @request: QDA_REQUEST_MUNMAP or QDA_REQUEST_MEM_UNMAP
+ * @dsp_handle: Identifier the DSP tracks the buffer by (QDA_REQUEST_MEM_UNMAP)
+ * @vaddr: Address the mapping was made at (QDA_REQUEST_MEM_UNMAP)
+ * @vaddrout: Address the mapping was made at (QDA_REQUEST_MUNMAP)
+ * @size: Length of the mapping in bytes
+ *
+ * Removes a mapping made with DRM_IOCTL_QDA_REMOTE_MMAP, named by the address
+ * that call returned in drm_qda_mem_map.vaddrout. The request must match the
+ * one the mapping was made with: QDA_REQUEST_MUNMAP uses @vaddrout and @size,
+ * QDA_REQUEST_MEM_UNMAP uses @dsp_handle, @vaddr and @size. The buffer is not

include/uapi/drm/qda_accel.h struct drm_qda_mem_map { · line 204

Quoted 10 lines
include/uapi/drm/qda_accel.h +10 -0
+ * affected, so its handle must stay open for as long as the DSP uses the
+ * mapping.
+ */
+struct drm_qda_mem_unmap {
+	__u32 request;
+	__s32 dsp_handle;
+	__u64 vaddr;
+	__u64 vaddrout;
+	__u64 size;
+};

[Severity: Low]
As reported by Sashiko on v1 [1], struct drm_qda_mem_unmap defines two separate address fields (vaddr and vaddrout) for the same DSP virtual address returned in drm_qda_mem_map.vaddrout, and qda_ioctl_munmap() does not verify that the fields unused by the selected request type (dsp_handle and vaddr for QDA_REQUEST_MUNMAP, or vaddrout for QDA_REQUEST_MEM_UNMAP) are zero. Could a single address field be used here, with unused fields checked for zero in qda_ioctl_munmap()?

[1] https://lore.kernel.org/r/[email protected]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=18
#46 lore raw

PATCH v4 19/25 accel/qda: Add remote memory map and unmap to the legacy shim

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Applications written for the /dev/fastrpc-* interface name buffers by file descriptor rather than GEM handle, and have no handle table to keep a mapped buffer alive between the map and unmap calls. Serve the four legacy map and unmap ioctls by importing the descriptor as usual, and track the live mapping on the open file so the buffer stays referenced for as long as the DSP has it mapped, exactly the job a GEM handle does for the native UAPI.

The buffer reaches the shared mapping code already imported, in place of a handle. As for a buffer named by handle, it must be mapped in the calling session's context bank, since a descriptor can name a buffer another session exported, and the requested length must fit in it.

Two lists are kept per open file, since the two legacy unmap paths name a mapping differently: MUNMAP only ever gets back the address map returned, while MEM_UNMAP uses the file descriptor as the identifier the DSP tracks the buffer by. A mapping is recorded with the address and length it was made with, and an unmap must match them: the length the DSP is asked to unmap is the recorded one, and MUNMAP with a different length is refused. The same descriptor cannot be mapped twice with MEM_MAP, as the DSP could not tell the two apart on unmap, and a negative buffer offset is refused.

If the DSP cannot unmap a mapping, it stays recorded so the unmap can be retried. If the mapping cannot be reported back to the caller, it is taken down again rather than left unreachable. Mappings still outstanding when the file closes are unmapped on the DSP before the buffers they hold are released, so the DSP is never left using memory that has been freed.

---
 drivers/accel/qda/qda_fastrpc.c      |  40 ++++--
 drivers/accel/qda/qda_fastrpc.h      |  17 +++
 drivers/accel/qda/qda_fastrpc_shim.c | 262 +++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.c        |  64 ++++++---
 drivers/accel/qda/qda_ioctl.h        |   4 +
 drivers/accel/qda/qda_legacy_ops.c   |  77 ++++++++++
 drivers/accel/qda/qda_legacy_ops.h   |  11 ++
 7 files changed, 447 insertions(+), 28 deletions(-)
drivers/accel/qda/qda_fastrpc.c +28 -12
index dbd14cd07cb2..b35604fce630 100644
@@ -633,7 +633,7 @@ void qda_fastrpc_flush_pending(struct qda_dev *qdev)
  */
 void qda_fastrpc_return_result(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
 {
-	struct drm_qda_mem_map *margs = argp;
+	struct qda_mem_map_args *map_args = argp;
 	struct qda_fastrpc_map_rsp_msg *rsp;
 	int idx;
 
@@ -650,7 +650,7 @@ void qda_fastrpc_return_result(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
 
 	rsp = (struct qda_fastrpc_map_rsp_msg *)
 	      ((u8 *)to_qda_gem_obj(ctx->gem_objs[idx])->virt + ctx->args[idx].offset);
-	margs->vaddrout = rsp->vaddrout;
+	map_args->map.vaddrout = rsp->vaddrout;
 }
 
 /**
@@ -935,19 +935,33 @@ static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c
  * buffer is held by the context until the request is done, so that closing its
  * handle meanwhile cannot free memory the DSP is being told to map. The
  * region must fit in the buffer, as the DSP would otherwise map past its end.
+ *
+ * A caller with no handle table supplies the buffer already imported. It is
+ * checked as a buffer named by handle would be: an fd can name a buffer that
+ * another session exported, which resolves to the original object, still
+ * mapped in that session's context bank.
  */
-static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct drm_qda_mem_map *margs,
+static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct qda_mem_map_args *args,
 			    struct qda_phy_page *pages)
 {
+	struct drm_qda_mem_map *margs = &args->map;
 	struct qda_gem_obj *qda_gem_obj;
 	int err;
 
-	if (!margs->handle)
-		return -EINVAL;
+	if (args->obj) {
+		if (to_qda_gem_obj(args->obj)->iommu_dev != ctx->session->assigned_iommu_dev)
+			return -EACCES;
 
-	err = get_gem_obj_from_handle(ctx, margs->handle, &ctx->map_obj);
-	if (err)
-		return err;
+		drm_gem_object_get(args->obj);
+		ctx->map_obj = args->obj;
+	} else {
+		if (!margs->handle)
+			return -EINVAL;
+
+		err = get_gem_obj_from_handle(ctx, margs->handle, &ctx->map_obj);
+		if (err)
+			return err;
+	}
 
 	qda_gem_obj = to_qda_gem_obj(ctx->map_obj);
 	if (margs->size > qda_gem_obj->size)
@@ -967,7 +981,8 @@ static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct drm_qda_m
  */
 static int qda_fastrpc_prepare_args_map(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
 {
-	struct drm_qda_mem_map *margs = argp;
+	struct qda_mem_map_args *map_args = argp;
+	struct drm_qda_mem_map *margs = &map_args->map;
 	struct qda_fastrpc_map_req_msg *req;
 	struct qda_gem_obj *kernel_gem;
 	u8 *gv;
@@ -996,7 +1011,7 @@ static int qda_fastrpc_prepare_args_map(struct qda_fastrpc_invoke_ctx *ctx, void
 	req->vaddr = margs->vaddrin;
 	req->num = sizeof(struct qda_phy_page);
 
-	err = setup_mmap_pages(ctx, margs, (struct qda_phy_page *)(gv + off_pages));
+	err = setup_mmap_pages(ctx, map_args, (struct qda_phy_page *)(gv + off_pages));
 	if (err)
 		goto out_put;
 
@@ -1020,7 +1035,8 @@ static int qda_fastrpc_prepare_args_map(struct qda_fastrpc_invoke_ctx *ctx, void
  */
 static int qda_fastrpc_prepare_args_mem_map(struct qda_fastrpc_invoke_ctx *ctx, void *argp)
 {
-	struct drm_qda_mem_map *margs = argp;
+	struct qda_mem_map_args *map_args = argp;
+	struct drm_qda_mem_map *margs = &map_args->map;
 	struct qda_fastrpc_mem_map_req_msg *req;
 	struct qda_gem_obj *kernel_gem;
 	u8 *gv;
@@ -1056,7 +1072,7 @@ static int qda_fastrpc_prepare_args_mem_map(struct qda_fastrpc_invoke_ctx *ctx,
 	req->num = sizeof(struct qda_phy_page);
 	req->data_len = 0;
 
-	err = setup_mmap_pages(ctx, margs, (struct qda_phy_page *)(gv + off_pages));
+	err = setup_mmap_pages(ctx, map_args, (struct qda_phy_page *)(gv + off_pages));
 	if (err)
 		goto out_put;
 
drivers/accel/qda/qda_fastrpc.h +17 -0
index 889b41dc20b0..10a99bbfa383 100644
@@ -360,6 +360,23 @@ struct qda_init_create_args {
 	struct drm_gem_object *elf_obj;
 };
 
+/**
+ * struct qda_mem_map_args - Kernel-side arguments for mapping a buffer into the DSP
+ *
+ * Wraps the UAPI arguments so that a caller with no handle table, such as the
+ * legacy shim, can pass an already imported buffer.
+ */
+struct qda_mem_map_args {
+	/** @map: Mapping parameters; @map.vaddrout is filled in on success */
+	struct drm_qda_mem_map map;
+	/**
+	 * @obj: Buffer to map, in the session's context bank, or NULL if
+	 * @map.handle names it. Borrowed: the caller keeps its reference for
+	 * the length of the call.
+	 */
+	struct drm_gem_object *obj;
+};
+
 struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev);
 void qda_fastrpc_context_free(struct kref *ref);
 void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx);
drivers/accel/qda/qda_fastrpc_shim.c +262 -0

310 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.c +48 -16

90 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +4 -0
index 9c053349c3fa..9b868189df0a 100644
@@ -8,7 +8,9 @@
 
 #include "qda_drv.h"
 
+struct drm_qda_mem_unmap;
 struct qda_init_create_args;
+struct qda_mem_map_args;
 
 int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
@@ -18,5 +20,7 @@ int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *f
 int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
+int qda_session_mem_map(struct qda_session *session, struct qda_mem_map_args *args);
+int qda_session_mem_unmap(struct qda_session *session, struct drm_qda_mem_unmap *unmap);
 
 #endif /* __QDA_IOCTL_H__ */
drivers/accel/qda/qda_legacy_ops.c +77 -0

89 lines not on this page · raw message

drivers/accel/qda/qda_legacy_ops.h +11 -0
index 05bd1f806c9a..e3126ac10507 100644
@@ -35,6 +35,17 @@ struct qda_legacy_ops {
 			   u32 filelen, u32 attrs, u32 siglen);
 	/** @release: Release the session's DSP process, if it has one */
 	int (*release)(struct qda_session *session);
+	/** @mmap: Map a buffer into the DSP's address space */
+	int (*mmap)(struct qda_session *session, struct drm_gem_object *obj, u64 vaddrin,
+		    u64 size, u32 flags, u64 *vaddrout);
+	/** @mem_map: Map a region of a buffer with explicit SMMU attributes */
+	int (*mem_map)(struct qda_session *session, struct drm_gem_object *obj, u32 flags,
+		       u32 attrs, u32 offset, u64 vaddrin, u64 size, s32 dsp_handle,
+		       u64 *vaddrout);
+	/** @munmap: Undo a mapping made with @mmap */
+	int (*munmap)(struct qda_session *session, u64 vaddrout, u64 size);
+	/** @mem_unmap: Undo a mapping made with @mem_map */
+	int (*mem_unmap)(struct qda_session *session, s32 dsp_handle, u64 vaddr, u64 size);
 	/** @alloc_dmabuf: Allocate a buffer and export it as a DMA-BUF */
 	struct dma_buf *(*alloc_dmabuf)(struct qda_session *session, u64 size);
 	/** @import_dmabuf: Map a DMA-BUF into a session's context bank */
Signature · 1 line
2.34.1
#47 lore raw

Re: [PATCH v4 19/25] accel/qda: Add remote memory map and unmap to the legacy shim

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider: - [Critical] accel/qda: use-after-free race on placeholder in qda_shim_mem_map() - [High] accel/qda: unmap…

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Critical] accel/qda: use-after-free race on placeholder in qda_shim_mem_map()
- [High] accel/qda: unmap size mismatch in qda_shim_mmap() when size is 0
- [High] accel/qda: missing unplug check in shim_mappings_release() on close
- [High] accel/qda: premature buffer release when legacy unmap fails or is interrupted
- [Medium] accel/qda: race in shim_mapping_detach() and shim_mapping_attach()
- [Medium] accel/qda: missing offset bounds check in setup_mmap_pages()
--

commit b1739b465f1ac98dac259c9a90724960699924f1
Author: Ekansh Gupta <[email protected]>

accel/qda: Add remote memory map and unmap to the legacy shim

This commit implements the four legacy FastRPC memory map and unmap ioctls in the QDA compatibility shim by importing DMA-BUF file descriptors and tracking live mappings per open file so buffers remain referenced while mapped on the DSP.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
index dbd14cd07cb20..b35604fce630c 100644

[ ... ]

Quoted 40 lines
drivers/accel/qda/qda_fastrpc.c +20 -6
@@ -935,19 +935,33 @@ static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *c
  * buffer is held by the context until the request is done, so that closing its
  * handle meanwhile cannot free memory the DSP is being told to map. The
  * region must fit in the buffer, as the DSP would otherwise map past its end.
+ *
+ * A caller with no handle table supplies the buffer already imported. It is
+ * checked as a buffer named by handle would be: an fd can name a buffer that
+ * another session exported, which resolves to the original object, still
+ * mapped in that session's context bank.
  */
-static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct drm_qda_mem_map *margs,
+static int setup_mmap_pages(struct qda_fastrpc_invoke_ctx *ctx, struct qda_mem_map_args *args,
 			    struct qda_phy_page *pages)
 {
+	struct drm_qda_mem_map *margs = &args->map;
 	struct qda_gem_obj *qda_gem_obj;
 	int err;
 
-	if (!margs->handle)
-		return -EINVAL;
+	if (args->obj) {
+		if (to_qda_gem_obj(args->obj)->iommu_dev != ctx->session->assigned_iommu_dev)
+			return -EACCES;
 
-	err = get_gem_obj_from_handle(ctx, margs->handle, &ctx->map_obj);
-	if (err)
-		return err;
+		drm_gem_object_get(args->obj);
+		ctx->map_obj = args->obj;
+	} else {
+		if (!margs->handle)
+			return -EINVAL;
+
+		err = get_gem_obj_from_handle(ctx, margs->handle, &ctx->map_obj);
+		if (err)
+			return err;
+	}
 
 	qda_gem_obj = to_qda_gem_obj(ctx->map_obj);
 	if (margs->size > qda_gem_obj->size)

drivers/accel/qda/qda_fastrpc.c

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
 		return -EINVAL;
 
 	pages->addr = qda_gem_obj->dma_addr;
 	pages->size = margs->size ? margs->size : qda_gem_obj->size;

[Severity: Medium]
Can a caller of FASTRPC_IOCTL_MEM_MAP or DRM_IOCTL_QDA_REMOTE_MMAP (with QDA_REQUEST_MEM_MAP) map past the end of qda_gem_obj by supplying a non-zero margs->offset?

In qda_shim_mem_map(), only args.offset < 0 is rejected, and qda_fastrpc_prepare_args_mem_map() only checks margs->offset > INT_MAX before setting req->offset = margs->offset.

Here in setup_mmap_pages(), only margs->size > qda_gem_obj->size is checked. If a caller passes 0 < margs->offset <= INT_MAX and margs->size <= qda_gem_obj->size (including margs->size == 0, which sets pages->size to qda_gem_obj->size) such that margs->offset + pages->size exceeds qda_gem_obj->size, shouldn't setup_mmap_pages() also check that margs->offset <= qda_gem_obj->size and pages->size <= qda_gem_obj->size - margs->offset?

Quoted 4 lines
drivers/accel/qda/qda_fastrpc_shim.c +0 -0
index 4a2ba20ee01ac..c46d2de4f5818 100644

[ ... ]

Quoted 46 lines
drivers/accel/qda/qda_fastrpc_shim.c +42 -0
@@ -79,6 +101,83 @@ static struct drm_gem_object *shim_import_fd(struct qda_session *session, s32 fd
 	return obj;
 }
 
+/* Takes ownership of @obj on success; on failure the caller still owns it */
+static struct qda_shim_mapping *shim_mapping_add(struct qda_shim_file *sf,
+						 struct list_head *list, u64 key, u64 vaddr,
+						 u64 size, struct drm_gem_object *obj)
+{
+	struct qda_shim_mapping *m, *new;
+
+	new = kzalloc_obj(*new);
+	if (!new)
+		return ERR_PTR(-ENOMEM);
+
+	new->key = key;
+	new->vaddr = vaddr;
+	new->size = size;
+	new->obj = obj;
+
+	guard(mutex)(&sf->maps_lock);
+
+	list_for_each_entry(m, list, node) {
+		if (m->key == key) {
+			kfree(new);
+			return ERR_PTR(-EEXIST);
+		}
+	}
+	list_add_tail(&new->node, list);
+
+	return new;
+}
+
+static struct qda_shim_mapping *shim_mapping_detach(struct qda_shim_file *sf,
+						    struct list_head *list, u64 key, u64 vaddr)
+{
+	struct qda_shim_mapping *m;
+
+	guard(mutex)(&sf->maps_lock);
+
+	list_for_each_entry(m, list, node) {
+		if (m->key == key && m->vaddr == vaddr) {
+			list_del(&m->node);
+			return m;
+		}
+	}

drivers/accel/qda/qda_fastrpc_shim.c static struct drm_gem_object *shim_import_fd(struct qda_session *session, s32 fd · line 155

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+
+	return NULL;
+}
+
+static void shim_mapping_attach(struct qda_shim_file *sf, struct list_head *list,
+				struct qda_shim_mapping *m)
+{
+	guard(mutex)(&sf->maps_lock);
+	list_add_tail(&m->node, list);
+}

[Severity: Medium]
Can detaching a mapping in shim_mapping_detach() and dropping sf->maps_lock before validating size or calling the blocking unmap RPC race with concurrent map and unmap calls on the same open file?

First, in qda_shim_mem_unmap(), m is removed from sf->mem_map_list while qda_legacy_ops_table.mem_unmap() runs without sf->maps_lock held. While m is detached, its key (args.fd) is absent from sf->mem_map_list, so a concurrent qda_shim_mem_map() with the same fd can pass the duplicate-key check in shim_mapping_add() and insert a second entry with the same fd. If the in-flight mem_unmap() then fails (for example, interrupted by a signal with -ERESTARTSYS in wait_for_completion_interruptible()), shim_mapping_attach() unconditionally appends m back onto sf->mem_map_list with list_add_tail(), leaving duplicate fd keys in sf->mem_map_list.

Second, in both qda_shim_mmap() and qda_shim_mem_map(), m is visible on the list before copy_to_user() runs. If a concurrent qda_shim_munmap() (for example, with a mismatched args.size since m->size != args.size is checked after shim_mapping_detach()) or a failing qda_shim_mem_unmap() temporarily detaches m while copy_to_user() faults, the error rollback call to shim_mapping_detach() in the map path will return NULL and skip unmapping and freeing m, after which the failing unmap path calls shim_mapping_attach() and re-attaches m even though the map ioctl returned -EFAULT.

Quoted 14 lines
drivers/accel/qda/qda_fastrpc_shim.c +14 -0
+
+static void shim_mapping_free(struct qda_shim_mapping *m)
+{
+	drm_gem_object_put(m->obj);
+	kfree(m);
+}
+
+/* The DSP unmap comes first: the buffer must outlive the DSP's use of it */
+static void shim_mappings_release(struct qda_shim_file *sf)
+{
+	struct qda_shim_mapping *m, *tmp;
+
+	list_for_each_entry_safe(m, tmp, &sf->mmap_list, node) {
+		list_del(&m->node);

drivers/accel/qda/qda_fastrpc_shim.c static struct drm_gem_object *shim_import_fd(struct qda_session *session, s32 fd · line 179

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+		qda_legacy_ops_table.munmap(sf->session, m->vaddr, m->size);
+		shim_mapping_free(m);
+	}
+
+	list_for_each_entry_safe(m, tmp, &sf->mem_map_list, node) {
+		list_del(&m->node);
+		qda_legacy_ops_table.mem_unmap(sf->session, m->key, m->vaddr, m->size);
+		shim_mapping_free(m);
+	}
+}

[Severity: High]
What happens in qda_shim_release() -> shim_mappings_release() if the file is closed after the underlying rpmsg device has been removed?

When the rpmsg device is removed while a /dev/fastrpc-* file is still open, qda_rpmsg_remove() calls drm_dev_unplug(), qda_cb_unpopulate() (destroying each context-bank platform device), and qda_deinit_device() (destroying qdev->iommu_mgr->assignment_lock and qdev->ctx_xa), and devres frees qdev->iommu_mgr.

In drivers/accel/qda/qda_legacy_ops.c, legacy_release() checks drm_dev_is_unplugged(&session->qda_dev->drm_dev) before calling qda_release_dsp_process(). However, qda_shim_release() calls shim_mappings_release() before legacy_release(), and neither shim_mappings_release() nor legacy_munmap() and legacy_mem_unmap() checks drm_dev_is_unplugged(). Won't closing a file with active mappings after unplug call into qda_fastrpc_invoke() -> xa_alloc(&qdev->ctx_xa, ...) and alloc_kernel_gem() -> qda_memory_manager_alloc(), dereferencing the destroyed ctx_xa, devm-freed qdev->iommu_mgr, and destroyed context-bank platform device?

[ ... ]

Quoted 18 lines
drivers/accel/qda/qda_fastrpc_shim.c +14 -0
@@ -191,6 +295,156 @@ static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp)
 	return err;
 }
 
+static long qda_shim_mmap(struct qda_shim_file *sf, void __user *argp)
+{
+	struct qda_shim_mapping *m;
+	struct fastrpc_req_mmap args;
+	struct drm_gem_object *obj;
+	int err;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	obj = shim_import_fd(sf->session, args.fd);
+	if (IS_ERR(obj))
+		return PTR_ERR(obj);
+	if (!obj)

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp) · line 321

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+		return -EINVAL;
+
+	err = qda_legacy_ops_table.mmap(sf->session, obj, args.vaddrin, args.size, args.flags,
+					&args.vaddrout);
+	if (err) {
+		drm_gem_object_put(obj);
+		return err;
+	}
+
+	m = shim_mapping_add(sf, &sf->mmap_list, args.vaddrout, args.vaddrout, args.size, obj);

[Severity: High]
If user space passes args.size == 0 to FASTRPC_IOCTL_MMAP (or args.length == 0 to FASTRPC_IOCTL_MEM_MAP in qda_shim_mem_map()), doesn't this record m->size = 0 while the DSP actually mapped the full GEM object size?

In setup_mmap_pages():

	pages->addr = qda_gem_obj->dma_addr;
	pages->size = margs->size ? margs->size : qda_gem_obj->size;

When args.size is 0, the DSP maps qda_gem_obj->size bytes, but qda_shim_mmap() and qda_shim_mem_map() record 0 in m->size. When the mapping is later unmapped via qda_shim_munmap(), qda_shim_mem_unmap(), copy_to_user() failure rollback, or shim_mappings_release(), the DSP is asked to unmap 0 bytes before shim_mapping_free() drops the reference on obj, and FASTRPC_IOCTL_MUNMAP calls specifying the actual buffer size will be rejected by m->size != args.size. Should m->size record the effective mapped size (or should a zero size/length be rejected)?

Quoted 12 lines
drivers/accel/qda/qda_fastrpc_shim.c +12 -0
+	if (IS_ERR(m)) {
+		err = PTR_ERR(m);
+		goto err_unmap;
+	}
+
+	if (copy_to_user(argp, &args, sizeof(args))) {
+		m = shim_mapping_detach(sf, &sf->mmap_list, args.vaddrout, args.vaddrout);
+		if (m) {
+			qda_legacy_ops_table.munmap(sf->session, m->vaddr, m->size);
+			shim_mapping_free(m);
+		}
+		return -EFAULT;

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp) · line 343

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+	}
+
+	return 0;
+
+err_unmap:
+	qda_legacy_ops_table.munmap(sf->session, args.vaddrout, args.size);
+	drm_gem_object_put(obj);
+
+	return err;
+}

[Severity: High]
Can ignoring the return value of munmap() and mem_unmap() in these rollback paths (and in shim_mappings_release()) release m->obj while the DSP still has the buffer mapped?

Here in qda_shim_mmap() (on copy_to_user() failure and at err_unmap), in qda_shim_mem_map() (on copy_to_user() failure), and in shim_mappings_release() on file close, qda_legacy_ops_table.munmap() or mem_unmap() waits in qda_rpmsg_wait_for_rsp() via wait_for_completion_interruptible(&ctx->work).

If a signal is pending or arrives while waiting (returning -ERESTARTSYS), or if allocating the unmap invocation context fails, the DSP has not unmapped the buffer, yet shim_mapping_free(m) or drm_gem_object_put(obj) immediately drops the reference on the GEM object (which for an imported DMA-BUF in the shim is the sole reference, tearing down its IOMMU mapping or freeing its DMA memory in qda_gem_free_object()).

[ ... ]

Quoted 27 lines
drivers/accel/qda/qda_fastrpc_shim.c +27 -0
+static long qda_shim_mem_map(struct qda_shim_file *sf, void __user *argp)
+{
+	struct qda_shim_mapping *m;
+	struct fastrpc_mem_map args;
+	struct drm_gem_object *obj;
+	int err;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	if (args.offset < 0)
+		return -EINVAL;
+
+	obj = shim_import_fd(sf->session, args.fd);
+	if (IS_ERR(obj))
+		return PTR_ERR(obj);
+	if (!obj)
+		return -EINVAL;
+
+	/* Reserved before the DSP is asked, so the same fd cannot be mapped twice */
+	m = shim_mapping_add(sf, &sf->mem_map_list, args.fd, 0, args.length, obj);
+	if (IS_ERR(m)) {
+		drm_gem_object_put(obj);
+		return PTR_ERR(m);
+	}
+
+	err = qda_legacy_ops_table.mem_map(sf->session, obj, args.flags, args.attrs, args.offset,

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp) · line 410

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+					   args.vaddrin, args.length, args.fd, &args.vaddrout);
+	if (err) {
+		m = shim_mapping_detach(sf, &sf->mem_map_list, args.fd, 0);
+		if (m)
+			shim_mapping_free(m);
+		return err;
+	}
+
+	scoped_guard(mutex, &sf->maps_lock)
+		m->vaddr = args.vaddrout;

[Severity: Critical]
Can a concurrent FASTRPC_IOCTL_MEM_UNMAP with args.vaddr == 0 detach and free the placeholder m while qda_legacy_ops_table.mem_map() is still running?

In qda_shim_mem_map(), shim_mapping_add() inserts m into sf->mem_map_list with m->key = args.fd and m->vaddr = 0, and transfers the caller's only reference on obj into m->obj before dropping sf->maps_lock:

	m = shim_mapping_add(sf, &sf->mem_map_list, args.fd, 0, args.length, obj);
	...
	err = qda_legacy_ops_table.mem_map(sf->session, obj, ...);

While qda_legacy_ops_table.mem_map() is executing without sf->maps_lock, a concurrent qda_shim_mem_unmap() on the same file with the same args.fd and args.vaddr == 0 will match the placeholder in shim_mapping_detach():

	list_for_each_entry(m, list, node) {
		if (m->key == key && m->vaddr == vaddr) {
			list_del(&m->node);
			return m;
		}
	}

If qda_legacy_ops_table.mem_unmap() then succeeds, qda_shim_mem_unmap() calls shim_mapping_free(m), which puts m->obj (potentially before setup_mmap_pages() has called drm_gem_object_get(args->obj)) and frees m. When qda_legacy_ops_table.mem_map() finishes and returns 0, qda_shim_mem_map() writes m->vaddr = args.vaddrout to the freed m and leaves the DSP mapping active with m->obj already released.

Conversely, if qda_legacy_ops_table.mem_map() fails while the concurrent qda_shim_mem_unmap() has detached m, qda_shim_mem_map()'s cleanup shim_mapping_detach() returns NULL, and if mem_unmap() then fails and calls shim_mapping_attach(), the failed placeholder with vaddr == 0 is re-attached to sf->mem_map_list, leaking m->obj until file close and blocking future FASTRPC_IOCTL_MEM_MAP calls on that fd with -EEXIST.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=19
#48 lore raw

PATCH v4 20/25 accel/qda: Add remote attach to root and sensors protection domains

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Some daemons need to reach a protection domain that already exists on the DSP, rather than create a new user process with DRM_IOCTL_QDA_REMOTE_SESSION_CREATE. Add an ioctl that attaches the calling session to one, directing every later request on that session at the attached domain.

Root and Sensors PD attach send the same message to the DSP, which tells them apart by the domain the request is directed at. The session only switches domain once the DSP has accepted the attach, and attaching is refused once a process has been created on the session.

Attaching requires CAP_SYS_ADMIN.

---
 drivers/accel/qda/qda_drv.c     |  1 +
 drivers/accel/qda/qda_fastrpc.c | 13 +++++++++----
 drivers/accel/qda/qda_fastrpc.h |  2 ++
 drivers/accel/qda/qda_ioctl.c   | 36 ++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.h   |  1 +
 include/uapi/drm/qda_accel.h    | 25 +++++++++++++++++++++++++
 6 files changed, 74 insertions(+), 4 deletions(-)
drivers/accel/qda/qda_drv.c +1 -0
index c5b41b869692..1e72dce769b9 100644
@@ -127,6 +127,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_CREATE, qda_ioctl_init_create, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MMAP, qda_ioctl_mmap, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MUNMAP, qda_ioctl_munmap, 0),
+	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_ATTACH, qda_ioctl_attach, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_fastrpc.c +9 -4
index b35604fce630..5bf55b7953c8 100644
@@ -819,15 +819,15 @@ static int qda_fastrpc_use_elf_obj(struct qda_fastrpc_invoke_ctx *ctx,
 }
 
 /*
- * INIT_RELEASE packed layout:
+ * INIT_RELEASE and INIT_ATTACH packed layout:
  *   [0] remote_session_id	sizeof(int)	IN
  */
-static int qda_fastrpc_prepare_args_release_process(struct qda_fastrpc_invoke_ctx *ctx)
+static int qda_fastrpc_prepare_args_session(struct qda_fastrpc_invoke_ctx *ctx, u32 rmid)
 {
 	struct qda_gem_obj *kernel_gem;
 	int err;
 
-	ctx->sc = QDA_FASTRPC_SCALARS(QDA_FASTRPC_RMID_INIT_RELEASE, 1, 0);
+	ctx->sc = QDA_FASTRPC_SCALARS(rmid, 1, 0);
 	ctx->handle = QDA_FASTRPC_INIT_HANDLE;
 
 	err = alloc_kernel_args(ctx, 1);
@@ -1211,7 +1211,12 @@ int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx,
 
 	switch (ctx->type) {
 	case QDA_FASTRPC_RMID_INIT_RELEASE:
-		err = qda_fastrpc_prepare_args_release_process(ctx);
+		err = qda_fastrpc_prepare_args_session(ctx, QDA_FASTRPC_RMID_INIT_RELEASE);
+		break;
+	case QDA_FASTRPC_RMID_INIT_ATTACH:
+		/* The DSP tells the domains apart by the one the request is directed at */
+		ctx->pd = *(int *)argp;
+		err = qda_fastrpc_prepare_args_session(ctx, QDA_FASTRPC_RMID_INIT_ATTACH);
 		break;
 	case QDA_FASTRPC_RMID_INIT_CREATE:
 		/* The new process is in the user PD, and so is the request creating it */
drivers/accel/qda/qda_fastrpc.h +2 -0
index 10a99bbfa383..29e1c3946d60 100644
@@ -44,6 +44,7 @@ struct qda_session;
 #define QDA_FASTRPC_MAX_CRCLIST		64	/* CRC list entries */
 
 /* Methods of the initialisation handle */
+#define QDA_FASTRPC_RMID_INIT_ATTACH		0
 #define QDA_FASTRPC_RMID_INIT_RELEASE		1
 #define QDA_FASTRPC_RMID_INIT_MMAP		4
 #define QDA_FASTRPC_RMID_INIT_MUNMAP		5
@@ -59,6 +60,7 @@ struct qda_session;
 /* Protection domains a request can be directed at */
 #define QDA_ROOT_PD			0
 #define QDA_USER_PD			1
+#define QDA_SENSORS_PD			2
 
 /* Largest ELF image a process may be created from */
 #define QDA_FASTRPC_INIT_FILELEN_MAX	(4 * 1024 * 1024)
drivers/accel/qda/qda_ioctl.c +36 -0
index 999afa9e70c2..9b8cccfea1e3 100644
@@ -121,6 +121,8 @@ static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
 	if (type == QDA_FASTRPC_RMID_INIT_CREATE) {
 		qda_session_set_init_mem(session, ctx->init_mem_gem_obj);
 		WRITE_ONCE(session->pd, QDA_USER_PD);
+	} else if (type == QDA_FASTRPC_RMID_INIT_ATTACH) {
+		WRITE_ONCE(session->pd, ctx->pd);
 	}
 
 out_put:
@@ -174,6 +176,40 @@ int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session)
 	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_RELEASE, NULL, session);
 }
 
+/**
+ * qda_ioctl_attach() - Attach to a DSP-resident protection domain
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_remote_attach)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct drm_qda_remote_attach *args = data;
+	struct qda_session *session = file_priv->driver_priv;
+	int pd;
+
+	if (args->pad)
+		return -EINVAL;
+
+	switch (args->pd_type) {
+	case QDA_ATTACH_ROOT_PD:
+		pd = QDA_ROOT_PD;
+		break;
+	case QDA_ATTACH_SENSORS_PD:
+		pd = QDA_SENSORS_PD;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (READ_ONCE(session->init_mem_gem_obj))
+		return -EBUSY;
+
+	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_ATTACH, &pd, session);
+}
+
 /**
  * qda_ioctl_mmap() - Map a buffer into the DSP's address space
  * @dev: DRM device structure
drivers/accel/qda/qda_ioctl.h +1 -0
index 9b868189df0a..f6f5d64e36ce 100644
@@ -19,6 +19,7 @@ int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_p
 int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 int qda_session_mem_map(struct qda_session *session, struct qda_mem_map_args *args);
 int qda_session_mem_unmap(struct qda_session *session, struct drm_qda_mem_unmap *unmap);
include/uapi/drm/qda_accel.h +25 -0
index 43598ddef461..59ad5b8cc997 100644
@@ -19,6 +19,7 @@ extern "C" {
 #define DRM_QDA_REMOTE_SESSION_CREATE	0x04
 #define DRM_QDA_REMOTE_MMAP		0x05
 #define DRM_QDA_REMOTE_MUNMAP		0x06
+#define DRM_QDA_REMOTE_SESSION_ATTACH	0x07
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -35,6 +36,9 @@ extern "C" {
 					 struct drm_qda_mem_map)
 #define DRM_IOCTL_QDA_REMOTE_MUNMAP	DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_MUNMAP, \
 					 struct drm_qda_mem_unmap)
+#define DRM_IOCTL_QDA_REMOTE_SESSION_ATTACH					\
+	DRM_IOW(DRM_COMMAND_BASE + DRM_QDA_REMOTE_SESSION_ATTACH,		\
+		struct drm_qda_remote_attach)
 
 /* Request types for drm_qda_mem_map.request */
 #define QDA_REQUEST_MMAP	1	/* Map a whole buffer */
@@ -203,6 +207,27 @@ struct drm_qda_mem_unmap {
 	__u64 size;
 };
 
+/* Protection domains for drm_qda_remote_attach.pd_type */
+#define QDA_ATTACH_ROOT_PD	1
+#define QDA_ATTACH_SENSORS_PD	2
+
+/**
+ * struct drm_qda_remote_attach - Parameters for attaching to a DSP protection domain
+ * @pd_type: QDA_ATTACH_ROOT_PD or QDA_ATTACH_SENSORS_PD
+ * @pad: Must be zero
+ *
+ * Binds this file to a protection domain that already exists on the DSP,
+ * instead of creating one with DRM_IOCTL_QDA_REMOTE_SESSION_CREATE. Every
+ * later request on the file is directed at it. Fails with EBUSY if a process
+ * has already been created on the file.
+ *
+ * Requires CAP_SYS_ADMIN.
+ */
+struct drm_qda_remote_attach {
+	__u32 pd_type;
+	__u32 pad;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#49 lore raw

Re: [PATCH v4 20/25] accel/qda: Add remote attach to root and sensors protection domains

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] accel/qda: race between qda_ioctl_attach() and qda_ioctl_init_create() --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] accel/qda: race between qda_ioctl_attach() and qda_ioctl_init_create() --

commit c03ef3a6851102dcdbec94040ce6c4949c48a5b9
Author: Ekansh Gupta <[email protected]>

accel/qda: Add remote attach to root and sensors protection domains

This commit adds an ioctl to attach a calling session to an existing root or sensors protection domain on the DSP instead of creating a new user process.

Quoted 40 lines
drivers/accel/qda/qda_ioctl.c +25 -0
index 999afa9e70c23..9b8cccfea1e3a 100644
@@ -121,6 +121,8 @@ static int qda_fastrpc_invoke(u32 type, void *data, struct qda_session *session)
 	if (type == QDA_FASTRPC_RMID_INIT_CREATE) {
 		qda_session_set_init_mem(session, ctx->init_mem_gem_obj);
 		WRITE_ONCE(session->pd, QDA_USER_PD);
+	} else if (type == QDA_FASTRPC_RMID_INIT_ATTACH) {
+		WRITE_ONCE(session->pd, ctx->pd);
 	}
 
 out_put:
@@ -174,6 +176,40 @@ int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session)
 	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_RELEASE, NULL, session);
 }
 
+/**
+ * qda_ioctl_attach() - Attach to a DSP-resident protection domain
+ * @dev: DRM device structure
+ * @data: User-space data (struct drm_qda_remote_attach)
+ * @file_priv: DRM file private data
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv)
+{
+	struct drm_qda_remote_attach *args = data;
+	struct qda_session *session = file_priv->driver_priv;
+	int pd;
+
+	if (args->pad)
+		return -EINVAL;
+
+	switch (args->pd_type) {
+	case QDA_ATTACH_ROOT_PD:
+		pd = QDA_ROOT_PD;
+		break;
+	case QDA_ATTACH_SENSORS_PD:
+		pd = QDA_SENSORS_PD;

drivers/accel/qda/qda_ioctl.c int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session) · line 211

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (READ_ONCE(session->init_mem_gem_obj))
+		return -EBUSY;
+
+	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_ATTACH, &pd, session);
+}

[Severity: High]
Can a concurrent call to qda_ioctl_init_create() race with qda_ioctl_attach() on the same file descriptor and corrupt session->pd?

As noted in the struct qda_session definition for init_mem_gem_obj, ioctls on a single file can run concurrently. In qda_ioctl_attach(), session->init_mem_gem_obj is checked without holding a lock before calling qda_fastrpc_invoke(), which sleeps in qda_fastrpc_invoke_common() while waiting for the DSP response.

Meanwhile, qda_ioctl_init_create() -> qda_session_init_create() -> qda_fastrpc_invoke() only populates session->init_mem_gem_obj via qda_session_set_init_mem() after its own DSP invocation completes:

	if (type == QDA_FASTRPC_RMID_INIT_CREATE) {
		qda_session_set_init_mem(session, ctx->init_mem_gem_obj);
		WRITE_ONCE(session->pd, QDA_USER_PD);
	} else if (type == QDA_FASTRPC_RMID_INIT_ATTACH) {
		WRITE_ONCE(session->pd, ctx->pd);
	}

If thread 1 runs qda_ioctl_init_create() and thread 2 runs qda_ioctl_attach() before thread 1's RPC finishes, thread 2 sees session->init_mem_gem_obj as null and proceeds. If thread 1 finishes first and sets session->init_mem_gem_obj and session->pd, and thread 2 finishes second, thread 2 will overwrite session->pd with the attached root or sensors domain (or conversely, qda_ioctl_init_create() can be called on an already-attached session).

When the file is later closed, qda_postclose() checks session->init_mem_gem_obj and sends the process release request to session->pd:

drivers/accel/qda/qda_drv.c:qda_postclose() {
	if (READ_ONCE(session->init_mem_gem_obj) && drm_dev_enter(dev, &idx)) {
		qda_release_dsp_process(session->qda_dev, session);
		drm_dev_exit(idx);
	}

	qda_session_put(session);
}

Because qda_fastrpc_prepare_args() sets ctx->pd from session->pd, wouldn't qda_release_dsp_process() send the release message to the wrong protection domain, leaving the user process running on the DSP while qda_session_put() frees session->init_mem_gem_obj?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=20
#50 lore raw

PATCH v4 21/25 accel/qda: Add Audio protection domain attach and its remote heap

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Attaching to the Audio PD differs from Root and Sensors PD attach in two
ways: it names a static PD by a string rather than a fixed protection domain value, and the DSP expects a region of memory reserved for it in the device tree to be handed over the first time any session attaches to it.

Parse the "qcom,vmids" property into a VM permission table and the reserved-memory region into a physical range at probe time, and use qcom_scm_assign_mem() to grant the DSP's VMs access to that range. Only the first successful attach on a device sends the region to the DSP; later attaches, including after a daemon restart, reuse it. The region is handed back to HLOS when the rpmsg channel goes away.

The region is optional; without it an Audio PD attach fails.

---
 drivers/accel/qda/Kconfig       |  1 +
 drivers/accel/qda/qda_drv.h     | 19 +++++++++
 drivers/accel/qda/qda_fastrpc.c | 54 ++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h | 30 ++++++++++++++
 drivers/accel/qda/qda_ioctl.c   | 72 ++++++++++++++++++++++++++------
 drivers/accel/qda/qda_rpmsg.c   | 92 ++++++++++++++++++++++++++++++++++++++++-
 include/uapi/drm/qda_accel.h    | 15 +++++--
 7 files changed, 267 insertions(+), 16 deletions(-)
drivers/accel/qda/Kconfig +1 -0
index c440191a54ff..72b5ee6d58ec 100644
@@ -9,6 +9,7 @@ config DRM_ACCEL_QDA
 	depends on ARCH_QCOM || COMPILE_TEST
 	depends on RPMSG
 	select AUXILIARY_BUS
+	select QCOM_SCM
 	help
 	  Enables the DRM-based accelerator driver for Qualcomm's Hexagon DSPs.
 	  This driver provides a standardized interface for offloading computational
drivers/accel/qda/qda_drv.h +19 -0
index 2f98fd83fdda..1d083ea9fdd7 100644
@@ -9,6 +9,7 @@
 #include <linux/atomic.h>
 #include <linux/auxiliary_bus.h>
 #include <linux/device.h>
+#include <linux/firmware/qcom/qcom_scm.h>
 #include <linux/kref.h>
 #include <linux/list.h>
 #include <linux/mutex.h>
@@ -31,6 +32,9 @@
  */
 #define QDA_LEGACY_AUXDEV_NAME "fastrpc"
 
+/* Upper bound on the "qcom,vmids" DT property */
+#define QDA_MAX_VMIDS	16
+
 /**
  * struct qda_session - One user of a DSP
  *
@@ -118,6 +122,21 @@ struct qda_dev {
 	 * to, or NULL if it could not be created
 	 */
 	struct auxiliary_device *legacy_auxdev;
+	/** @vmperms: VMs the remote heap is shared with, from qcom,vmids */
+	struct qcom_scm_vmperm vmperms[QDA_MAX_VMIDS];
+	/** @vmcount: Number of valid entries in @vmperms */
+	int vmcount;
+	/** @remote_heap_addr: Physical base of the reserved region, or 0 */
+	phys_addr_t remote_heap_addr;
+	/** @remote_heap_size: Size of the reserved region, or 0 */
+	u64 remote_heap_size;
+	/**
+	 * @remote_heap_lock: Held across an Audio PD attach, so the heap is
+	 * handed to the DSP by exactly one of them. Protects @audio_init_mem.
+	 */
+	struct mutex remote_heap_lock;
+	/** @audio_init_mem: True once the DSP has been given the remote heap */
+	bool audio_init_mem;
 };
 
 /**
drivers/accel/qda/qda_fastrpc.c +54 -0

66 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +30 -0
index 29e1c3946d60..de04cadcaeb0 100644
@@ -50,6 +50,7 @@ struct qda_session;
 #define QDA_FASTRPC_RMID_INIT_MUNMAP		5
 #define QDA_FASTRPC_RMID_INIT_CREATE		6
 #define QDA_FASTRPC_RMID_INIT_CREATE_ATTR	7
+#define QDA_FASTRPC_RMID_INIT_CREATE_STATIC	8
 #define QDA_FASTRPC_RMID_INIT_MEM_MAP		10
 #define QDA_FASTRPC_RMID_INIT_MEM_UNMAP		11
 #define QDA_FASTRPC_RMID_INVOKE_DYNAMIC		0xFFFFFFFFU
@@ -65,6 +66,9 @@ struct qda_session;
 /* Largest ELF image a process may be created from */
 #define QDA_FASTRPC_INIT_FILELEN_MAX	(4 * 1024 * 1024)
 
+/* Longest static PD name, including the NUL */
+#define QDA_FASTRPC_INIT_NAMELEN_MAX	128
+
 /* Idle invoke header buffers a session keeps for reuse */
 #define QDA_MSG_BUF_POOL_MAX		8
 
@@ -155,6 +159,20 @@ struct qda_fastrpc_create_process_inbuf {
 	u32 siglen;
 };
 
+/**
+ * struct qda_fastrpc_create_static_inbuf - Input buffer for attaching to a static PD
+ *
+ * Layout the DSP expects; do not reorder.
+ */
+struct qda_fastrpc_create_static_inbuf {
+	/** @remote_session_id: Session the attach belongs to */
+	int remote_session_id;
+	/** @namelen: Length of the static PD name including the NUL */
+	u32 namelen;
+	/** @pageslen: Number of remote heap descriptors that follow the name (0 or 1) */
+	u32 pageslen;
+};
+
 /**
  * struct qda_fastrpc_map_req_msg - Request to map a whole buffer
  *
@@ -379,6 +397,18 @@ struct qda_mem_map_args {
 	struct drm_gem_object *obj;
 };
 
+/**
+ * struct qda_static_pd_args - Kernel-side arguments for attaching to a static PD
+ */
+struct qda_static_pd_args {
+	/** @name: Kernel copy of the NUL-terminated PD name */
+	const char *name;
+	/** @namelen: Length of @name including the NUL */
+	u32 namelen;
+	/** @heap: Remote heap to hand the DSP, or zero size if it already has it */
+	struct qda_phy_page heap;
+};
+
 struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev);
 void qda_fastrpc_context_free(struct kref *ref);
 void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx);
drivers/accel/qda/qda_ioctl.c +60 -12

97 lines not on this page · raw message

drivers/accel/qda/qda_rpmsg.c +91 -1

126 lines not on this page · raw message

include/uapi/drm/qda_accel.h +12 -3
index 59ad5b8cc997..7c439058876e 100644
@@ -210,22 +210,31 @@ struct drm_qda_mem_unmap {
 /* Protection domains for drm_qda_remote_attach.pd_type */
 #define QDA_ATTACH_ROOT_PD	1
 #define QDA_ATTACH_SENSORS_PD	2
+#define QDA_ATTACH_AUDIO_PD	3
 
 /**
  * struct drm_qda_remote_attach - Parameters for attaching to a DSP protection domain
- * @pd_type: QDA_ATTACH_ROOT_PD or QDA_ATTACH_SENSORS_PD
- * @pad: Must be zero
+ * @pd_type: QDA_ATTACH_ROOT_PD, QDA_ATTACH_SENSORS_PD or QDA_ATTACH_AUDIO_PD
+ * @namelen: Length of the name in bytes including the NUL terminator
+ *           (QDA_ATTACH_AUDIO_PD only, otherwise must be zero)
+ * @name: User pointer to the NUL-terminated name of the static PD to attach
+ *        to (QDA_ATTACH_AUDIO_PD only, otherwise must be zero)
  *
  * Binds this file to a protection domain that already exists on the DSP,
  * instead of creating one with DRM_IOCTL_QDA_REMOTE_SESSION_CREATE. Every
  * later request on the file is directed at it. Fails with EBUSY if a process
  * has already been created on the file.
  *
+ * For QDA_ATTACH_AUDIO_PD, the first successful attach on a device also
+ * hands the DSP the remote heap reserved for it in the device tree; later
+ * attaches reuse that heap.
+ *
  * Requires CAP_SYS_ADMIN.
  */
 struct drm_qda_remote_attach {
 	__u32 pd_type;
-	__u32 pad;
+	__u32 namelen;
+	__u64 name;
 };
 
 #if defined(__cplusplus)
Signature · 1 line
2.34.1
#51 lore raw

Re: [PATCH v4 21/25] accel/qda: Add Audio protection domain attach and its remote heap

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider: - [High] accel/qda: race between qda_attach_audio_pd() and qda_rpmsg_remove() - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] accel/qda: race between qda_attach_audio_pd() and qda_rpmsg_remove()
- [High] accel/qda: unallocated CMA memory hyp-assigned in qda_init_remote_heap()
- [Medium] accel/qda: signal in qda_attach_audio_pd() desyncs audio_init_mem state
- [Medium] accel/qda: 32-bit BIT() overflow and unvalidated VMIDs in remote heap setup
- [Low] accel/qda: qda_attach_audio_pd() mixes goto cleanup with scoped_guard()
- [Low] accel/qda: missing mutex_destroy() for remote_heap_lock on device teardown --

commit cd6c4ef6e7fc95f52a56e49a2e18e2d259cf294c
Author: Ekansh Gupta <[email protected]>

accel/qda: Add Audio protection domain attach and its remote heap

This commit adds support for attaching to the static Audio protection domain by name via QDA_ATTACH_AUDIO_PD and hands the DSP its reserved remote heap memory region on the first attach. At probe time, it parses the qcom,vmids property and reserved-memory region and uses qcom_scm_assign_mem() to share the heap with the DSP VMs until the rpmsg device is removed.

Quoted 4 lines
drivers/accel/qda/qda_ioctl.c +0 -0
index 9b8cccfea1e3a..41234fc576a9a 100644

[ ... ]

Quoted 35 lines
drivers/accel/qda/qda_ioctl.c +31 -0
@@ -176,6 +177,54 @@ int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session)
 	return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_RELEASE, NULL, session);
 }
 
+/*
+ * The Audio PD keeps the remote heap across daemon restarts, so only the first
+ * attach on a device hands it to the DSP. The lock is held across the attach
+ * so that a concurrent one cannot skip the heap while the first is still in
+ * flight, and the flag is set only once the DSP has accepted it.
+ */
+static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remote_attach *args)
+{
+	struct qda_dev *qdev = session->qda_dev;
+	struct qda_static_pd_args sargs = {};
+	char *name;
+	int err;
+
+	if (strcmp(qdev->dsp_name, "adsp") || !qdev->remote_heap_size)
+		return -ENODEV;
+
+	if (!args->namelen || args->namelen > QDA_FASTRPC_INIT_NAMELEN_MAX)
+		return -EINVAL;
+
+	name = memdup_user(u64_to_user_ptr(args->name), args->namelen);
+	if (IS_ERR(name))
+		return PTR_ERR(name);
+
+	if (name[args->namelen - 1]) {
+		err = -EINVAL;
+		goto out_free;
+	}
+
+	sargs.name = name;
+	sargs.namelen = args->namelen;
+

drivers/accel/qda/qda_ioctl.c int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session) · line 220

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+	scoped_guard(mutex, &qdev->remote_heap_lock) {
+		if (!qdev->audio_init_mem) {
+			sargs.heap.addr = qdev->remote_heap_addr;
+			sargs.heap.size = qdev->remote_heap_size;
+		}
+
+		err = qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE_STATIC, &sargs, session);
+		if (!err && sargs.heap.size)
+			qdev->audio_init_mem = true;
+	}

[Severity: Medium]
What happens if the calling task receives a signal while waiting for the DSP response in qda_attach_audio_pd()?

The call chain:

qda_attach_audio_pd()
qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE_STATIC, &sargs, session)
qda_fastrpc_invoke_common()
qda_rpmsg_send_msg()
qda_rpmsg_wait_for_rsp()
wait_for_completion_interruptible(&ctx->work)

waits interruptibly after the QDA_FASTRPC_RMID_INIT_CREATE_STATIC message carrying sargs.heap has already been transmitted to the DSP.

If a signal arrives before the DSP replies, qda_fastrpc_invoke() returns -ERESTARTSYS while the DSP continues processing the remote heap initialization. Because err is non-zero, qdev->audio_init_mem remains false and qdev->remote_heap_lock is released, allowing a restarted syscall or a concurrent caller to observe !qdev->audio_init_mem and hand the remote heap to the DSP a second time while the first attach is still in flight or after the DSP has already accepted it.

drivers/accel/qda/qda_ioctl.c int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session) · line 226

Quoted 6 lines
drivers/accel/qda/qda_ioctl.c +6 -0
+
+out_free:
+	kfree(name);
+
+	return err;
+}

[Severity: Low]
This isn't a bug, but qda_attach_audio_pd() mixes goto out_free error handling for name with scoped_guard(mutex, &qdev->remote_heap_lock) in the same function.

Could name be declared with __free(kfree) so that the NUL-termination check can return -EINVAL directly without mixing goto cleanup and scope-based cleanup helpers?

Quoted 4 lines
drivers/accel/qda/qda_rpmsg.c +0 -0
index 7e58df3eb2e17..b93e906b57930 100644

[ ... ]

Quoted 47 lines
drivers/accel/qda/qda_rpmsg.c +43 -0
@@ -145,6 +147,81 @@ static int qda_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len,
 	return ret;
 }
 
+static int qda_init_vmperms(struct qda_dev *qdev)
+{
+	u32 vmids[QDA_MAX_VMIDS];
+	int vmcount, i;
+
+	vmcount = of_property_read_variable_u32_array(qdev->dev->of_node, "qcom,vmids",
+						      vmids, 0, QDA_MAX_VMIDS);
+	if (vmcount == -EINVAL || vmcount == -ENODATA)
+		return 0;
+	if (vmcount < 0)
+		return vmcount;
+
+	if (!qcom_scm_is_available())
+		return -EPROBE_DEFER;
+
+	for (i = 0; i < vmcount; i++) {
+		qdev->vmperms[i].vmid = vmids[i];
+		qdev->vmperms[i].perm = QCOM_SCM_PERM_RWX;
+	}
+	qdev->vmcount = vmcount;
+
+	return 0;
+}
+
+/*
+ * The region is optional: without it an Audio PD attach is refused. It is
+ * recorded only once it has been assigned, so teardown hands back exactly
+ * what was handed over.
+ */
+static int qda_init_remote_heap(struct qda_dev *qdev)
+{
+	u64 src_perms = BIT(QCOM_SCM_VMID_HLOS);
+	struct resource res;
+	int ret;
+
+	if (strcmp(qdev->dsp_name, "adsp") && strcmp(qdev->dsp_name, "sdsp"))
+		return 0;
+
+	if (of_reserved_mem_region_to_resource(qdev->dev->of_node, 0, &res))
+		return 0;
+
+	if (qdev->vmcount) {
+		ret = qcom_scm_assign_mem(res.start, resource_size(&res), &src_perms,

drivers/accel/qda/qda_rpmsg.c static int qda_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len, · line 202

Quoted 10 lines
drivers/accel/qda/qda_rpmsg.c +10 -0
+					  qdev->vmperms, qdev->vmcount);
+		if (ret)
+			return ret;
+	}
+
+	qdev->remote_heap_addr = res.start;
+	qdev->remote_heap_size = resource_size(&res);
+
+	return 0;
+}

[Severity: High]
Can qda_init_remote_heap() hyp-assign memory away from HLOS without first allocating it from CMA?

In the Qualcomm FastRPC device tree bindings and upstream SoC device trees, the ADSP remote heap memory-region (adsp_rpc_remote_heap_mem) is defined as a reusable shared-dma-pool without no-map. For a reusable CMA region, those pages remain in the Linux page allocator for movable HLOS allocations until allocated via CMA or DMA APIs.

Taking the raw physical range from of_reserved_mem_region_to_resource() in qda_init_remote_heap() and revoking QCOM_SCM_VMID_HLOS access via qcom_scm_assign_mem() without allocating the pages first leaves active or future movable HLOS pages inside that range, which will trigger stage-2 permission faults when accessed by the CPU and corrupt memory when used by the ADSP.

Quoted 5 lines
drivers/accel/qda/qda_rpmsg.c +5 -0
+
+static void qda_deinit_remote_heap(struct qda_dev *qdev)
+{
+	struct qcom_scm_vmperm dst_perms = {
+		.vmid = QCOM_SCM_VMID_HLOS,

drivers/accel/qda/qda_rpmsg.c static int qda_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len, · line 217

Quoted 10 lines
drivers/accel/qda/qda_rpmsg.c +10 -0
+		.perm = QCOM_SCM_PERM_RWX,
+	};
+	u64 src_perms = 0;
+	int ret, i;
+
+	if (!qdev->remote_heap_size || !qdev->vmcount)
+		return;
+
+	for (i = 0; i < qdev->vmcount; i++)
+		src_perms |= BIT(qdev->vmperms[i].vmid);

[Severity: Medium]
Could BIT(qdev->vmperms[i].vmid) overflow a 32-bit unsigned long or shift past 63?

In qda_deinit_remote_heap() (and in qda_init_remote_heap() when initializing src_perms), BIT() uses 1UL << nr, whereas src_perms is a u64 and ADSP remote heap VMIDs such as QCOM_SCM_VMID_ADSP_HEAP (0x25) or QCOM_SCM_VMID_CP_ADSP_SHARED (0x33) exceed 31. On 32-bit builds (allowed via ARCH_QCOM || COMPILE_TEST), shifting 1UL by 37 or 51 is undefined behavior and fails to populate the upper 32 bits of src_perms.

In addition, qda_init_vmperms() copies u32 values from the qcom,vmids property without checking that each vmid is less than BITS_PER_TYPE(u64). Should qda_init_remote_heap() and qda_deinit_remote_heap() use BIT_ULL(), and should qda_init_vmperms() reject VMIDs >= BITS_PER_TYPE(u64)?

Quoted 11 lines
drivers/accel/qda/qda_rpmsg.c +7 -0
+
+	ret = qcom_scm_assign_mem(qdev->remote_heap_addr, qdev->remote_heap_size,
+				  &src_perms, &dst_perms, 1);
+	if (ret)
+		dev_err(qdev->dev, "Failed to assign remote heap back to HLOS: %d\n", ret);
+}
+
 static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 {
 	struct qda_dev *qdev = dev_get_drvdata(&rpdev->dev);

drivers/accel/qda/qda_rpmsg.c static void qda_rpmsg_remove(struct rpmsg_device *rpdev) · line 244

Quoted 5 lines
drivers/accel/qda/qda_rpmsg.c +1 -0
@@ -163,6 +240,7 @@ static void qda_rpmsg_remove(struct rpmsg_device *rpdev)
 	qda_fastrpc_flush_pending(qdev);
 	qda_cb_unpopulate(qdev);
 	qda_deinit_device(qdev);
+	qda_deinit_remote_heap(qdev);
 }

[Severity: High]
Can qda_attach_audio_pd() race with qda_rpmsg_remove() and access destroyed device state after teardown?

If thread A is inside qda_attach_audio_pd() holding qdev->remote_heap_lock while waiting for the DSP response, a concurrent thread B calling qda_ioctl_attach() -> qda_attach_audio_pd() can already be blocked waiting to acquire qdev->remote_heap_lock before allocating an invoke context in qdev->ctx_xa.

When qda_rpmsg_remove() runs:

qda_rpmsg_remove()

    drm_dev_unplug(&qdev->drm_dev)
    qdev->rpdev = NULL
    qda_fastrpc_flush_pending(qdev)
    qda_cb_unpopulate(qdev)
    qda_deinit_device(qdev)
    qda_deinit_remote_heap(qdev)

drm_dev_unplug() does not wait for threads already inside drm_ioctl(), and qda_fastrpc_flush_pending() only wakes thread A. While thread A releases qdev->remote_heap_lock and thread B acquires it, qda_rpmsg_remove() proceeds to destroy the context bank platform devices in qda_cb_unpopulate() and free qdev->iommu_mgr and destroy qdev->ctx_xa in qda_deinit_device().

Thread B then calls qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE_STATIC, ...) without checking whether the device was unplugged, executing qda_fastrpc_context_alloc() on the destroyed qdev->ctx_xa and qda_fastrpc_prepare_args_create_static() -> alloc_kernel_gem() -> qda_gem_create_object() -> qda_memory_manager_alloc() on the destroyed iommu_mgr and freed context bank device before reaching the drm_dev_enter() check in qda_rpmsg_send_msg().

drivers/accel/qda/qda_rpmsg.c static int qda_rpmsg_probe(struct rpmsg_device *rpdev) · line 276

Quoted 6 lines
drivers/accel/qda/qda_rpmsg.c +1 -1
@@ -195,10 +273,20 @@ static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
 		return ret;
 	}
 
-	ret = qda_init_device(qdev, qda_count_cb_nodes(rpdev->dev.of_node));
+	mutex_init(&qdev->remote_heap_lock);

[Severity: Low]
Unlike qdev->import_lock, which is initialized in qda_alloc_device() and destroyed in qda_deinit_device(), qdev->remote_heap_lock is initialized here in qda_rpmsg_probe() and never passed to mutex_destroy() on probe failure or in qda_deinit_device(). Should remote_heap_lock be initialized and destroyed alongside import_lock?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=21
maintainer
#52 lore raw

Re: [PATCH v4 21/25] accel/qda: Add Audio protection domain attach and its remote heap

In reply to Ekansh Gupta

include/uapi/drm/qda_accel.h struct drm_qda_mem_unmap { · line 238

Quoted 7 lines · Ekansh Gupta

struct drm_qda_remote_attach {

include/uapi/drm/qda_accel.h +2 -1
 	__u32 pd_type;
-	__u32 pad;
+	__u32 namelen;
+	__u64 name;
 };

This is an ABI change in the middle of the series, which isn't great even though it is harmless as there are no users that would see the initial ABI.

I'd suggest defining the structure in the final length from the start.

Arnd

#53 lore raw

Re: [PATCH v4 21/25] accel/qda: Add Audio protection domain attach and its remote heap

In reply to Ekansh Gupta
Quoted 5 lines · Ekansh Gupta
drivers/accel/qda/qda_ioctl.c +4 -0
+/*
+ * The Audio PD keeps the remote heap across daemon restarts, so only the first
+ * attach on a device hands it to the DSP. The lock is held across the attach
+ * so that a concurrent one cannot skip the heap while the first is still in

drivers/accel/qda/qda_ioctl.c int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session) · line 193

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +10 -0
+ * flight, and the flag is set only once the DSP has accepted it.
+ */
+static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remote_attach *args)
+{
+	struct qda_dev *qdev = session->qda_dev;
+	struct qda_static_pd_args sargs = {};
+	char *name;
+	int err;
+
+	if (strcmp(qdev->dsp_name, "adsp") || !qdev->remote_heap_size)

That's value from DTB, so where did you document the ABI?

I see such undocumented ABI in multiple places. Usually requirement would be to document it, but not label. Why label 'ADSP' is not correct? Why "Audio DSP" would not be correct?

Drop all this label dependency, completely (and do not introduce other names, like node name).

Best regards,
Krzysztof

#54 lore raw

PATCH v4 22/25 accel/qda: Add remote session attach to the legacy shim

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Wire FASTRPC_IOCTL_INIT_ATTACH, FASTRPC_IOCTL_INIT_ATTACH_SNS and FASTRPC_IOCTL_INIT_CREATE_STATIC into the legacy FastRPC compatibility shim, so existing user space using the /dev/fastrpc-* ioctl interface can attach to the Root, Sensors and Audio protection domains.

Root and Sensors PD attach carry no payload. INIT_CREATE_STATIC forwards its name and length; memlen is ignored, since the Audio PD remote heap size always comes from the device tree rather than from user space.

Move the attach logic out of the ioctl handler into a session-level function that both interfaces call. It checks CAP_SYS_ADMIN itself, so the legacy path is subject to the same privilege requirement as the native one.

---
 drivers/accel/qda/qda_fastrpc_shim.c | 24 ++++++++++++++++
 drivers/accel/qda/qda_ioctl.c        | 54 +++++++++++++++++++++++++-----------
 drivers/accel/qda/qda_ioctl.h        |  2 ++
 drivers/accel/qda/qda_legacy_ops.c   |  1 +
 drivers/accel/qda/qda_legacy_ops.h   |  3 ++
 5 files changed, 68 insertions(+), 16 deletions(-)
drivers/accel/qda/qda_fastrpc_shim.c +24 -0
index c46d2de4f581..c1464b1adbc2 100644
@@ -18,6 +18,7 @@
 #include <linux/uaccess.h>
 #include <drm/drm_drv.h>
 #include <drm/drm_gem.h>
+#include <drm/qda_accel.h>
 #include <uapi/misc/fastrpc.h>
 #include "qda_drv.h"
 #include "qda_fastrpc.h"
@@ -295,6 +296,23 @@ static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp)
 	return err;
 }
 
+static long qda_shim_attach(struct qda_shim_file *sf, u32 pd_type)
+{
+	return qda_legacy_ops_table.attach(sf->session, pd_type, NULL, 0);
+}
+
+/* memlen is ignored: the Audio PD heap size comes from the device tree */
+static long qda_shim_init_create_static(struct qda_shim_file *sf, void __user *argp)
+{
+	struct fastrpc_init_create_static args;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	return qda_legacy_ops_table.attach(sf->session, QDA_ATTACH_AUDIO_PD,
+					   u64_to_user_ptr(args.name), args.namelen);
+}
+
 static long qda_shim_mmap(struct qda_shim_file *sf, void __user *argp)
 {
 	struct qda_shim_mapping *m;
@@ -516,6 +534,12 @@ static long qda_shim_dispatch(struct qda_shim_file *sf, unsigned int cmd, void _
 		return qda_shim_alloc_dma_buf(sf, argp);
 	case FASTRPC_IOCTL_INIT_CREATE:
 		return qda_shim_init_create(sf, argp);
+	case FASTRPC_IOCTL_INIT_ATTACH:
+		return qda_shim_attach(sf, QDA_ATTACH_ROOT_PD);
+	case FASTRPC_IOCTL_INIT_ATTACH_SNS:
+		return qda_shim_attach(sf, QDA_ATTACH_SENSORS_PD);
+	case FASTRPC_IOCTL_INIT_CREATE_STATIC:
+		return qda_shim_init_create_static(sf, argp);
 	case FASTRPC_IOCTL_MMAP:
 		return qda_shim_mmap(sf, argp);
 	case FASTRPC_IOCTL_MUNMAP:
drivers/accel/qda/qda_ioctl.c +38 -16

103 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +2 -0
index f6f5d64e36ce..94adba549989 100644
@@ -21,6 +21,8 @@ int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_pri
 int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
+int qda_session_attach(struct qda_session *session, u32 pd_type, const char __user *name,
+		       u32 namelen);
 int qda_session_mem_map(struct qda_session *session, struct qda_mem_map_args *args);
 int qda_session_mem_unmap(struct qda_session *session, struct drm_qda_mem_unmap *unmap);
 
drivers/accel/qda/qda_legacy_ops.c +1 -0
index ea815ab0c52a..fc5e94c0c9bc 100644
@@ -192,6 +192,7 @@ const struct qda_legacy_ops qda_legacy_ops_table = {
 	.session_create = legacy_session_create,
 	.session_destroy = legacy_session_destroy,
 	.init_create = legacy_init_create,
+	.attach = qda_session_attach,
 	.release = legacy_release,
 	.mmap = legacy_mmap,
 	.mem_map = legacy_mem_map,
drivers/accel/qda/qda_legacy_ops.h +3 -0
index e3126ac10507..95a169edc594 100644
@@ -33,6 +33,9 @@ struct qda_legacy_ops {
 	/** @init_create: Create a DSP process from an imported ELF image */
 	int (*init_create)(struct qda_session *session, struct drm_gem_object *elf_obj,
 			   u32 filelen, u32 attrs, u32 siglen);
+	/** @attach: Attach the session to a DSP-resident protection domain */
+	int (*attach)(struct qda_session *session, u32 pd_type, const char __user *name,
+		      u32 namelen);
 	/** @release: Release the session's DSP process, if it has one */
 	int (*release)(struct qda_session *session);
 	/** @mmap: Map a buffer into the DSP's address space */
Signature · 1 line
2.34.1
#55 lore raw

Re: [PATCH v4 22/25] accel/qda: Add remote session attach to the legacy shim

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] accel/qda: race between qda_session_attach() and qda_session_init_create() - [High] accel/qda…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] accel/qda: race between qda_session_attach() and qda_session_init_create()
- [High] accel/qda: use-after-free in qda_session_attach() during rpmsg removal
- [Medium] accel/qda: interrupted wait in qda_attach_audio_pd() resends remote heap --

commit ae4f1060dc81ff84d55ddf1c5fd82d79333077f8
Author: Ekansh Gupta <[email protected]>

accel/qda: Add remote session attach to the legacy shim

This commit wires FASTRPC_IOCTL_INIT_ATTACH, FASTRPC_IOCTL_INIT_ATTACH_SNS, and FASTRPC_IOCTL_INIT_CREATE_STATIC into the legacy FastRPC compatibility shim by extracting qda_session_attach() and exposing it via qda_legacy_ops_table.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc_shim.c +0 -0
index c46d2de4f5818..c1464b1adbc2c 100644

[ ... ]

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +6 -0
@@ -295,6 +296,23 @@ static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp)
 	return err;
 }
 
+static long qda_shim_attach(struct qda_shim_file *sf, u32 pd_type)
+{
+	return qda_legacy_ops_table.attach(sf->session, pd_type, NULL, 0);
+}
+
+/* memlen is ignored: the Audio PD heap size comes from the device tree */

drivers/accel/qda/qda_fastrpc_shim.c static long qda_shim_init_create(struct qda_shim_file *sf, void __user *argp) · line 314

Quoted 10 lines
drivers/accel/qda/qda_fastrpc_shim.c +10 -0
+static long qda_shim_init_create_static(struct qda_shim_file *sf, void __user *argp)
+{
+	struct fastrpc_init_create_static args;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	return qda_legacy_ops_table.attach(sf->session, QDA_ATTACH_AUDIO_PD,
+					   u64_to_user_ptr(args.name), args.namelen);
+}

[Severity: High]
Can qda_shim_attach() and qda_shim_init_create_static() (as well as qda_ioctl_attach()) trigger a use-after-free if the underlying rpmsg device is removed while an attach ioctl is in flight?

Both qda_shim_ioctl() and drm_ioctl() only perform a point-in-time drm_dev_is_unplugged() check on entry without entering a drm_dev_enter() critical section:

drivers/accel/qda/qda_fastrpc_shim.c:qda_shim_ioctl() {
	if (drm_dev_is_unplugged(&sf->session->qda_dev->drm_dev))
		return -ENODEV;

	return qda_shim_dispatch(sf, cmd, (void __user *)arg);
}

If qda_rpmsg_remove() runs after that check passes (for example while qda_shim_init_create_static() or qda_attach_audio_pd() sleeps in copy_from_user() or memdup_user()), drm_dev_unplug() does not wait for the in-flight qda_session_attach() call before tearing down the device:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_remove() {
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_fastrpc_flush_pending(qdev);
	qda_cb_unpopulate(qdev);
	qda_deinit_device(qdev);
	qda_deinit_remote_heap(qdev);
}

qda_deinit_device() destroys qdev->iommu_mgr->assignment_lock and qdev->ctx_xa, and devres teardown on &rpdev->dev frees qdev->iommu_mgr (allocated with devm_kzalloc(qdev->dev, ...) in qda_init_device()). When the in-flight qda_session_attach() proceeds into:

qda_fastrpc_invoke()

    -> qda_fastrpc_context_alloc()
    -> qda_fastrpc_prepare_args()
      -> alloc_kernel_gem()
        -> qda_gem_create_object()
          -> qda_memory_manager_alloc()
            -> qda_memory_manager_assign_device()

wouldn't it access the destroyed qdev->ctx_xa and dereference the freed qdev->iommu_mgr (or have its newly allocated ctx freed early if qda_fastrpc_flush_pending() runs between qda_fastrpc_context_alloc() and qda_rpmsg_send_msg())?

Quoted 28 lines
drivers/accel/qda/qda_ioctl.c +4 -4
index 41234fc576a9a..aaf3e5e6b5b18 100644
@@ -183,7 +183,7 @@ int qda_release_dsp_process(struct qda_dev *qdev, struct qda_session *session)
  * so that a concurrent one cannot skip the heap while the first is still in
  * flight, and the flag is set only once the DSP has accepted it.
  */
-static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remote_attach *args)
+static int qda_attach_audio_pd(struct qda_session *session, const char __user *uname, u32 namelen)
 {
 	struct qda_dev *qdev = session->qda_dev;
 	struct qda_static_pd_args sargs = {};
@@ -193,20 +193,20 @@ static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remot
 	if (strcmp(qdev->dsp_name, "adsp") || !qdev->remote_heap_size)
 		return -ENODEV;
 
-	if (!args->namelen || args->namelen > QDA_FASTRPC_INIT_NAMELEN_MAX)
+	if (!namelen || namelen > QDA_FASTRPC_INIT_NAMELEN_MAX)
 		return -EINVAL;
 
-	name = memdup_user(u64_to_user_ptr(args->name), args->namelen);
+	name = memdup_user(uname, namelen);
 	if (IS_ERR(name))
 		return PTR_ERR(name);
 
-	if (name[args->namelen - 1]) {
+	if (name[namelen - 1]) {

drivers/accel/qda/qda_ioctl.c static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remot · line 212

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +1 -1
 		err = -EINVAL;
 		goto out_free;
 	}
 
 	sargs.name = name;
-	sargs.namelen = args->namelen;
+	sargs.namelen = namelen;
 
 	scoped_guard(mutex, &qdev->remote_heap_lock) {
 		if (!qdev->audio_init_mem) {

[Severity: Medium]
What happens in qda_attach_audio_pd() if qda_fastrpc_invoke() is interrupted by a signal after sending the request to the DSP?

Inside the qdev->remote_heap_lock guard:

	scoped_guard(mutex, &qdev->remote_heap_lock) {
		if (!qdev->audio_init_mem) {
			sargs.heap.addr = qdev->remote_heap_addr;
			sargs.heap.size = qdev->remote_heap_size;
		}

		err = qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_CREATE_STATIC, &sargs, session);
		if (!err && sargs.heap.size)
			qdev->audio_init_mem = true;
	}

qda_fastrpc_invoke_common() calls qda_rpmsg_send_msg() and then blocks in qda_rpmsg_wait_for_rsp() via wait_for_completion_interruptible(&ctx->work). If a signal interrupts the wait before the DSP replies, qda_fastrpc_invoke() returns -ERESTARTSYS while the first QDA_FASTRPC_RMID_INIT_CREATE_STATIC request is still in flight on the DSP.

Because err is non-zero, qdev->audio_init_mem remains false when qdev->remote_heap_lock is released. Could a concurrent or restarted FASTRPC_IOCTL_INIT_CREATE_STATIC / DRM_IOCTL_QDA_REMOTE_SESSION_ATTACH call then observe !qdev->audio_init_mem and hand the remote heap to the DSP a second time?

Quoted 42 lines
drivers/accel/qda/qda_ioctl.c +15 -10
@@ -226,38 +226,60 @@ static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remot
 }
 
 /**
- * qda_ioctl_attach() - Attach to a DSP-resident protection domain
- * @dev: DRM device structure
- * @data: User-space data (struct drm_qda_remote_attach)
- * @file_priv: DRM file private data
+ * qda_session_attach() - Attach a session to a DSP-resident protection domain
+ * @session: Session to attach
+ * @pd_type: QDA_ATTACH_ROOT_PD, QDA_ATTACH_SENSORS_PD or QDA_ATTACH_AUDIO_PD
+ * @name: User pointer to the static PD name (QDA_ATTACH_AUDIO_PD only)
+ * @namelen: Length of @name including the NUL (QDA_ATTACH_AUDIO_PD only)
+ *
+ * Shared by the accel UAPI and the legacy shim, so the privilege check lives
+ * here rather than on the ioctl.
  *
  * Return: 0 on success, negative error code on failure
  */
-int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv)
+int qda_session_attach(struct qda_session *session, u32 pd_type, const char __user *name,
+		       u32 namelen)
 {
-	struct drm_qda_remote_attach *args = data;
-	struct qda_session *session = file_priv->driver_priv;
 	int pd;
 
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
 	if (READ_ONCE(session->init_mem_gem_obj))
 		return -EBUSY;
 
-	switch (args->pd_type) {
+	switch (pd_type) {
 	case QDA_ATTACH_ROOT_PD:
 	case QDA_ATTACH_SENSORS_PD:
-		if (args->name || args->namelen)
+		if (name || namelen)
 			return -EINVAL;
 
-		pd = args->pd_type == QDA_ATTACH_ROOT_PD ? QDA_ROOT_PD : QDA_SENSORS_PD;

drivers/accel/qda/qda_ioctl.c static int qda_attach_audio_pd(struct qda_session *session, struct drm_qda_remot · line 265

Quoted 10 lines
drivers/accel/qda/qda_ioctl.c +2 -1
+		pd = pd_type == QDA_ATTACH_ROOT_PD ? QDA_ROOT_PD : QDA_SENSORS_PD;
 
 		return qda_fastrpc_invoke(QDA_FASTRPC_RMID_INIT_ATTACH, &pd, session);
 	case QDA_ATTACH_AUDIO_PD:
-		return qda_attach_audio_pd(session, args);
+		return qda_attach_audio_pd(session, name, namelen);
 	default:
 		return -EINVAL;
 	}
 }

[Severity: High]
Does qda_session_attach() race with concurrent qda_session_init_create() calls on the same session?

Both the accel UAPI and the legacy shim allow concurrent ioctls on the same open file without holding a per-session lock. In qda_session_attach(), the check:

	if (READ_ONCE(session->init_mem_gem_obj))
		return -EBUSY;

runs unlocked before qda_fastrpc_invoke() sleeps waiting for the DSP response, and qda_session_init_create() does not check whether the session has already attached or is attaching. State updates only happen after the DSP responds in qda_fastrpc_invoke():

	if (type == QDA_FASTRPC_RMID_INIT_CREATE) {
		qda_session_set_init_mem(session, ctx->init_mem_gem_obj);
		WRITE_ONCE(session->pd, QDA_USER_PD);
	} else if (type == QDA_FASTRPC_RMID_INIT_ATTACH ||
		   type == QDA_FASTRPC_RMID_INIT_CREATE_STATIC) {
		WRITE_ONCE(session->pd, ctx->pd);
	}

If one thread calls qda_session_attach() and another concurrently calls qda_session_init_create() on the same file descriptor, both can pass validation and send RPCs to the DSP. If QDA_FASTRPC_RMID_INIT_CREATE finishes first (setting session->init_mem_gem_obj and session->pd = QDA_USER_PD) and QDA_FASTRPC_RMID_INIT_ATTACH finishes second, session->pd is overwritten with QDA_ROOT_PD or QDA_SENSORS_PD while session->init_mem_gem_obj remains non-NULL.

When the file is later closed, legacy_release() or qda_postclose() sees session->init_mem_gem_obj is non-NULL and calls qda_release_dsp_process(), which sends QDA_FASTRPC_RMID_INIT_RELEASE directed at READ_ONCE(session->pd) (now the attached PD instead of QDA_USER_PD). Wouldn't that leave the User PD process running on the DSP while qda_session_release() frees session->init_mem_gem_obj underneath it?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=22
#56 lore raw

PATCH v4 23/25 accel/qda: Add DSP capability query

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Add a DRM_IOCTL_QDA_QUERY query type that returns a DSP capability attribute, such as async support, unsigned PD offload or VTCM size, so user space can ask the DSP what it supports without a separate ioctl.

The attributes are fetched from the DSP through its stateless utilities handle, which needs no process context, and cached on the device the first time any session queries them. Later queries from any session are served from the cache. A DSP without capability support makes the query fail with -EOPNOTSUPP.

---
 drivers/accel/qda/qda_drv.c     |  2 ++
 drivers/accel/qda/qda_drv.h     |  9 ++++++++
 drivers/accel/qda/qda_fastrpc.c | 47 +++++++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_fastrpc.h | 12 +++++++++++
 drivers/accel/qda/qda_ioctl.c   | 42 ++++++++++++++++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.h   |  2 ++
 include/uapi/drm/qda_accel.h    | 15 +++++++++++--
 7 files changed, 127 insertions(+), 2 deletions(-)
drivers/accel/qda/qda_drv.c +2 -0
index 1e72dce769b9..51d8ef10c6c1 100644
@@ -154,6 +154,7 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 
 	INIT_LIST_HEAD(&qdev->cb_devs);
 	mutex_init(&qdev->import_lock);
+	mutex_init(&qdev->attr_lock);
 	xa_init_flags(&qdev->ctx_xa, XA_FLAGS_ALLOC1);
 	atomic64_set(&qdev->ctx_seq, 0);
 	atomic_set(&qdev->session_id_seq, 0);
@@ -181,6 +182,7 @@ void qda_deinit_device(struct qda_dev *qdev)
 	qda_memory_manager_exit(qdev->iommu_mgr);
 	xa_destroy(&qdev->ctx_xa);
 	mutex_destroy(&qdev->import_lock);
+	mutex_destroy(&qdev->attr_lock);
 }
 
 int qda_register_device(struct qda_dev *qdev)
drivers/accel/qda/qda_drv.h +9 -0
index 1d083ea9fdd7..cbb580d03e0b 100644
@@ -35,6 +35,9 @@
 /* Upper bound on the "qcom,vmids" DT property */
 #define QDA_MAX_VMIDS	16
 
+/* Size of the cached DSP attribute array; index 0 is reserved */
+#define QDA_MAX_DSP_ATTRIBUTES	256
+
 /**
  * struct qda_session - One user of a DSP
  *
@@ -122,6 +125,12 @@ struct qda_dev {
 	 * to, or NULL if it could not be created
 	 */
 	struct auxiliary_device *legacy_auxdev;
+	/** @attr_lock: Serialises the DSP capability query and protects the cache below */
+	struct mutex attr_lock;
+	/** @valid_attributes: True once @dsp_attributes has been queried from the DSP */
+	bool valid_attributes;
+	/** @dsp_attributes: Cached DSP capability attributes */
+	u32 dsp_attributes[QDA_MAX_DSP_ATTRIBUTES];
 	/** @vmperms: VMs the remote heap is shared with, from qcom,vmids */
 	struct qcom_scm_vmperm vmperms[QDA_MAX_VMIDS];
 	/** @vmcount: Number of valid entries in @vmperms */
drivers/accel/qda/qda_fastrpc.c +47 -0

65 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +12 -0
index de04cadcaeb0..7d5cd04943a3 100644
@@ -55,9 +55,21 @@ struct qda_session;
 #define QDA_FASTRPC_RMID_INIT_MEM_UNMAP		11
 #define QDA_FASTRPC_RMID_INVOKE_DYNAMIC		0xFFFFFFFFU
 
+/*
+ * ctx->type tag for the DSP capability query. Not a wire method ID: the query
+ * is addressed by QDA_FASTRPC_DSP_UTILITIES_HANDLE, method 0.
+ */
+#define QDA_FASTRPC_TYPE_GET_DSP_INFO		0xFFFFFFFEU
+
 /* Handle the initialisation methods are addressed to */
 #define QDA_FASTRPC_INIT_HANDLE		0x1
 
+/* Handle of the DSP's utility calls, which include the capability query */
+#define QDA_FASTRPC_DSP_UTILITIES_HANDLE	0x2
+
+/* Status the DSP returns when it does not support the capability query */
+#define QDA_FASTRPC_DSP_UNSUPPORTED_API		0x80000414U
+
 /* Protection domains a request can be directed at */
 #define QDA_ROOT_PD			0
 #define QDA_USER_PD			1
drivers/accel/qda/qda_ioctl.c +42 -0

54 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +2 -0
index 94adba549989..329f3809c8ce 100644
@@ -23,6 +23,8 @@ int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_p
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 int qda_session_attach(struct qda_session *session, u32 pd_type, const char __user *name,
 		       u32 namelen);
+int qda_get_dsp_capability(struct qda_dev *qdev, struct qda_session *session,
+			   u32 attribute_id, u32 *capability);
 int qda_session_mem_map(struct qda_session *session, struct qda_mem_map_args *args);
 int qda_session_mem_unmap(struct qda_session *session, struct drm_qda_mem_unmap *unmap);
 
include/uapi/drm/qda_accel.h +13 -2
index 7c439058876e..07b943a3c822 100644
@@ -50,19 +50,30 @@ extern "C" {
 
 /* Query type definitions for drm_qda_query */
 #define QDA_QUERY_DSP_NAME		1
+#define QDA_QUERY_DSP_CAPABILITIES	2
 
 /**
  * struct drm_qda_query - Device information query structure
  * @query_type: Type of query (input)
  * @dsp_name: Null-terminated name of the DSP (returned when query_type is
  *            QDA_QUERY_DSP_NAME)
+ * @cap: DSP capability query (used when query_type is
+ *       QDA_QUERY_DSP_CAPABILITIES): @cap.attribute_id selects the attribute
+ *       (input) and @cap.capability returns its value (output)
  *
  * This structure is used with DRM_IOCTL_QDA_QUERY to query device attributes
- * based on @query_type.
+ * based on @query_type. @dsp_name and @cap share storage, as only one query
+ * is made per call.
  */
 struct drm_qda_query {
 	__u32 query_type;
-	__u8 dsp_name[16];
+	union {
+		__u8 dsp_name[16];
+		struct {
+			__u32 attribute_id;
+			__u32 capability;
+		} cap;
+	};
 };
 
 /**
Signature · 1 line
2.34.1
#57 lore raw

Re: [PATCH v4 23/25] accel/qda: Add DSP capability query

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] accel/qda: non-zero remote_session_id in qda_fastrpc_invoke_pack() for dsp info - [Medium]…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] accel/qda: non-zero remote_session_id in qda_fastrpc_invoke_pack() for dsp info
- [Medium] accel/qda: destroyed mutex qdev->attr_lock while held in qda_deinit_device() --

commit 8c89a0764b44acd4840198a5f29101cbf6dc706a
Author: Ekansh Gupta <[email protected]>

accel/qda: Add DSP capability query

This commit adds a QDA_QUERY_DSP_CAPABILITIES query type to DRM_IOCTL_QDA_QUERY to fetch DSP capability attributes through the DSP utilities handle and cache them on the device for subsequent queries.

Quoted 13 lines
drivers/accel/qda/qda_drv.c +1 -0
index 1e72dce769b9a..51d8ef10c6c1a 100644
@@ -154,6 +154,7 @@ struct qda_dev *qda_alloc_device(struct device *dev)
 
 	INIT_LIST_HEAD(&qdev->cb_devs);
 	mutex_init(&qdev->import_lock);
+	mutex_init(&qdev->attr_lock);
 	xa_init_flags(&qdev->ctx_xa, XA_FLAGS_ALLOC1);
 	atomic64_set(&qdev->ctx_seq, 0);
 	atomic_set(&qdev->session_id_seq, 0);

drivers/accel/qda/qda_drv.c void qda_deinit_device(struct qda_dev *qdev) · line 186

Quoted 5 lines
drivers/accel/qda/qda_drv.c +1 -0
@@ -181,6 +182,7 @@ void qda_deinit_device(struct qda_dev *qdev)
 	qda_memory_manager_exit(qdev->iommu_mgr);
 	xa_destroy(&qdev->ctx_xa);
 	mutex_destroy(&qdev->import_lock);
+	mutex_destroy(&qdev->attr_lock);
 }

[Severity: Medium]
Can mutex_destroy(&qdev->attr_lock) in qda_deinit_device() run while qdev->attr_lock is still held or waited on by qda_get_dsp_capability()?

In qda_get_dsp_capability(), qdev->attr_lock is held across qda_fastrpc_invoke():

drivers/accel/qda/qda_ioctl.c:qda_get_dsp_capability() {
	guard(mutex)(&qdev->attr_lock);

	if (!qdev->valid_attributes) {
		err = qda_fastrpc_invoke(QDA_FASTRPC_TYPE_GET_DSP_INFO, qdev->dsp_attributes,
					 session);
}

Inside qda_fastrpc_invoke() -> qda_fastrpc_invoke_common(), qda_rpmsg_send_msg() calls drm_dev_exit() after rpmsg_send(), and then qda_rpmsg_wait_for_rsp() sleeps in wait_for_completion_interruptible(&ctx->work) while qdev->attr_lock remains held.

If the remote processor crashes or the rpmsg device is removed before the DSP replies, qda_rpmsg_remove() runs:

drivers/accel/qda/qda_rpmsg.c:qda_rpmsg_remove() {
	drm_dev_unplug(&qdev->drm_dev);
	qdev->rpdev = NULL;
	qda_fastrpc_flush_pending(qdev);
	qda_cb_unpopulate(qdev);
	qda_deinit_device(qdev);
}

Because the waiting thread already exited the drm_dev_enter() section, drm_dev_unplug() does not wait for it. qda_fastrpc_flush_pending() wakes the invocation with complete(&ctx->work) and qda_rpmsg_remove() proceeds directly to qda_deinit_device() -> mutex_destroy(&qdev->attr_lock) before the woken thread in qda_get_dsp_capability() has unlocked qdev->attr_lock.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
index e9688e3ba4048..6044cfad53e96 100644

[ ... ]

Quoted 12 lines
drivers/accel/qda/qda_fastrpc.c +8 -0
@@ -980,6 +987,43 @@ static int qda_fastrpc_prepare_args_create_static(struct qda_fastrpc_invoke_ctx
 	return 0;
 }
 
+/*
+ * DSP capability query packed layout:
+ *   [0] count	sizeof(u32), the capacity of the array below	IN
+ *   [1] attrs	(QDA_MAX_DSP_ATTRIBUTES - 1) * sizeof(u32)	OUT
+ *
+ * Index 0 of the cached array is reserved, so only the rest is exchanged.
+ */
+static int qda_fastrpc_prepare_args_get_dsp_info(struct qda_fastrpc_invoke_ctx *ctx)

drivers/accel/qda/qda_fastrpc.c static int qda_fastrpc_prepare_args_create_static(struct qda_fastrpc_invoke_ctx · line 1007

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+{
+	const u32 attr_count = QDA_MAX_DSP_ATTRIBUTES - 1;
+	struct qda_gem_obj *kernel_gem;
+	int err;
+
+	const size_t off_attrs = sizeof(u32);
+	const size_t total_size = off_attrs + attr_count * sizeof(u32);
+
+	ctx->sc = QDA_FASTRPC_SCALARS(0, 1, 1);
+	ctx->handle = QDA_FASTRPC_DSP_UTILITIES_HANDLE;

[Severity: High]
When ctx->handle is set to QDA_FASTRPC_DSP_UTILITIES_HANDLE here, should qda_fastrpc_invoke_pack() also set msg->fastrpc.remote_session_id to 0?

Currently, qda_fastrpc_invoke_pack() only checks for QDA_FASTRPC_INIT_HANDLE:

drivers/accel/qda/qda_fastrpc.c:qda_fastrpc_invoke_pack() {
	/* The session has no process yet to identify while one is being created */
	if (ctx->handle == QDA_FASTRPC_INIT_HANDLE)
		msg->fastrpc.remote_session_id = 0;
	else
		msg->fastrpc.remote_session_id = ctx->session->remote_session_id;
}

When userspace issues DRM_IOCTL_QDA_QUERY with QDA_QUERY_DSP_CAPABILITIES before creating a DSP process on the session, qda_fastrpc_invoke_pack() will pack the session's non-zero remote_session_id into the RPMsg wire header for a stateless utilities call that has no remote process context (whereas drivers/misc/fastrpc.c passes kernel = true in fastrpc_get_info_from_dsp() so fastrpc_invoke_send() sets msg->client_id = 0).

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=23
maintainer
#58 lore raw

Re: [PATCH v4 23/25] accel/qda: Add DSP capability query

In reply to Ekansh Gupta
Quoted 7 lines · Ekansh Gupta

*
* This structure is used with DRM_IOCTL_QDA_QUERY to query device attributes
- * based on @query_type.
+ * based on @query_type. @dsp_name and @cap share storage, as only one query
+ * is made per call.
*/
struct drm_qda_query {

include/uapi/drm/qda_accel.h extern "C" { · line 77

Quoted 10 lines
include/uapi/drm/qda_accel.h +7 -1
 	__u32 query_type;
-	__u8 dsp_name[16];
+	union {
+		__u8 dsp_name[16];
+		struct {
+			__u32 attribute_id;
+			__u32 capability;
+		} cap;
+	};
 };

I think it would be better to make this separate ioctl commands instead of a multiplexer for each thing you query. ioctl numbers are cheap, but complexity is not.

Arnd

#59 lore raw

PATCH v4 24/25 accel/qda: Add legacy shim support for DSP capability query

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Wire FASTRPC_IOCTL_GET_DSP_INFO into the legacy FastRPC compatibility shim on top of qda_fastrpc_get_dsp_info(), so existing user space can query DSP capabilities through the /dev/fastrpc-* interface exactly as it could with the driver this one replaces.

The device-wide capability cache introduced for the native DRM_IOCTL_QDA_QUERY path is reused as-is; the shim only has to translate between the legacy fastrpc_ioctl_capability layout and the shared qda_fastrpc_get_dsp_info() call.

---
 drivers/accel/qda/qda_fastrpc_shim.c | 20 ++++++++++++++++++++
 drivers/accel/qda/qda_legacy_ops.c   |  6 ++++++
 drivers/accel/qda/qda_legacy_ops.h   |  2 ++
 3 files changed, 28 insertions(+)
drivers/accel/qda/qda_fastrpc_shim.c +20 -0
index c1464b1adbc2..2be1dd116ef7 100644
@@ -463,6 +463,24 @@ static long qda_shim_mem_unmap(struct qda_shim_file *sf, void __user *argp)
 	return 0;
 }
 
+static long qda_shim_get_dsp_info(struct qda_shim_file *sf, void __user *argp)
+{
+	struct fastrpc_ioctl_capability args;
+	int err;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	err = qda_legacy_ops_table.get_dsp_info(sf->session, args.attribute_id, &args.capability);
+	if (err)
+		return err;
+
+	if (copy_to_user(argp, &args, sizeof(args)))
+		return -EFAULT;
+
+	return 0;
+}
+
 static long qda_shim_invoke(struct qda_shim_file *sf, void __user *argp)
 {
 	struct qda_legacy_invoke_arg *largs;
@@ -550,6 +568,8 @@ static long qda_shim_dispatch(struct qda_shim_file *sf, unsigned int cmd, void _
 		return qda_shim_mem_unmap(sf, argp);
 	case FASTRPC_IOCTL_INVOKE:
 		return qda_shim_invoke(sf, argp);
+	case FASTRPC_IOCTL_GET_DSP_INFO:
+		return qda_shim_get_dsp_info(sf, argp);
 	default:
 		/*
 		 * Includes FASTRPC_IOCTL_FREE_DMA_BUFF, which the driver this
drivers/accel/qda/qda_legacy_ops.c +6 -0
index fc5e94c0c9bc..8d9d597c76c2 100644
@@ -188,6 +188,11 @@ static int legacy_invoke(struct qda_session *session, u32 handle, u32 sc,
 	return qda_fastrpc_invoke_legacy(session->qda_dev, session, handle, sc, args);
 }
 
+static int legacy_get_dsp_info(struct qda_session *session, u32 attribute_id, u32 *capability)
+{
+	return qda_get_dsp_capability(session->qda_dev, session, attribute_id, capability);
+}
+
 const struct qda_legacy_ops qda_legacy_ops_table = {
 	.session_create = legacy_session_create,
 	.session_destroy = legacy_session_destroy,
@@ -201,5 +206,6 @@ const struct qda_legacy_ops qda_legacy_ops_table = {
 	.alloc_dmabuf = legacy_alloc_dmabuf,
 	.import_dmabuf = legacy_import_dmabuf,
 	.invoke = legacy_invoke,
+	.get_dsp_info = legacy_get_dsp_info,
 };
 EXPORT_SYMBOL_FOR_MODULES(qda_legacy_ops_table, "qda_fastrpc_shim");
drivers/accel/qda/qda_legacy_ops.h +2 -0
index 95a169edc594..c79003021fc1 100644
@@ -57,6 +57,8 @@ struct qda_legacy_ops {
 	/** @invoke: Run a method on the DSP; takes ownership of each @args[i].obj */
 	int (*invoke)(struct qda_session *session, u32 handle, u32 sc,
 		      struct qda_legacy_invoke_arg *args);
+	/** @get_dsp_info: Query a DSP capability attribute */
+	int (*get_dsp_info)(struct qda_session *session, u32 attribute_id, u32 *capability);
 };
 
 extern const struct qda_legacy_ops qda_legacy_ops_table;
Signature · 1 line
2.34.1
#60 lore raw

Re: [PATCH v4 24/25] accel/qda: Add legacy shim support for DSP capability query

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] accel/qda: non-zero remote_session_id sent in qda_fastrpc_invoke_pack() - [Low] accel/qda…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] accel/qda: non-zero remote_session_id sent in qda_fastrpc_invoke_pack()
- [Low] accel/qda: commit message references non-existent qda_fastrpc_get_dsp_info() --

commit a3e1eaffe6784dab5dbbf7ef1443bdf4fdd1f0a5
Author: Ekansh Gupta <[email protected]>

accel/qda: Add legacy shim support for DSP capability query

This commit wires FASTRPC_IOCTL_GET_DSP_INFO into the legacy FastRPC compatibility shim so user space can query DSP capabilities through /dev/fastrpc-* devices using the shared capability cache.

Quoted 7 lines

accel/qda: Add legacy shim support for DSP capability query

Wire FASTRPC_IOCTL_GET_DSP_INFO into the legacy FastRPC compatibility shim on top of qda_fastrpc_get_dsp_info(), so existing user space can query DSP capabilities through the /dev/fastrpc-* interface exactly as it could with the driver this one replaces.

Quoted 6 lines

The device-wide capability cache introduced for the native DRM_IOCTL_QDA_QUERY path is reused as-is; the shim only has to translate between the legacy fastrpc_ioctl_capability layout and the shared qda_fastrpc_get_dsp_info() call.

[Severity: Low]
This isn't a bug, but the commit message states twice that FASTRPC_IOCTL_GET_DSP_INFO is wired on top of qda_fastrpc_get_dsp_info(). Should this reference qda_get_dsp_capability() instead, since legacy_get_dsp_info() in drivers/accel/qda/qda_legacy_ops.c delegates to qda_get_dsp_capability() and no function named qda_fastrpc_get_dsp_info() exists in the codebase?

drivers/accel/qda/qda_legacy_ops.c static int legacy_invoke(struct qda_session *session, u32 handle, u32 sc, · line 194

Quoted 12 lines
drivers/accel/qda/qda_legacy_ops.c +4 -0
index fc5e94c0c9bcd..8d9d597c76c26 100644
@@ -188,6 +188,11 @@ static int legacy_invoke(struct qda_session *session, u32 handle, u32 sc,
 	return qda_fastrpc_invoke_legacy(session->qda_dev, session, handle, sc, args);
 }
 
+static int legacy_get_dsp_info(struct qda_session *session, u32 attribute_id, u32 *capability)
+{
+	return qda_get_dsp_capability(session->qda_dev, session, attribute_id, capability);
+}

[Severity: Medium]
When user space opens /dev/fastrpc-* (which assigns a positive session->remote_session_id via qda_session_alloc()) and issues FASTRPC_IOCTL_GET_DSP_INFO before creating a DSP process, will the DSP accept the capability query with a non-zero msg->fastrpc.remote_session_id?

When qdev->valid_attributes is false, the call chain:

qda_shim_get_dsp_info()
legacy_get_dsp_info()
qda_get_dsp_capability()
qda_fastrpc_invoke(QDA_FASTRPC_TYPE_GET_DSP_INFO, ...)
qda_fastrpc_invoke_common()
qda_fastrpc_invoke_pack()

packs the request with ctx->handle set to QDA_FASTRPC_DSP_UTILITIES_HANDLE.

However, in drivers/accel/qda/qda_fastrpc.c, qda_fastrpc_invoke_pack() only zeroes remote_session_id for QDA_FASTRPC_INIT_HANDLE:

	/* The session has no process yet to identify while one is being created */
	if (ctx->handle == QDA_FASTRPC_INIT_HANDLE)
		msg->fastrpc.remote_session_id = 0;
	else
		msg->fastrpc.remote_session_id = ctx->session->remote_session_id;

In contrast, in drivers/misc/fastrpc.c, fastrpc_get_info_from_dsp() calls fastrpc_internal_invoke() with kernel = true, and fastrpc_invoke_send() sets msg->client_id = 0 whenever kernel is true.

Should qda_fastrpc_invoke_pack() also set msg->fastrpc.remote_session_id = 0 when ctx->handle == QDA_FASTRPC_DSP_UTILITIES_HANDLE so that capability queries match the legacy driver and do not pass an uninitialized DSP process's session ID?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=24
#61 lore raw

PATCH v4 25/25 accel/qda: Add poll mode for invocation completion

In reply to Ekansh Gupta

From: Ekansh Gupta <[email protected]>

Let a session opt in to completion polling for dynamically loaded module invocations in a user PD via a new DRM_IOCTL_QDA_SET_OPTION ioctl (and the equivalent legacy FASTRPC_IOCTL_SET_OPTION), mirroring drivers/misc/fastrpc.c's FASTRPC_POLL_MODE.

A poll word is reserved at the end of every invocation's metadata region. When polling is enabled, in use, and the invocation targets a dynamically loaded handle in a user PD, the driver busy-waits on that word for QDA_FASTRPC_POLL_RESPONSE instead of immediately blocking on the rpmsg response, trading CPU cycles for lower round-trip latency. If the DSP has not answered by the time the poll loop times out, the driver falls back to waiting for the interrupt-based rpmsg response as usual; the two paths share an is_work_done flag so whichever notices completion first wins without racing the other.

Only SoCs whose DSP firmware actually implements this are allowed to enable it: qda_soc_data gets a poll_mode_supported flag, set for Kaanapali, and a closed list of older platforms using the generic default_soc_data is checked as a fallback.

---
 drivers/accel/qda/qda_drv.c          |  1 +
 drivers/accel/qda/qda_drv.h          |  8 +++++
 drivers/accel/qda/qda_fastrpc.c      | 67 +++++++++++++++++++++++++++++++++---
 drivers/accel/qda/qda_fastrpc.h      | 15 ++++++++
 drivers/accel/qda/qda_fastrpc_shim.c | 30 ++++++++++++++++
 drivers/accel/qda/qda_ioctl.c        | 45 ++++++++++++++++++++++++
 drivers/accel/qda/qda_ioctl.h        |  2 ++
 drivers/accel/qda/qda_legacy_ops.c   |  1 +
 drivers/accel/qda/qda_legacy_ops.h   |  2 ++
 drivers/accel/qda/qda_rpmsg.c        |  3 ++
 drivers/accel/qda/qda_soc.c          | 33 ++++++++++++++++++
 drivers/accel/qda/qda_soc.h          |  3 ++
 include/uapi/drm/qda_accel.h         | 27 +++++++++++++++
 13 files changed, 233 insertions(+), 4 deletions(-)
drivers/accel/qda/qda_drv.c +1 -0
index 51d8ef10c6c1..912dcb2b593d 100644
@@ -128,6 +128,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = {
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MMAP, qda_ioctl_mmap, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_MUNMAP, qda_ioctl_munmap, 0),
 	DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_ATTACH, qda_ioctl_attach, 0),
+	DRM_IOCTL_DEF_DRV(QDA_SET_OPTION, qda_ioctl_set_option, 0),
 };
 
 static const struct drm_driver qda_drm_driver = {
drivers/accel/qda/qda_drv.h +8 -0
index cbb580d03e0b..ff58bac77319 100644
@@ -71,6 +71,12 @@ struct qda_session {
 	 * ioctls on one file can run concurrently.
 	 */
 	struct qda_gem_obj *init_mem_gem_obj;
+	/**
+	 * @poll_mode: Poll for completion of invocations on dynamically loaded
+	 * modules in a user PD. Set through the set-option ioctl, which can run
+	 * concurrently with invocations on the same file.
+	 */
+	bool poll_mode;
 	/**
 	 * @msg_buf_pool: Invoke header buffers this session is done with,
 	 * kept for its later invocations to reuse.
@@ -146,6 +152,8 @@ struct qda_dev {
 	struct mutex remote_heap_lock;
 	/** @audio_init_mem: True once the DSP has been given the remote heap */
 	bool audio_init_mem;
+	/** @poll_mode_supported: Sessions may opt in to invocation completion polling */
+	bool poll_mode_supported;
 };
 
 /**
drivers/accel/qda/qda_fastrpc.c +63 -4

113 lines not on this page · raw message

drivers/accel/qda/qda_fastrpc.h +15 -0
index 7d5cd04943a3..2e01d126addc 100644
@@ -75,6 +75,15 @@ struct qda_session;
 #define QDA_USER_PD			1
 #define QDA_SENSORS_PD			2
 
+/* Handles up to this value name static DSP services, not loaded modules */
+#define QDA_FASTRPC_MAX_STATIC_HANDLE	20
+
+/* Value the DSP writes to the poll word once a polled invocation completes */
+#define QDA_FASTRPC_POLL_RESPONSE	0xdecaf
+
+/* How long to poll before falling back to waiting for the rpmsg response */
+#define QDA_FASTRPC_POLL_MAX_TIMEOUT_US	10000
+
 /* Largest ELF image a process may be created from */
 #define QDA_FASTRPC_INIT_FILELEN_MAX	(4 * 1024 * 1024)
 
@@ -342,6 +351,12 @@ struct qda_fastrpc_invoke_ctx {
 	struct drm_gem_object *map_obj;
 	/** @pages: Array of physical page descriptors inside the payload */
 	struct qda_phy_page *pages;
+	/** @poll_addr: Poll word at the end of the payload, written by the DSP */
+	u32 *poll_addr;
+	/** @is_polled: Wait for completion by polling @poll_addr first */
+	bool is_polled;
+	/** @is_work_done: Set once either the poll or the rpmsg path saw completion */
+	bool is_work_done;
 	/** @work: Completed once the DSP has answered */
 	struct completion work;
 	/**
drivers/accel/qda/qda_fastrpc_shim.c +30 -0
index 2be1dd116ef7..acd1b1b2f2b7 100644
@@ -463,6 +463,34 @@ static long qda_shim_mem_unmap(struct qda_shim_file *sf, void __user *argp)
 	return 0;
 }
 
+/*
+ * The legacy request IDs and values match the accel ones, but they are
+ * translated rather than passed through, so the two ABIs stay independent.
+ */
+static long qda_shim_set_option(struct qda_shim_file *sf, void __user *argp)
+{
+	struct fastrpc_ioctl_set_option args;
+	int i;
+
+	if (copy_from_user(&args, argp, sizeof(args)))
+		return -EFAULT;
+
+	for (i = 0; i < ARRAY_SIZE(args.reserved); i++)
+		if (args.reserved[i])
+			return -EINVAL;
+
+	if (args.request_id != FASTRPC_POLL_MODE)
+		return -EINVAL;
+
+	if (args.value != FASTRPC_POLL_MODE_ENABLE &&
+	    args.value != FASTRPC_POLL_MODE_DISABLE)
+		return -EINVAL;
+
+	return qda_legacy_ops_table.set_option(sf->session, QDA_SET_OPTION_POLL_MODE,
+					       args.value == FASTRPC_POLL_MODE_ENABLE ?
+					       QDA_POLL_MODE_ENABLE : QDA_POLL_MODE_DISABLE);
+}
+
 static long qda_shim_get_dsp_info(struct qda_shim_file *sf, void __user *argp)
 {
 	struct fastrpc_ioctl_capability args;
@@ -570,6 +598,8 @@ static long qda_shim_dispatch(struct qda_shim_file *sf, unsigned int cmd, void _
 		return qda_shim_invoke(sf, argp);
 	case FASTRPC_IOCTL_GET_DSP_INFO:
 		return qda_shim_get_dsp_info(sf, argp);
+	case FASTRPC_IOCTL_SET_OPTION:
+		return qda_shim_set_option(sf, argp);
 	default:
 		/*
 		 * Includes FASTRPC_IOCTL_FREE_DMA_BUFF, which the driver this
drivers/accel/qda/qda_ioctl.c +45 -0

48 lines not on this page · raw message

drivers/accel/qda/qda_ioctl.h +2 -0
index 329f3809c8ce..277c16742196 100644
@@ -20,6 +20,8 @@ int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *f
 int qda_ioctl_mmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_munmap(struct drm_device *dev, void *data, struct drm_file *file_priv);
 int qda_ioctl_attach(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_ioctl_set_option(struct drm_device *dev, void *data, struct drm_file *file_priv);
+int qda_session_set_option(struct qda_session *session, u32 request_id, u32 value);
 int qda_session_init_create(struct qda_session *session, struct qda_init_create_args *create);
 int qda_session_attach(struct qda_session *session, u32 pd_type, const char __user *name,
 		       u32 namelen);
drivers/accel/qda/qda_legacy_ops.c +1 -0
index 8d9d597c76c2..18c17d5a9d2b 100644
@@ -198,6 +198,7 @@ const struct qda_legacy_ops qda_legacy_ops_table = {
 	.session_destroy = legacy_session_destroy,
 	.init_create = legacy_init_create,
 	.attach = qda_session_attach,
+	.set_option = qda_session_set_option,
 	.release = legacy_release,
 	.mmap = legacy_mmap,
 	.mem_map = legacy_mem_map,
drivers/accel/qda/qda_legacy_ops.h +2 -0
index c79003021fc1..ffb3ebd9b1c5 100644
@@ -36,6 +36,8 @@ struct qda_legacy_ops {
 	/** @attach: Attach the session to a DSP-resident protection domain */
 	int (*attach)(struct qda_session *session, u32 pd_type, const char __user *name,
 		      u32 namelen);
+	/** @set_option: Set a per-session option */
+	int (*set_option)(struct qda_session *session, u32 request_id, u32 value);
 	/** @release: Release the session's DSP process, if it has one */
 	int (*release)(struct qda_session *session);
 	/** @mmap: Map a buffer into the DSP's address space */
drivers/accel/qda/qda_rpmsg.c +3 -0
index b93e906b5793..c2a6798cb50d 100644
@@ -132,6 +132,7 @@ static int qda_rpmsg_cb(struct rpmsg_device *rpdev, void *data, int len,
 	}
 
 	ctx->retval = resp_msg->retval;
+	WRITE_ONCE(ctx->is_work_done, true);
 	complete(&ctx->work);
 
 	/*
@@ -267,6 +268,8 @@ static int qda_rpmsg_probe(struct rpmsg_device *rpdev)
 	if (!qdev->soc_data)
 		return -ENODEV;
 
+	qdev->poll_mode_supported = qda_soc_poll_mode_supported(qdev->soc_data);
+
 	ret = of_property_read_string(rpdev->dev.of_node, "label", &qdev->dsp_name);
 	if (ret) {
 		dev_err(&rpdev->dev, "Missing 'label' property in DT node: %d\n", ret);
drivers/accel/qda/qda_soc.c +33 -0
index fb316b74103d..c670f747e1f5 100644
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-only
 // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+#include <linux/of.h>
 #include <linux/string.h>
 #include "qda_drv.h"
 #include "qda_soc.h"
@@ -8,6 +9,7 @@ const struct qda_soc_data kaanapali_soc_data = {
 	.sid_pos = 56,
 	.dma_addr_bits_cdsp = 34,
 	.dma_addr_bits_default = 32,
+	.poll_mode_supported = true,
 };
 
 const struct qda_soc_data default_soc_data = {
@@ -16,6 +18,37 @@ const struct qda_soc_data default_soc_data = {
 	.dma_addr_bits_default = 32,
 };
 
+/*
+ * Platforms that share default_soc_data but whose DSP firmware does implement
+ * invocation completion polling. The list is closed: new SoCs advertise
+ * polling support through their own qda_soc_data instead.
+ */
+static const struct of_device_id qda_poll_mode_machines[] = {
+	{ .compatible = "qcom,milos" },
+	{ .compatible = "qcom,qcs8300" },
+	{ .compatible = "qcom,sa8775p" },
+	{ .compatible = "qcom,sar2130p" },
+	{ .compatible = "qcom,sm8450" },
+	{ .compatible = "qcom,sm8550" },
+	{ .compatible = "qcom,sm8650" },
+	{ .compatible = "qcom,sm8750" },
+	{ .compatible = "qcom,x1e80100" },
+	{ .compatible = "qcom,x1p42100" },
+	{ }
+};
+
+/**
+ * qda_soc_poll_mode_supported() - Whether the DSP supports completion polling
+ * @soc_data: SoC parameters of the device
+ *
+ * Return: true if invocations on this SoC may use poll mode
+ */
+bool qda_soc_poll_mode_supported(const struct qda_soc_data *soc_data)
+{
+	return soc_data->poll_mode_supported ||
+	       of_machine_get_match(qda_poll_mode_machines);
+}
+
 /**
  * qda_soc_dma_addr_bits() - DMA address width of a device's context banks
  * @qdev: QDA device
drivers/accel/qda/qda_soc.h +3 -0
index fb5b786d0928..db8f10a30f7b 100644
@@ -24,11 +24,14 @@ struct qda_soc_data {
 	u32 dma_addr_bits_cdsp;
 	/** @dma_addr_bits_default: DMA address width of all other context banks */
 	u32 dma_addr_bits_default;
+	/** @poll_mode_supported: DSP firmware supports invocation completion polling */
+	bool poll_mode_supported;
 };
 
 extern const struct qda_soc_data kaanapali_soc_data;
 extern const struct qda_soc_data default_soc_data;
 
+bool qda_soc_poll_mode_supported(const struct qda_soc_data *soc_data);
 u32 qda_soc_dma_addr_bits(const struct qda_dev *qdev);
 
 /**
include/uapi/drm/qda_accel.h +27 -0
index 07b943a3c822..816f2c10accf 100644
@@ -20,6 +20,7 @@ extern "C" {
 #define DRM_QDA_REMOTE_MMAP		0x05
 #define DRM_QDA_REMOTE_MUNMAP		0x06
 #define DRM_QDA_REMOTE_SESSION_ATTACH	0x07
+#define DRM_QDA_SET_OPTION		0x08
 
 #define DRM_IOCTL_QDA_QUERY		DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_QUERY, \
 					 struct drm_qda_query)
@@ -39,6 +40,8 @@ extern "C" {
 #define DRM_IOCTL_QDA_REMOTE_SESSION_ATTACH					\
 	DRM_IOW(DRM_COMMAND_BASE + DRM_QDA_REMOTE_SESSION_ATTACH,		\
 		struct drm_qda_remote_attach)
+#define DRM_IOCTL_QDA_SET_OPTION	DRM_IOW(DRM_COMMAND_BASE + DRM_QDA_SET_OPTION, \
+					 struct drm_qda_set_option)
 
 /* Request types for drm_qda_mem_map.request */
 #define QDA_REQUEST_MMAP	1	/* Map a whole buffer */
@@ -248,6 +251,30 @@ struct drm_qda_remote_attach {
 	__u64 name;
 };
 
+/* Request IDs for drm_qda_set_option.request_id */
+#define QDA_SET_OPTION_POLL_MODE	1
+
+/* Values for drm_qda_set_option.value with QDA_SET_OPTION_POLL_MODE */
+#define QDA_POLL_MODE_DISABLE	0
+#define QDA_POLL_MODE_ENABLE	1
+
+/**
+ * struct drm_qda_set_option - Set a per-session option
+ * @request_id: Option to set (QDA_SET_OPTION_*)
+ * @value: Request-specific value
+ *
+ * For QDA_SET_OPTION_POLL_MODE, @value is QDA_POLL_MODE_ENABLE or
+ * QDA_POLL_MODE_DISABLE. Enabling poll mode lets the driver poll shared
+ * memory for completion of a dynamically loaded module's invocations in a
+ * user PD instead of waiting for an interrupt-based response, trading CPU
+ * cycles for lower round-trip latency. DSPs that do not support it reject
+ * the request with EOPNOTSUPP.
+ */
+struct drm_qda_set_option {
+	__u32 request_id;
+	__u32 value;
+};
+
 #if defined(__cplusplus)
 }
 #endif
Signature · 1 line
2.34.1
#62 lore raw

Re: [PATCH v4 25/25] accel/qda: Add poll mode for invocation completion

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] accel/qda: missing memory ordering on ctx->retval and ctx->is_work_done - [High] accel/qda: UAF…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] accel/qda: missing memory ordering on ctx->retval and ctx->is_work_done
- [High] accel/qda: UAF and teardown race in qda_fastrpc_flush_pending()
--

commit ff1bb82ba9e7f31a0c10a5ce3a19f4029819ffbf
Author: Ekansh Gupta <[email protected]>

accel/qda: Add poll mode for invocation completion

This commit adds per-session completion polling support for dynamically loaded module invocations in a user PD via DRM_IOCTL_QDA_SET_OPTION and FASTRPC_IOCTL_SET_OPTION. It reserves a poll word at the end of the invocation metadata buffer and busy-waits on it before falling back to the interrupt-based RPMsg response.

Quoted 4 lines
drivers/accel/qda/qda_fastrpc.c +0 -0
index 6044cfad53e96..0617bb4842b52 100644

[ ... ]

Quoted 41 lines
drivers/accel/qda/qda_fastrpc.c +37 -0
@@ -598,6 +604,54 @@ int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg *
 	return 0;
 }
 
+static u32 qda_fastrpc_read_poll_addr(struct qda_fastrpc_invoke_ctx *ctx)
+{
+	dma_rmb();
+
+	return READ_ONCE(*ctx->poll_addr);
+}
+
+/*
+ * Busy-wait for the DSP to write QDA_FASTRPC_POLL_RESPONSE into the poll word,
+ * which saves the interrupt and scheduling latency of waiting for the rpmsg
+ * response. The response still arrives afterwards and drops the reference
+ * qda_rpmsg_send_msg() took for it as usual.
+ *
+ * Return: 0 if the invocation completed, negative error code if it has not
+ * answered within QDA_FASTRPC_POLL_MAX_TIMEOUT_US
+ */
+static int qda_fastrpc_poll_for_rsp(struct qda_fastrpc_invoke_ctx *ctx)
+{
+	u32 val;
+	int err;
+
+	err = read_poll_timeout_atomic(qda_fastrpc_read_poll_addr, val,
+				       val == QDA_FASTRPC_POLL_RESPONSE ||
+				       READ_ONCE(ctx->is_work_done),
+				       1, QDA_FASTRPC_POLL_MAX_TIMEOUT_US, false, ctx);
+	if (err)
+		return err;
+
+	/* The rpmsg path records its own status, so only the poll path's is set here */
+	if (val == QDA_FASTRPC_POLL_RESPONSE) {
+		WRITE_ONCE(ctx->is_work_done, true);
+		ctx->retval = 0;
+	}
+
+	return 0;
+}
+

drivers/accel/qda/qda_fastrpc.c int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * · line 653

Quoted 10 lines
drivers/accel/qda/qda_fastrpc.c +10 -0
+static int qda_fastrpc_wait_for_completion(struct qda_fastrpc_invoke_ctx *ctx)
+{
+	if (ctx->is_polled && !qda_fastrpc_poll_for_rsp(ctx))
+		return 0;
+
+	/* Polling timed out, so fall back to waiting for the rpmsg response */
+	ctx->is_polled = false;
+
+	return qda_rpmsg_wait_for_rsp(ctx);
+}

[Severity: High]
Can a polled invocation observe ctx->is_work_done == true before the store to ctx->retval is visible, or race on ctx->retval between the poll and RPMsg paths?

In qda_fastrpc_context_alloc(), ctx->retval is initialized to -1. When qda_rpmsg_cb() or qda_fastrpc_flush_pending() completes an invocation, it writes ctx->retval with a plain store and then sets ctx->is_work_done with WRITE_ONCE():

    ctx->retval = resp_msg->retval;
    WRITE_ONCE(ctx->is_work_done, true);
    complete(&ctx->work);

If read_poll_timeout_atomic() in qda_fastrpc_poll_for_rsp() exits because READ_ONCE(ctx->is_work_done) became true while val != QDA_FASTRPC_POLL_RESPONSE, qda_fastrpc_poll_for_rsp() returns 0 and qda_fastrpc_wait_for_completion() returns 0 immediately without calling qda_rpmsg_wait_for_rsp(ctx), skipping wait_for_completion_interruptible(&ctx->work).

Because wait_for_completion_interruptible(&ctx->work) is bypassed and there is no release-acquire ordering between writing ctx->retval and setting ctx->is_work_done (the dma_rmb() in qda_fastrpc_invoke_common() only orders reads on the polling CPU, not the two stores on the CPU running qda_rpmsg_cb() or qda_fastrpc_flush_pending()), couldn't the polling CPU read the stale initial ctx->retval == -1 (-EPERM) in qda_fastrpc_invoke_common()?

Also, neither qda_rpmsg_cb() nor qda_fastrpc_flush_pending() checks ctx->is_work_done before writing ctx->retval, so when qda_fastrpc_poll_for_rsp() sees val == QDA_FASTRPC_POLL_RESPONSE and sets ctx->retval = 0, a concurrent qda_rpmsg_cb() or qda_fastrpc_flush_pending() will still write ctx->retval without synchronization against the polling thread's write or its subsequent read in qda_fastrpc_invoke_common(). Should the writers and reader use smp_store_release() and smp_load_acquire() (or atomic state transition) to order and arbitrate updates to ctx->retval?

drivers/accel/qda/qda_fastrpc.c void qda_fastrpc_flush_pending(struct qda_dev *qdev) · line 677

Quoted 8 lines
drivers/accel/qda/qda_fastrpc.c +1 -0
@@ -617,6 +671,7 @@ void qda_fastrpc_flush_pending(struct qda_dev *qdev)
 
 	xa_for_each(&qdev->ctx_xa, idx, ctx) {
 		ctx->retval = -EPIPE;
+		WRITE_ONCE(ctx->is_work_done, true);
 		complete(&ctx->work);
 		kref_put(&ctx->refcount, qda_fastrpc_context_free);
 	}

[Severity: High]
Does qda_fastrpc_flush_pending() risk a use-after-free or premature device teardown when polled invocations are in flight or have recently completed?

There appear to be two teardown races when qda_rpmsg_remove() runs:

    drm_dev_unplug(&qdev->drm_dev);
    qdev->rpdev = NULL;
    qda_fastrpc_flush_pending(qdev);
    qda_cb_unpopulate(qdev);
    qda_deinit_device(qdev);

First, when a polled invocation finishes via qda_fastrpc_poll_for_rsp(), the calling thread returns from qda_fastrpc_invoke() and drops its reference via kref_put(&ctx->refcount, qda_fastrpc_context_free), leaving ctx in qdev->ctx_xa with refcount == 1 until the RPMsg reply arrives. When qda_rpmsg_cb() later receives that reply, it schedules ctx->put_work to drop the final reference from process context, because ctx is only erased from qdev->ctx_xa inside qda_fastrpc_context_free(). If qda_rpmsg_remove() runs after qda_rpmsg_cb() schedules ctx->put_work but before qda_fastrpc_context_put_wq() executes, qda_fastrpc_flush_pending() will find ctx still in qdev->ctx_xa and drop its refcount to 0, freeing ctx while ctx->put_work is still pending on system_wq.

Second, qda_fastrpc_poll_for_rsp() runs outside drm_dev_enter(). If a thread is actively polling in qda_fastrpc_poll_for_rsp() when qda_rpmsg_remove() runs, qda_fastrpc_flush_pending() sets ctx->is_work_done = true and drops the RPMsg reference (decrementing refcount from 2 to 1), and qda_rpmsg_remove() immediately proceeds to qda_cb_unpopulate(qdev) and qda_deinit_device(qdev) without waiting for the polling thread to exit and drop its final reference. When the polling thread subsequently runs qda_fastrpc_context_free(), won't it call xa_erase() on the destroyed qdev->ctx_xa and free DMA buffers against the destroyed context-bank platform devices?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=25
#63 lore raw
In reply to Ekansh Gupta
Quoted 8 lines · Ekansh Gupta
 drivers/accel/qda/qda_soc.c            |   66 ++
 drivers/accel/qda/qda_soc.h            |   61 ++
 drivers/misc/Kconfig                   |    1 +
 include/uapi/drm/qda_accel.h           |  282 +++++++
 33 files changed, 5877 insertions(+), 1 deletion(-)

---
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
change-id: 20261102-v4-e8feb321263f

What is happening here? How are we supposed to work with this?

v1: 20260223-qda-firstpost-4ab05249e2cc
v2: 20260519-qda-series-78c2bf0ed78b
v2 again: 20260817-qda-v2-78e2d1f10529

And now completely different one?

This makes the change id completely unusable. b4 diff obviously does not work. Any tracking of this does not work.

NAK, really, I already complained internally months ago about such practice, so apparently not enough.

Best regards,
Krzysztof

#64 lore raw
Quoted 18 lines · Krzysztof Kozlowski
Quoted 8 lines · Ekansh Gupta
 drivers/accel/qda/qda_soc.c            |   66 ++
 drivers/accel/qda/qda_soc.h            |   61 ++
 drivers/misc/Kconfig                   |    1 +
 include/uapi/drm/qda_accel.h           |  282 +++++++
 33 files changed, 5877 insertions(+), 1 deletion(-)

---
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
change-id: 20261102-v4-e8feb321263f

What is happening here? How are we supposed to work with this?

v1: 20260223-qda-firstpost-4ab05249e2cc
v2: 20260519-qda-series-78c2bf0ed78b
v2 again: 20260817-qda-v2-78e2d1f10529

And now completely different one?

Quoted 8 lines

This makes the change id completely unusable. b4 diff obviously does not work. Any tracking of this does not work.

NAK, really, I already complained internally months ago about such practice, so apparently not enough.

Best regards,
Krzysztof

Sorry about this. I lost the original b4 branch and re-enrolled the series for v4, which generated a new change-id. I should have carried the existing one over.

I have restored the v3 change-id (20260817-qda-v2-78e2d1f10529) on the branch, and it will stay fixed for all future revisions of this series. v5 will use it.

Note that v4 is a substantial rework of v3 (the custom bus and iommu patches are gone, the legacy shim is new and the series is reordered), so a range-diff against v3 would be of limited use; the "Changes in v4" section in the cover letter lists what changed and why.

//Ekansh

#65 lore raw
Quoted 22 lines · Ekansh Gupta
Quoted 21 lines · Krzysztof Kozlowski
Quoted 8 lines · Ekansh Gupta
 drivers/accel/qda/qda_soc.c            |   66 ++
 drivers/accel/qda/qda_soc.h            |   61 ++
 drivers/misc/Kconfig                   |    1 +
 include/uapi/drm/qda_accel.h           |  282 +++++++
 33 files changed, 5877 insertions(+), 1 deletion(-)

---
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
change-id: 20261102-v4-e8feb321263f

What is happening here? How are we supposed to work with this?

v1: 20260223-qda-firstpost-4ab05249e2cc
v2: 20260519-qda-series-78c2bf0ed78b
v2 again: 20260817-qda-v2-78e2d1f10529

And now completely different one?

This makes the change id completely unusable. b4 diff obviously does not work. Any tracking of this does not work.

Quoted 7 lines
Quoted 5 lines

NAK, really, I already complained internally months ago about such practice, so apparently not enough.

Best regards,
Krzysztof

Sorry about this. I lost the original b4 branch and re-enrolled the

You lost original branch four times. It's not a coincidence, it's not random data loss because then after first loss you would solve the problem. It's a broken workflow.

Best regards,
Krzysztof

#66 lore raw
Quoted 31 lines · Krzysztof Kozlowski
Quoted 22 lines · Ekansh Gupta
Quoted 21 lines · Krzysztof Kozlowski
Quoted 8 lines · Ekansh Gupta
 drivers/accel/qda/qda_soc.c            |   66 ++
 drivers/accel/qda/qda_soc.h            |   61 ++
 drivers/misc/Kconfig                   |    1 +
 include/uapi/drm/qda_accel.h           |  282 +++++++
 33 files changed, 5877 insertions(+), 1 deletion(-)

---
base-commit: 7649ba73f9e7e1e04d0b46213e39c79ae09dd55c
change-id: 20261102-v4-e8feb321263f

What is happening here? How are we supposed to work with this?

v1: 20260223-qda-firstpost-4ab05249e2cc
v2: 20260519-qda-series-78c2bf0ed78b
v2 again: 20260817-qda-v2-78e2d1f10529

And now completely different one?

This makes the change id completely unusable. b4 diff obviously does not work. Any tracking of this does not work.

Quoted 7 lines
Quoted 5 lines

NAK, really, I already complained internally months ago about such practice, so apparently not enough.

Best regards,
Krzysztof

Sorry about this. I lost the original b4 branch and re-enrolled the

Quoted 7 lines

You lost original branch four times. It's not a coincidence, it's not random data loss because then after first loss you would solve the problem. It's a broken workflow.

Best regards,
Krzysztof

You are right, and I'm not going to call it data loss again. I started each posting from a new tree and re-enrolled the series every time, which is what produced a new change-id for every revision. I've changed how I work so this cannot happen again:
- one long-lived tree and one b4 branch for this series, used for all future revisions;
- before every b4 send, a check that the change-id is still 20260817-qda-v2-78e2d1f10529.

v5 will continue from that change-id, with the v3 and v4 postings recorded in its history.

#67 lore raw
In reply to Ekansh Gupta
Quoted 6 lines · Ekansh Gupta

User space
==========

- FastRPC library: https://github.com/qualcomm/fastrpc
(accel support: https://github.com/qualcomm/fastrpc/tree/accel/staging)
- QAIC compiler: https://github.com/qualcomm/QAIC

Hi Ekansh,

I'm still confused on how userspace gets to execute its compute kernels with this uAPI. Let's say I have a compiler and have compiled a function that reads some values from a buffer and writes to another buffer. Could you explain to me how should I use the uAPI so this kernel executes and I get the expected results in the buffers?

Regards,

Tomeu