Hi, On Mon Oct 5, 2026 at 8:41 AM CEST, Karl Mehltretter wrote: > DMABUF_DEBUG hands importers a copy of the exporter's sg_table without > the struct pages. An importer that uses them then fails, often far from > the cause and without a message that points at it. Where sg_dma_len() > is sg->length, the copy cannot hide the length either. > > Add DMABUF_DEBUG_WARN. With it the copy keeps the CPU side of the > exporter's table: page, offset and length of all orig_nents entries, > and the exporter's nents and orig_nents. A table without a CPU side > (orig_nents == 0) stays that way. Every entry of the copy is marked > with a new dma_flags bit, SG_DMA_DMABUF_DEBUG. > > sg_page(), sg_nents_for_len() and sg_split() test the bit and print a > rate limited message with a stack trace. sg_page() is mostly reached > through DMA and scatterlist helpers, so the trace is what names the > importer. These CPU-side accesses can continue after the report instead > of failing because the fields were cleared. > > The report is not a WARN(). It does not taint the kernel and does not > trigger panic_on_warn. > > Reads of sg->offset and sg->length that do not go through these > helpers are not seen. The option adds a test to every sg_page() and > selects NEED_SG_DMA_FLAGS. > > Strict mode remains the default and continues to remove the CPU-side > fields.
Thanks for this patch set! I build a 7.3-rc6 based kernel with this patch set included. And I (ofc) enabled DMABUF_DEBUG and DMABUF_DEBUG_WARN. I then tested it on my NanoPC-T6 Plus (RK3588 SoC) connected to my 1080p monitor and also on my NanoPC-T6 LTS (RK3588) connected to my 4K TV (which is HDMI 2.0, not 2.1, capable). On both devices I got an identical call trace. I did get a new (identical) one every time I resized the video window, ie going from windowed to full-screen and back again. ```sh [ 1091.981354] rc rc3: two consecutive events of type space [ 1104.670513] input: EDIFIER e235 (AVRCP) as /devices/virtual/input/input12 [ 1172.090231] devfreq fb000000.gpu: Couldn't update frequency transition information. [ 1189.403278] devfreq fb000000.gpu: Couldn't update frequency transition information. [ 1206.309295] input: EDIFIER e235 (AVRCP) as /devices/virtual/input/input13 [ 1268.968696] DMA-BUF: importer used the CPU side of an exporter's sg_table [ 1268.968710] CPU: 6 UID: 1000 PID: 29025 Comm: sway Not tainted 7.3-rc6+unreleased-arm64-cknow #1 PREEMPTLAZY Debian 7.3~rc6-3 [ 1268.968715] Hardware name: FriendlyElec NanoPC-T6 Plus (DT) [ 1268.968717] Call trace: [ 1268.968719] show_stack+0x20/0x38 (C) [ 1268.968726] dump_stack_lvl+0x60/0x80 [ 1268.968730] sg_dmabuf_cpu_access_warn.part.0+0x24/0x30 [ 1268.968734] sg_dmabuf_cpu_access_warn+0x34/0x38 [ 1268.968739] iommu_map_sg+0xc8/0x1e0 [ 1268.968745] rockchip_gem_iommu_map+0x8c/0x128 [rockchipdrm] [ 1268.968757] rockchip_gem_prime_import_sg_table+0x58/0x160 [rockchipdrm] [ 1268.968761] drm_gem_prime_import_dev+0xa8/0x1d0 [drm] [ 1268.968776] drm_gem_prime_fd_to_handle+0x1a4/0x280 [drm] [ 1268.968786] drm_prime_fd_to_handle_ioctl+0x40/0x58 [drm] [ 1268.968796] drm_ioctl_kernel+0xc8/0x140 [drm] [ 1268.968806] drm_ioctl+0x254/0x558 [drm] [ 1268.968816] __arm64_sys_ioctl+0xac/0x118 [ 1268.968821] invoke_syscall.constprop.0+0xac/0x110 [ 1268.968825] el0_svc_common.constprop.0+0x40/0xf0 [ 1268.968829] do_el0_svc+0x24/0x40 [ 1268.968832] el0_svc+0x40/0x260 [ 1268.968836] el0t_64_sync_handler+0xa0/0xe8 [ 1268.968838] el0t_64_sync+0x198/0x1a0 [ 1290.531300] sg_dmabuf_cpu_access_warn: 36 callbacks suppressed [ 1290.531310] DMA-BUF: importer used the CPU side of an exporter's sg_table [ 1290.531319] CPU: 5 UID: 1000 PID: 29025 Comm: sway Not tainted 7.3-rc6+unreleased-arm64-cknow #1 PREEMPTLAZY Debian 7.3~rc6-3 [ 1290.531323] Hardware name: FriendlyElec NanoPC-T6 Plus (DT) [ 1290.531325] Call trace: [ 1290.531327] show_stack+0x20/0x38 (C) [ 1290.531335] dump_stack_lvl+0x60/0x80 [ 1290.531338] sg_dmabuf_cpu_access_warn.part.0+0x24/0x30 [ 1290.531342] sg_dmabuf_cpu_access_warn+0x34/0x38 [ 1290.531347] iommu_map_sg+0xc8/0x1e0 [ 1290.531352] rockchip_gem_iommu_map+0x8c/0x128 [rockchipdrm] [ 1290.531364] rockchip_gem_prime_import_sg_table+0x58/0x160 [rockchipdrm] [ 1290.531368] drm_gem_prime_import_dev+0xa8/0x1d0 [drm] [ 1290.531384] drm_gem_prime_fd_to_handle+0x1a4/0x280 [drm] [ 1290.531394] drm_prime_fd_to_handle_ioctl+0x40/0x58 [drm] [ 1290.531404] drm_ioctl_kernel+0xc8/0x140 [drm] [ 1290.531414] drm_ioctl+0x254/0x558 [drm] [ 1290.531424] __arm64_sys_ioctl+0xac/0x118 [ 1290.531429] invoke_syscall.constprop.0+0xac/0x110 [ 1290.531433] el0_svc_common.constprop.0+0x40/0xf0 [ 1290.531437] do_el0_svc+0x24/0x40 [ 1290.531439] el0_svc+0x40/0x260 [ 1290.531444] el0t_64_sync_handler+0xa0/0xe8 [ 1290.531446] el0t_64_sync+0x198/0x1a0 [ 1294.197699] devfreq fb000000.gpu: Couldn't update frequency transition information. [ 1313.464098] sg_dmabuf_cpu_access_warn: 149 callbacks suppressed [ 1313.464121] DMA-BUF: importer used the CPU side of an exporter's sg_table [ 1313.464144] CPU: 2 UID: 1000 PID: 29025 Comm: sway Not tainted 7.3-rc6+unreleased-arm64-cknow #1 PREEMPTLAZY Debian 7.3~rc6-3 [ 1313.464156] Hardware name: FriendlyElec NanoPC-T6 Plus (DT) [ 1313.464163] Call trace: [ 1313.464168] show_stack+0x20/0x38 (C) ... ``` Full dmesg output is posted here: https://paste.sr.ht/~diederik/3d0020b4f3b0e8547397c20d5c10387d88d32341 Hopefully this helps fixing the underlaying issue. Cheers, Diederik > Assisted-by: LLM > Signed-off-by: Karl Mehltretter <[email protected]> > --- > > Notes: > Tested on v7.3-rc4-70-gfe2ec83746e5 with GCC 15.2.0. > > Builds, without warnings: > > - x86_64 with DMABUF_DEBUG_WARN=y, full build > - arm64 defconfig with DMABUF_DEBUG_WARN=y: Image, and the msm and > rockchip DRM drivers > - ARM926 (SAM9X75) with DMABUF_DEBUG_WARN=y and without > NEED_SG_DMA_LENGTH, full build > - ARM926 with DMABUF_DEBUG=n and without NEED_SG_DMA_FLAGS, full build. > dma-buf.o and lib/scatterlist.o disassemble as on the base commit, > except for __LINE__ constants. > - x86_64 with SG_SPLIT, W=1, the objects touched here: with > DMABUF_DEBUG_WARN=y, with DMABUF_DEBUG=y alone, and with > DMABUF_DEBUG=n > > Runtime, both in QEMU with local device models, not on hardware. > > Zynq with an AXI ADC, IIO DMABUF capture into udmabuf buffers, > NEED_SG_DMA_LENGTH=y. The IIO dmaengine buffer calls sg_nents_for_len() > on the attachment's table. With DMABUF_DEBUG=y alone the capture fails: > > DMABUF_ENQUEUE failed: Device or resource busy > > With DMABUF_DEBUG_WARN=y all 16 blocks arrive with the right data, the > kernel is not tainted, and the log has one report: > > DMA-BUF: importer used the CPU side of an exporter's sg_table > CPU: 0 UID: 0 PID: 48 Comm: iio-dmabuf Not tainted 7.3.0-rc4+ #2 > VOLUNTARY > Hardware name: Xilinx Zynq Platform > Call trace: > unwind_backtrace from show_stack+0x10/0x14 > show_stack from dump_stack_lvl+0x54/0x68 > dump_stack_lvl from sg_nents_for_len+0xd8/0xe4 > sg_nents_for_len from iio_dmaengine_buffer_submit_block+0x4c/0x33c > iio_dmaengine_buffer_submit_block from > iio_dma_buffer_submit_block.part.0+0x5c/0x104 > iio_dma_buffer_submit_block.part.0 from > iio_dma_buffer_enqueue_dmabuf+0x68/0xa0 > iio_dma_buffer_enqueue_dmabuf from iio_buffer_chrdev_ioctl+0x520/0x9a4 > iio_buffer_chrdev_ioctl from sys_ioctl+0x460/0x914 > sys_ioctl from ret_fast_syscall+0x0/0x4c > > x86_64 with intel-iommu, xHCI and a SUR40, capture into udmabuf > buffers. This kernel had a one-line test-only change in sur40 that > makes the vb2 queue use the USB controller's DMA device. There was no > report during boot. The capture gives one, from sg_page() in > iommu_dma_map_sg(): > > DMA-BUF: importer used the CPU side of an exporter's sg_table > CPU: 0 UID: 0 PID: 9 Comm: kworker/0:0 Not tainted 7.3.0-rc4+ #5 > PREEMPT(lazy) > Workqueue: events_freezable input_dev_poller_work > Call Trace: > <TASK> > dump_stack_lvl+0x4d/0x70 > iommu_dma_map_sg+0x4c0/0x1010 > __dma_map_sg_attrs+0x254/0x3b0 > dma_map_sg_attrs+0xe/0x20 > usb_hcd_map_urb_for_dma+0x7e0/0x1620 > usb_hcd_submit_urb+0x162/0x1af0 > usb_sg_wait+0x17c/0x550 > sur40_poll+0xb3e/0xff0 > input_dev_poller_work+0x54/0x90 > process_one_work+0x692/0xf90 > [...] > > The capture did not finish in this setup. The guest stalled in > xhci_queue_bulk_tx() after the report, so this run only shows the > report. > > The SG_DMA_* defines move up in scatterlist.h because sg_page() needs > the new one. > > drivers/dma-buf/Kconfig | 23 +++++++++++++++++++ > drivers/dma-buf/dma-buf.c | 44 ++++++++++++++++++++++++++++++++++++- > include/linux/scatterlist.h | 26 +++++++++++++++++++--- > lib/scatterlist.c | 22 +++++++++++++++++++ > lib/sg_split.c | 2 ++ > 5 files changed, 113 insertions(+), 4 deletions(-) > > diff --git a/drivers/dma-buf/Kconfig b/drivers/dma-buf/Kconfig > index e4f078a326a4..06177465091a 100644 > --- a/drivers/dma-buf/Kconfig > +++ b/drivers/dma-buf/Kconfig > @@ -49,6 +49,29 @@ config DMABUF_DEBUG > exporters. Specifically it validates that importers do not peek at the > underlying struct page when they import a buffer. > > +config DMABUF_DEBUG_WARN > + bool "Warn instead of hiding the pages from DMA-BUF importers" > + depends on DMABUF_DEBUG > + select NEED_SG_DMA_FLAGS > + help > + DMABUF_DEBUG normally hands importers a copy of the exporter's > + sg_table without the struct page pointers, so that an importer which > + uses them fails early. > + > + With this option the copy keeps the pages, offsets and lengths and is > + only marked. sg_page() and the helpers built on it, sg_nents_for_len() > + and sg_split() then print a rate limited message with a stack trace > + when they are used on such a table. The access itself continues > + instead of failing because the fields were cleared. The message is > + not a WARN(): it does not taint the kernel or trigger panic_on_warn. > + > + Importers that read sg->offset or sg->length directly are not > + noticed. The option adds a test to every sg_page() call. It selects > + NEED_SG_DMA_FLAGS, which adds dma_flags and may increase the size of > + struct scatterlist. > + > + If unsure, say N. > + > config DMABUF_KUNIT_TEST > tristate "KUnit tests for DMA-BUF" if !KUNIT_ALL_TESTS > depends on KUNIT > diff --git a/drivers/dma-buf/dma-buf.c b/drivers/dma-buf/dma-buf.c > index b3d311acb883..bd82b6b7f5ff 100644 > --- a/drivers/dma-buf/dma-buf.c > +++ b/drivers/dma-buf/dma-buf.c > @@ -57,6 +57,7 @@ > struct dma_buf_sg_table_wrapper { > struct sg_table *original; > struct sg_table wrapper; > + unsigned int alloc_nents; > }; > > static inline int is_dma_buf_file(struct file *); > @@ -874,11 +875,42 @@ void dma_buf_put(struct dma_buf *dmabuf) > } > EXPORT_SYMBOL_NS_GPL(dma_buf_put, "DMA_BUF"); > > +#ifdef CONFIG_DMABUF_DEBUG_WARN > +/* > + * Warn mode: the importer also gets the CPU side of the exporter's table, > + * marked so that sg_page() and friends can report who uses it. > + */ > +static void dma_buf_wrap_cpu_side(struct sg_table *to, struct sg_table *from) > +{ > + struct scatterlist *to_sg, *from_sg; > + int i; > + > + for_each_sgtable_sg(to, to_sg, i) > + to_sg->dma_flags |= SG_DMA_DMABUF_DEBUG; > + > + to_sg = to->sgl; > + for_each_sgtable_sg(from, from_sg, i) { > + sg_assign_page(to_sg, sg_page(from_sg)); > + to_sg->offset = from_sg->offset; > + to_sg->length = from_sg->length; > + to_sg = sg_next(to_sg); > + } > + > + to->nents = from->nents; > + to->orig_nents = from->orig_nents; > +} > +#else > +static void dma_buf_wrap_cpu_side(struct sg_table *to, struct sg_table *from) > +{ > +} > +#endif > + > static int dma_buf_wrap_sg_table(struct sg_table **sg_table) > { > struct scatterlist *to_sg, *from_sg; > struct sg_table *from = *sg_table; > struct dma_buf_sg_table_wrapper *to; > + unsigned int nents = from->nents; > int i, ret; > > if (!IS_ENABLED(CONFIG_DMABUF_DEBUG)) > @@ -888,14 +920,21 @@ static int dma_buf_wrap_sg_table(struct sg_table > **sg_table) > * To catch abuse of the underlying struct page by importers copy the > * sg_table without copying the page_link and give only the copy back to > * the importer. > + * > + * With DMABUF_DEBUG_WARN the copy keeps the CPU side, which can have > + * more entries than the DMA side. > */ > to = kzalloc_obj(*to); > if (!to) > return -ENOMEM; > > - ret = sg_alloc_table(&to->wrapper, from->nents, GFP_KERNEL); > + if (IS_ENABLED(CONFIG_DMABUF_DEBUG_WARN)) > + nents = max(nents, from->orig_nents); > + > + ret = sg_alloc_table(&to->wrapper, nents, GFP_KERNEL); > if (ret) > goto free_to; > + to->alloc_nents = nents; > > to_sg = to->wrapper.sgl; > for_each_sgtable_dma_sg(from, from_sg, i) { > @@ -910,6 +949,7 @@ static int dma_buf_wrap_sg_table(struct sg_table > **sg_table) > #endif > to_sg = sg_next(to_sg); > } > + dma_buf_wrap_cpu_side(&to->wrapper, from); > > to->original = from; > *sg_table = &to->wrapper; > @@ -929,6 +969,8 @@ static void dma_buf_unwrap_sg_table(struct sg_table > **sg_table) > > copy = container_of(*sg_table, typeof(*copy), wrapper); > *sg_table = copy->original; > + /* sg_free_table() needs the number of allocated entries */ > + copy->wrapper.orig_nents = copy->alloc_nents; > sg_free_table(©->wrapper); > kfree(copy); > } > diff --git a/include/linux/scatterlist.h b/include/linux/scatterlist.h > index 6de1a2434299..5dc8c134ca4c 100644 > --- a/include/linux/scatterlist.h > +++ b/include/linux/scatterlist.h > @@ -21,6 +21,13 @@ struct scatterlist { > #endif > }; > > +/* Bits in dma_flags, see below. sg_page() needs SG_DMA_DMABUF_DEBUG. */ > +#ifdef CONFIG_NEED_SG_DMA_FLAGS > +#define SG_DMA_BUS_ADDRESS (1 << 0) > +#define SG_DMA_SWIOTLB (1 << 1) > +#define SG_DMA_DMABUF_DEBUG (1 << 2) > +#endif > + > /* > * These macros should be used after a dma_map_sg call has been done > * to get bus addresses of each of the SG entries and their lengths. > @@ -188,11 +195,27 @@ static inline void sg_set_folio(struct scatterlist *sg, > struct folio *folio, > sg->length = len; > } > > +#ifdef CONFIG_DMABUF_DEBUG_WARN > +void sg_dmabuf_cpu_access_warn(void); > + > +/* Report use of the CPU side of a table that a DMA-BUF importer was given */ > +static inline void sg_dmabuf_cpu_access_check(struct scatterlist *sg) > +{ > + if (unlikely(sg->dma_flags & SG_DMA_DMABUF_DEBUG)) > + sg_dmabuf_cpu_access_warn(); > +} > +#else > +static inline void sg_dmabuf_cpu_access_check(struct scatterlist *sg) > +{ > +} > +#endif > + > static inline struct page *sg_page(struct scatterlist *sg) > { > #ifdef CONFIG_DEBUG_SG > BUG_ON(sg_is_chain(sg)); > #endif > + sg_dmabuf_cpu_access_check(sg); > return (struct page *)((sg)->page_link & ~SG_PAGE_LINK_MASK); > } > > @@ -303,9 +326,6 @@ static inline void sg_unmark_end(struct scatterlist *sg) > */ > #ifdef CONFIG_NEED_SG_DMA_FLAGS > > -#define SG_DMA_BUS_ADDRESS (1 << 0) > -#define SG_DMA_SWIOTLB (1 << 1) > - > /** > * sg_dma_is_bus_address - Return whether a given segment was marked > * as a bus address > diff --git a/lib/scatterlist.c b/lib/scatterlist.c > index 6ea40d2e6247..55a3697df881 100644 > --- a/lib/scatterlist.c > +++ b/lib/scatterlist.c > @@ -12,6 +12,27 @@ > #include <linux/bvec.h> > #include <linux/uio.h> > #include <linux/folio_queue.h> > +#include <linux/printk.h> > +#include <linux/ratelimit.h> > + > +#ifdef CONFIG_DMABUF_DEBUG_WARN > +/* > + * Not a WARN(): a wrong importer must not taint the kernel or trigger > + * panic_on_warn. sg_page() is mostly reached through DMA or scatterlist > + * helpers, so the stack trace is what identifies the importer. > + */ > +void sg_dmabuf_cpu_access_warn(void) > +{ > + static DEFINE_RATELIMIT_STATE(rs, DEFAULT_RATELIMIT_INTERVAL, 1); > + > + if (!__ratelimit(&rs)) > + return; > + > + pr_warn("DMA-BUF: importer used the CPU side of an exporter's > sg_table\n"); > + dump_stack_lvl(KERN_WARNING); > +} > +EXPORT_SYMBOL(sg_dmabuf_cpu_access_warn); > +#endif > > /** > * sg_nents - return total count of entries in scatterlist > @@ -54,6 +75,7 @@ int sg_nents_for_len(struct scatterlist *sg, u64 len) > return 0; > > for (nents = 0, total = 0; sg; sg = sg_next(sg)) { > + sg_dmabuf_cpu_access_check(sg); > nents++; > total += sg->length; > if (total >= len) > diff --git a/lib/sg_split.c b/lib/sg_split.c > index 24e8f5e48e63..cbbc7f9a206d 100644 > --- a/lib/sg_split.c > +++ b/lib/sg_split.c > @@ -33,6 +33,8 @@ static int sg_calculate_split(struct scatterlist *in, int > nents, int nb_splits, > } > > for_each_sg(in, sg, nents, i) { > + if (!mapped) > + sg_dmabuf_cpu_access_check(sg); > sglen = mapped ? sg_dma_len(sg) : sg->length; > if (skip > sglen) { > skip -= sglen;
