Hi dri-devel,
udmabuf_create() accepts hugetlb memfds (check_memfd_seals() in
drivers/dma-buf/udmabuf.c:256-272 explicitly allows
is_file_hugepages()), but the buffer fill logic mishandles
offsets that are not aligned to the huge page size, causing the
exported dma_buf to serve data displaced from what the caller
requested — and in a partial-fill case, a mismatch between the
declared buffer size and the actually filled folio array.
== AFFECTED ==
- Linux 6.12.112 (linux-6.12.y stable), commit 7aba70ab
- Linux mainline, HEAD 24a19b953e2a (torvalds/linux), as of this
report
== ROOT CAUSE ==
memfd_pin_folios() (mm/gup.c:3700-3809) pins the folios
overlapping the requested range and returns, per folio, the
offset of the range start *inside* that folio
(offset_in_folio(), gup.c:3777-3778). For a hugetlb memfd
(huge pages, e.g. 2MB), a range starting mid-folio returns a
large unaligned pgoff.
The udmabuf fill loop (udmabuf.c:288-340) then does:
for each pinned folio:
subpgoff = pgoff; /* starts unaligned */
for (; subpgoff < folio_size(folio); subpgoff += PAGE_SIZE) {
ubuf->folios[upgcnt] = folio;
ubuf->offsets[upgcnt] = subpgoff;
++upgcnt;
if (++cur_pgcnt >= pgcnt) goto end;
}
pgoff = 0; /* reset only from the SECOND folio on */
Two consequences:
1) The first (huge) folio fills only
(folio_size - unaligned_pgoff) / PAGE_SIZE slots instead of
folio_size / PAGE_SIZE — so the accounting between
cur_pgcnt (pages of the requested range) and upgcnt (slots
filled) diverges from what the caller sized
(pgcnt = list[i].size >> PAGE_SHIFT, udmabuf.c:364).
2) Subsequent folios restart their offsets at 0 (the pgoff
reset at udmabuf.c:334), so the resulting buffer maps the
folios with displaced data: the first folio serves bytes
from the requested offset, but later folios serve from their
own start — the exported dma_buf does not contain the
contiguous range the caller asked for.
Also, when cur_pgcnt reaches pgcnt early (goto end at
udmabuf.c:324-325), upgcnt can end lower than the pgcnt the
user requested in some unaligned configurations — the declared
buffer size (exp_info.size = pagecount << PAGE_SHIFT,
udmabuf.c:281) then reflects the shortened fill.
There is no alignment check on the requested offsets/sizes
against the huge page size anywhere in udmabuf_create()
(udmabuf.c:342-382) or check_memfd_seals() — is_file_hugepages()
is accepted with no hstate-size validation.
== IMPACT ==
Data integrity: any consumer of the exported dma_buf (GPU
render paths, virtio buffers, cross-process sharing) reads
buffer contents displaced from the producer's layout — silent
data corruption within the buffer the caller created.
Reachability: udmabuf_create is reachable from any process via
/dev/udmabuf (misc device) with a memfd that it creates itself
(memfd_create with MFD_HUGETLB) — no special privileges.
Not a privilege escalation: the defect corrupts/reads the
caller's own buffer data, not kernel memory outside the pinned
folios.
== PROPOSED REMEDIATION ==
Either:
(a) Reject unaligned ranges for hugetlb memfds in
udmabuf_create(): if is_file_hugepages(memfd), require
(offset % huge_page_size(memfd)) == 0 for every list entry
(or round handling per the QEMU/KVM usage expectations);
or (b) handle the first folio's unaligned pgoff consistently:
count the first folio's contribution correctly so pgcnt and
upgcnt stay in sync (the fill loop's cur_pgcnt accounting
against the range pages does not currently match the
folio-relative subpgoff walk).
Option (b) preserves the ability to share sub-folio ranges.
== NOTES ==
- Found by static analysis of the 6.12.y stable tree,
cross-verified against mainline. No dynamic PoC built; the
reproduction is a memfd_create(MFD_HUGETLB) + UDMABUF_CREATE
with an unaligned offset, then comparing the mmap'd buffer
content against the memfd layout.
- Happy to test patches and send the formal fix.
- Please credit "kerhunt <[email protected]>".
Best regards,
kerhunt