fix(os): let the SWIOTLB bounce buffer grow at runtime - #1192
Merged
Merged
Conversation
Every DMA in a CVM is bounced through SWIOTLB, because the host cannot reach
the guest's private memory, and both guest kernels shipped with
CONFIG_SWIOTLB_DYNAMIC off. The pool a guest gets at boot is therefore the only
pool it ever gets, sized by arch/x86/mm/mem_encrypt.c at 6% of RAM clamped to
1 GiB. Bursty virtio traffic runs it dry:
virtio-pci 0000:00:02.0: swiotlb buffer is full (sz: 262144 bytes),
total 524288 (slots), used 523564 (slots)
524288 slots is that 1 GiB cap, 262144 bytes is IO_TLB_SIZE * IO_TLB_SEGSIZE,
the largest mapping swiotlb can serve at all. The mapping fails with -ENOMEM
and the caller reports dropped packets or block I/O errors, so nothing on the
path to the operator names SWIOTLB.
Raising the pool with a swiotlb= command line argument was the alternative and
is worse on three counts. The command line is measured into the RTMRs and has
to stay in step across dstack-uki.bb and os/image/kernel-cmdline.sh, for a
value that can only guess at the peak. The boot pool has to fit below 4 GiB
regardless: the x86 KVM path never sets SWIOTLB_ANY (only Xen does, in
arch/x86/kernel/pci-dma.c), so swiotlb_memblock_alloc() takes the
memblock_alloc_low() branch. And swiotlb_init_remap() halves a request it
cannot satisfy and carries on, so a larger value is not even reliably granted.
Pools allocated at runtime are bounded by the device's DMA mask instead, which
for virtio is 64-bit.
The cost falls on the unmap and sync paths, where swiotlb_find_pool() stops
being an inline range check against the single pool and becomes an RCU walk of
the pool list. The synchronous allocation path also needs
CONFIG_DMA_COHERENT_POOL, since force_dma_unencrypted() is true here and
GFP_NOWAIT cannot block; CONFIG_AMD_MEM_ENCRYPT already selects it, confirmed
=y in the built config.
Asserted in parity.json so the mkosi fragment cannot drift from the Yocto one,
on top of check-kernel-config.sh, which both backends already run against their
fragments and which turns each of these lines into a build-time assertion.
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
It changes guest kernel behavior/config (affecting image hashes/RTMRs) and warrants full platform boot/exhaustion testing before merge.
Pull request overview
Enables runtime growth of the SWIOTLB bounce buffer in dstack guest kernels to avoid DMA mapping failures under bursty virtio I/O, and adds a parity assertion so mkosi can’t drift from the Yocto kernel config.
Changes:
- Enable
CONFIG_SWIOTLB_DYNAMIC=yin the Yocto kernel config fragment. - Enable
CONFIG_SWIOTLB_DYNAMIC=yin the mkosi kernel config fragment. - Require
CONFIG_SWIOTLB_DYNAMIC=yviaos/mkosi/parity.jsonto enforce backend parity.
File summaries
| File | Description |
|---|---|
| os/yocto/layers/meta-dstack/recipes-kernel/linux/files/dstack.cfg | Enables CONFIG_SWIOTLB_DYNAMIC for the Yocto guest kernel build. |
| os/mkosi/components/kernel/kernel.config | Enables CONFIG_SWIOTLB_DYNAMIC for the mkosi guest kernel build. |
| os/mkosi/parity.json | Adds CONFIG_SWIOTLB_DYNAMIC=y to the required kernel config assertions to prevent drift. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every DMA in a CVM is bounced through SWIOTLB, because the host cannot reach the guest's private memory. Both guest kernels ship with
CONFIG_SWIOTLB_DYNAMICoff, so the pool allocated at boot is the only pool the guest ever gets —arch/x86/mm/mem_encrypt.csizes it at 6% of RAM clamped to 1 GiB:Bursty virtio traffic runs it dry:
total 524288slots × 2 KiB is exactly that 1 GiB cap.sz: 262144isIO_TLB_SIZE * IO_TLB_SEGSIZE, the largest mapping swiotlb can serve at all, so the device was asking for the hardest possible allocation against a 99.9% full pool. The mapping fails with-ENOMEMand the caller reports dropped packets or block I/O errors — nothing on the path to the operator names SWIOTLB, which is what makes this expensive to diagnose.Neither backend had the symbol:
# CONFIG_SWIOTLB_DYNAMIC is not setin the builtkernel-configx86_64_defconfig,default ninkernel/dma/Kconfig→olddefconfigsettles onnFix
Set
CONFIG_SWIOTLB_DYNAMIC=yin both guest kernel fragments, and assert it inparity.jsonso the mkosi fragment cannot drift from the Yocto one.check-kernel-config.sh, which both backends already run, turns each fragment line into a build-time assertion, so a defconfig change that flips this fails the build instead of shipping.Why not just raise
swiotlb=Enlarging the fixed pool from the kernel command line was the alternative. It is worse on three counts:
dstack-uki.bbandos/image/kernel-cmdline.sh— for a value that can only guess at the peak.swiotlb_memblock_alloc()picksmemblock_alloc_low()unlessSWIOTLB_ANYis set, and on x86 that flag is only set on the Xen path (arch/x86/kernel/pci-dma.c), never for KVM/TDX. So the pool must fit underARCH_LOW_ADDRESS_LIMIT, competing with the kernel image, e820 reservations and the MMIO hole.swiotlb_init_remap()halvesnslabsand retries until it fits, leaving only apr_info. Asking for 4 GiB and quietly getting 1 GiB looks exactly like not having changed anything.Runtime pools have none of these constraints — the per-device transient pool uses
min_not_zero(*dev->dma_mask, dev->bus_dma_limit), which for virtio is 64-bit.Costs, stated plainly
swiotlb_find_pool()stops being an inline range check against the single pool and becomessmp_rmb()+ an RCU walk ofmem->poolsanddev->dma_io_tlb_pools. In a CVMforce_bounceis always on, so this is taken for every bounced DMA. With one pool the walk is one iteration.CONFIG_DMA_COHERENT_POOLis load-bearing. The synchronous path allocates withGFP_NOWAIT, andforce_dma_unencrypted()is true here, soswiotlb_alloc_tlb()takes the atomic-pool branch and returnsNULLoutright without that symbol. It is already=y—CONFIG_AMD_MEM_ENCRYPTselects it (arch/x86/Kconfig) and the built Yocto config confirms it.swiotlb_adjust_nareas(num_possible_cpus())), and dynamic growth runs as a singlework_structonsystem_wq, not a pool of threads.Verification
Applied the symbol to the real production Yocto config and ran
olddefconfigagainst the 6.18.39 source tree to confirm the dependency is satisfiable and nothing else moves:The symbol survives
olddefconfigand is the only functional difference; the filtered-out entries are all toolchain probes that differ because the run used the host gcc 13.3 rather than the Yocto cross gcc 15.3.CONFIG_DMA_COHERENT_POOL=yis present in the result.Not yet verified: a boot on real hardware, and behaviour under an actual exhaustion burst. Both backends need a full image build for that, and this changes the guest kernel, so it wants a boot test on QEMU TDX, GCP and AWS before merge.
Note for reviewers
This changes the guest kernel and therefore the OS image hash and RTMRs. Deployments need the new image registered in the KMS whitelist; it is not a drop-in for running CVMs.
The command line is deliberately untouched, so the three places that have to agree on it stay untouched too.