Skip to content

fix(os): backport upstream DMA fixes for confidential guests - #1194

Merged
kvinwang merged 2 commits into
nextfrom
fix/os-dma-coco-backports
Sep 10, 2026
Merged

kvinwang merged 2 commits into
nextfrom
fix/os-dma-coco-backports

Conversation

@kvinwang

@kvinwang kvinwang commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

A CVM built from current next panics under bursty DMA:

BUG: scheduling while atomic
Voluntary context switch within RCU read-side critical section!
Kernel panic - not syncing: Fatal exception

with swiotlb_dyn_freeset_memory_encryptedmutex/schedule on the stack.

#1192 (CONFIG_SWIOTLB_DYNAMIC=y) is what makes this reachable — swiotlb_dyn_free() is
only compiled in under that symbol. But the defect it exposes is older and wider than
swiotlb, and it originates in our own 0001-x86-tdx-select-dma-direct-remap.patch.

atomic_pool_expand() registers every atomic DMA pool chunk in the gen_pool under the
address returned by dma_common_contiguous_remap():

#ifdef CONFIG_DMA_DIRECT_REMAP
	addr = dma_common_contiguous_remap(page, pool_size, ...);
#else
	addr = page_to_virt(page);
#endif
	...
	ret = gen_pool_add_virt(pool, (unsigned long)addr, page_to_phys(page), ...);

With CONFIG_DMA_DIRECT_REMAP=y that is a vmap alias, not the direct-map address. Three
call sites reconstruct a direct-map address from a struct page and hand it to the pool
release path, which cannot match the registration:

site reconstruction consequence
dma_direct_alloc_pages() returns dma_direct_alloc_from_pool()'s void * as struct page * type confusion — callers treat a CPU address as a page
dma_direct_free_pages() page_address(page) dma_free_from_pool() misses, so the page is re-encrypted and returned to the page allocator while the pool still owns it
swiotlb_init_io_tlb_pool() phys_to_virt(start) stored in pool->vaddr swiotlb_free_tlb() misses, falls through to set_memory_encrypted(), which takes vmap_purge_lock from an RCU callback — the panic above

0001 is what puts x86 into this configuration; upstream INTEL_TDX_GUEST does not select
DMA_DIRECT_REMAP. arm64 selects it unconditionally (arch/arm64/Kconfig), which is why
the upstream fixes come from Arm and are Tested-by pKVM and Hyper-V.

The first two rows do not involve CONFIG_SWIOTLB_DYNAMIC at all. They have been live in
every image built since 0001 landed.

Fix

Backport the four upstream commits that fix these, from the dma-mapping tree merged for
v7.3. None is in linux-6.18.y, so a stable bump does not pick them up.

patch upstream change
0003 94a04ad732c9 (Cc: stable) dma_direct_alloc_from_pool() returns struct page * again; CPU address moves to an out-parameter
0004 8a9dc4a028e7 look the pool chunk up by physical address in dma_direct_free_pages()
0005 57d29044d0f2 thread the allocator's virtual address into pool->vaddr instead of recomputing it
0006 e13d4d9a4915 free dynamic pools from a workqueue via queue_rcu_work()

Applied in upstream series order — these are patches 2, 5, 6 and 22 of a 32-patch series.
Only 0006 needed rebasing: upstream context carries struct io_tlb_pool.cc_shared, added
later in the same series by d5fd03cdd6c0. The rebase is context-only; the hunks are
otherwise identical to upstream.

CONFIG_SWIOTLB_DYNAMIC is deliberately left =y. #1192's reasoning still holds — this
fixes the defect it exposed rather than hiding it again. The kernel command line is
untouched.

Wired into both backends exactly as 0001/0002 already are: SRC_URI in the bbappend,
the patch loop in kernel-build.sh, and the cache key in kernel.sh — omitting the last
one would serve a stale kernel out of the component cache.

Verification

Build

