From 43722ce97eb834770a0b8140e09e2c686fc300bc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:00:30 +1100 Subject: [PATCH 1/4] media: apple: avd: keep DMA blocked once the decoder is retired When the recovery budget runs out, or a reset fails, the decoder is retired: its jobs fail without touching the hardware until the module is reloaded. The frame that triggered it is still finished with an error, so its buffers go back to userspace while the engine that just timed out may still be running, which is the DMA-after-free pattern behind the J616s hangs. Retire through the same first steps as a recovery: disable the interrupt, stop the CM3 and attach the empty IOMMU domain, then leave it attached. A still-running engine can then only fault at the DART. A reset that fails after DMA was blocked keeps it blocked as well. Detach the empty domain at remove so a reload starts from the DMA domain. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-drv.c | 29 +++++++++++++++++++++- drivers/media/platform/apple/avd/avd.h | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index 462df919265d5..7ffc93034e4b2 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -238,6 +238,27 @@ int avd_submit_job(struct avd_ctx *ctx) * to drain, and only then assert the pmgr reset. Recover sparingly, and * retire the decoder after a failed or a third recovery within a minute. */ +/* + * Retiring: stop the CM3 and leave the decoder's DMA blocked at its DART + * until the module is unloaded, so an engine that is still running (the + * frame timed out, after all) can only fault, never write memory whose + * buffers are about to go back to userspace. + */ +static void avd_park(struct avd_dev *avd) +{ + if (pm_runtime_resume_and_get(avd->dev) < 0) + return; + disable_irq(avd->irq); + avd_shutdown(avd); + if (avd->empty_domain && !avd->dma_parked) { + if (iommu_attach_device(avd->empty_domain, avd->dev)) + dev_err(avd->dev, "retire: could not block DMA\n"); + else + avd->dma_parked = true; + } + pm_runtime_put_autosuspend(avd->dev); +} + static int avd_reset(struct avd_dev *avd) { int ret = 0; @@ -251,6 +272,7 @@ static int avd_reset(struct avd_dev *avd) } if (++avd->recoveries > AVD_RECOVER_MAX) { ret = -EBUSY; + avd_park(avd); goto retire; } if (avd->last_recover && @@ -278,7 +300,10 @@ static int avd_reset(struct avd_dev *avd) ret = reset_control_reset(avd->rstc); if (ret) dev_err(avd->dev, "recover: reset failed: %d\n", ret); - if (avd->empty_domain) { + if (avd->empty_domain && ret) { + /* A decoder that failed its reset keeps its DMA blocked. */ + avd->dma_parked = true; + } else if (avd->empty_domain) { dev_warn(avd->dev, "recover: restore DMA\n"); iommu_detach_device(avd->empty_domain, avd->dev); if (avd->domain && iommu_get_domain_for_dev(avd->dev) != avd->domain) { @@ -1087,6 +1112,8 @@ static void avd_remove(struct platform_device *pdev) avd_v4l2_cleanup(avd); + if (avd->dma_parked) + iommu_detach_device(avd->empty_domain, avd->dev); if (avd->empty_domain) iommu_domain_free(avd->empty_domain); diff --git a/drivers/media/platform/apple/avd/avd.h b/drivers/media/platform/apple/avd/avd.h index 20411c991ae31..10cd73a90977b 100644 --- a/drivers/media/platform/apple/avd/avd.h +++ b/drivers/media/platform/apple/avd/avd.h @@ -241,6 +241,8 @@ struct avd_dev { unsigned int recoveries; unsigned long last_recover; bool retired; + /* Retired with the DART on the empty domain; detached at remove. */ + bool dma_parked; const struct avd_variant *variant; unsigned int capabilities; From eebbd2219e1edd981962ab09d3f7bdebbec21193 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:00:40 +1100 Subject: [PATCH 2/4] media: apple: avd: rest the decoder instead of retiring it on a reset storm A third recovery within a minute retired the decoder until the module was reloaded. That is too harsh for a desktop: on an M2 the H.264 conformance suite retired it within three seconds, because a few vectors use features the hardware rejects with a VP error, and every later stream failed until a reload. A user with a bad file would lose hardware decode until a reboot. Recoveries are now quiesced (DMA blocked at the DART before the reset) and have held up under forced errors, so the budget only needs to bound their rate. When it runs out, park the decoder (interrupt off, CM3 stopped, DMA blocked) and fail new frames without touching it until the budget window ends. The next frame after that resets it with DMA still blocked, restores the DMA domain and boots the firmware. Only a reset, restore or boot that fails retires the decoder. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-drv.c | 80 ++++++++++++++++++---- drivers/media/platform/apple/avd/avd.h | 5 +- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index 7ffc93034e4b2..13f87523226d1 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -235,20 +235,22 @@ int avd_submit_job(struct avd_ctx *ctx) * frame), so: stop the interrupt handler and the CM3 first, block the * decoder's DMA at its DART so a transfer still in flight faults instead * of reaching memory that may already be reused, give such transfers time - * to drain, and only then assert the pmgr reset. Recover sparingly, and - * retire the decoder after a failed or a third recovery within a minute. + * to drain, and only then assert the pmgr reset. Recover sparingly: a + * third recovery within a minute parks the decoder (DMA blocked) until the + * minute is over, and only a failed reset retires it. */ /* - * Retiring: stop the CM3 and leave the decoder's DMA blocked at its DART - * until the module is unloaded, so an engine that is still running (the - * frame timed out, after all) can only fault, never write memory whose - * buffers are about to go back to userspace. + * Parking (resting or retired): stop the CM3 and leave the decoder's DMA + * blocked at its DART, so an engine that is still running (the frame timed + * out, after all) can only fault, never write memory whose buffers are about + * to go back to userspace. */ static void avd_park(struct avd_dev *avd) { if (pm_runtime_resume_and_get(avd->dev) < 0) return; disable_irq(avd->irq); + avd->irq_parked = true; avd_shutdown(avd); if (avd->empty_domain && !avd->dma_parked) { if (iommu_attach_device(avd->empty_domain, avd->dev)) @@ -271,9 +273,19 @@ static int avd_reset(struct avd_dev *avd) avd->recoveries = 0; } if (++avd->recoveries > AVD_RECOVER_MAX) { - ret = -EBUSY; + /* + * Out of budget: stop the decoder with its DMA blocked and let + * it rest until the window ends. Frames fail without touching + * the hardware meanwhile; the next frame after that recovers + * it. Bad streams (or conformance vectors using features the + * hardware lacks) must not cost the user the decoder until a + * reboot. + */ avd_park(avd); - goto retire; + avd->rest_until = avd->recover_window_start + AVD_RECOVER_WINDOW; + dev_warn(avd->dev, "decoder resting after repeated resets, new frames fail for up to %u s\n", + jiffies_to_msecs(avd->rest_until - jiffies) / 1000 + 1); + return -EBUSY; } if (avd->last_recover && time_before(jiffies, avd->last_recover + msecs_to_jiffies(AVD_RECOVER_GAP_MS))) @@ -328,8 +340,44 @@ static int avd_reset(struct avd_dev *avd) return 0; retire: avd->retired = true; - dev_err(avd->dev, "decoder retired after %s (%d): reload the module to use it again\n", - ret == -EBUSY ? "repeated resets" : "a failed reset", ret); + dev_err(avd->dev, "decoder retired after a failed reset (%d): reload the module to use it again\n", + ret); + return ret; +} + +/* Bring a rested decoder back: reset with DMA still blocked, restore, boot. */ +static int avd_wake(struct avd_dev *avd) +{ + int ret; + + ret = pm_runtime_resume_and_get(avd->dev); + if (ret < 0) + return ret; + dev_warn(avd->dev, "recover: wake after rest\n"); + avd_shutdown(avd); + ret = reset_control_reset(avd->rstc); + if (!ret && avd->dma_parked) { + iommu_detach_device(avd->empty_domain, avd->dev); + if (avd->domain && iommu_get_domain_for_dev(avd->dev) != avd->domain) + ret = -EIO; + else + avd->dma_parked = false; + } + if (!ret) + ret = avd_boot(avd); + if (!ret) { + avd->recoveries = 0; + avd->last_recover = jiffies ?: 1; + if (avd->irq_parked) + enable_irq(avd->irq); + avd->irq_parked = false; + dev_warn(avd->dev, "recover: done\n"); + } else { + avd->retired = true; + dev_err(avd->dev, "decoder retired after a failed wake (%d): reload the module to use it again\n", + ret); + } + pm_runtime_put_autosuspend(avd->dev); return ret; } @@ -485,14 +533,22 @@ static void avd_device_run(void *priv) enum vb2_buffer_state result = VB2_BUF_STATE_ERROR; const char *outcome = "already finished"; unsigned long flags; - bool finish = false; + bool finish = false, fail; u64 seq; int ret; if (WARN_ON(!desc)) return; - if (READ_ONCE(avd->retired) || ctx->broken) { + fail = READ_ONCE(avd->retired) || ctx->broken; + /* A rested decoder stays parked until its window ends, then wakes. */ + if (!fail && avd->rest_until) { + if (time_before(jiffies, avd->rest_until) || avd_wake(avd)) + fail = true; + else + avd->rest_until = 0; + } + if (fail) { struct vb2_v4l2_buffer *src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); /* Complete the request's controls too, or its fd never signals. */ diff --git a/drivers/media/platform/apple/avd/avd.h b/drivers/media/platform/apple/avd/avd.h index 10cd73a90977b..1f2fa8e312d92 100644 --- a/drivers/media/platform/apple/avd/avd.h +++ b/drivers/media/platform/apple/avd/avd.h @@ -241,8 +241,11 @@ struct avd_dev { unsigned int recoveries; unsigned long last_recover; bool retired; - /* Retired with the DART on the empty domain; detached at remove. */ + /* DMA blocked at the DART (retired or resting); detached at remove. */ bool dma_parked; + /* Out of recovery budget: frames fail without hardware access until then. */ + unsigned long rest_until; + bool irq_parked; const struct avd_variant *variant; unsigned int capabilities; From 40d6cf92bac0a76e02ca31814d46b4aba5179bf8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:00:50 +1100 Subject: [PATCH 3/4] iommu/apple-dart: use strict flushing for media and ML engine clients The video decoder, video encoder, neural engine and camera ISP are run by their own coprocessor firmware, which can still have DMA in flight after the host driver gives up on a job: a frame timeout, a reset, a stream torn down mid-frame. With deferred flushing a freed buffer's IOVAs keep translating to its pages until the flush queue drains, while the pages are already back in the allocator, so such a late write corrupts unrelated memory without any fault. The M4 Pro hard hangs under AVD stress all followed decoder resets and left no panic and an empty pstore, which fits this. Give these clients a strict DMA domain, chosen from the client's device tree compatible ("apple,-avd", "-ave", "-ane", "-isp" and "apple,isp"), so a late write faults at the DART instead. They map their buffers once per stream or model, so the per-unmap invalidate stays off the hot path. Per-stream T8140/T8142 DARTs were already strict through apple_dart_capable(). Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/iommu/apple-dart-test.c | 24 ++++++++++++++++ drivers/iommu/apple-dart.c | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/drivers/iommu/apple-dart-test.c b/drivers/iommu/apple-dart-test.c index 51e504321798d..5f512e18bfd50 100644 --- a/drivers/iommu/apple-dart-test.c +++ b/drivers/iommu/apple-dart-test.c @@ -940,7 +940,31 @@ static void apple_dart_test_resume_reset_retry(struct kunit *test) dart->hw->ttbr_valid | 0x1000); } +static void apple_dart_test_strict_engines(struct kunit *test) +{ + static const char * const strict[] = { + "apple,t8103-avd", "apple,t6000-avd", "apple,t8112-avd", "apple,t6041-avd", + "apple,t8132-avd", "apple,t8140-avd", "apple,t6000-ave", "apple,t8112-ave", + "apple,t6041-ave", "apple,t8103-ane", "apple,t6021-ane", "apple,t6041-ane", + "apple,t8112-isp", "apple,t6041-isp", "apple,isp", + }; + static const char * const lazy[] = { + "apple,t6021-ane-mailbox", "apple,asc-mailbox-v4", "apple,t8103-display-pipe", + "apple,nvme-ans2", "apple,t8103-dart", "apple,t6000-display-crossbar", + "brcm,bcm4378-fmac", "apple,", "avd", "foo,t8112-avd", "apple,t8112-avdx", + }; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(strict); i++) + KUNIT_EXPECT_TRUE_MSG(test, apple_dart_compat_is_strict_engine(strict[i]), + "%s", strict[i]); + for (i = 0; i < ARRAY_SIZE(lazy); i++) + KUNIT_EXPECT_FALSE_MSG(test, apple_dart_compat_is_strict_engine(lazy[i]), + "%s", lazy[i]); +} + static struct kunit_case apple_dart_test_cases[] = { + KUNIT_CASE(apple_dart_test_strict_engines), KUNIT_CASE(apple_dart_test_iommu_specifiers), KUNIT_CASE(apple_dart_test_independent_remap_roots), KUNIT_CASE(apple_dart_test_before_attach), diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 384e243d5d0a2..ba23e2c239cd8 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -3279,6 +3280,52 @@ static bool apple_dart_capable(struct device *dev, enum iommu_cap cap) } } +/* + * Media and ML engines (video decode and encode, neural engine, camera ISP) + * are run by their own coprocessor firmware, which can still have DMA in + * flight after the host driver has given up on a job: a timeout, a reset, + * a stream torn down mid-frame. With deferred flushing the DART keeps + * translating a freed buffer's addresses until the flush queue drains, so + * such a late write lands silently in pages that already went back to the + * allocator. Strict flushing makes it fault at the DART instead. These + * engines map their buffers once per stream or model, so the per-unmap + * invalidate is not on any hot path. + * + * The client is identified by its device tree compatible, "apple,-avd" + * and so on, so every SoC generation is covered without a per-SoC table. + */ +static const char * const apple_dart_strict_engines[] = { + "avd", "ave", "ane", "isp", +}; + +static bool apple_dart_compat_is_strict_engine(const char *compat) +{ + const char *engine; + unsigned int i; + + if (!str_has_prefix(compat, "apple,")) + return false; + engine = strrchr(compat, '-'); + engine = engine ? engine + 1 : compat + strlen("apple,"); + for (i = 0; i < ARRAY_SIZE(apple_dart_strict_engines); i++) + if (!strcmp(engine, apple_dart_strict_engines[i])) + return true; + return false; +} + +static bool apple_dart_master_needs_strict(struct device *dev) +{ + struct property *prop; + const char *compat; + + if (!dev->of_node) + return false; + of_property_for_each_string(dev->of_node, "compatible", prop, compat) + if (apple_dart_compat_is_strict_engine(compat)) + return true; + return false; +} + static int apple_dart_def_domain_type(struct device *dev) { struct apple_dart_master_cfg *cfg = dev_iommu_priv_get(dev); @@ -3298,6 +3345,8 @@ static int apple_dart_def_domain_type(struct device *dev) return IOMMU_DOMAIN_DMA; if (cfg->locked) return IOMMU_DOMAIN_DMA; + if (apple_dart_master_needs_strict(dev)) + return IOMMU_DOMAIN_DMA; return 0; } From baf3c39cc6d47e9edac3e246dfaa0715eeefb39f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:53:18 +1100 Subject: [PATCH 4/4] media: apple: avd: run the frame watchdog on the unbound system workqueue The first frame watchdog of every boot logged "workqueue: work func avd_watchdog_func enqueued on deprecated workqueue" because it was queued with schedule_delayed_work() and mod_delayed_work(system_wq). The watchdog has no CPU affinity to keep, so queue it on system_dfl_wq. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-drv.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index 13f87523226d1..215ac921ce671 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -483,7 +483,7 @@ static irqreturn_t avd_irq_handler(int irq, void *data) * timeout: it resets the AVD and fails only this frame, and it * ignores a job the hardware does not own yet. */ - mod_delayed_work(system_wq, &ctx->watchdog_work, 0); + mod_delayed_work(system_dfl_wq, &ctx->watchdog_work, 0); goto done; } @@ -608,7 +608,7 @@ static void avd_device_run(void *priv) } else { avd->job_state = AVD_JOB_SUBMITTED; ctx->wd_seq = seq; - schedule_delayed_work(&ctx->watchdog_work, msecs_to_jiffies(2000)); + queue_delayed_work(system_dfl_wq, &ctx->watchdog_work, msecs_to_jiffies(2000)); outcome = "submitted"; } }