Skip to content

dma-mapping: add DMA_ATTR_NO_BOUNCE - #1608

Merged
RyanTheTide merged 1 commit into
integration/aurorafrom
j716c/dma-attr-no-bounce
Nov 12, 2026
Merged

RyanTheTide merged 1 commit into
integration/aurorafrom
j716c/dma-attr-no-bounce

Conversation

@erikwb

@erikwb erikwb commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

The problem

Some drivers map memory that a device uses for as long as the mapping exists
while the CPU keeps using the same memory through mappings of its own, with
no synchronization points in between: long-lived mappings of pinned userspace
memory, for instance. Such a driver needs the device to be given that memory
itself. Where the DMA layer cannot do that (the device cannot address the
memory, bouncing is forced, the device needs unencrypted memory), it
substitutes a bounce buffer from SWIOTLB, a copy that is brought up to date
only by the map, sync and unmap calls. The mapping succeeds, and the driver
has no supported way to ask for a failure instead or to tell afterwards that
it got a copy.

DMA_ATTR_REQUIRE_COHERENT makes the mapping functions fail where they would
bounce, but it stands for more: the request also fails for every device that
is not marked DMA coherent, and cache maintenance is left out. A driver whose
device is not marked DMA coherent cannot use it.

The change

DMA_ATTR_NO_BOUNCE is the bounce half of DMA_ATTR_REQUIRE_COHERENT alone.
A mapping request that carries it fails where the DMA core would otherwise go
through SWIOTLB:

  • dma-direct: where bouncing is forced for the device (swiotlb=force, a
    restricted DMA pool), and where the device cannot address the memory, needs
    it unencrypted, or the buffer is a kmalloc() one too small to be aligned
    for DMA. The second group fails without the address overflow warning,
    whether or not the system has a SWIOTLB that could have made the mapping:
    the refusal is what the caller asked for, and the caller may be passing on
    a request of an unprivileged process. Requests without the attribute warn
    as before.
  • dma-iommu: where a buffer that is not aligned to the IOMMU granule would be
    bounced (an untrusted device, an unaligned kmalloc() buffer), in
    iommu_dma_map_phys(), which the scatterlist path goes through, and in
    dma_iova_link().
  • A device that is mapped by dma_map_ops of its own and not bypassed to
    dma-direct: the request fails, since the DMA core cannot tell what those
    operations map onto.

What a refusal returns: DMA_MAPPING_ERROR from dma_map_page_attrs(),
dma_map_single_attrs() and dma_map_phys(); 0 from dma_map_sg_attrs();
-EIO from dma_map_sgtable() where a bounce buffer would have been needed
(the error of any segment that cannot be mapped, so it does not tell a
refused bounce buffer from another failure) and -EOPNOTSUPP for a device
with DMA operations of its own; -EPERM from dma_iova_link().

The device's coherence is not looked at, and cache maintenance is done as for
any other mapping unless DMA_ATTR_SKIP_CPU_SYNC is given as well. Only
mapping requests look at the attribute: the allocation functions,
dma_mmap_attrs() and dma_get_sgtable_attrs() ignore it, and the unmap and
sync paths need nothing new. A DMA_ATTR_MMIO mapping is never bounced and
is left as it is. Nothing changes for a caller that does not pass the
attribute. It is bit 15, decoded in the DMA trace events and described in
Documentation/core-api/dma-attributes.rst; the kernel-doc of
dma_map_sgtable() gains -EOPNOTSUPP, which DMA_ATTR_REQUIRE_COHERENT
already returned.

