* [RFC PATCH] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping
@ 2026-05-14 13:14 Christopher Lusk
2026-07-24 22:21 ` [PATCH v2] " Christopher Lusk
0 siblings, 1 reply; 3+ messages in thread
From: Christopher Lusk @ 2026-05-14 13:14 UTC (permalink / raw)
To: Lucas Stach
Cc: Russell King, Christian Gmeiner, David Airlie, Simona Vetter,
etnaviv, dri-devel, linux-kernel
A userptr buffer object created with ETNA_USERPTR_READ (no
ETNA_USERPTR_WRITE) is pinned without FOLL_WRITE in
etnaviv_gem_userptr_get_pages(); the kernel agrees to allow only
read access to the underlying user pages, and the state is
recorded as etnaviv_obj->userptr.ro = true.
etnaviv_iommu_map_gem() then maps the object's sg_table into
the GPU MMU with ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE
unconditionally, regardless of userptr.ro. A submission that
names the BO as a write target therefore causes the GPU to write
pages that the kernel pinned without write permission, including
page-cache pages of files the caller has only read access to.
The ioctl entry points (DRM_ETNAVIV_GEM_NEW_USERPTR, GEM_SUBMIT)
are DRM_RENDER_ALLOW, so the path is reachable from any
unprivileged render-group user. No intervening permission check,
no RO-aware branch, no MMU-level fallback.
This is the same class shape as drivers/accel/ivpu
commit 7dd57d7a6350 ("accel/ivpu: Disallow re-exporting imported GEM objects"),
fixed 2026-04-30. The Etnaviv manifestation has a simpler trigger
path that does not require PRIME re-import. Source-level analysis
only - no Vivante hardware was available to validate the mutation
primitive end-to-end.
Mask ETNAVIV_PROT_WRITE in the call to etnaviv_iommu_map() when
the buffer originates as a read-only userptr. The userptr.mm
check guards against any non-userptr object that might somehow
have userptr.ro set.
This work is LLM-assisted: Codex (gpt-5.5) drove the static
analysis and class sweep that identified the candidate site;
Claude (claude-opus-4-7) drove the writeup and patch refinement.
Operator review at each gate. Methodology context at
https://northecho.dev/posts/codex-vs-claude-code-vuln-research/.
Posted to the public list per the security-bugs.rst exception
for findings trivial to discover via automated tooling, as
interpreted by the kernel security team for LLM-assisted reports.
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
Assisted-by: Codex:gpt-5.5
Assisted-by: Claude:claude-opus-4-7
---
v7.1-rc2 reference: drivers/gpu/drm/etnaviv/etnaviv_mmu.c:303-304
Open questions for review:
1. v1 MMU shortcut. etnaviv_iommu_map_gem() takes a shortcut for
v1 MMU + single-segment sg_table at lines 278-291, bypassing
etnaviv_iommu_map() entirely and using sg_dma_address directly.
For typical multi-page userptr BOs sgt->nents > 1 so the
shortcut is not taken, but for small contiguous regions on v1
MMU hardware it may apply. This patch does not address the v1
MMU path. If you believe v1 MMU also needs handling, I am happy
to follow up with a separate patch.
2. ETNA_SUBMIT_BO_WRITE per-BO flag. etnaviv_gem_submit accepts
ETNA_SUBMIT_BO_READ / ETNA_SUBMIT_BO_WRITE per buffer object,
currently used for synchronization dependency tracking. Should
a submit naming an RO userptr BO with ETNA_SUBMIT_BO_WRITE be
rejected at submit time as defense-in-depth? Orthogonal to
this patch; happy to follow up with a separate small patch in
etnaviv_gem_submit.c if you would like that.
3. Fixes: tag. My v7.1-rc2 clone is shallow; I could not trace
the introducing commit for the userptr.ro / ETNA_USERPTR_WRITE
distinction. The bug is present in v7.0, v7.1-rc2, and
linux-next (2026-05-08). If you have the introducing commit
handy from deep Etnaviv history, please add the Fixes: tag in
your applied version, or let me know and I will supply it from
an unshallowed mirror.
A reviewer with Vivante hardware can confirm the primitive with
the following pseudocode (before/after this patch):
fd = open("/dev/dri/renderD128", O_RDWR);
ptr = mmap(...); /* page-aligned, page-cache-backed file */
bo_handle = drm_ioctl(fd, DRM_IOCTL_ETNAVIV_GEM_USERPTR,
.ptr = ptr, .size = page,
.flags = ETNA_USERPTR_READ);
/* GEM_SUBMIT with the BO listed as ETNA_SUBMIT_BO_WRITE and a
command stream that writes to the BO GPU VA */
drm_ioctl(fd, DRM_IOCTL_ETNAVIV_GEM_SUBMIT, ...);
/* observe: on unpatched kernel, ptr contents have changed;
on patched kernel, the write does not land in the
user page-cache pages. */
I am happy to source a Vivante board (Sabre Lite, Wandboard,
HummingBoard Pro) and post a v2 with Tested-by trailers if you
prefer hardware validation before this lands. Source-only is
offered first to avoid asking for a hardware-spend decision that
may not be necessary.
drivers/gpu/drm/etnaviv/etnaviv_mmu.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
index e3572461b599..c4dcb7ee3e49 100644
--- a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
+++ b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
@@ -301,7 +301,9 @@ int etnaviv_iommu_map_gem(struct etnaviv_iommu_context *context,
mapping->iova = node->start;
ret = etnaviv_iommu_map(context, node->start, etnaviv_obj->size, sgt,
- ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE);
+ (etnaviv_obj->userptr.mm && etnaviv_obj->userptr.ro) ?
+ ETNAVIV_PROT_READ :
+ (ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE));
if (ret < 0) {
drm_mm_remove_node(node);
--
2.54.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH v2] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping
2026-05-14 13:14 [RFC PATCH] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping Christopher Lusk
@ 2026-07-24 22:21 ` Christopher Lusk
2026-07-24 22:35 ` sashiko-bot
0 siblings, 1 reply; 3+ messages in thread
From: Christopher Lusk @ 2026-07-24 22:21 UTC (permalink / raw)
To: l.stach, linux+etnaviv, christian.gmeiner, airlied, simona
Cc: etnaviv, dri-devel, linux-kernel, guoziyi114, n7l8m4, stable
The userptr interface records the requested access mode in
etnaviv_obj->userptr.ro (set when ETNA_USERPTR_WRITE is absent), but
etnaviv_iommu_map_gem() ignores it and maps every buffer with
ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE. A buffer pinned without
write permission is therefore writable by the GPU, which can mutate
page-cache data visible to other processes.
Build the protection mask from userptr.ro instead.
On MMUv2 this is a real enforcement change: etnaviv_iommuv2_map()
encodes the writeable bit per entry, so omitting ETNAVIV_PROT_WRITE
clears MMUv2_PTE_WRITEABLE and the hardware refuses GPU writes.
MMUv1 cannot enforce read-only at all. Its page table entries are
bare physical addresses -- etnaviv_iommuv1_map() accepts a prot
argument and discards it -- so passing ETNAVIV_PROT_READ has no
effect on v1 hardware. What can be improved there is the mapping
path taken: a single-entry contiguous userptr BO currently takes the
MMUv1 linear-window shortcut in etnaviv_iommu_map_gem(), which hands
the GPU an offset into a window of up to 2 GiB rather than a
page-table mapping of just the pinned pages. Setting
ETNA_BO_FORCE_MMU on read-only userptr BOs at creation time
suppresses that shortcut and confines the GPU to the mapped pages.
To be explicit, because the two halves differ: this makes read-only
userptr genuinely read-only on MMUv2, and on MMUv1 it only narrows
what the GPU can reach. A read-only userptr BO on v1 hardware
remains GPU-writable. Rejecting such mappings outright was
considered and dropped.
Fixes: a8c21a5451d8 ("drm/etnaviv: add initial etnaviv DRM driver")
Link: https://lore.kernel.org/all/20260508180518.1417371-1-n7l8m4@u.northwestern.edu/
Suggested-by: Lucas Stach <l.stach@pengutronix.de>
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5.5
Assisted-by: Claude:claude-opus-5
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
---
Changes since RFC v1 [1]:
- Per Lucas Stachs feedback on Ziyi Guos parallel patch [2][3]:
set ETNA_BO_FORCE_MMU on read-only userptr BOs at creation time
rather than rejecting read-only mappings on MMUv1 with -ENODEV.
- Commit message now states the MMUv1 limitation explicitly:
forcing the page-table path is containment, not write protection,
because MMUv1 PTEs have no writeable bit.
- Two-file change (etnaviv_gem.c + etnaviv_mmu.c) instead of the
single-file etnaviv_mmu.c change in the RFC.
The same bug was independently found by Ziyi Guo, who posted a fix [2]
six days before my RFC. Neither patch was merged. This v2 takes the
approach Lucas preferred in reply to his [3], and keeps his
prot-in-a-local shape rather than inlining the condition at the call
site.
Open question for Lucas: on MMUv1 a caller asking for a read-only
userptr BO now gets a mapping that is not read-only, silently. I
have left it silent to keep the diff small for stable, but a
drm_warn_once() in etnaviv_gem_new_userptr() would make the
limitation visible to userspace developers. Happy to add one if you
prefer.
Tooling and testing, per Documentation/process/generated-content.rst:
- The bug was found by static analysis rather than by hand: a
mechanism-first variant-analysis pass over page-backed buffer
sinks, looking for sites where a recorded access-mode flag is not
carried into the mapping that would enforce it. The same pass
flagged an equivalent shape in drivers/accel/ivpu, which turned out
to duplicate commit 7dd57d7a6350.
- The patch, the changelog and this text were drafted with LLM
assistance (see the Assisted-by tags) and reviewed line by line by
me; I take responsibility for all of it.
- The MMUv1/MMUv2 asymmetry above was established by reading
etnaviv_iommuv1_map() and etnaviv_iommuv2_map() directly, not by
trusting the tool: an earlier draft of this patch claimed MMUv1
enforcement and was wrong.
- Testing: compile-tested only. Built on x86_64 with COMPILE_TEST=y,
CONFIG_DRM_ETNAVIV=m, gcc 15.2.1, W=1 -- clean, no new warnings.
Base: drm-misc-next abc1e559f8e5. No Vivante hardware is available
to me, so neither the MMUv2 enforcement path nor the MMUv1
containment change is verified at runtime. A test from anyone with
GC hardware would be very welcome, and I am happy to arrange
hardware validation before merge if you would rather have it first.
[1] https://lore.kernel.org/all/20260514131401.2660079-1-clusk@northecho.dev/
[2] https://lore.kernel.org/all/20260508180518.1417371-1-n7l8m4@u.northwestern.edu/
[3] https://lore.kernel.org/all/3e298ed6a361a0aa5526d859b0f3a98c0cd47090.camel@pengutronix.de/
drivers/gpu/drm/etnaviv/etnaviv_gem.c | 13 ++++++++++++-
drivers/gpu/drm/etnaviv/etnaviv_mmu.c | 10 +++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/etnaviv/etnaviv_gem.c b/drivers/gpu/drm/etnaviv/etnaviv_gem.c
index b0436a1e10..b043a56e3e 100644
--- a/drivers/gpu/drm/etnaviv/etnaviv_gem.c
+++ b/drivers/gpu/drm/etnaviv/etnaviv_gem.c
@@ -735,9 +735,20 @@ int etnaviv_gem_new_userptr(struct drm_device *dev, struct drm_file *file,
uintptr_t ptr, u32 size, u32 flags, u32 *handle)
{
struct etnaviv_gem_object *etnaviv_obj;
+ u32 bo_flags = ETNA_BO_CACHED;
int ret;
- ret = etnaviv_gem_new_private(dev, size, ETNA_BO_CACHED,
+ /*
+ * Keep read-only userptr BOs out of the MMUv1 linear window, which
+ * would expose far more than the pinned pages to the GPU. MMUv1
+ * PTEs have no writeable bit, so this confines the GPU rather than
+ * making the BO read-only; MMUv2 enforces read-only per PTE in
+ * etnaviv_iommu_map_gem().
+ */
+ if (!(flags & ETNA_USERPTR_WRITE))
+ bo_flags |= ETNA_BO_FORCE_MMU;
+
+ ret = etnaviv_gem_new_private(dev, size, bo_flags,
&etnaviv_gem_userptr_ops, &etnaviv_obj);
if (ret)
return ret;
diff --git a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
index e3572461b5..569f729681 100644
--- a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
+++ b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
@@ -269,10 +269,18 @@ int etnaviv_iommu_map_gem(struct etnaviv_iommu_context *context,
{
struct sg_table *sgt = etnaviv_obj->sgt;
struct drm_mm_node *node;
+ int prot = ETNAVIV_PROT_READ;
int ret;
lockdep_assert_held(&etnaviv_obj->lock);
+ /*
+ * Read-only userptr BOs drop ETNAVIV_PROT_WRITE. MMUv2 honors this
+ * via MMUv2_PTE_WRITEABLE; MMUv1 ignores prot entirely.
+ */
+ if (!etnaviv_obj->userptr.mm || !etnaviv_obj->userptr.ro)
+ prot |= ETNAVIV_PROT_WRITE;
+
mutex_lock(&context->lock);
/* v1 MMU can optimize single entry (contiguous) scatterlists */
@@ -301,7 +309,7 @@ int etnaviv_iommu_map_gem(struct etnaviv_iommu_context *context,
mapping->iova = node->start;
ret = etnaviv_iommu_map(context, node->start, etnaviv_obj->size, sgt,
- ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE);
+ prot);
if (ret < 0) {
drm_mm_remove_node(node);
--
2.54.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH v2] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping
2026-07-24 22:21 ` [PATCH v2] " Christopher Lusk
@ 2026-07-24 22:35 ` sashiko-bot
0 siblings, 0 replies; 3+ messages in thread
From: sashiko-bot @ 2026-07-24 22:35 UTC (permalink / raw)
To: Christopher Lusk; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
Pre-existing issues:
- [Critical] MMUv1 hardware lacks read-only page protection, allowing unprivileged users to overwrite arbitrary read-only CPU pages (e.g., `/etc/passwd`) via the GPU.
- [Critical] The MMUv1 linear window shortcut for single-entry BOs bypasses page-table isolation, allowing unprivileged users to arbitrarily read and write physical memory.
- [High] Read-only userptr BOs are mapped using DMA_BIDIRECTIONAL, causing memory corruption when SWIOTLB bounce buffers are unmapped.
- [High] Calling drm_gem_object_put() on a partially initialized GEM object triggers a kernel panic via list_del() on an uninitialized list node.
--
--- Patch [1]: [PATCH v2] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping ---
commit 3b31d2a8b362305b34038576c89dbe65957eea85
Author: Christopher Lusk <clusk@northecho.dev>
drm/etnaviv: honor read-only userptr flag in GPU MMU mapping
The userptr interface records the requested access mode in
etnaviv_obj->userptr.ro, but etnaviv_iommu_map_gem() ignores it and maps
every buffer with ETNAVIV_PROT_READ | ETNAVIV_PROT_WRITE. This patch
builds the protection mask from userptr.ro instead, enforcing read-only
mappings on MMUv2 and confining the GPU to mapped pages on MMUv1.
Link: https://lore.kernel.org/all/20260508180518.1417371-1-n7l8m4@u.northwestern.edu/
> diff --git a/drivers/gpu/drm/etnaviv/etnaviv_gem.c b/drivers/gpu/drm/etnaviv/etnaviv_gem.c
> --- a/drivers/gpu/drm/etnaviv/etnaviv_gem.c
> +++ b/drivers/gpu/drm/etnaviv/etnaviv_gem.c
[ ... ]
> @@ -735,9 +735,20 @@ int etnaviv_gem_new_userptr(struct drm_device *dev, struct drm_file *file,
> uintptr_t ptr, u32 size, u32 flags, u32 *handle)
> {
> struct etnaviv_gem_object *etnaviv_obj;
> + u32 bo_flags = ETNA_BO_CACHED;
> int ret;
>
> - ret = etnaviv_gem_new_private(dev, size, ETNA_BO_CACHED,
> + /*
> + * Keep read-only userptr BOs out of the MMUv1 linear window, which
> + * would expose far more than the pinned pages to the GPU. MMUv1
> + * PTEs have no writeable bit, so this confines the GPU rather than
> + * making the BO read-only; MMUv2 enforces read-only per PTE in
> + * etnaviv_iommu_map_gem().
> + */
> + if (!(flags & ETNA_USERPTR_WRITE))
> + bo_flags |= ETNA_BO_FORCE_MMU;
[Severity: Critical]
This is a pre-existing issue, but does this explicitly leave read-only
userptr BOs writable by the GPU on MMUv1 hardware?
Since MMUv1 lacks read-only page protection and ignores the protection
argument, an unprivileged user could pin a read-only file via the userptr
ioctl and submit a GPU command stream to write to the mapped buffer.
Does this allow arbitrary modification of read-only files or the zero
page?
[Severity: Critical]
This is a pre-existing issue, but does the MMUv1 linear window shortcut
bypass page-table isolation for single-entry BOs?
While this patch forces ETNA_BO_FORCE_MMU for read-only userptrs, writable
userptrs or standard shmem BOs lacking this flag are given an IOVA directly
offset into a 2GB linear window in etnaviv_iommu_map_gem():
if (context->global->version == ETNAVIV_IOMMU_V1 &&
sgt->nents == 1 && !(etnaviv_obj->flags & ETNA_BO_FORCE_MMU)) {
u32 iova;
iova = sg_dma_address(sgt->sgl) - memory_base;
Without bounds checking, could a malicious command stream access this offset
to read or write arbitrary physical pages in the contiguous memory area?
[ ... ]
> diff --git a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
> --- a/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
> +++ b/drivers/gpu/drm/etnaviv/etnaviv_mmu.c
[ ... ]
> @@ -269,10 +269,18 @@ int etnaviv_iommu_map_gem(struct etnaviv_iommu_context *context,
> {
> struct sg_table *sgt = etnaviv_obj->sgt;
> struct drm_mm_node *node;
> + int prot = ETNAVIV_PROT_READ;
> int ret;
>
> lockdep_assert_held(&etnaviv_obj->lock);
>
> + /*
> + * Read-only userptr BOs drop ETNAVIV_PROT_WRITE. MMUv2 honors this
> + * via MMUv2_PTE_WRITEABLE; MMUv1 ignores prot entirely.
> + */
> + if (!etnaviv_obj->userptr.mm || !etnaviv_obj->userptr.ro)
> + prot |= ETNAVIV_PROT_WRITE;
> +
[Severity: Critical]
This is a pre-existing issue, but does dropping ETNAVIV_PROT_WRITE only
protect MMUv2?
In etnaviv_iommu_map_gem(), since MMUv1 ignores the protection flag entirely,
the read-only userptr remains writable by the GPU. Could this overwrite
arbitrary read-only CPU pages?
[ ... ]
> diff --git a/drivers/gpu/drm/etnaviv/etnaviv_gem.c b/drivers/gpu/drm/etnaviv/etnaviv_gem.c
[ ... ]
> if (etnaviv_obj->flags & ETNA_BO_CACHE_MASK)
> dma_map_sgtable(dev->dev, sgt, DMA_BIDIRECTIONAL, 0);
[Severity: High]
This is a pre-existing issue, but does etnaviv_gem_scatter_map() hardcode
DMA_BIDIRECTIONAL for all cached BOs, including read-only userptrs?
If the system uses SWIOTLB bounce buffering, dma_unmap_sgtable() with
DMA_BIDIRECTIONAL in etnaviv_gem_scatterlist_unmap() will copy the
modified bounce buffer contents back over the original CPU pages.
For a userptr pinned without FOLL_WRITE, could this forcefully overwrite
read-only file mappings or the zero page?
[ ... ]
> diff --git a/drivers/gpu/drm/etnaviv/etnaviv_gem_prime.c b/drivers/gpu/drm/etnaviv/etnaviv_gem_prime.c
[ ... ]
> fail:
> drm_gem_object_put(&etnaviv_obj->base);
[Severity: High]
This is a pre-existing issue, but can calling drm_gem_object_put() on a
partially initialized GEM object trigger a kernel panic?
In etnaviv_gem_prime_import_sg_table(), if object initialization fails,
the error path drops the refcount to 0 and calls etnaviv_gem_free_object():
mutex_lock(&priv->gem_lock);
list_del(&etnaviv_obj->gem_node);
mutex_unlock(&priv->gem_lock);
Since etnaviv_gem_obj_add() was not yet called, the gem_node is
uninitialized. Will list_del() dereference NULL pointers and crash here?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724222124.537101-1-clusk@northecho.dev?part=1
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-24 22:36 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-14 13:14 [RFC PATCH] drm/etnaviv: honor read-only userptr flag in GPU MMU mapping Christopher Lusk
2026-07-24 22:21 ` [PATCH v2] " Christopher Lusk
2026-07-24 22:35 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.