Full kernel build through the repo's own script against 6.18.40, with the current
kernel.config in place (i.e. CONFIG_SWIOTLB_DYNAMIC=y from #1192 still on):

$ SOURCE_DATE_EPOCH=1757376000 JOBS=64 \
    ./os/mkosi/components/kernel/kernel-build.sh /tmp/kb-verify /tmp/kb-verify/stage
...
  DEPMOD  /tmp/kb-verify/stage/lib/modules/6.18.40-dstack
[exited with code 0]

All six patches applied at --fuzz=0, check-kernel-config.sh and
check-lxc-kernel-config.sh passed, bzImage and modules built and installed.
Resulting config:

CONFIG_INTEL_TDX_GUEST=y   CONFIG_AMD_MEM_ENCRYPT=y   CONFIG_SWIOTLB_DYNAMIC=y
CONFIG_DMA_COHERENT_POOL=y CONFIG_DMA_DIRECT_REMAP=y

swiotlb_dyn_free — the symbol on the panic stack — no longer exists in System.map;
swiotlb_dyn_free_work (0006) and dma_free_from_pool_page (0004) do.

Applied at --fuzz=0 against both backends' real source trees:

tree version result
mkosi component-work 6.18.40 all four apply, --fuzz=0
Yocto work-shared/dstack/kernel-source 6.18.24 all four apply, --fuzz=0

kernel/dma/swiotlb.c, include/linux/swiotlb.h and kernel/dma/pool.c are byte-identical
between 6.18.24 and 6.18.40, so one patch set covers both backends. kernel/dma/direct.c
differs by a single unrelated hunk (breakfallthrough in the P2PDMA switch), not in
any region these patches touch.

All four also apply under git apply (strict, no fuzz), which is what Yocto's do_patch
uses. reuse lint and prek run --from-ref origin/next --to-ref HEAD both pass.

A/B reproduction on real TDX hardware

Both defects need force_dma_unencrypted() to be true, so neither is reachable outside a
real TD — a plain guest cannot exercise them. Two kernels were built from the same script,
differing only in whether 0003-0006 are applied (0001+0002 kept in both, so the
baseline is exactly the kernel that ships today), and each was booted as a TD directly via
-kernel with a minimal initramfs, isolating the kernel as the only variable.

The baseline source confirms the 0003 defect verbatim — a struct page * function
returning the pool's CPU address:

struct page *dma_direct_alloc_pages(struct device *dev, size_t size, ...)
{
	if (force_dma_unencrypted(dev) && dma_direct_use_pool(dev, gfp))
		return dma_direct_alloc_from_pool(dev, size, dma_handle, gfp);

Load was generated by a small out-of-tree module driving the two paths directly. Note that
CONFIG_DEBUG_FS is off in kernel.config, so io_tlb_transient_nslabs does not exist;
the module walks dev->dma_io_tlb_pools and counts pool->transient itself. Without that
count a "no crash" result would prove nothing, because it would not show the dynamic path
was reached at all.

test baseline (0001+0002 only) with 0003-0006
A — dma_alloc_pages(GFP_ATOMIC) / dma_free_pages fails on the first call: returns ff505e40c0169000, a remap address whose PFN is invalid passes 4096 alloc/free cycles
B — exhaust swiotlb=2048,1,force (4 MiB) with 256 KiB maps, then unmap WARNING ... smp.c:809 with swiotlb_dyn_free on the stack clean; transient_pools=6
B under load, 200 cycles / 8 vCPU one WARNING (it is WARN_ON_ONCE) clean
B under load, 1000 cycles / 16 vCPU BUG: scheduling while atomic, guest wedges and has to be killed at the 300 s timeout 1000/1000 cycles, clean shutdown

Both sides of row B allocated the same six transient pools before unmapping, so the two
runs did the same work; only the teardown differs.

The reproduced stack matches the path described in the report:

BUG: scheduling while atomic: rmmod/159/0x00000101
<IRQ>
 __schedule_bug → __schedule → schedule_preempt_disabled
 __mutex_lock.constprop.0          <- sleeping lock taken from softirq
 _vm_unmap_aliases
 __set_memory_enc_pgtable
 set_memory_encrypted
 swiotlb_free_tlb.part.0
 swiotlb_dyn_free                  <- RCU callback
 rcu_core → handle_softirqs → irq_exit_rcu

which is literally what 0006's commit message describes ("may allocate page tables or take
sleeping locks").

Two details worth noting for anyone triaging this in the field:

  • The guard is WARN_ON_ONCE(!in_task()), so a production guest emits one warning and
    every later transient-pool free takes the same path silently. The observed failure mode is
    "the CVM disappeared", not "the log filled with warnings".
  • Test A's 4096 cycles pass through a 512 KiB atomic pool — 32× its capacity — so a leaking
    free (the 0004 defect) would have exhausted it long before the end.

Guest image and CVM boot

A full guest image was built from this branch with the mkosi backend and deployed as a real
TDX CVM through dstack-vmm. All nine components built cold; kernel was a cache miss,
which exercises the kernel.sh cache-key hookup described above — had it been omitted, the
image would have shipped a stale cached kernel and every check in this PR would still pass.

The CVM reached Boot Progress: done (app keys → data disk → gateway setup → docker →
containers) with no kernel warnings on the console, and /proc/kallsyms inside the running
guest confirms the shipped kernel carries the fix:

uname: Linux 6.18.40-dstack #1 SMP PREEMPT_DYNAMIC x86_64
swiotlb_dyn_free_work    (0006) = 1
swiotlb_dyn_free         (old)  = 0
dma_free_from_pool_page  (0004) = 1

Not verified

  • The CVM boot test does not discriminate between patched and unpatched. That guest's
    static bounce pool is 256 MiB; boot plus one container comes nowhere near exhausting it,
    so no transient pool is ever allocated and the fixed teardown path never runs. An
    unpatched image boots identically. The fixed paths were exercised only in the directed
    harness above, never inside the production image.
  • No GCP or AWS boot test. Only QEMU TDX was covered.
  • No KMS or Gateway in the CVM run, so attestation and key provisioning were not
    exercised end to end. test-suites/full-stack-compose is the suite that would cover this.
  • The hang is a race, not deterministic. 200 cycles on 8 vCPUs produced only the
    WARN_ON_ONCE; 1000 cycles on 16 vCPUs wedged the guest. The claim is "the unpatched
    kernel enters the unsafe path under identical load and hangs under pressure", not "it
    hangs every time".
  • 0004 was not isolated from 0003. No third kernel with only 0003 applied was
    built, so the atomic-pool result covers the pair together.
  • The reproduced signature is BUG: scheduling while atomic from the same call path rather
    than a byte-identical copy of the originally reported panic text.

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.

Copilot AI lite review requested due to automatic review settings September 9, 2026 15:46

Copilot AI 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.

🔵 Needs a closer look

It changes guest-kernel DMA/SWIOTLB behavior without boot/exhaustion validation, so it needs human review and runtime testing before merge.

Pull request overview

This PR backports four upstream DMA/SWIOTLB fixes into the guest kernel build used for dstack confidential guests, addressing panics triggered by freeing dynamic SWIOTLB pools from atomic/RCU context and fixing atomic DMA pool bookkeeping under CONFIG_DMA_DIRECT_REMAP.

Changes:

  • Add Yocto SRC_URI entries for four upstream backport patches (0003–0006) covering dma-direct, dma pool freeing, and swiotlb dynamic pool handling.
  • Add the same patch set to the mkosi kernel build patch loop to keep both backends aligned.
  • Extend the mkosi kernel component cache key to include the new patch files (preventing stale cached kernels).
File summaries
File Description
os/yocto/layers/meta-dstack/recipes-kernel/linux/linux-yocto%.bbappend Wires in the four upstream backport patches via SRC_URI with detailed rationale/comments.
os/yocto/layers/meta-dstack/recipes-kernel/linux/files/0003-dma-direct-return-struct-page-from-alloc-from-pool.patch Backport: restores dma_direct_alloc_from_pool() returning struct page * and threads CPU addr via out-param.
os/yocto/layers/meta-dstack/recipes-kernel/linux/files/0004-dma-pool-free-atomic-pool-pages-by-physical-address.patch Backport: introduces page-based atomic-pool free helper keyed by physical address.
os/yocto/layers/meta-dstack/recipes-kernel/linux/files/0005-swiotlb-preserve-allocation-virtual-address.patch Backport: preserves allocator-provided virtual address for dynamic swiotlb pools.
os/yocto/layers/meta-dstack/recipes-kernel/linux/files/0006-swiotlb-free-dynamic-pools-from-process-context.patch Backport: switches dynamic pool teardown from RCU callback to queue_rcu_work() process context.
os/mkosi/components/kernel/kernel.sh Updates mkosi kernel cache key to include the new patch files.
os/mkosi/components/kernel/kernel-build.sh Applies the new patch set during mkosi kernel source preparation.
Review details
  • Files reviewed: 7/7 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.

@kvinwang
kvinwang merged commit e73985d into next Sep 10, 2026
15 checks passed
@kvinwang
kvinwang deleted the fix/os-dma-coco-backports branch September 10, 2026 03:01
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.

2 participants