The commit adds a KUnit suite, dma_attr_no_bounce
(kernel/dma/no_bounce_kunit.c, CONFIG_DMA_NO_BOUNCE_KUNIT_TEST, with
kernel/dma/.kunitconfig). Its test device has a dma_range_map that
reaches all memory, at its CPU address, except one page, so the DMA layer can
serve it that page only through SWIOTLB. The suite is built in only (it looks
at the device's SWIOTLB state through functions that are not exported), and
it is not offered where the architecture has a phys_to_dma() of its own
(ARCH_HAS_PHYS_TO_DMA), which does not follow the range map. It skips where
the device would be mapped by DMA operations, where bouncing is forced, where
the device needs unencrypted memory, and where the DMA layer's translation
does not reach the one page and miss the other as the range map says. It
compares DMA addresses with that translation (phys_to_dma(),
dma_to_phys()), not with physical addresses, and a mapping that a test
expected to fail and that was made all the same is unmapped before the
failure is reported.

Testing

Hardware: M5 Max MacBook Pro (J716c) for the build configuration and one
boot; the tests of this pull request run in QEMU.

  • Build: integration/aurora ea10c4d plus this commit, with the J716c
    configuration (Image, modules, device trees). No new warning: the 31 Rust
    warnings and 12 C warnings of the base (none in DMA code).
  • The new suite, arm64 in QEMU, with 4K pages
    (./tools/testing/kunit/kunit.py run --arch=arm64 --kunitconfig=kernel/dma)
    and with 16K pages (the same with CONFIG_ARM64_16K_PAGES=y added):
    Ran 3 tests: passed: 3 both times, no warning in the kernel log.
    • no_bounce_refuses_a_bounce_buffer: an ordinary mapping of the page the
      device cannot address succeeds and is a bounce buffer (its DMA address
      lies outside the page, the memory there holds what the page held when it
      was mapped and does not follow a later store to the page). With the
      attribute the mapping fails, and a page the device addresses is mapped at
      its own address.
    • no_bounce_fails_the_whole_scatterlist: a scatterlist of a page the
      device addresses followed by the one it does not fails with -EIO from
      dma_map_sgtable() and with 0 from dma_map_sg_attrs(); the first page
      alone is mapped.
    • no_bounce_refusal_does_not_warn: for a device without SWIOTLB, the
      refused mapping leaves the kernel untainted by a warning.
  • The new suite, x86_64 in QEMU
    (./tools/testing/kunit/kunit.py run --arch=x86_64 --kunitconfig=kernel/dma/.kunitconfig):
    with --kernel_args iommu=soft, which sets up SWIOTLB,
    Ran 3 tests: passed: 3, no warning in the kernel log. Without it the
    guest has no SWIOTLB and the first test skips ("no SWIOTLB to show the
    bounce buffer with"): Ran 3 tests: passed: 2, skipped: 1. With
    --kernel_args swiotlb=force all three skip ("bouncing is forced for the
    device"): Ran 3 tests: skipped: 3.
  • The suite against broken dma-direct code, on arm64 (4K pages) and on x86_64
    with iommu=soft: with dma-direct changed to ignore the attribute where
    the device cannot address the page, Ran 3 tests: failed: 3; with the
    refusal placed after the test for an active SWIOTLB,
    Ran 3 tests: passed: 2, failed: 1, the third failing on the address
    overflow warning.
  • CONFIG_DMA_NO_BOUNCE_KUNIT_TEST=m asked for (the J716c configuration and
    x86_64 defconfig, each with CONFIG_KUNIT=y and with CONFIG_KUNIT=m):
    make olddefconfig reports symbol value 'm' invalid for DMA_NO_BOUNCE_KUNIT_TEST and leaves the option unset.
  • The other KUnit suites, arm64 with 16K pages in QEMU
    (--kunitconfig=drivers/soc/apple/sep.kunitconfig):
    Ran 534 tests: passed: 532, skipped: 2, no warning in the kernel log.
  • Host tests, as CI runs them: python3 -I tools/asahi/run-host-tests.py:
    219/224 passed. The five that fail (apple-dcp, g15-pb-growth-tests
    and g15-stats-drain-tests, each of the last two also with --mutations)
    fail identically on integration/aurora ea10c4d on the build host,
    which lacks the Rust 1.93.1 toolchain the g15 tests ask for.
  • scripts/checkpatch.pl: 0 errors; one warning, the MAINTAINERS note for
    the new files (kernel/dma/ has an entry).
  • On the J716c, kernel 7.3.0-ewb111+ (integration/aurora ea10c4d plus
    the two open J716c device tree pull requests and the kernel branches of
    this set, with this commit's kernel code as it is here; only the KUnit
    suite, which that configuration does not build, has changed since) boots
    with its root file system on the internal NVMe storage, Wi-Fi and the
    three displays working, and no Oops, BUG, WARNING, DMA mapping error or
    SWIOTLB error in the kernel log, and there drm/asahi: GEM_USERPTR, buffer objects over the caller's memory #1609 maps
    every user-memory object with the attribute and passes its tests.

Depends on

Nothing. #1609 is stacked on this branch and is the
attribute's first user.

Not covered

  • On hardware, the attribute is used only by drm/asahi: GEM_USERPTR, buffer objects over the caller's memory #1609, for a
    device that dma-direct maps and never bounces: there every mapping with it
    succeeds. A refusal has run in QEMU only.
  • In dma-direct, the suite produces the bounce through a page the device
    cannot address. A device that needs unencrypted memory and a small
    kmalloc() buffer take the same branch, and forced bouncing
    (swiotlb=force, a restricted DMA pool) the one before it; none of them
    was produced.
  • The dma-iommu branches (a buffer not aligned to the IOMMU granule of an
    untrusted device, in iommu_dma_map_phys() and dma_iova_link()) and the
    refusal for a device with dma_map_ops of its own are by reading the
    code: no test has a device of either kind.
  • The address overflow warning for requests without the attribute is
    unchanged code and was not triggered on purpose.
  • The suite ran on arm64 and x86_64 only. It is not offered on architectures
    with a phys_to_dma() of their own (PowerPC, several MIPS platforms), and
    it skips in a guest with memory encryption that forces unencrypted DMA; no
    such configuration was run. Other architectures were not built.

🤖 Generated with Claude Code

Some callers map memory that a device uses for as long as the mapping
exists while the CPU keeps using the same memory through mappings of
its own: long-lived mappings of pinned userspace memory, for instance.
They need the device to be given that memory itself. A bounce buffer is
a copy, brought up to date only by the map, sync and unmap calls, and
such a caller has no points at which to make them.

DMA_ATTR_REQUIRE_COHERENT makes the mapping functions fail where they
would bounce, but it stands for more: the request also fails for every
device that is not DMA coherent, and cache maintenance is left out. A
caller whose device is not marked DMA coherent, or that wants the usual
cache maintenance, cannot use it, and has no supported way to tell
afterwards whether a mapping was bounced.

DMA_ATTR_NO_BOUNCE is that first half alone. A mapping request that
carries it fails where it would otherwise go through SWIOTLB:

- dma-direct: where bouncing is forced for the device (swiotlb=force, a
  restricted DMA pool), and where the device cannot address the memory,
  needs it unencrypted, or the buffer is a kmalloc() one too small to
  be aligned for DMA. The second group fails without the address
  overflow warning, whether or not there is a SWIOTLB that could have
  made the mapping: the refusal is what the caller asked for, and the
  caller may be passing on a request of an unprivileged process.
  Requests without the attribute warn as before.
- dma-iommu: where a buffer that is not aligned to the IOMMU granule
  would be bounced (an untrusted device, an unaligned kmalloc() buffer),
  in iommu_dma_map_phys(), which the scatterlist path goes through, and
  in dma_iova_link().
- A device that is mapped by dma_map_ops of its own, and not bypassed
  to dma-direct: the request fails, with -EOPNOTSUPP from
  dma_map_sgtable(), since the DMA core cannot tell what those
  operations map onto. DMA_ATTR_REQUIRE_COHERENT is left as it is in
  this respect.

A refusal is DMA_MAPPING_ERROR from dma_map_page_attrs(),
dma_map_single_attrs() and dma_map_phys(), 0 from dma_map_sg_attrs(),
-EIO (the error of any segment that cannot be mapped) or -EOPNOTSUPP
from dma_map_sgtable(), and -EPERM from dma_iova_link().

The device's coherence is not looked at and cache maintenance is done
as for any other mapping. Only mapping requests look at the attribute:
the allocation functions, dma_mmap_attrs() and dma_get_sgtable_attrs()
ignore it, and unmapping needs nothing new, since the unmap paths find
no SWIOTLB slot behind such a mapping. A DMA_ATTR_MMIO mapping is never
bounced and is left as it is. The attribute is decoded in the DMA trace
events and described in dma-attributes.rst, and the kernel-doc of
dma_map_sgtable() gains -EOPNOTSUPP, which DMA_ATTR_REQUIRE_COHERENT
already returned.

A KUnit suite, dma_attr_no_bounce (CONFIG_DMA_NO_BOUNCE_KUNIT_TEST,
kernel/dma/.kunitconfig), covers the dma-direct refusal for a device
that cannot address a page: a test device whose dma_range_map reaches
all memory but that page. It is built in only, since it looks at the
device's SWIOTLB state, and is not offered where the architecture has
a phys_to_dma() of its own, which does not follow the range map. It
skips where the device would be mapped by DMA operations, where
bouncing is forced, where the device needs unencrypted memory, and
where the DMA layer's translation does not reach the one page and
miss the other as the range map says. DMA addresses are compared with
that translation, and a mapping that a test expected to fail and that
was made all the same is unmapped before the failure is reported.

Tested: the suite in QEMU on arm64, with 4K and with 16K pages, 3 of 3
passed, and on x86_64 with SWIOTLB set up (iommu=soft), 3 of 3 passed,
no warning in the kernel log:
- an ordinary mapping of the page succeeds and is a bounce buffer (its
  DMA address lies outside the page, the memory there holds what the
  page held when it was mapped and does not follow a later store to
  the page); with the attribute the mapping fails, and a page the
  device addresses is mapped at its own address;
- a scatterlist of a page the device addresses followed by the one it
  does not fails with -EIO from dma_map_sgtable() and 0 from
  dma_map_sg_attrs(), and the first page alone is mapped;
- for a device without SWIOTLB, the refusal does not taint the kernel
  with a warning.
On x86_64 without iommu=soft the guest has no SWIOTLB and the first
test skips (2 passed), and with swiotlb=force all three skip. With
dma-direct changed to ignore the attribute on that path all three
fail, and with the refusal placed after the test for an active SWIOTLB
the third fails on the overflow warning, on both architectures. Forced
bouncing, a device that needs unencrypted memory, small kmalloc()
buffers, the dma-iommu branches and the dma_map_ops branch are not
exercised by a test.

On the J716c (M5 Max MacBook Pro), a kernel with this change and its
first user, GEM_USERPTR in drm/asahi, boots with storage, Wi-Fi and the
displays working and no DMA mapping error in the kernel log.

Developed with Claude Code.
Signed-off-by: Erik Bourget <erik@ebourget.net>

@DjDeveloperr DjDeveloperr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at a4602b6, and run on an M5 Pro (J714s, T6050) as the base of #1609/#1610.

Verdict: approve.

Opt-in only. I read every changed path in direct.c, mapping.c and dma-iommu.c. Each new branch tests attrs & DMA_ATTR_NO_BOUNCE, so a caller that doesn't pass the attribute takes exactly the code it took before. That covers the address-overflow warning, the SWIOTLB fallback, the iommu bounce paths and dma_map_ops devices.

  • Bit 15 is free. __DMA_ATTR_ALLOC_CC_SHARED (bit 14) is the highest one used, and no driver or Rust binding defines a private attribute in that range.
  • In-tree, only #1609 passes the attribute.

Checked by reading:

  • The refusal for an ops device sits after the dma_map_direct()/arch_dma_map_*_direct() and use_dma_iommu() tests. A device that is bypassed to dma-direct, or uses dma-iommu, therefore gets the precise check, and only a real ops device gets the blanket refusal.
  • Like the existing is_cc_shared refusal, it returns before the trace and dma-debug hooks, so nothing is recorded for a mapping that was never made.
  • With the attribute, iommu_dma_map_phys() never bounces. On a later __iommu_dma_map() failure it still calls swiotlb_tbl_unmap_single(), but that is a no-op for a non-SWIOTLB address (swiotlb_find_pool()), as it already is for unbounced mappings without the attribute. The unmap paths need nothing new, as the description says.
  • dma-debug: the overlap exemption doesn't list the new attribute. A caller that maps the same pages twice without DMA_ATTR_SKIP_CPU_SYNC could therefore get an EEXIST report. #1609 passes SKIP_CPU_SYNC, which already suppresses that report, so this is only a note for future users.
  • The KUnit suite builds only with KUNIT=y and !ARCH_HAS_PHYS_TO_DMA. Its range map (two regions plus the zeroed terminator) is freed with the device by device_release().

On hardware (M5 Pro J714s, test kernel = integration/aurora 69a9fbb + d76f650 + 86708ce + #1608..#1611):
Booted on the M5 Pro (firmware 26A428) with 0 Oops/WARN. The kernel log's only DMA-related lines are the two boot-time SWIOTLB size notes, identical on a baseline kernel without the series. SWIOTLB is set up on this machine, so ordinary dma-direct mappings could bounce. Every GEM_USERPTR object #1609 created in the runs below was mapped with the attribute, and none was refused: the selftests, and the external_memory_host CTS cases through mesa #242. The GPU here is mapped by dma-direct and never needs a bounce, so, as on the J716c, the refusal paths themselves ran only in your QEMU suite.

Build shortcut, for the record: the M5 test kernel was built incrementally with include/linux/dma-mapping.h and include/drm/drm_gem_shmem_helper.h at their old mtimes, so files that only include them weren't recompiled. The hunks are a file-scope #define plus comment, and a same-type, same-position field rename; the compile proof is CI's, below.

CI on this head: only host-tests ran (green). sep-source's path filter skips kernel/dma, so this PR has no CI kernel build of its own. Its commit is compiled in #1609's green kunit build (arm64, DRM_ASAHI=y + Rust, 257/257: https://github.com/aurora-silicon/linux/actions/runs/43125697227/job/130877078058), which is stacked on it.

@RyanTheTide

Copy link
Copy Markdown
Contributor

Regression rows, fleet kernel with #1608 and #1609 against the same fleet kernel without them: M2 (G14G) Vulkan CTS subset 3155 pass / 697 not supported / 0 fail on both, glmark2 6364 6363 vs 6309-6360, vkmark 8637 8545 vs 8503 8534, s2idle 2/2; J700 (T8140) and J616s (T6040) rows are on #1609; no AVC, DART, GPU fault or SWIOTLB error on any boot.

@RyanTheTide
RyanTheTide merged commit 6ed0b6c into integration/aurora Nov 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants