Repository navigation
dma-mapping: add DMA_ATTR_NO_BOUNCE - #1608
Conversation
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
left a comment
There was a problem hiding this comment.
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()anduse_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_sharedrefusal, 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 callsswiotlb_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_SYNCcould 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=yand!ARCH_HAS_PHYS_TO_DMA. Its range map (two regions plus the zeroed terminator) is freed with the device bydevice_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.
|
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. |
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_COHERENTmakes the mapping functions fail where they wouldbounce, 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_BOUNCEis the bounce half ofDMA_ATTR_REQUIRE_COHERENTalone.A mapping request that carries it fails where the DMA core would otherwise go
through SWIOTLB:
swiotlb=force, arestricted 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.
bounced (an untrusted device, an unaligned kmalloc() buffer), in
iommu_dma_map_phys(), which the scatterlist path goes through, and indma_iova_link().dma_map_opsof its own and not bypassed todma-direct: the request fails, since the DMA core cannot tell what those
operations map onto.
What a refusal returns:
DMA_MAPPING_ERRORfromdma_map_page_attrs(),dma_map_single_attrs()anddma_map_phys(); 0 fromdma_map_sg_attrs();-EIOfromdma_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
-EOPNOTSUPPfor a devicewith DMA operations of its own;
-EPERMfromdma_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_SYNCis given as well. Onlymapping requests look at the attribute: the allocation functions,
dma_mmap_attrs()anddma_get_sgtable_attrs()ignore it, and the unmap andsync paths need nothing new. A
DMA_ATTR_MMIOmapping is never bounced andis 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 ofdma_map_sgtable()gains-EOPNOTSUPP, whichDMA_ATTR_REQUIRE_COHERENTalready returned.
The commit adds a KUnit suite,
dma_attr_no_bounce(
kernel/dma/no_bounce_kunit.c,CONFIG_DMA_NO_BOUNCE_KUNIT_TEST, withkernel/dma/.kunitconfig). Its test device has adma_range_mapthatreaches 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 wherethe 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 testexpected 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.
integration/auroraea10c4d plus this commit, with the J716cconfiguration (Image, modules, device trees). No new warning: the 31 Rust
warnings and 12 C warnings of the base (none in DMA code).
(
./tools/testing/kunit/kunit.py run --arch=arm64 --kunitconfig=kernel/dma)and with 16K pages (the same with
CONFIG_ARM64_16K_PAGES=yadded):Ran 3 tests: passed: 3both times, no warning in the kernel log.no_bounce_refuses_a_bounce_buffer: an ordinary mapping of the page thedevice 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 thedevice addresses followed by the one it does not fails with
-EIOfromdma_map_sgtable()and with 0 fromdma_map_sg_attrs(); the first pagealone is mapped.
no_bounce_refusal_does_not_warn: for a device without SWIOTLB, therefused mapping leaves the kernel untainted by a warning.
(
./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 theguest 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=forceall three skip ("bouncing is forced for thedevice"):
Ran 3 tests: skipped: 3.with
iommu=soft: with dma-direct changed to ignore the attribute wherethe device cannot address the page,
Ran 3 tests: failed: 3; with therefusal placed after the test for an active SWIOTLB,
Ran 3 tests: passed: 2, failed: 1, the third failing on the addressoverflow warning.
CONFIG_DMA_NO_BOUNCE_KUNIT_TEST=masked for (the J716c configuration andx86_64
defconfig, each withCONFIG_KUNIT=yand withCONFIG_KUNIT=m):make olddefconfigreportssymbol value 'm' invalid for DMA_NO_BOUNCE_KUNIT_TESTand leaves the option unset.(
--kunitconfig=drivers/soc/apple/sep.kunitconfig):Ran 534 tests: passed: 532, skipped: 2, no warning in the kernel log.python3 -I tools/asahi/run-host-tests.py:219/224 passed. The five that fail (apple-dcp,g15-pb-growth-testsand
g15-stats-drain-tests, each of the last two also with--mutations)fail identically on
integration/auroraea10c4d 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 forthe new files (
kernel/dma/has an entry).integration/auroraea10c4d plusthe 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
device that dma-direct maps and never bounces: there every mapping with it
succeeds. A refusal has run in QEMU only.
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 themwas produced.
untrusted device, in
iommu_dma_map_phys()anddma_iova_link()) and therefusal for a device with
dma_map_opsof its own are by reading thecode: no test has a device of either kind.
unchanged code and was not triggered on purpose.
with a
phys_to_dma()of their own (PowerPC, several MIPS platforms), andit 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