From 946d7aa83fbb3fdcf07986238a8d19f39b69577c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 11:48:08 +1100 Subject: [PATCH 1/6] ASoC: tas2764: Keep SDZ high across sleep only within an SDZ group "ASoC: tas2764: keep SDZ high across system sleep" changed suspend and resume for every TAS2764 and SN012776 amp, but it has only run on the J616s, whose six amps share one SDZ supply as an SDZ group. The M1 and M2 machines also use this driver and have not been tested with the amps left in software shutdown across sleep. Leave SDZ high only for amps in an SDZ group. Amps outside a group drop SDZ on suspend and raise it on resume, as before. Losing the page-0xfd quirk writes over sleep on those machines stays a separate fix. When a group has failed, its supply was forced off and no enable is left for codec_remove() to balance, so skip the disable there. Assisted-by: LLM Signed-off-by: Ryan Murray --- sound/soc/codecs/tas2764.c | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index a6e4bda499883..54c1629c410fc 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -269,11 +269,15 @@ static int tas2764_update_pwr_ctrl(struct tas2764_priv *tas2764) #ifdef CONFIG_PM /* - * System sleep leaves the SDZ line high: the amp waits in software shutdown, as it does between - * streams, and keeps its registers. Pulling SDZ low here meant a power-on reset at resume, and - * soc_resume_deferred() unmutes the amps of a running stream a few milliseconds later: their I/V - * sense then read a negative average power for several seconds, and the uncached page-0xfd quirk - * writes were lost. After cold boot no amp is unmuted that fresh out of reset. + * Members of an SDZ group keep the shared SDZ line high across system sleep: the amp waits in + * software shutdown, as it does between streams, and keeps its registers. Pulling SDZ low here + * meant a power-on reset at resume, and soc_resume_deferred() unmutes the amps of a running stream + * a few milliseconds later: their I/V sense then read a negative average power for several + * seconds, and the uncached page-0xfd quirk writes were lost. After cold boot no amp is unmuted + * that fresh out of reset. + * + * Amps outside a group keep the earlier behaviour (SDZ low across sleep) until the change has run + * on the machines that use them. */ static int tas2764_codec_suspend(struct snd_soc_component *component) { @@ -287,6 +291,13 @@ static int tas2764_codec_suspend(struct snd_soc_component *component) if (ret < 0) return ret; + if (!tas2764->sdz_group) { + if (tas2764->sdz_gpio) + gpiod_set_value_cansleep(tas2764->sdz_gpio, 0); + + regulator_disable(tas2764->sdz_reg); + } + regcache_cache_only(tas2764->regmap, true); regcache_mark_dirty(tas2764->regmap); @@ -300,6 +311,19 @@ static int tas2764_codec_resume(struct snd_soc_component *component) struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); int ret; + if (!tas2764->sdz_group) { + ret = regulator_enable(tas2764->sdz_reg); + if (ret) { + dev_err(tas2764->dev, "Failed to enable regulator\n"); + return ret; + } + + if (tas2764->sdz_gpio) + gpiod_set_value_cansleep(tas2764->sdz_gpio, 1); + + usleep_range(1000, 2000); + } + regcache_cache_only(tas2764->regmap, false); ret = regcache_sync(tas2764->regmap); @@ -983,6 +1007,10 @@ static void tas2764_codec_remove(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); + /* A failed group's supply was forced off: there is no enable left to balance. */ + if (tas2764_group_failed(tas2764)) + return; + regulator_disable(tas2764->sdz_reg); } From 2b490ed63cc7a59879e08cb034848c73ce424b18 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 11:48:08 +1100 Subject: [PATCH 2/6] ASoC: apple: macaudio: Refuse playback starts after a failed speaker lock When a speaker volume lock fails, prepares are refused and the stop work moves every running playback stream to XRUN. A stream whose prepare passed just before the failure was recorded, and which was not yet PREPARED when the stop work walked the card, could still be started. Refuse START, RESUME and PAUSE_RELEASE of any playback stream in the front-end trigger once the lock has failed. The stop walk also visited back-end runtimes, whose substream shares the front-end's runtime. That was only harmless because the front-end links come first in the card's list. Skip no_pcm links there. Assisted-by: LLM Signed-off-by: Ryan Murray --- sound/soc/apple/macaudio.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 99b79bf7337c9..cc1123e10f398 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -406,7 +406,8 @@ static void macaudio_stop_playback(struct macaudio_snd_data *ma, bool sw_gain_on * substream on the card carries a runtime as its private data. */ for_each_card_rtds(&ma->card, rtd) { - if (!rtd->pcm) + /* A back-end's substream shares its front-end's runtime: stop front-ends only. */ + if (!rtd->pcm || rtd->dai_link->no_pcm) continue; if (sw_gain_only && !ma->link_props[rtd->dai_link->id].sw_gain) continue; @@ -1272,6 +1273,22 @@ static int macaudio_fe_trigger(struct snd_pcm_substream *substream, int cmd) struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card); + /* + * A prepare can pass just before a failed lock is recorded and the + * stop work then find the stream not yet prepared: refuse its start. + */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && + READ_ONCE(ma->speaker_lock_failed)) { + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + return -EIO; + default: + break; + } + } + if (!macaudio_sw_gain_pcm(substream)) return 0; From 28d0f3bb1a7b2a6c9f89c895c6d84fe64769668c Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 11:48:28 +1100 Subject: [PATCH 3/6] ASoC: apple: aop_audio: Describe the T6040 microphone setup without macOS symbols The comments on the T6040 microphone flow named a macOS class and method. Say what is sent instead: the property and the ADT masks it carries are the facts the code relies on. Assisted-by: LLM Signed-off-by: Ryan Murray --- sound/soc/apple/aop_audio.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sound/soc/apple/aop_audio.rs b/sound/soc/apple/aop_audio.rs index 984cb763f70f8..da77d37be3855 100644 --- a/sound/soc/apple/aop_audio.rs +++ b/sound/soc/apple/aop_audio.rs @@ -328,8 +328,8 @@ impl AudioAttachDevice { } } -// macOS IOPAudioLPMicDevice::sendChannelControlInfoToIOP sets node property 300 with the three -// masks of the ADT node /product/audio (supportedChannels, enabledChannels, historyChannels). +// macOS sets node property 300 to the three channel masks of the ADT node /product/audio +// (supportedChannels, enabledChannels, historyChannels). #[repr(C, packed(4))] #[derive(Clone, Copy, Default)] struct LpaiChannelControl { @@ -518,7 +518,7 @@ impl AudioSetDeviceProp { struct SndSocAopHwConfig { bytes_per_sample: u32, hf_decimator: bool, - // The T6040 firmware follows macOS's IOPAudioLPMicDevice flow. LPAI property 301 is the + // The T6040 firmware takes the microphone setup macOS sends. LPAI property 301 is the // read-only stream description; the channel masks are property 300 (12 bytes). A refusal of // any of the configuration steps is logged and not fatal, so the next step's answer is seen. t6040_flow: bool, From 2c8754f8e0109bd6de6bfa0235b05217249ba3e9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 12:46:07 +1100 Subject: [PATCH 4/6] ASoC: dt-bindings: apple,t6041-aop-audio: Select only T6041 nodes The schema lists the apple,t6030-aop-audio fallback, so dtbs_check also applied it to the T6030, T6031 and T8122 AOP audio nodes, which carry that fallback but only a microphone stream. They then failed on the T6041 compatible, the DMA list and #sound-dai-cells. Select the schema by the T6041 compatible only. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/sound/apple,t6041-aop-audio.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml index b9660e080a91c..1eadf9ff3b6eb 100644 --- a/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml @@ -20,6 +20,16 @@ description: | The headset jack's channels are optional: a node without them has no jack streams. +# Only select T6041 nodes: other AOP audio nodes carry the +# apple,t6030-aop-audio fallback too and have a different set of streams. +select: + properties: + compatible: + contains: + const: apple,t6041-aop-audio + required: + - compatible + allOf: - $ref: dai-common.yaml# From 79de87a95447539276c318a6ac9bf230e3a659a5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 14:04:34 +1100 Subject: [PATCH 5/6] dt-bindings: soc: apple: apple,t6041-aop: Add memory-region The AOP firmware runs its external code and its log from DRAM. Describe those segments as memory-region entries, which the bootloader fills in from the ADT, with the address the firmware uses for each one in the region's iommu-addresses, as for the other Apple coprocessors whose firmware the bootloader loads. This replaces the aop-etext and aop-oslog reserved-memory nodes that the description named, which no binding defined and nothing referred to. Assisted-by: LLM Signed-off-by: Ryan Murray --- .../bindings/soc/apple/apple,t6041-aop.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml index d81cde8a8d534..e8a2df805059a 100644 --- a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml +++ b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml @@ -13,10 +13,8 @@ description: The Always-On Processor provides audio and sensor services through firmware loaded by the bootloader. The application processor patches the firmware's boot arguments in the AOP SRAM, starts it through RTKit over one mailbox and - reaches its services through the RTKit endpoints. The bootloader reserves - the firmware's external code and log segments as /reserved-memory nodes - named aop-etext and aop-oslog; no mapping of them survives the reset of the - AOP's DART, so the operating system maps them again before the start. + reaches its services through the RTKit endpoints. The firmware's external + code and log segments live in DRAM, and the bootloader reserves them. properties: compatible: @@ -47,6 +45,14 @@ properties: iommus: maxItems: 1 + memory-region: + minItems: 1 + maxItems: 4 + description: + The firmware segments in DRAM (its external code and its log), filled in + by the bootloader from the ADT. The iommu-addresses property of each + region gives the address the firmware uses for it. + audio: type: object $ref: /schemas/sound/apple,t6041-aop-audio.yaml# @@ -105,6 +111,7 @@ examples: mboxes = <&aop_mbox>; mbox-names = "mbox"; iommus = <&aop_dart 0>; + memory-region = <&aop_etext>, <&aop_oslog>; /* Filled by the bootloader */ als { compatible = "apple,t6041-aop-als", "apple,aop-als"; From b10c2f9d72939d565b83102ce4ddf542aa7c55f7 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 24 Oct 2026 14:04:34 +1100 Subject: [PATCH 6/6] soc: apple: aop: Take the T6040 firmware segments from memory-region On the T6040 the driver found the firmware's external code and log segments by looking for /reserved-memory nodes named aop-etext and aop-oslog, and mapped them with iommu_map() into the device's DMA API domain, without its IOVA allocator knowing about them. Take them from the AOP's memory-region instead, which is how the bootloader describes the firmware of the other Apple coprocessors. Each region's iommu-addresses gives the IOVA the firmware uses. The IOMMU core already maps such regions in the default domain when the device is added, one to one for the log and translated for the external code, and keeps their IOVAs out of the allocator, so the driver no longer maps or unmaps anything itself. Before the firmware is started, check that every page of every region is mapped where the DT says, that one region sits at EC0p and that any other one is mapped at its physical address, and refuse to start it otherwise. The check now runs before anything is set up, so a missing region no longer ends in a failed shutdown of an AOP that never ran. The other AOP generations are unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/soc/apple/aop.rs | 344 +++++++++++++++++++++------------------ 1 file changed, 185 insertions(+), 159 deletions(-) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 4bcf43a7060e3..e72f025bb3459 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -91,17 +91,10 @@ const AOP_MMIO_SIZE: usize = 0x1e0000; const ASC_MMIO_SIZE: usize = 0x4000; const BOOTARGS_OFFSET: usize = 0x22c; const BOOTARGS_SIZE: usize = 0x230; -/// On the T6040 the bootloader reserves the firmware's external code segment -/// (`__ETEXT`, from the ADT) as `/reserved-memory/aop-etext@`. No -/// mapping of it survives the DART reset, so this driver maps it itself. -const ETEXT_NODE_PREFIX: &CStr = c_str!("aop-etext"); -/// The firmware's log segment (`__OS_LOG`), reserved the same way as -/// `/reserved-memory/aop-oslog@` and used by the firmware at its -/// physical address. Without it the firmware stops within a second on a DART -/// fault storm. -const OSLOG_NODE_PREFIX: &CStr = c_str!("aop-oslog"); -const ETEXT_MAX_SIZE: u64 = 0x10_0000; -const ETEXT_PAGE: u64 = 0x4000; +/// The largest firmware segment the bootloader reserves in DRAM (T6040). +const FW_REGION_MAX_SIZE: u64 = 0x10_0000; +/// The DART page size the firmware segments are mapped with. +const FW_REGION_PAGE: u64 = 0x4000; const AFK_ENDPOINT_START: u8 = 0x20; const AFK_ENDPOINT_COUNT: u8 = 0xf; const AFK_OPC_GET_BUF: u64 = 0x89; @@ -1195,18 +1188,6 @@ impl ChildDevice { } } -/// The IOVA mapping of the firmware's external code, owned by this driver -/// (T6040 only). -struct EtextMapping { - domain: *mut bindings::iommu_domain, - iova: u64, - size: usize, -} - -// SAFETY: The domain pointer is only passed to `iommu_unmap()`, which may be -// called from any thread; the domain outlives the device's binding. -unsafe impl Send for EtextMapping {} - #[pin_data] struct AopData { dev: ARef, @@ -1225,9 +1206,6 @@ struct AopData { /// The co-processor was started; only then is there anything to shut /// down and to retain. cpu_started: Atomic, - /// The firmware mappings made by `map_reserved()`. - #[pin] - etext: Mutex>, /// Shut the co-processor down on removal, and if that cannot be /// confirmed, retain everything it may still DMA to. quiesce_on_unbind: bool, @@ -1375,7 +1353,6 @@ impl AopData { removing: Atomic::new(false), transport_closing: Atomic::new(false), cpu_started: Atomic::new(false), - etext <- new_mutex!(KVec::new()), quiesce_on_unbind: cfg.quiesce_on_unbind, required_endpoints: cfg.required_endpoints, setup_queue, @@ -1576,126 +1553,186 @@ impl AopData { Ok(()) } - /// The physical range of the one `/reserved-memory/@*` node. - fn find_reserved(prefix: &CStr) -> Result<(u64, u64)> { - // SAFETY: The path is NUL-terminated. The node reference is released - // below (OF_DYNAMIC is on since 7.3: pmdomain/apple selects it). - let resv = unsafe { - bindings::of_find_node_opts_by_path( - c_str!("/reserved-memory").as_char_ptr(), - ptr::null_mut(), - ) - }; - if resv.is_null() { - return Err(ENODEV); + /// The physical address, IOVA and size of the `idx`-th `memory-region` of + /// `np`. The IOVA comes from the region's `iommu-addresses` entry for + /// `np`. + fn fw_region(np: *mut bindings::device_node, idx: c_int) -> Result<(u64, u64, u64)> { + // SAFETY: `np` is a valid node and the name is NUL-terminated. + let region = + unsafe { bindings::of_parse_phandle(np, c_str!("memory-region").as_char_ptr(), idx) }; + if region.is_null() { + return Err(EINVAL); } - let mut found: Result> = Ok(None); - let mut child: *mut bindings::device_node = ptr::null_mut(); - loop { - // SAFETY: `resv` is a valid node; `child` is null or its previous - // child, whose reference this call drops. - child = unsafe { bindings::of_get_next_child(resv, child) }; - if child.is_null() { - break; - } - // SAFETY: `child` is a valid node and the prefix is NUL-terminated. - if !unsafe { bindings::of_node_name_prefix(child, prefix.as_char_ptr()) } { - continue; - } - let mut res = mem::MaybeUninit::::zeroed(); - // SAFETY: `res` is writable storage for one resource. - if unsafe { bindings::of_address_to_resource(child, 0, res.as_mut_ptr()) } != 0 { - found = Err(EINVAL); - break; - } - // SAFETY: of_address_to_resource() succeeded and filled it in. - let res = unsafe { res.assume_init() }; - if !matches!(found, Ok(None)) || res.end < res.start { - found = Err(EINVAL); - break; + // SAFETY: of_parse_phandle() returned a reference on `region`. + let _put = ScopeGuard::new(|| unsafe { bindings::of_node_put(region) }); + let mut res = mem::MaybeUninit::::zeroed(); + // SAFETY: `region` is a valid node and `res` is writable storage for + // one resource. + if unsafe { bindings::of_address_to_resource(region, 0, res.as_mut_ptr()) } != 0 { + return Err(EINVAL); + } + // SAFETY: of_address_to_resource() succeeded and filled it in. + let res = unsafe { res.assume_init() }; + if res.end < res.start { + return Err(EINVAL); + } + let phys = res.start as u64; + let size = (res.end - res.start) as u64 + 1; + + let mut len: c_int = 0; + // SAFETY: `region` is a valid node and the name is NUL-terminated. + let prop = unsafe { + bindings::of_get_property(region, c_str!("iommu-addresses").as_char_ptr(), &mut len) + } + .cast::(); + let len = usize::try_from(len).map_err(|_| EINVAL)?; + if prop.is_null() || len == 0 || len % mem::size_of::() != 0 { + return Err(ENOENT); + } + let cells = len / mem::size_of::(); + // SAFETY: `np` is a valid node. + let own = unsafe { (*np).phandle }; + let mut found = None; + let mut pos = 0; + // Each entry is an owner phandle and an address and size in the + // owner's DMA address space. + while pos < cells { + // SAFETY: `pos` is below `cells`, inside the property. + let phandle = u32::from_be(unsafe { prop.add(pos).read_unaligned() }); + pos += 1; + if own != 0 && phandle == own { + let mut iova: bindings::phys_addr_t = 0; + let mut length: usize = 0; + // SAFETY: `cells - pos` cells are left in the property, which + // bounds the tuple. + let next = unsafe { + bindings::of_translate_dma_region_checked( + np, + prop.add(pos), + cells - pos, + &mut iova, + &mut length, + ) + }; + if next.is_null() || found.is_some() { + return Err(EINVAL); + } + found = Some((iova as u64, length as u64)); + // SAFETY: `next` points into the same property, past `pos`. + pos = usize::try_from(unsafe { next.offset_from(prop) }).map_err(|_| EINVAL)?; + } else { + // Another device's entry: skip it by its owner's cell count. + // SAFETY: A lookup; the reference is dropped right below. + let owner = unsafe { bindings::of_find_node_by_phandle(phandle) }; + if owner.is_null() { + return Err(EINVAL); + } + // SAFETY: `owner` is a valid node reference. + let n = unsafe { bindings::of_dma_region_cells(owner) }; + // SAFETY: The reference taken by of_find_node_by_phandle(). + unsafe { bindings::of_node_put(owner) }; + let n = usize::try_from(n).map_err(|_| EINVAL)?; + if n > cells - pos { + return Err(EINVAL); + } + pos += n; } - found = Ok(Some((res.start as u64, (res.end - res.start) as u64 + 1))); } - // SAFETY: Both are null or node references taken above; of_node_put() - // accepts null. - unsafe { - bindings::of_node_put(child); - bindings::of_node_put(resv); - } - found?.ok_or(ENOENT) - } - - /// Maps the `/reserved-memory/@*` segment into this device's DART - /// domain at `iova`, or at its physical address when `iova` is `None`. - /// Nothing is mapped unless every check passes. - fn map_reserved(&self, dev: &device::Device, prefix: &CStr, iova: Option) -> Result<()> { - let (phys, size) = Self::find_reserved(prefix)?; - let iova = iova.unwrap_or(phys); - if phys % ETEXT_PAGE != 0 - || size == 0 - || size % ETEXT_PAGE != 0 - || size > ETEXT_MAX_SIZE - || iova % ETEXT_PAGE != 0 - || iova.checked_add(size).is_none() - || phys.checked_add(size).is_none() - { + let (iova, length) = found.ok_or(ENOENT)?; + if length != size { return Err(EINVAL); } + Ok((phys, iova, size)) + } + + /// Checks the firmware segments the AOP runs from DRAM (T6040). The + /// bootloader reserves them and lists them in `memory-region`, and each + /// one's `iommu-addresses` gives the IOVA the firmware uses for it. The + /// IOMMU core maps them in the default domain when the device is added + /// (one to one when the IOVA is the physical address, translated + /// otherwise), so they are only checked here. Every page has to be mapped + /// where the DT says, the external code (`__ETEXT`) at `ec0p` and any + /// other segment (`__OS_LOG`) at its physical address. + fn check_fw_regions(dev: &device::Device, ec0p: u64) -> Result<()> { // SAFETY: `dev` is a live device being probed. - let domain = unsafe { bindings::iommu_get_domain_for_dev(dev.as_raw()) }; - if domain.is_null() { + let np = unsafe { (*dev.as_raw()).of_node }; + if np.is_null() { return Err(ENODEV); } - // SAFETY: `domain` is the non-null domain returned above. - let ty = unsafe { (*domain).type_ }; + // SAFETY: `np` is the device's node, valid while it is bound. + let count = unsafe { bindings::of_reserved_mem_region_count(np) }; + if count <= 0 { + dev_err!(dev, "No firmware memory-region from the bootloader"); + return Err(ENOENT); + } + // SAFETY: `dev` is a live device being probed. + let domain = unsafe { bindings::iommu_get_domain_for_dev(dev.as_raw()) }; + let ty = if domain.is_null() { + bindings::IOMMU_DOMAIN_BLOCKED + } else { + // SAFETY: `domain` is the non-null domain returned above. + unsafe { (*domain).type_ } + }; if ty == bindings::IOMMU_DOMAIN_IDENTITY || ty == bindings::IOMMU_DOMAIN_BLOCKED { + dev_err!(dev, "No translated IOMMU domain for the firmware"); return Err(ENODEV); } - let size_us = usize::try_from(size).map_err(|_| EOVERFLOW)?; - // SAFETY: The range was checked above; the DART domain validates the - // IOVA itself. - let ret = unsafe { - bindings::iommu_map( - domain, - iova as _, - phys as _, - size_us, - (bindings::IOMMU_READ | bindings::IOMMU_WRITE) as _, - bindings::GFP_KERNEL, - ) - }; - if ret != 0 { - return Err(Error::from_errno(ret)); - } - // SAFETY: as above. - let back = unsafe { bindings::iommu_iova_to_phys(domain, iova as _) } as u64; - if back != phys { - // SAFETY: The range was mapped just above. - unsafe { bindings::iommu_unmap(domain, iova as _, size_us) }; - return Err(EIO); + let mut etext = false; + for idx in 0..count { + let (phys, iova, size) = Self::fw_region(np, idx).inspect_err(|e| { + dev_err!(dev, "Invalid firmware memory-region {} ({:?})", idx, e); + })?; + if phys % FW_REGION_PAGE != 0 + || size % FW_REGION_PAGE != 0 + || size > FW_REGION_MAX_SIZE + || iova % FW_REGION_PAGE != 0 + || iova.checked_add(size).is_none() + || phys.checked_add(size).is_none() + { + dev_err!(dev, "Invalid firmware memory-region {}", idx); + return Err(EINVAL); + } + if iova == ec0p { + if etext { + dev_err!(dev, "Two firmware memory-regions at EC0p {:#x}", ec0p); + return Err(EINVAL); + } + etext = true; + } else if iova != phys { + dev_err!( + dev, + "Firmware memory-region {} at IOVA {:#x}, neither EC0p {:#x} nor its physical address", + idx, + iova, + ec0p + ); + return Err(EINVAL); + } + let mut off = 0; + while off < size { + // SAFETY: `domain` is the device's domain, live while it is + // bound. + let back = + unsafe { bindings::iommu_iova_to_phys(domain, (iova + off) as _) } as u64; + if back != phys + off { + dev_err!( + dev, + "Firmware memory-region {} is not mapped at IOVA {:#x}", + idx, + iova + off + ); + return Err(EIO); + } + off += FW_REGION_PAGE; + } } - let map = EtextMapping { - domain, - iova, - size: size_us, - }; - if self.etext.lock().push(map, GFP_KERNEL).is_err() { - // SAFETY: The range was mapped just above. - unsafe { bindings::iommu_unmap(domain, iova as _, size_us) }; - return Err(ENOMEM); + if !etext { + dev_err!(dev, "No firmware memory-region at EC0p {:#x}", ec0p); + return Err(ENOENT); } Ok(()) } - /// Undoes `map_reserved()`; only once the firmware no longer runs. - fn unmap_etext(&self) { - let mut maps = self.etext.lock(); - while let Some(map) = maps.pop() { - // SAFETY: The mapping was created by map_reserved() on this domain. - unsafe { bindings::iommu_unmap(map.domain, map.iova as _, map.size) }; - } - } - /// Leaks every allocation the firmware may still access. Called when the /// shutdown could not be confirmed; the memory is lost until reboot. fn retain_dma_buffers(&self) { @@ -1955,9 +1992,6 @@ impl AOP for AopData { ); return; } - // The firmware is stopped or was never started: it no longer fetches - // its external code. - self.unmap_etext(); let arena = self.setup.lock().arena.take(); drop(arena); // The ring is DMA of the audio child; free it while the child is @@ -2413,10 +2447,10 @@ struct AopHwConfig { quiesce_on_unbind: bool, /// The endpoints that have to start; empty means all advertised ones. required_endpoints: &'static [u8], - /// Map the firmware's external code at `ec0p` and its `__OS_LOG` at its - /// physical address before starting it: no bootloader mapping survives - /// the DART reset (T6040). - map_etext: bool, + /// The firmware runs segments from DRAM, which the bootloader reserves + /// and lists in `memory-region`; check their IOMMU mappings before + /// starting it (T6040). + fw_regions: bool, /// Halt the co-processor again when its start fails (T6040). halt_on_failed_start: bool, } @@ -2430,7 +2464,7 @@ const HW_CFG_T8103: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], - map_etext: false, + fw_regions: false, halt_on_failed_start: false, }; const HW_CFG_T8112: AopHwConfig = AopHwConfig { @@ -2442,7 +2476,7 @@ const HW_CFG_T8112: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], - map_etext: false, + fw_regions: false, halt_on_failed_start: false, }; const HW_CFG_T6000: AopHwConfig = AopHwConfig { @@ -2454,7 +2488,7 @@ const HW_CFG_T6000: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], - map_etext: false, + fw_regions: false, halt_on_failed_start: false, }; const HW_CFG_T6020: AopHwConfig = AopHwConfig { @@ -2466,13 +2500,13 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], - map_etext: false, + fw_regions: false, halt_on_failed_start: false, }; /// T6040 (J616s): macOS writes the same four boot arguments as for the -/// T6020. The bootloader's mapping of the external code does not survive the -/// DART reset, so it is mapped here; the RTKit firmware is shut down on -/// unbind. +/// T6020. The firmware's external code and log live in DRAM, mapped through +/// the reserved regions the bootloader describes; the RTKit firmware is shut +/// down on unbind. const HW_CFG_T6040: AopHwConfig = AopHwConfig { ec0p: 0x0100_00000000, aopt: 0, @@ -2482,7 +2516,7 @@ const HW_CFG_T6040: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: true, required_endpoints: &[], - map_etext: true, + fw_regions: true, halt_on_failed_start: true, }; /// T8140: the firmware is started with the boot arguments the bootloader @@ -2499,7 +2533,7 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { setup_port: true, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], - map_etext: false, + fw_regions: false, halt_on_failed_start: false, }; @@ -2541,23 +2575,15 @@ impl platform::Driver for AopDriver { let asc_req = pdev.io_request_by_index(1).ok_or(EINVAL)?; let asc_mmio = asc_req.iomap_sized::()?; let asc_mmio = asc_mmio.as_view().relaxed(); + if cfg.fw_regions { + // Before anything is written to the AOP: no mapping, no start. + AopData::check_fw_regions(pdev.as_ref(), cfg.ec0p)?; + } let data = AopData::new(pdev, cfg)?; // Whatever fails below leaves the AOP running and its children // registering; the same teardown as unbind's cleans that up. let probe_guard = ScopeGuard::new_with_data(data.clone(), |data| data.remove()); let aop_mmio = aop_mmio.as_view(); - if cfg.map_etext { - // Before anything is written to the AOP: no mapping, no start. - for (prefix, iova) in [ - (ETEXT_NODE_PREFIX, Some(cfg.ec0p)), - (OSLOG_NODE_PREFIX, None), - ] { - if let Err(e) = data.map_reserved(pdev.as_ref(), prefix, iova) { - dev_err!(pdev.as_ref(), "Failed to map {:?} ({:?})", prefix, e); - return Err(e); - } - } - } if cfg.patch_bootargs { data.patch_bootargs( &aop_mmio,