From 9be39bf9b5da609ebed2313bd519f54ab7050d93 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 10 Oct 2026 03:23:52 +1100 Subject: [PATCH 1/4] apple: prepare gated J616S AOP, ISP and inherited USB2 support Signed-off-by: Ryan Murray Assisted-by: LLM --- .../bindings/media/apple,isp-profile.yaml | 60 ++ .../phy/apple,t6040-usb2-inherited.yaml | 49 ++ .../reserved-memory/apple,asc-mem.yaml | 10 + .../bindings/sound/apple,j616s-aop-jack.yaml | 38 ++ Documentation/sound/cards/apple-j616s.rst | 219 ++++++ Documentation/sound/cards/index.rst | 1 + MAINTAINERS | 3 +- drivers/base/dd.c | 40 +- drivers/dma/apple-admac.c | 16 + drivers/iommu/of_iommu.c | 15 +- drivers/iommu/of_iommu_firmware.h | 29 + drivers/media/platform/apple/isp/Kconfig | 11 + drivers/media/platform/apple/isp/Makefile | 4 +- .../platform/apple/isp/isp-buffer-pages.h | 61 ++ .../platform/apple/isp/isp-buffer-test.c | 187 +++++ drivers/media/platform/apple/isp/isp-buffer.h | 120 ++++ drivers/media/platform/apple/isp/isp-cam.c | 78 ++- drivers/media/platform/apple/isp/isp-cam.h | 2 +- drivers/media/platform/apple/isp/isp-cmd.c | 45 +- drivers/media/platform/apple/isp/isp-cmd.h | 3 +- drivers/media/platform/apple/isp/isp-drv.c | 80 ++- drivers/media/platform/apple/isp/isp-drv.h | 48 +- drivers/media/platform/apple/isp/isp-fw.c | 152 ++++- drivers/media/platform/apple/isp/isp-fw.h | 3 +- .../platform/apple/isp/isp-ipc-layout-test.c | 120 ++++ .../media/platform/apple/isp/isp-ipc-layout.h | 49 ++ drivers/media/platform/apple/isp/isp-ipc.c | 16 +- .../media/platform/apple/isp/isp-profile.c | 121 ++++ .../media/platform/apple/isp/isp-profile.h | 51 ++ drivers/media/platform/apple/isp/isp-v4l2.c | 642 ++++++++++++------ drivers/media/platform/apple/isp/isp-v4l2.h | 2 + drivers/phy/apple/Kconfig | 10 + drivers/phy/apple/Makefile | 1 + drivers/phy/apple/usb2-t6040-inherited.c | 134 ++++ drivers/soc/apple/aop.rs | 370 +++++++--- drivers/soc/apple/aop_bootargs.rs | 205 ++++++ drivers/soc/apple/aop_shutdown.rs | 183 +++++ drivers/soc/apple/rtkit.c | 6 + include/linux/device.h | 34 + include/linux/soc/apple/rtkit.h | 3 + rust/helpers/of.c | 5 + rust/kernel/soc/apple/aop.rs | 157 ++++- rust/kernel/soc/apple/rtkit.rs | 6 + sound/soc/apple/Kconfig | 11 + sound/soc/apple/Makefile | 3 + sound/soc/apple/aop_audio.rs | 9 +- sound/soc/apple/j616s_aop_jack.rs | 132 ++++ sound/soc/apple/j616s_jack_profile.rs | 96 +++ sound/soc/apple/t8140_aop_audio.rs | 4 +- .../testing/selftests/drivers/apple/Makefile | 1 + .../drivers/apple/admac-descriptor-range.py | 111 +++ .../selftests/drivers/apple/aop-bootargs.py | 113 +++ .../selftests/drivers/apple/aop-retirement.py | 196 ++++++ .../drivers/apple/aop-service-race.py | 402 +++++++++++ .../selftests/drivers/apple/aop-shutdown.py | 30 + .../drivers/apple/firmware-permissions.py | 165 +++++ .../drivers/apple/j616s-jack-profile.py | 129 ++++ 57 files changed, 4375 insertions(+), 416 deletions(-) create mode 100644 Documentation/devicetree/bindings/media/apple,isp-profile.yaml create mode 100644 Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml create mode 100644 Documentation/devicetree/bindings/sound/apple,j616s-aop-jack.yaml create mode 100644 Documentation/sound/cards/apple-j616s.rst create mode 100644 drivers/iommu/of_iommu_firmware.h create mode 100644 drivers/media/platform/apple/isp/isp-buffer-pages.h create mode 100644 drivers/media/platform/apple/isp/isp-buffer-test.c create mode 100644 drivers/media/platform/apple/isp/isp-buffer.h create mode 100644 drivers/media/platform/apple/isp/isp-ipc-layout-test.c create mode 100644 drivers/media/platform/apple/isp/isp-ipc-layout.h create mode 100644 drivers/media/platform/apple/isp/isp-profile.c create mode 100644 drivers/media/platform/apple/isp/isp-profile.h create mode 100644 drivers/phy/apple/usb2-t6040-inherited.c create mode 100644 drivers/soc/apple/aop_bootargs.rs create mode 100644 drivers/soc/apple/aop_shutdown.rs create mode 100644 sound/soc/apple/j616s_aop_jack.rs create mode 100644 sound/soc/apple/j616s_jack_profile.rs create mode 100755 tools/testing/selftests/drivers/apple/admac-descriptor-range.py create mode 100755 tools/testing/selftests/drivers/apple/aop-bootargs.py create mode 100755 tools/testing/selftests/drivers/apple/aop-retirement.py create mode 100755 tools/testing/selftests/drivers/apple/aop-service-race.py create mode 100755 tools/testing/selftests/drivers/apple/aop-shutdown.py create mode 100755 tools/testing/selftests/drivers/apple/firmware-permissions.py create mode 100755 tools/testing/selftests/drivers/apple/j616s-jack-profile.py diff --git a/Documentation/devicetree/bindings/media/apple,isp-profile.yaml b/Documentation/devicetree/bindings/media/apple,isp-profile.yaml new file mode 100644 index 0000000000000..1f19f00549af4 --- /dev/null +++ b/Documentation/devicetree/bindings/media/apple,isp-profile.yaml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/media/apple,isp-profile.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple ISP firmware profile memory preparation + +maintainers: + - Ryan Murray + +description: | + Version-specific ISP memory and wire preparation. This profile identifies + the measured T6040 board6 resident 25G76 image. It does not admit native + startup, interrupt delivery, inherited DART ownership or a firmware lifetime + transition. The producer keeps this device disabled until those boundaries + have been qualified. Firmware physical ranges are supplied at boot. + +select: + required: + - apple,isp-profile + +properties: + apple,isp-profile: + const: t6040-25G76 + + apple,isp-firmware-uuid: + const: 4C8FBA5E-45CC-308E-9BE5-053518C19D76 + + apple,isp-board-id: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 6 + + apple,isp-mcc-mask: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 15 + + memory-region: + minItems: 3 + maxItems: 3 + + memory-region-names: + items: + - const: firmware-text + - const: firmware-data + - const: heap + + status: + const: disabled + +required: + - apple,isp-profile + - apple,isp-firmware-uuid + - apple,isp-board-id + - apple,isp-mcc-mask + - memory-region + - memory-region-names + - status + +additionalProperties: true diff --git a/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml b/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml new file mode 100644 index 0000000000000..4be48381b41d2 --- /dev/null +++ b/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/phy/apple,t6040-usb2-inherited.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: J616s inherited USB2 device PHY for RAM experiments + +maintainers: + - Sven Peter + +description: | + Admission for the USB2 device-mode state prepared by m1n1 on J616s. + The provider reads its USB2/PIPE state and performs no power, reset or + calibration writes. Its caller owns controller/DART handoff and must retain + the bootloader's PHY clocks and physical watchdog until cold recovery. + This does not describe complete T6040 Type-C, host or SuperSpeed support. + +properties: + compatible: + const: apple,t6040-usb2-inherited + + reg: + minItems: 2 + maxItems: 2 + + reg-names: + items: + - const: usb2phy + - const: pipehandler + + '#phy-cells': + const: 0 + + apple,inherited-handoff: + type: boolean + description: | + The bootloader has retired its DWC3/DART consumers, transferred DMA + ownership and retained initialized USB2 PHY clocks and a physical + watchdog. This assertion does not perform those operations. + +required: + - compatible + - reg + - reg-names + - '#phy-cells' + - apple,inherited-handoff + +additionalProperties: false diff --git a/Documentation/devicetree/bindings/reserved-memory/apple,asc-mem.yaml b/Documentation/devicetree/bindings/reserved-memory/apple,asc-mem.yaml index 05540204078be..c3a832cabd197 100644 --- a/Documentation/devicetree/bindings/reserved-memory/apple,asc-mem.yaml +++ b/Documentation/devicetree/bindings/reserved-memory/apple,asc-mem.yaml @@ -31,6 +31,16 @@ properties: iommu-addresses: true + apple,firmware-read-only: + type: boolean + description: | + Map this reserved firmware region read-only in each device IOMMU + mapping described by iommu-addresses. Cache/coherency attributes + are unchanged. This describes immutable firmware text; it does not + restrict CPU access. If absent, existing read/write mappings remain + the default. The bootloader derives this from the firmware segment + permissions rather than a fixed physical address. + required: - compatible - reg diff --git a/Documentation/devicetree/bindings/sound/apple,j616s-aop-jack.yaml b/Documentation/devicetree/bindings/sound/apple,j616s-aop-jack.yaml new file mode 100644 index 0000000000000..5a4a050671b7b --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,j616s-aop-jack.yaml @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,j616s-aop-jack.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple J616s AOP retained-profile jack control + +maintainers: + - Ryan Murray + +description: | + Read-only control inspection of the J616s AOP audio service on EPIC v2 + endpoint 0x22. This node is a service child of the AOP provider. The + bootloader copies the current firmware identity from the AOP firmware nub. + The driver accepts the qualified identity and exact retained coS/cin MCA + profiles. It does not upload configuration, power a path, expose a PCM or + enable speakers. DMA and shared-clock lifecycle require separate admission. + +properties: + compatible: + const: apple,j616s-aop-jack + + apple,firmware-uuid: + $ref: /schemas/types.yaml#/definitions/string + pattern: '^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$' + description: Current firmware UUID copied from the AOP nub, not the SoC identity. + + apple,firmware-name: + $ref: /schemas/types.yaml#/definitions/string + description: Current firmware name copied from the AOP nub. + +required: + - compatible + - apple,firmware-uuid + - apple,firmware-name + +additionalProperties: false diff --git a/Documentation/sound/cards/apple-j616s.rst b/Documentation/sound/cards/apple-j616s.rst new file mode 100644 index 0000000000000..7a9fa854c6f2c --- /dev/null +++ b/Documentation/sound/cards/apple-j616s.rst @@ -0,0 +1,219 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +Apple J616s audio bring-up +========================== + +Status +------ + +J616sAP (Mac16,7), chip 0x6040, board 6 has bounded m1n1 proxy audio +evidence. No Linux boot, ALSA card, PCM or mixer ABI has been qualified. +This document records the implementation and test boundary; it does not +enable a device-tree node, add a compatible or select a speaker model. + +The native display names are MacBook Pro Microphone, External Microphone, +External Headphones and MacBook Pro Speakers. They are macOS names, not +ALSA identifiers. + +Routes +------ + +The rates below are nominal profile values, not clock measurements. + +.. list-table:: + :header-rows: 1 + :widths: 20 35 45 + + * - Route + - Proxy transport + - Evidence + * - Internal high-power microphone (hpai) + - admac-leap-ns, stream 10, split RX0 + - Three-channel Float32LE at 48 kHz; completed 2 MiB and 6 MiB + captures with recognizable audio + * - Internal low-power microphone (lpai) + - Owned 768,000-byte ring on AOP DART stream 8 + - Producer reports and recognizable reconstructed speech; + snapshot-boundary integrity remains unqualified + * - Headset microphone (cin, with trailing space) + - admac-base-ns, stream 8, RX2 + - Mono 24-bit samples in 32-bit slots at 48 kHz; recognizable audio + * - Headphones (cout) + - admac-base-ns, stream 8, TX2; native coS (trailing space)/ms02 + - Stereo 24-bit samples in 32-bit slots at 48 kHz; user-confirmed + left/right finite output and passing codec/controller restoration + * - Speakers (spkr) + - admac-base-ns, stream 8, TX0; MCA group 0, ms00 + - Six 24-bit samples in 32-bit slots at 48 kHz; all six drivers heard + individually, first left/right woofers also paired for five seconds + +A trailing space in a FourCC is significant. DMA channel and DART stream +numbers are not ALSA PCM indices. These tests use separate owned mappings; +they do not establish concurrent ownership of stream 8. + +The native speaker provider order and bus addresses are: + +.. list-table:: + :header-rows: 1 + + * - Provider index + - Speaker + - Bus/address + * - 0 + - Left woofer 1 + - i2c1:0x38 + * - 1 + - Right woofer 1 + - i2c3:0x3b + * - 2 + - Left woofer 2 + - i2c1:0x39 + * - 3 + - Right woofer 2 + - i2c3:0x3c + * - 4 + - Left tweeter + - i2c1:0x3a + * - 5 + - Right tweeter + - i2c3:0x3d + +All six SN012776 amps passed identity and native protected shutdown +configuration and now have user-confirmed individual tone output. The second +woofers use slots 2/3 at 300 Hz; tweeters use slots 4/5 at 2 kHz. Each new test +lasts one second with peak 0.125 and 20 ms fades. Matching TX reports, zero +residue/DART faults, mute before TX stop, protected shutdown and exact +controller/GPIO restoration pass, followed by watchdog recovery. DVC is 0x65 +for the second woofers and left tweeter, 0x78 for the right tweeter. These are +finite test settings, not calibrated system volume. Simultaneous all-six and +continuous playback remain unqualified. +Native feedback describes RX1 with twelve 16-bit slots and sample-width +field zero. Signedness, packing, I/V order, scales and frame integrity +still require qualification; this is not an established S16_LE PCM contract. + +Implementation order +-------------------- + +1. Resolve current ADT registers, clocks, resets, DMA parents and mapper + streams. Bind the AOP firmware and EPIC version-2 command/reply layouts, + fresh service identities and profile sizes. Reject malformed replies + and nonzero return codes before using their payloads. +2. Implement LP owned-buffer publication and producer readiness before + the tested HQ path. LP idle state alone does not release the mapping. + Preserve the three-channel HQ float capture; a normalized mono preview + does not qualify beamforming. +3. Integrate the CS42L84 at i2c2 address 0x4b using its own jack detection, + input bias, PLL and ASP lifecycle. The existing codec driver already + sets CS42L84_MIC_DET_CTL4_LATCH_TO_VP; the working proxy input + requires that detection latch. Matching TX framing alone is insufficient. + Qualify input and output independently, including muted stop and clock + restoration, before simultaneous headset duplex. +4. Qualify speaker initialization, native mute/dwell/shutdown and reliable + I2C completion. Complete protected configuration before activation. + The paired five-second test had a matching completed DMA report, + zero residue/DART faults, both mute writes and TX configuration restore, + then timed out in the later amplifier shutdown wrapper. Watchdog + recovery was required; complete cleanup was not established. +5. Qualify feedback capture and per-speaker model parameters, then the + kernel/userspace volume interlock and its failure behavior. Enable + speaker userspace routing only after those requirements pass. + +The existing T8140 AOP audio driver supplies an integration pattern, not a +J616s compatibility match. Its J700 LP ring/stream geometry, CS42L83 jack, +MAX98360A speaker path and feed-forward sense stream differ from this board. +The existing CS42L84 and SN012776 codec drivers are source references; +the J616s machine path and firmware-owned bus still need integration and +live validation. Do not replace SN012776 I/V protection with the J700 +feed-forward speakerguardd model. + +The PA Semi I2C driver requires XEN (bit 27) completion; the unexplained +bit 29 seen in proxy failures is not an acknowledgement. Keep bounded +completion/error checks and do not continue cleanup after an ambiguous +transfer. DMA ownership must survive uncertainty until recovery; a timeout +or frontend idle reply is not permission to free mapped memory. + +The shared Python I2C helper now clears only its observed status snapshot +and rejects XIP before the write. Direct guarded calls pass on both idle +speaker buses; private bounded amp trials use the same policy with their +ownership, errors and XEN/no-XIP gates unchanged. Later shutdown and four +individual playback cleanups pass, but do not prove the intermittent STOP +fault resolved. Existing generic transfer methods still reset FIFOs before +this check; the helper change does not make a whole transfer concurrency-safe. + +Next kernel integration step +---------------------------- + +Add a separate T6040 AOP control-only provider profile and validate service +discovery before an ALSA audio child can probe. The AOP OF table currently +has no T6040 match. The required provider is the RTKit/AOP transport with +42-bit DART mappings, own ADT resources and EPIC version 2; the tested audio +service is on endpoint 0x22. J700 instead uses EPIC version 4, a setup mailbox +and audio on endpoint 0x21. + +The provider now selects scalar boot-argument overrides from its hardware +profile. It validates the complete record layout, required unique fields, +scalar widths and value fit before changing any bytes. Existing SoC values +and J700's untouched boot arguments are preserved. The qualified J616s +startup changes only EC0p to the ADT remap address 1 TiB; copying the legacy +nCal/alig/AOPt overrides remains unqualified. + +T6040 activation remains disabled. The paired bootloader producer exports +current ADT external firmware reservations. The reserved-memory +apple,firmware-read-only flag is consumed by the IOMMU reservation allocator +and preserves read-only text without changing existing read/write defaults. +The full guest DT provider, cold stream-0 four-level DART attach, DAPF ranges +and CPU/mailbox ownership still need qualification before startup. + +Teardown now retains firmware-visible buffers after partial startup or +unconfirmed AFK/power shutdown, and rejects new buffer publication during +closing. Confirmed power-quiescence release is preserved. Legacy profiles +which do not request power quiescence also retain RTKit system buffers; +unbind then requires reboot before reprobe. Service devices remain registered +to retain their DMA domains, and checked child lookup defers after parent +driver data has gone away. + +Keep automatic legacy audio-child probing gated. Its Rust probe uploads a +632-byte PDM structure, including an explicit alignment byte, and writes +lpai property 301. The older proxy structure is 631 bytes; own J616s evidence +reads 626 bytes and identifies 301 as non-writable. A board-specific child +must instead use +the proven LP owned-ring/producer-readiness sequence before the three-channel +HQ Float32 path. This provider/child ABI mismatch is the first integration +blocker, before Linux card/PCM/control names or userspace profiles can be +qualified. CS42L84 jack integration and SN012776 I/V protection follow that +transport; J700 feed-forward protection is not a substitute. + +First Linux inventory and acceptance +------------------------------------ + +Record the exact kernel, firmware and DT revisions and save probe logs +before adding runtime profiles. The following commands list devices +without starting an audio stream:: + + cat /proc/asound/cards + cat /proc/asound/pcm + aplay -l + arecord -l + +For each observed card, record its driver and ID, mixer names/types/ranges, +dB metadata and jack events. Use the observed card ID when reading mixer +contents. Do not copy J700 IDs, PCM indices, mixer names or safe-volume +settings. + +Record supported PCM formats/rates/channels and explicit channel mapping. +Then qualify bounded capture and headphones, stop/restart, and headset +plug/unplug. Shared-clock duplex and continuous streams require separate +evidence. For speakers, additionally test all-six routing, feedback frame +integrity, measured I/V scaling and thermal parameters, and daemon-exit, +feedback-loss and stalled-stream relock behavior. A successful probe or +audible tone does not satisfy these acceptance checks. + +Sources +------- + +The published m1n1 bring-up evidence [1]_ and driver contracts [2]_ pin the +proxy evidence used here. Private recordings, firmware and probe harnesses +are not included in this documentation. + +.. [1] https://github.com/aurora-silicon/m1n1/blob/fd360ef69054e42724b975d7a4c61e0242b6b341/docs/j616s-bringup.md +.. [2] https://github.com/aurora-silicon/m1n1/blob/fd360ef69054e42724b975d7a4c61e0242b6b341/docs/j616s-driver-contracts.md diff --git a/Documentation/sound/cards/index.rst b/Documentation/sound/cards/index.rst index e68bbb13c384b..33309e676f94f 100644 --- a/Documentation/sound/cards/index.rst +++ b/Documentation/sound/cards/index.rst @@ -19,3 +19,4 @@ Card-Specific Information serial-u16550 img-spdif-in pcmtest + apple-j616s diff --git a/MAINTAINERS b/MAINTAINERS index 99987f9eca95b..c5c73d37ae3f5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2004,13 +2004,14 @@ F: arch/arm64/kernel/idle-apple.S F: drivers/hid/dockchannel-hid/dockchannel-hid-protocol* F: drivers/iio/common/aop_sensors/ F: drivers/rtc/rtc-apple-pmu.c -F: drivers/soc/apple/aop.rs +F: drivers/soc/apple/aop*.rs F: drivers/soc/apple/mcc.c F: drivers/tty/serial/apple_dockchannel* F: rust/kernel/iio/common/aop_sensors.rs F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs F: tools/testing/apple/ +F: tools/testing/selftests/drivers/apple/ APPLETALK NETWORK LAYER L: netdev@vger.kernel.org diff --git a/drivers/base/dd.c b/drivers/base/dd.c index 51c7132e98a07..b3f7b7e40380d 100644 --- a/drivers/base/dd.c +++ b/drivers/base/dd.c @@ -376,12 +376,12 @@ static void __exit deferred_probe_exit(void) } __exitcall(deferred_probe_exit); -int __device_set_driver_override(struct device *dev, const char *s, size_t len) +const char *__device_prepare_driver_override(const char *s, size_t len) { - const char *new = NULL, *old; + const char *new = NULL; if (!s) - return -EINVAL; + return ERR_PTR(-EINVAL); /* * The stored value will be used in sysfs show callback (sysfs_emit()), @@ -390,7 +390,7 @@ int __device_set_driver_override(struct device *dev, const char *s, size_t len) * show. */ if (len >= (PAGE_SIZE - 1)) - return -EINVAL; + return ERR_PTR(-EINVAL); /* * Compute the real length of the string in case userspace sends us a @@ -414,8 +414,15 @@ int __device_set_driver_override(struct device *dev, const char *s, size_t len) if (len) { new = kstrndup(s, len, GFP_KERNEL); if (!new) - return -ENOMEM; + return ERR_PTR(-ENOMEM); } + return new; +} +EXPORT_SYMBOL_GPL(__device_prepare_driver_override); + +void __device_install_driver_override(struct device *dev, const char *new) +{ + const char *old; scoped_guard(spinlock, &dev->driver_override.lock) { old = dev->driver_override.name; @@ -423,7 +430,16 @@ int __device_set_driver_override(struct device *dev, const char *s, size_t len) } kfree(old); +} +EXPORT_SYMBOL_GPL(__device_install_driver_override); +int __device_set_driver_override(struct device *dev, const char *s, size_t len) +{ + const char *new = __device_prepare_driver_override(s, len); + + if (IS_ERR(new)) + return PTR_ERR(new); + __device_install_driver_override(dev, new); return 0; } EXPORT_SYMBOL_GPL(__device_set_driver_override); @@ -1398,6 +1414,20 @@ void device_release_driver(struct device *dev) } EXPORT_SYMBOL_GPL(device_release_driver); +void __device_retire_driver(struct device *dev, const char *override) +{ + /* + * Pair with attach's device lock. A match made before this lock can + * still reach probe later; the consumer must close its lookup context + * before calling this helper. + */ + __device_driver_lock(dev, dev->parent); + __device_install_driver_override(dev, override); + __device_release_driver(dev, dev->parent); + __device_driver_unlock(dev, dev->parent); +} +EXPORT_SYMBOL_GPL(__device_retire_driver); + /** * device_driver_detach - detach driver from a specific device * @dev: device to detach driver from diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index a1aeaf3e4dc8f..16fa34a1565bf 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -274,6 +274,20 @@ static int admac_desc_free(struct dma_async_tx_descriptor *tx) return 0; } +static bool admac_cyclic_range_valid(dma_addr_t address, size_t bytes, + size_t period) +{ + /* + * Each descriptor has a 32-bit byte count; the provider's DMA mask + * is 42 bits. Every period must stay inside the complete owned buffer. + */ + if (!bytes || !period || period > U32_MAX || period > bytes || bytes % period) + return false; + if (address > DMA_BIT_MASK(42) || bytes - 1 > DMA_BIT_MASK(42) - address) + return false; + return true; +} + static struct dma_async_tx_descriptor *admac_prep_dma_cyclic( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_transfer_direction direction, @@ -284,6 +298,8 @@ static struct dma_async_tx_descriptor *admac_prep_dma_cyclic( if (direction != admac_chan_direction(adchan->no)) return NULL; + if (!admac_cyclic_range_valid(buf_addr, buf_len, period_len)) + return NULL; adtx = kzalloc_obj(*adtx, GFP_NOWAIT); if (!adtx) diff --git a/drivers/iommu/of_iommu.c b/drivers/iommu/of_iommu.c index 69377addd6ceb..8181636d2c988 100644 --- a/drivers/iommu/of_iommu.c +++ b/drivers/iommu/of_iommu.c @@ -18,6 +18,7 @@ #include #include "iommu-priv.h" +#include "of_iommu_firmware.h" static int of_iommu_xlate(struct device *dev, struct of_phandle_args *iommu_spec) @@ -244,14 +245,11 @@ void of_iommu_get_resv_regions(struct device *dev, struct list_head *list) np = of_find_node_by_phandle(phandle); if (np == dev->of_node) { - int prot = IOMMU_READ | IOMMU_WRITE; struct iommu_resv_region *region; enum iommu_resv_type type; phys_addr_t iova; size_t length; - - if (of_dma_is_coherent(dev->of_node)) - prot |= IOMMU_CACHE; + bool read_only = of_iommu_firmware_read_only(it.node); maps = of_translate_dma_region(np, maps, &iova, &length); if (length == 0) { @@ -260,12 +258,9 @@ void of_iommu_get_resv_regions(struct device *dev, struct list_head *list) } type = iommu_resv_region_get_type(dev, &phys, iova, length); - if (type == IOMMU_RESV_TRANSLATED) - region = iommu_alloc_resv_region_tr(phys.start, iova, length, prot, type, - GFP_KERNEL); - else - region = iommu_alloc_resv_region(iova, length, prot, type, - GFP_KERNEL); + region = of_iommu_alloc_firmware_region( + phys.start, iova, length, type, + of_dma_is_coherent(dev->of_node), read_only); if (region) list_add_tail(®ion->list, list); diff --git a/drivers/iommu/of_iommu_firmware.h b/drivers/iommu/of_iommu_firmware.h new file mode 100644 index 0000000000000..686d0659c6b07 --- /dev/null +++ b/drivers/iommu/of_iommu_firmware.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __OF_IOMMU_FIRMWARE_H +#define __OF_IOMMU_FIRMWARE_H + +static inline bool of_iommu_firmware_read_only(struct device_node *node) +{ + return of_device_is_compatible(node, "apple,asc-mem") && + of_property_read_bool(node, "apple,firmware-read-only"); +} + +/* The caller supplies the usual OF/IOMMU types and the opt-in DT policy. */ +static inline struct iommu_resv_region * +of_iommu_alloc_firmware_region(phys_addr_t phys, phys_addr_t iova, size_t length, + enum iommu_resv_type type, bool coherent, + bool read_only) +{ + int prot = IOMMU_READ; + + if (!read_only) + prot |= IOMMU_WRITE; + if (coherent) + prot |= IOMMU_CACHE; + if (type == IOMMU_RESV_TRANSLATED) + return iommu_alloc_resv_region_tr(phys, iova, length, prot, type, + GFP_KERNEL); + return iommu_alloc_resv_region(iova, length, prot, type, GFP_KERNEL); +} + +#endif diff --git a/drivers/media/platform/apple/isp/Kconfig b/drivers/media/platform/apple/isp/Kconfig index 5d818e55448a4..1523f2bb34cb2 100644 --- a/drivers/media/platform/apple/isp/Kconfig +++ b/drivers/media/platform/apple/isp/Kconfig @@ -10,6 +10,7 @@ config VIDEO_APPLE_ISP depends on OF_ADDRESS depends on V4L_PLATFORM_DRIVERS depends on VIDEO_DEV + depends on DRM help Driver for the image signal processor that runs the built-in camera of Apple silicon machines. The ISP firmware is loaded by @@ -17,3 +18,13 @@ config VIDEO_APPLE_ISP calibration file for the detected camera module, apple/isp__XX.dat, from the firmware search path; if it is missing, the stream runs without calibration. + +config VIDEO_APPLE_ISP_KUNIT_TEST + tristate "Apple ISP IPC layout tests" if !KUNIT_ALL_TESTS + depends on KUNIT + default KUNIT_ALL_TESTS + help + Test firmware-selected IPC ring extents, including 64-bit addresses, + overflow, alignment and overlap with the channel table or other rings. + These tests run without starting or accessing the ISP and do not + require a supported Apple device or the ISP driver itself. diff --git a/drivers/media/platform/apple/isp/Makefile b/drivers/media/platform/apple/isp/Makefile index 4649f32987f02..2c905783128a2 100644 --- a/drivers/media/platform/apple/isp/Makefile +++ b/drivers/media/platform/apple/isp/Makefile @@ -1,3 +1,5 @@ # SPDX-License-Identifier: GPL-2.0-only -apple-isp-y := isp-cam.o isp-cmd.o isp-drv.o isp-fw.o isp-iommu.o isp-ipc.o isp-v4l2.o +apple-isp-y := isp-cam.o isp-cmd.o isp-drv.o isp-fw.o isp-iommu.o isp-ipc.o isp-profile.o isp-v4l2.o obj-$(CONFIG_VIDEO_APPLE_ISP) += apple-isp.o +obj-$(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) += isp-ipc-layout-test.o +obj-$(CONFIG_VIDEO_APPLE_ISP_KUNIT_TEST) += isp-buffer-test.o diff --git a/drivers/media/platform/apple/isp/isp-buffer-pages.h b/drivers/media/platform/apple/isp/isp-buffer-pages.h new file mode 100644 index 0000000000000..2c9f8f0eb0b18 --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-buffer-pages.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __ISP_BUFFER_PAGES_H__ +#define __ISP_BUFFER_PAGES_H__ + +#include +#include +#include + +struct isp_pinned_pages { + struct page **pages; + unsigned int count; +}; + +/* The ISP queue permits MMAP only: its SG extents contain whole pages. */ +static inline int isp_pin_buffer_pages(struct sg_table *sgt, + struct isp_pinned_pages *pins) +{ + struct scatterlist *sg; + u64 count = 0; + unsigned int i, index = 0; + + if (!sgt || !sgt->sgl || pins->pages || pins->count) + return -EINVAL; + + for_each_sg(sgt->sgl, sg, sgt->orig_nents, i) { + if (sg->offset % PAGE_SIZE || !sg->length || sg->length % PAGE_SIZE) + return -EINVAL; + count += sg->length / PAGE_SIZE; + if (count > UINT_MAX) + return -EOVERFLOW; + } + if (!count) + return -EINVAL; + pins->pages = kvmalloc_array(count, sizeof(*pins->pages), GFP_KERNEL); + if (!pins->pages) + return -ENOMEM; + for_each_sg(sgt->sgl, sg, sgt->orig_nents, i) { + unsigned int first = sg->offset / PAGE_SIZE; + unsigned int pages = sg->length / PAGE_SIZE; + + for (unsigned int j = 0; j < pages; j++) { + struct page *page = pfn_to_page(page_to_pfn(sg_page(sg)) + first + j); + + get_page(page); + pins->pages[index++] = page; + } + } + pins->count = count; + return 0; +} + +static inline void isp_unpin_buffer_pages(struct isp_pinned_pages *pins) +{ + for (unsigned int i = 0; i < pins->count; i++) + put_page(pins->pages[i]); + kvfree(pins->pages); + pins->pages = NULL; + pins->count = 0; +} + +#endif /* __ISP_BUFFER_PAGES_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-buffer-test.c b/drivers/media/platform/apple/isp/isp-buffer-test.c new file mode 100644 index 0000000000000..2b0a6498c38fd --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-buffer-test.c @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include + +#include "isp-buffer.h" +#include "isp-buffer-pages.h" + +static struct isp_buflist_buffer preview_descriptor(void) +{ + /* Recorded completed1080p geometry/tag; addresses are synthetic leases. */ + return (struct isp_buflist_buffer) { + .iovas = { 0x10008000000ULL, 0x100083f8000ULL }, + .flags = { 0x40000000, 0x40000000 }, + .num_planes = 2, .pool_type = 9, .tag = 0x1010, + }; +} + +static void isp_buffer_retirement_test(struct kunit *test) +{ + struct isp_buffer_lease lease = {}; + struct isp_buflist_buffer descriptor = preview_descriptor(); + struct isp_buflist_buffer report = descriptor; + + report.iovas[0] = (u32)report.iovas[0]; + report.iovas[1] = (u32)report.iovas[1]; + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&lease, &descriptor)); + KUNIT_ASSERT_TRUE(test, isp_buffer_return(&lease, &report)); + KUNIT_EXPECT_FALSE(test, isp_buffer_reclaimable(&lease, false)); + KUNIT_EXPECT_FALSE(test, isp_buffer_submit(&lease, &descriptor)); + KUNIT_ASSERT_TRUE(test, isp_buffer_acknowledge(&lease)); + KUNIT_EXPECT_TRUE(test, isp_buffer_reclaimable(&lease, false)); + descriptor.tag++; + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&lease, &descriptor)); + KUNIT_EXPECT_FALSE(test, isp_buffer_return(&lease, &report)); + KUNIT_EXPECT_EQ(test, lease.owner, ISP_BUFFER_SUBMITTED); +} + +static void isp_buffer_full_record_test(struct kunit *test) +{ + struct isp_buflist_buffer descriptor = preview_descriptor(); + struct isp_buffer_lease lease = {}; + struct isp_buflist_buffer report; + + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&lease, &descriptor)); + for (unsigned int field = 0; field < 4; field++) { + report = descriptor; + switch (field) { + case 0: + report.iovas[1] += 0x4000; + break; + case 1: + report.num_planes = 1; + break; + case 2: + report.pool_type = 1; + break; + case 3: + report.tag--; + break; + } + KUNIT_EXPECT_FALSE(test, isp_buffer_return(&lease, &report)); + KUNIT_EXPECT_EQ(test, lease.owner, ISP_BUFFER_SUBMITTED); + } +} + +static void isp_buffer_alias_ambiguity_test(struct kunit *test) +{ + struct isp_buflist_buffer descriptor = preview_descriptor(); + struct isp_buffer_lease a = {}, b = {}; + struct isp_buffer_match match = {}; + + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&a, &descriptor)); + descriptor.iovas[0] += 1ULL << 32; + descriptor.iovas[1] += 1ULL << 32; + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&b, &descriptor)); + isp_buffer_match_candidate(&match, &a, &descriptor); + isp_buffer_match_candidate(&match, &b, &descriptor); + KUNIT_EXPECT_PTR_EQ(test, isp_buffer_unique_match(&match), NULL); +} + +static void isp_buffer_uncertainty_test(struct kunit *test) +{ + struct isp_buflist_buffer descriptor = preview_descriptor(); + struct isp_buffer_lease lease = {}; + + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&lease, &descriptor)); + /* Losing the submission ACK does not return the lease to the host. */ + KUNIT_EXPECT_FALSE(test, isp_buffer_reclaimable(&lease, false)); + KUNIT_EXPECT_FALSE(test, isp_buffer_submit(&lease, &descriptor)); + KUNIT_EXPECT_TRUE(test, isp_buffer_reclaimable(&lease, true)); +} + +static void isp_buffer_legacy_tag_test(struct kunit *test) +{ + /* Legacy m1n1 native request example: metadata pool0/tag0x13f. */ + struct isp_buflist_buffer descriptor = { + .iovas = { 0x056a8000 }, .flags = { 0x40000000 }, + .num_planes = 1, .pool_type = 0, .tag = 0x13f, + }; + struct isp_buffer_lease lease = {}; + + KUNIT_ASSERT_TRUE(test, isp_buffer_submit(&lease, &descriptor)); + KUNIT_EXPECT_TRUE(test, isp_buffer_return(&lease, &descriptor)); + KUNIT_EXPECT_TRUE(test, isp_buffer_acknowledge(&lease)); +} + +static void isp_buffer_modern_tag_test(struct kunit *test) +{ + struct isp_buflist_buffer descriptor = preview_descriptor(); + struct isp_buflist_buffer report; + struct isp_buffer_lease lease = {}; + + descriptor.tag = 1; + descriptor.pad = 1; /* Modern u64 tag 0x100000001 at wire offset 0x38. */ + KUNIT_ASSERT_TRUE(test, isp_buffer_submit_tag(&lease, &descriptor, true)); + report = descriptor; + report.pad = 2; + KUNIT_EXPECT_FALSE(test, isp_buffer_return(&lease, &report)); + KUNIT_EXPECT_EQ(test, lease.owner, ISP_BUFFER_SUBMITTED); + report.pad = 1; + KUNIT_EXPECT_TRUE(test, isp_buffer_return(&lease, &report)); + KUNIT_EXPECT_TRUE(test, isp_buffer_acknowledge(&lease)); +} + +static void isp_buffer_pages_retained_test(struct kunit *test) +{ + struct isp_pinned_pages pins = {}; + struct sg_table table; + struct page *page = alloc_page(GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, page); + KUNIT_ASSERT_EQ(test, sg_alloc_table(&table, 1, GFP_KERNEL), 0); + sg_set_page(table.sgl, page, PAGE_SIZE, 0); + KUNIT_ASSERT_EQ(test, isp_pin_buffer_pages(&table, &pins), 0); + KUNIT_EXPECT_EQ(test, page_ref_count(page), 2); + /* Model vb2's normal allocation release while DMA is unretired. */ + put_page(page); + sg_free_table(&table); + KUNIT_EXPECT_EQ(test, page_ref_count(pins.pages[0]), 1); + /* A witness reference lets us inspect the release without touching a + * freed page. Reclaim occurs only after the independent pins are put. + */ + get_page(page); + isp_unpin_buffer_pages(&pins); + KUNIT_EXPECT_EQ(test, page_ref_count(page), 1); + KUNIT_EXPECT_EQ(test, pins.count, 0); + put_page(page); +} + +static void isp_buffer_bad_sg_test(struct kunit *test) +{ + struct isp_pinned_pages pins = {}; + struct scatterlist sg; + struct sg_table table = { .sgl = &sg, .orig_nents = 1 }; + struct page *page = alloc_page(GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, page); + sg_init_table(&sg, 1); + sg_set_page(&sg, page, PAGE_SIZE - 1, 0); + KUNIT_EXPECT_EQ(test, isp_pin_buffer_pages(&table, &pins), -EINVAL); + KUNIT_EXPECT_EQ(test, page_ref_count(page), 1); + KUNIT_EXPECT_PTR_EQ(test, pins.pages, NULL); + put_page(page); +} + +static struct kunit_case isp_buffer_cases[] = { + KUNIT_CASE(isp_buffer_retirement_test), + KUNIT_CASE(isp_buffer_full_record_test), + KUNIT_CASE(isp_buffer_alias_ambiguity_test), + KUNIT_CASE(isp_buffer_uncertainty_test), + KUNIT_CASE(isp_buffer_legacy_tag_test), + KUNIT_CASE(isp_buffer_modern_tag_test), + KUNIT_CASE(isp_buffer_pages_retained_test), + KUNIT_CASE(isp_buffer_bad_sg_test), + {} +}; + +static struct kunit_suite isp_buffer_suite = { + .name = "apple-isp-buffer-ownership", + .test_cases = isp_buffer_cases, +}; + +kunit_test_suite(isp_buffer_suite); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Apple ISP buffer ownership tests"); diff --git a/drivers/media/platform/apple/isp/isp-buffer.h b/drivers/media/platform/apple/isp/isp-buffer.h new file mode 100644 index 0000000000000..72e4acd6dfbc1 --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-buffer.h @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __ISP_BUFFER_H__ +#define __ISP_BUFFER_H__ + +#include +#include + +#define ISP_BUFFER_PLANES 4 + +/* Existing legacy/H17 buffer record. The reserved high tag word stays zero. */ +struct isp_buflist_buffer { + u64 iovas[ISP_BUFFER_PLANES]; + u32 flags[ISP_BUFFER_PLANES]; + u32 num_planes; + u32 pool_type; + u32 tag; + u32 pad; +} __packed; +static_assert(sizeof(struct isp_buflist_buffer) == 0x40); + +enum isp_buffer_owner { + ISP_BUFFER_HOST, + ISP_BUFFER_SUBMITTED, + ISP_BUFFER_REPORT_PENDING, +}; + +struct isp_buffer_lease { + struct isp_buflist_buffer submitted; + enum isp_buffer_owner owner; + bool tag64; +}; + +struct isp_buffer_match { + struct isp_buffer_lease *lease; + bool ambiguous; +}; + +static inline bool isp_buffer_matches(const struct isp_buffer_lease *lease, + const struct isp_buflist_buffer *report) +{ + const struct isp_buflist_buffer *submitted = &lease->submitted; + + if (lease->owner != ISP_BUFFER_SUBMITTED || + !report->num_planes || report->num_planes > ISP_BUFFER_PLANES || + report->num_planes != submitted->num_planes || + report->pool_type != submitted->pool_type || report->tag != submitted->tag) + return false; + if (lease->tag64 && report->pad != submitted->pad) + return false; + + /* Existing firmware exports the low32 address, including for high IOVAs. */ + for (unsigned int i = 0; i < report->num_planes; i++) + if ((u32)report->iovas[i] != (u32)submitted->iovas[i]) + return false; + + return true; +} + +static inline void isp_buffer_match_candidate(struct isp_buffer_match *match, + struct isp_buffer_lease *lease, + const struct isp_buflist_buffer *report) +{ + if (!isp_buffer_matches(lease, report)) + return; + if (match->lease) + match->ambiguous = true; + else + match->lease = lease; +} + +static inline struct isp_buffer_lease * +isp_buffer_unique_match(const struct isp_buffer_match *match) +{ + return match->ambiguous ? NULL : match->lease; +} + +static inline bool isp_buffer_submit_tag(struct isp_buffer_lease *lease, + const struct isp_buflist_buffer *descriptor, + bool tag64) +{ + if (lease->owner != ISP_BUFFER_HOST || !descriptor->num_planes || + descriptor->num_planes > ISP_BUFFER_PLANES) + return false; + lease->submitted = *descriptor; + lease->tag64 = tag64; + lease->owner = ISP_BUFFER_SUBMITTED; + return true; +} + +static inline bool isp_buffer_submit(struct isp_buffer_lease *lease, + const struct isp_buflist_buffer *descriptor) +{ + return isp_buffer_submit_tag(lease, descriptor, false); +} + +static inline bool isp_buffer_return(struct isp_buffer_lease *lease, + const struct isp_buflist_buffer *report) +{ + /* Batch callers first validate that this is the unique live match. */ + if (!isp_buffer_matches(lease, report)) + return false; + lease->owner = ISP_BUFFER_REPORT_PENDING; + return true; +} + +static inline bool isp_buffer_acknowledge(struct isp_buffer_lease *lease) +{ + if (lease->owner != ISP_BUFFER_REPORT_PENDING) + return false; + lease->owner = ISP_BUFFER_HOST; + return true; +} + +static inline bool isp_buffer_reclaimable(const struct isp_buffer_lease *lease, + bool firmware_quiescent) +{ + return lease->owner == ISP_BUFFER_HOST || firmware_quiescent; +} + +#endif /* __ISP_BUFFER_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-cam.c b/drivers/media/platform/apple/isp/isp-cam.c index 5090ed2ed5b98..9b2e8a252ac8b 100644 --- a/drivers/media/platform/apple/isp/isp-cam.c +++ b/drivers/media/platform/apple/isp/isp-cam.c @@ -312,7 +312,7 @@ int apple_isp_detect_camera(struct apple_isp *isp) "failed to boot firmware for initial sensor detection: %d\n", err); /* Resident firmware would fail a deferred retry the same way. */ - return isp->hw->resident_fw ? err : -EPROBE_DEFER; + return isp->hw->resident_fw || !isp->firmware_quiescent ? err : -EPROBE_DEFER; } err = isp_detect_camera(isp); @@ -423,12 +423,78 @@ static int isp_ch_configure_pools(struct apple_isp *isp, u32 ch) fmt->strides[1]); } +static int isp_ch_configure_t6040_preview(struct apple_isp *isp, u32 ch) +{ + struct isp_format *fmt = isp_get_format(isp, ch); + struct isp_profile_geometry geometry; + int err; + + if (ch || fmt->preset->index != 5 || fmt->preset->input_dim.x != 1920 || + fmt->preset->input_dim.y != 2160) + return -EINVAL; + err = isp_profile_p010_geometry(fmt->preset->output_dim.x, + fmt->preset->output_dim.y, &geometry); + if (err) + return err; + if (fmt->strides[0] != geometry.stride || fmt->strides[1] != geometry.stride) + return -EINVAL; + err = isp_cmd_flicker_sensor_set(isp, 0); + if (err) + return err; + err = isp_cmd_ch_sbs_enable(isp, 0, 1); + if (err) + return err; + err = isp_cmd_ch_camera_config_select(isp, 0, 5); + if (err) + return err; + err = isp_cmd_ch_buffer_recycle_mode_set(isp, 0, 1); + if (err) + return err; + err = isp_cmd_ch_buffer_recycle_start(isp, 0); + if (err) + return err; + err = isp_cmd_ch_crop_set(isp, 0, 0, 0, geometry.width, geometry.height); + if (err) + return err; + err = isp_cmd_ch_output_config_set(isp, 0, geometry.width, geometry.height, + fmt->strides, 1, 0x12); + if (err) + return err; + err = isp_cmd_ch_ae_frame_rate_max_set(isp, 0, 7680); + if (err) + return err; + err = isp_cmd_ch_ae_frame_rate_min_set(isp, 0, 3840); + if (err) + return err; + err = isp_cmd_ch_buffer_pool_config_set(isp, 0, CISP_POOL_TYPE_META); + if (err) + return err; + err = isp_cmd_ch_buffer_pool_config_set_rendered(isp, 0, + isp->profile->output_count, + geometry.luma_size, + geometry.stride, + geometry.chroma_size, + geometry.stride); + if (err) + return err; + err = isp_cmd_ch_local_raw_buffer_enable(isp, 0, 1); + if (err) + return err; + err = isp_cmd_ch_preview_stream_set(isp, 0, 1); + if (err) + return err; + return isp_cmd_ch_master_slave_sync_mode_set(isp, 0, 0); +} + static int isp_ch_configure_capture(struct apple_isp *isp, u32 ch) { struct isp_format *fmt = isp_get_format(isp, ch); bool h17 = isp->hw->fw_abi == ISP_FW_ABI_H17; int err; + if (isp->profile) + return isp_ch_configure_t6040_preview(isp, ch); + isp_cmd_flicker_sensor_set(isp, 0); /* @@ -653,11 +719,15 @@ int apple_isp_start_capture(struct apple_isp *isp) return err; } -void apple_isp_stop_capture(struct apple_isp *isp) +int apple_isp_stop_capture(struct apple_isp *isp) { - isp_cmd_ch_stop(isp, 0); // TODO channel mask - isp_cmd_ch_buffer_return(isp, isp->current_ch); + int err; + + err = isp_cmd_ch_stop(isp, 0); // TODO channel mask + if (!err) + err = isp_cmd_ch_buffer_return(isp, isp->current_ch); /* ... until its buffers are back. */ apple_isp_wdt_stop(isp); + return err; } diff --git a/drivers/media/platform/apple/isp/isp-cam.h b/drivers/media/platform/apple/isp/isp-cam.h index 51c2f2ec6c079..48c0db8ed753e 100644 --- a/drivers/media/platform/apple/isp/isp-cam.h +++ b/drivers/media/platform/apple/isp/isp-cam.h @@ -19,7 +19,7 @@ int apple_isp_start_camera(struct apple_isp *isp); void apple_isp_stop_camera(struct apple_isp *isp); int apple_isp_start_capture(struct apple_isp *isp); -void apple_isp_stop_capture(struct apple_isp *isp); +int apple_isp_stop_capture(struct apple_isp *isp); void apple_isp_wdt_init(struct apple_isp *isp); void apple_isp_wdt_stop(struct apple_isp *isp); diff --git a/drivers/media/platform/apple/isp/isp-cmd.c b/drivers/media/platform/apple/isp/isp-cmd.c index 42c448fd0f30c..e73057fcdd508 100644 --- a/drivers/media/platform/apple/isp/isp-cmd.c +++ b/drivers/media/platform/apple/isp/isp-cmd.c @@ -23,6 +23,13 @@ static int cisp_send(struct apple_isp *isp, void *args, u32 insize, u32 outsize, struct isp_message *req = &chan->req; int err; + mutex_lock(&chan->lock); + /* Preserve the published command area after an ambiguous timeout. */ + if (chan->tx_poisoned) { + err = -EIO; + goto unlock; + } + req->arg0 = isp->cmd_iova; req->arg1 = insize; req->arg2 = outsize; @@ -34,23 +41,32 @@ static int cisp_send(struct apple_isp *isp, void *args, u32 insize, u32 outsize, memcpy(&opcode, args, sizeof(opcode)); dev_err(isp->dev, "%s: failed to send OPCODE 0x%04llx: [0x%llx, 0x%llx, 0x%llx]\n", - chan->name, CISP_OPCODE_GET(opcode), req->arg0, - req->arg1, req->arg2); + chan->name, CISP_OPCODE_GET(opcode), req->arg0, + req->arg1, req->arg2); } + if (!err && outsize) + memcpy(args, isp->cmd_virt, outsize); +unlock: + mutex_unlock(&chan->lock); return err; } static int cisp_send_read(struct apple_isp *isp, void *args, u32 insize, u32 outsize) { - /* TODO do I need to lock the iova space? */ - int err = cisp_send(isp, args, insize, outsize, CISP_TIMEOUT); - if (err) - return err; + return cisp_send(isp, args, insize, outsize, CISP_TIMEOUT); +} + +int isp_cmd_set_dsid_t6040(struct apple_isp *isp, u32 mcc_mask) +{ + struct isp_t6040_dsid_command args; + int err; - memcpy(args, isp->cmd_virt, outsize); - return 0; + if (!isp->profile) + return -EINVAL; + err = isp_profile_t6040_dsid(mcc_mask, &args); + return err ?: CISP_SEND_IN(isp, args); } int isp_cmd_start(struct apple_isp *isp, u32 mode) @@ -345,7 +361,7 @@ int isp_cmd_ch_crop_set(struct apple_isp *isp, u32 chan, u32 x1, u32 y1, u32 x2, u32 y2) { struct cmd_ch_crop_set args = { - .opcode = CISP_OPCODE(isp->hw->scl1 ? CISP_CMD_CH_CROP_SCL1_SET + .opcode = CISP_OPCODE((isp->profile || isp->hw->scl1) ? CISP_CMD_CH_CROP_SCL1_SET : CISP_CMD_CH_CROP_SET), .chan = chan, .x1 = x1, @@ -360,7 +376,8 @@ int isp_cmd_ch_output_config_set(struct apple_isp *isp, u32 chan, u32 width, u32 height, u32 strides[3], u32 colorspace, u32 format) { struct cmd_ch_output_config_set args = { - .opcode = CISP_OPCODE(isp->hw->scl1 ? CISP_CMD_CH_OUTPUT_CONFIG_SCL1_SET + .opcode = CISP_OPCODE((isp->profile || isp->hw->scl1) ? + CISP_CMD_CH_OUTPUT_CONFIG_SCL1_SET : CISP_CMD_CH_OUTPUT_CONFIG_SET), .chan = chan, .width = width, @@ -460,9 +477,9 @@ int isp_cmd_ch_buffer_pool_config_set(struct apple_isp *isp, u32 chan, u16 type) .opcode = CISP_OPCODE(CISP_CMD_CH_BUFFER_POOL_CONFIG_SET), .chan = chan, .type = type, - .count = ISP_MAX_BUFFERS, - .meta_size0 = size, - .meta_size1 = size, + .count = isp->profile ? isp->profile->meta_count : ISP_MAX_BUFFERS, + .meta_size0 = isp->profile ? isp->profile->meta_size : size, + .meta_size1 = isp->profile ? isp->profile->meta_size : size, .unk0 = 0, .unk1 = 0, .unk2 = 0, @@ -481,7 +498,7 @@ int isp_cmd_ch_buffer_pool_config_set_rendered(struct apple_isp *isp, u32 chan, struct cmd_ch_buffer_pool_config_set_rendered args = { .opcode = CISP_OPCODE(CISP_CMD_CH_BUFFER_POOL_CONFIG_SET), .chan = chan, - .type = CISP_POOL_TYPE_RENDERED, + .type = isp->profile ? CISP_POOL_TYPE_RENDERED_SCL1 : CISP_POOL_TYPE_RENDERED, .count = count, .plane0_size = plane0_size, .plane0_stride = plane0_stride, diff --git a/drivers/media/platform/apple/isp/isp-cmd.h b/drivers/media/platform/apple/isp/isp-cmd.h index ab0c85023bebf..be19a3700e054 100644 --- a/drivers/media/platform/apple/isp/isp-cmd.h +++ b/drivers/media/platform/apple/isp/isp-cmd.h @@ -261,7 +261,8 @@ int isp_cmd_set_isp_pmu_base(struct apple_isp *isp, u64 pmu_base); int isp_cmd_set_dsid_clr_req_base(struct apple_isp *isp, u64 dsid_clr_base, u32 dsid_clr_range); int isp_cmd_set_dsid_clr_multi_bc(struct apple_isp *isp, u64 dsid_clr_base, - u32 dsid_clr_range); + u32 dsid_clr_range); +int isp_cmd_set_dsid_t6040(struct apple_isp *isp, u32 mcc_mask); int isp_cmd_set_dsid_clr_req_base2(struct apple_isp *isp, u64 dsid_clr_base0, u64 dsid_clr_base1, u64 dsid_clr_base2, u64 dsid_clr_base3, u32 dsid_clr_range0, diff --git a/drivers/media/platform/apple/isp/isp-drv.c b/drivers/media/platform/apple/isp/isp-drv.c index 9f5f8f58b3748..4b0e45c6dabd4 100644 --- a/drivers/media/platform/apple/isp/isp-drv.c +++ b/drivers/media/platform/apple/isp/isp-drv.c @@ -195,6 +195,14 @@ static int apple_isp_init_iommu(struct apple_isp *isp) // FIXME: refactor this, maybe use regular iova stuff? isp->iova_size = vm_size - (heap_base & 0xffffffff); + /* An earlier failed session may still own this domain's first dynamic + * mapping. Do not create a fresh allocator over retained DMA storage. + */ + if (iommu_iova_to_phys(isp->domain, + ALIGN(isp->fw.heap_top, 1UL << isp->shift))) { + dev_err(dev, "dynamic ISP aperture is still mapped; reboot required\n"); + return -EBUSY; + } drm_mm_init(&isp->iovad, isp->fw.heap_top, isp->iova_size); err = apple_isp_map_fw_mmio(isp); @@ -380,12 +388,41 @@ static int apple_isp_probe(struct platform_device *pdev) if (err) return err; - isp = devm_kzalloc(dev, sizeof(*isp), GFP_KERNEL); + /* Keep this allocation when DMA cannot be quiesced. Its IOVA allocator + * and retained backing records must outlive driver-core devres cleanup. + */ + isp = kzalloc_obj(*isp); if (!isp) return -ENOMEM; isp->dev = dev; + isp->firmware_quiescent = true; + isp->next_buffer_tag = 1; isp->hw = of_device_get_match_data(dev); + if (of_property_present(dev->of_node, "apple,isp-profile")) { + const char *name, *uuid; + u32 board; + + if (!of_machine_is_compatible("apple,t6040") || + of_property_read_string(dev->of_node, "apple,isp-profile", &name) || + of_property_read_string(dev->of_node, "apple,isp-firmware-uuid", &uuid) || + of_property_read_u32(dev->of_node, "apple,isp-board-id", &board)) { + kfree(isp); + return -EINVAL; + } + isp->profile = isp_profile_select(0x6040, board, name, uuid); + if (!isp->profile) { + kfree(isp); + return -ENODEV; + } + /* Identity/layout qualification alone does not admit native IRQ, + * inherited table ownership or a full-firmware release boundary. + * No T6040 OF match is installed by this preparation patch. + */ + dev_err(dev, "T6040 profile selected; native ISP handoff remains gated\n"); + kfree(isp); + return -EOPNOTSUPP; + } platform_set_drvdata(pdev, isp); dev_set_drvdata(dev, isp); @@ -399,7 +436,7 @@ static int apple_isp_probe(struct platform_device *pdev) if (err) { dev_err(dev, "failed to get 'apple,platform-id' property: %d\n", err); - return err; + goto free_isp; } err = of_property_read_u32(dev->of_node, "apple,temporal-filter", @@ -410,13 +447,13 @@ static int apple_isp_probe(struct platform_device *pdev) err = apple_isp_init_presets(isp); if (err) { dev_err(dev, "failed to initialize presets\n"); - return err; + goto free_isp; } err = apple_isp_attach_genpd(isp); if (err) { dev_err(dev, "failed to attatch power domains\n"); - return err; + goto free_isp; } isp->coproc = devm_platform_ioremap_resource_byname(pdev, "coproc"); @@ -483,6 +520,8 @@ static int apple_isp_probe(struct platform_device *pdev) INIT_LIST_HEAD(&isp->gc); INIT_LIST_HEAD(&isp->bufs_pending); INIT_LIST_HEAD(&isp->bufs_submitted); + INIT_LIST_HEAD(&isp->bufs_retiring); + INIT_LIST_HEAD(&isp->retained_buffers); isp->wq = alloc_workqueue("apple-isp-wq", WQ_UNBOUND, 0); if (!isp->wq) { dev_err(dev, "failed to create workqueue\n"); @@ -521,6 +560,14 @@ static int apple_isp_probe(struct platform_device *pdev) halt_firmware: apple_isp_firmware_halt(isp); free_surface: + if (!READ_ONCE(isp->firmware_quiescent)) { + apple_isp_firmware_retain(isp); + destroy_workqueue(isp->wq); + /* Keep firmware, tables, backing and power references. The + * retained IOMMU mapping rejects a fresh allocator on rebind. + */ + return err; + } pm_runtime_disable(dev); apple_isp_free_firmware_surface(isp); free_iommu: @@ -529,6 +576,9 @@ static int apple_isp_probe(struct platform_device *pdev) destroy_workqueue(isp->wq); detach_genpd: apple_isp_detach_genpd(isp); +free_isp: + platform_set_drvdata(pdev, NULL); + kfree(isp); return err; } @@ -538,13 +588,19 @@ static void apple_isp_remove(struct platform_device *pdev) apple_isp_remove_video(isp); /* Stop resident firmware before freeing what it may still use. */ - apple_isp_firmware_halt(isp); + if (apple_isp_firmware_halt(isp)) { + apple_isp_firmware_retain(isp); + destroy_workqueue(isp->wq); + return; + } apple_isp_free_video(isp); pm_runtime_disable(isp->dev); apple_isp_free_firmware_surface(isp); apple_isp_free_iommu(isp); destroy_workqueue(isp->wq); apple_isp_detach_genpd(isp); + platform_set_drvdata(pdev, NULL); + kfree(isp); } static const struct apple_isp_hw apple_isp_hw_t8103 = { @@ -794,11 +850,14 @@ static __maybe_unused int apple_isp_runtime_resume(struct device *dev) static __maybe_unused int apple_isp_suspend(struct device *dev) { struct apple_isp *isp = dev_get_drvdata(dev); + int err; /* We must restore V4L2 context on system resume. If we were streaming * before, we (essentially) stop streaming and start streaming again. */ - apple_isp_video_suspend(isp); + err = apple_isp_video_suspend(isp); + if (err) + return err; /* * Resident firmware stays up, idle, across suspend-to-idle and @@ -810,8 +869,11 @@ static __maybe_unused int apple_isp_suspend(struct device *dev) static __maybe_unused int apple_isp_freeze(struct device *dev) { struct apple_isp *isp = dev_get_drvdata(dev); + int err; - apple_isp_video_suspend(isp); + err = apple_isp_video_suspend(isp); + if (err) + return err; /* * Hibernation hands the system from one kernel to another, and @@ -823,8 +885,10 @@ static __maybe_unused int apple_isp_freeze(struct device *dev) mutex_lock(&isp->video_lock); if (isp->fw_state == ISP_FW_RUNNING) dev_warn(dev, "stopping the firmware for hibernation, the camera is unavailable until the next boot\n"); - apple_isp_firmware_halt(isp); + err = apple_isp_firmware_halt(isp); mutex_unlock(&isp->video_lock); + if (err) + return err; } return 0; diff --git a/drivers/media/platform/apple/isp/isp-drv.h b/drivers/media/platform/apple/isp/isp-drv.h index ac93aba409840..86c498052a844 100644 --- a/drivers/media/platform/apple/isp/isp-drv.h +++ b/drivers/media/platform/apple/isp/isp-drv.h @@ -16,6 +16,10 @@ #include #include +#include "isp-buffer.h" +#include "isp-buffer-pages.h" +#include "isp-profile.h" + #define APPLE_ISP_DEVICE_NAME "apple-isp" #define APPLE_ISP_CARD_NAME "FaceTime HD Camera" @@ -90,7 +94,7 @@ struct isp_surf { void *virt; refcount_t refcount; bool gc; - bool submitted; + struct isp_buffer_lease lease; }; struct isp_message { @@ -119,6 +123,7 @@ struct isp_channel { struct isp_message req; struct isp_message rsp; const struct isp_chan_ops *ops; + bool tx_poisoned; }; struct coord { @@ -261,6 +266,7 @@ struct isp_format { struct apple_isp { struct device *dev; const struct apple_isp_hw *hw; + const struct isp_profile *profile; enum isp_firmware_version fw_compat; u32 platform_id; u32 temporal_filter; @@ -314,6 +320,7 @@ struct apple_isp { struct isp_surf *ipc_surf; struct isp_surf *extra_surf; struct isp_surf *data_surf; + struct isp_surf *buflist_surf; struct isp_surf *log_surf; struct isp_surf *bt_surf; struct isp_surf *meta_surfs[ISP_MAX_BUFFERS]; @@ -334,21 +341,41 @@ struct apple_isp { wait_queue_head_t wait; dma_addr_t cmd_iova; void *cmd_virt; + dma_addr_t ipc_boot_iova; + size_t ipc_boot_size; unsigned long state; spinlock_t buf_lock; struct list_head bufs_pending; struct list_head bufs_submitted; + struct list_head bufs_retiring; + struct list_head retained_buffers; + u64 next_buffer_tag; + bool capture_failed; + bool firmware_quiescent; + bool video_registered; + bool irq_requested; }; struct isp_chan_ops { int (*handle)(struct apple_isp *isp, struct isp_channel *chan); + void (*acknowledged)(struct apple_isp *isp, struct isp_channel *chan); +}; + +/* Separate from vb2's buffer metadata, which close/REQBUFS may destroy. */ +struct isp_buffer_memory { + struct list_head link; + unsigned int num_planes; + struct isp_surf surfs[VB2_MAX_PLANES]; + struct isp_pinned_pages pins[VB2_MAX_PLANES]; }; struct isp_buffer { struct vb2_v4l2_buffer vb; struct list_head link; - struct isp_surf surfs[VB2_MAX_PLANES]; + struct isp_buffer_memory *memory; + struct isp_buffer_lease lease; + bool rendered; }; #define to_isp_buffer(x) container_of((x), struct isp_buffer, vb) @@ -372,8 +399,21 @@ enum { */ #define isp_fw_iova(isp, x) ((x) & (isp)->fw_iova_mask) #define isp_get_format(isp, ch) (&(isp)->fmts[(ch)]) -#define isp_num_capmeta(isp) \ - ((isp)->hw->capture_meta_size ? ISP_CAPMETA_BUFFERS : 0) +static inline unsigned int isp_num_capmeta(struct apple_isp *isp) +{ + return isp->profile ? 0 : (isp->hw->capture_meta_size ? ISP_CAPMETA_BUFFERS : 0); +} + +static inline unsigned int isp_num_meta(struct apple_isp *isp) +{ + return isp->profile ? isp->profile->meta_count : ARRAY_SIZE(isp->meta_surfs); +} + +static inline unsigned int isp_max_capture_buffers(struct apple_isp *isp) +{ + return isp->profile ? isp->profile->output_count : ISP_MAX_BUFFERS; +} + #define isp_get_current_format(isp) (isp_get_format(isp, isp->current_ch)) #endif /* __ISP_DRV_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-fw.c b/drivers/media/platform/apple/isp/isp-fw.c index ed35f4701c04a..a82ae94f6d608 100644 --- a/drivers/media/platform/apple/isp/isp-fw.c +++ b/drivers/media/platform/apple/isp/isp-fw.c @@ -2,6 +2,7 @@ /* Copyright 2023 Eileen Yoon */ #include "isp-fw.h" +#include "isp-ipc-layout.h" #include #include @@ -209,6 +210,7 @@ static const struct isp_chan_ops sm_ops = { static const struct isp_chan_ops bt_ops = { .handle = ipc_bt_handle, + .acknowledged = ipc_bt_acknowledged, }; static irqreturn_t apple_isp_isr(int irq, void *dev) @@ -240,12 +242,15 @@ static irqreturn_t apple_isp_isr_thread(int irq, void *dev) static void isp_disable_irq(struct apple_isp *isp) { + if (!isp->irq_requested) + return; isp_mbox_write32(isp, isp->hw->mbox_irq_enable, 0x0); if (isp->hw->mbox_irq_route) { isp_mbox_write32(isp, ISP_MBOX_IRQ_ENABLE1_T8140, 0x0); isp_mbox_write32(isp, ISP_MBOX_IRQ_ENABLE2_T8140, 0x0); } free_irq(isp->irq, isp); + isp->irq_requested = false; isp_gpio_write32(isp, ISP_GPIO_1, 0xfeedbabe); /* real funny */ } @@ -259,6 +264,7 @@ static int isp_enable_irq(struct apple_isp *isp) isp_err(isp, "failed to request IRQ#%u (%d)\n", isp->irq, err); return err; } + isp->irq_requested = true; isp_dbg(isp, "about to enable interrupts...\n"); @@ -406,6 +412,7 @@ static int isp_firmware_boot_stage1(struct apple_isp *isp) isp_mbox_write32(isp, isp->hw->mbox_irq_enable, 0x0); isp_coproc_write32(isp, isp_coproc_control(isp), 0x0); + WRITE_ONCE(isp->firmware_quiescent, false); isp_coproc_write32(isp, isp_coproc_control(isp), 0x10); /* Wait for ISP_GPIO_7 to 0x0 -> 0x8042006 */ @@ -420,7 +427,8 @@ static int isp_firmware_boot_stage1(struct apple_isp *isp) return 0; shutdown: - isp_firmware_shutdown_stage1(isp); + if (READ_ONCE(isp->firmware_quiescent)) + isp_firmware_shutdown_stage1(isp); return err; } @@ -434,6 +442,17 @@ int apple_isp_alloc_firmware_surface(struct apple_isp *isp) } dev_dbg(isp->dev, "IPC surface iova: 0x%llx\n", (long long)isp->ipc_surf->iova); + if (isp->profile) { + /* This one-page immutable batch area supplies the recorded gap + * between IPC and the negotiated extra heap. + */ + isp->buflist_surf = isp_alloc_surface_vmap(isp, ISP_T6040_PAGE_SIZE); + if (!isp->buflist_surf) { + isp_free_surface(isp, isp->ipc_surf); + return -ENOMEM; + } + return 0; + } isp->data_surf = isp_alloc_surface_vmap(isp, ISP_FIRMWARE_DATA_SIZE); if (!isp->data_surf) { @@ -443,12 +462,20 @@ int apple_isp_alloc_firmware_surface(struct apple_isp *isp) } dev_dbg(isp->dev, "Data surface iova: 0x%llx\n", (long long)isp->data_surf->iova); + isp->buflist_surf = isp_alloc_surface_vmap(isp, + ISP_CMD_AREA_SIZE(isp_num_capmeta(isp))); + if (!isp->buflist_surf) { + isp_free_surface(isp, isp->data_surf); + isp_free_surface(isp, isp->ipc_surf); + return -ENOMEM; + } return 0; } void apple_isp_free_firmware_surface(struct apple_isp *isp) { + isp_free_surface(isp, isp->buflist_surf); isp_free_surface(isp, isp->data_surf); isp_free_surface(isp, isp->ipc_surf); } @@ -498,10 +525,10 @@ static void isp_write_bootargs_h16(struct apple_isp *isp, void *virt, static int isp_firmware_boot_stage2(struct apple_isp *isp) { - bool h16 = isp->hw->fw_abi == ISP_FW_ABI_H17; + bool h16 = isp->profile || isp->hw->fw_abi == ISP_FW_ABI_H17; size_t args_size = h16 ? sizeof(struct isp_firmware_bootargs_h16) : sizeof(struct isp_firmware_bootargs); - size_t cmd_size = ISP_CMD_AREA_SIZE(isp_num_capmeta(isp)); + size_t cmd_size = isp->profile ? 0x400 : ISP_CMD_AREA_SIZE(isp_num_capmeta(isp)); dma_addr_t args_iova, cmd_iova; void *args_virt, *cmd_virt; int err; @@ -557,12 +584,25 @@ static int isp_firmware_boot_stage2(struct apple_isp *isp) } isp->cmd_iova = cmd_iova; + isp->ipc_boot_iova = args_iova; + isp->ipc_boot_size = args_size + 0x40 + cmd_size; isp->cmd_virt = cmd_virt; - if (h16) + if (isp->profile) { + struct isp_profile_boot boot; + + err = isp_profile_t6040_boot(isp->ipc_surf->iova, args_offset, + isp->extra_surf->iova, extra_size, &boot); + if (err || args_iova != boot.args_iova || cmd_iova != boot.command_iova) + return err ?: -EINVAL; + err = isp_profile_t6040_bootargs(args_virt, &boot); + if (err) + return err; + } else if (h16) { isp_write_bootargs_h16(isp, args_virt, args_offset); - else + } else { isp_write_bootargs(isp, args_virt, args_iova); + } isp_gpio_write32(isp, ISP_GPIO_0, args_iova); /* TODO: handle this via Kconfig depends? hardware is only present on @@ -586,7 +626,11 @@ static int isp_firmware_boot_stage2(struct apple_isp *isp) return 0; free_extra: - isp_free_surface(isp, isp->extra_surf); + /* The boot block already exposed this surface to running firmware. + * A missing handshake ACK cannot authorize freeing its DMA backing. + */ + if (READ_ONCE(isp->firmware_quiescent)) + isp_free_surface(isp, isp->extra_surf); return err; } @@ -619,6 +663,11 @@ static void isp_free_channel_info(struct apple_isp *isp) static int isp_fill_channel_info(struct apple_isp *isp) { + struct isp_ipc_extent reserved[ISP_IPC_MAX_CHANNELS + 2]; + struct isp_ipc_extent ipc = { + .iova = isp->ipc_surf->iova, + .size = isp->ipc_surf->size, + }; u64 table_iova = isp_gpio_read32(isp, ISP_GPIO_0) | ((u64)isp_gpio_read32(isp, ISP_GPIO_1)) << 32; void *table_virt = apple_isp_ipc_translate( @@ -630,6 +679,18 @@ static int isp_fill_channel_info(struct apple_isp *isp) dev_err(isp->dev, "Failed to find channel table\n"); return -EIO; } + reserved[0].iova = isp_fw_iova(isp, table_iova); + reserved[0].size = array_size(sizeof(struct isp_chan_desc), + isp->num_ipc_chans); + /* Use the argument/command span already validated by stage 2. */ + reserved[1].iova = isp->ipc_boot_iova; + reserved[1].size = isp->ipc_boot_size; + if (!isp_ipc_extent_contains(ipc, reserved[0]) || + !isp_ipc_extent_contains(ipc, reserved[1]) || + isp_ipc_extents_overlap(reserved[0], reserved[1])) { + isp_err(isp, "invalid ipc channel table or boot storage\n"); + return -EIO; + } isp->ipc_chans = kcalloc(isp->num_ipc_chans, sizeof(struct isp_channel *), GFP_KERNEL); @@ -677,6 +738,13 @@ static int isp_fill_channel_info(struct apple_isp *isp) goto out; } chan->doorbell = BIT(chan->src); + reserved[i + 2].iova = chan->iova; + reserved[i + 2].size = chan->size; + if (!isp_ipc_ring_valid(ipc, reserved[i + 2], reserved, i + 2)) { + isp_err(isp, "invalid ipc chan %s layout\n", + chan->name); + goto out; + } chan->virt = apple_isp_ipc_translate(isp, chan->iova, chan->size); @@ -795,6 +863,14 @@ static int isp_set_dsid_clr(struct apple_isp *isp) const struct apple_isp_hw *hw = isp->hw; int err; + if (isp->profile) { + u32 mcc_mask; + + if (of_property_read_u32(isp->dev->of_node, "apple,isp-mcc-mask", &mcc_mask)) + return -EINVAL; + return isp_cmd_set_dsid_t6040(isp, mcc_mask); + } + /* * The H17 firmware takes no PMU base, and its single broadcast-clear * window in the multi-window form. @@ -843,7 +919,7 @@ static int isp_start_command_processor(struct apple_isp *isp) return err; } - err = isp_cmd_start(isp, 0); + err = isp_cmd_start(isp, isp->profile ? isp->profile->start_argument : 0); if (err) return err; @@ -873,6 +949,10 @@ static int isp_firmware_boot(struct apple_isp *isp) err = isp_firmware_boot_stage1(isp); if (err < 0) { isp_err(isp, "failed firmware boot stage 1: %d\n", err); + if (!READ_ONCE(isp->firmware_quiescent)) { + apple_isp_firmware_retain(isp); + return err; + } goto garbage_collect; } @@ -905,34 +985,55 @@ static int isp_firmware_boot(struct apple_isp *isp) return 0; disable_irqs: - isp_disable_irq(isp); shutdown_stage3: - isp_firmware_shutdown_stage3(isp); shutdown_stage2: - isp_firmware_shutdown_stage2(isp); shutdown_stage1: + if (!READ_ONCE(isp->firmware_quiescent)) { + apple_isp_firmware_retain(isp); + return err; + } + isp_disable_irq(isp); + isp_firmware_shutdown_stage3(isp); + isp_firmware_shutdown_stage2(isp); isp_firmware_shutdown_stage1(isp); garbage_collect: isp_collect_gc_surface(isp); return err; } -static void isp_firmware_shutdown(struct apple_isp *isp) +void apple_isp_firmware_retain(struct apple_isp *isp) { + /* Stop host callbacks without touching firmware GPIO, CPU or tables. */ + WRITE_ONCE(isp->capture_failed, true); + apple_isp_wdt_stop(isp); + if (isp->irq_requested) { + free_irq(isp->irq, isp); + isp->irq_requested = false; + } flush_workqueue(isp->wq); + isp->fw_state = ISP_FW_DEAD; + dev_err(isp->dev, "firmware quiescence unknown; retaining DMA resources until reboot\n"); +} - /* - * Stop the coprocessor before releasing any memory the firmware - * uses, also when it did not acknowledge the suspend request. - */ - if (isp_stop_command_processor(isp)) - dev_warn(isp->dev, "firmware did not suspend, stopping it\n"); +static int isp_firmware_shutdown(struct apple_isp *isp) +{ + int err; + + flush_workqueue(isp->wq); + err = isp_stop_command_processor(isp); + if (err) { + /* A reset request is not a DMA release acknowledgment. */ + apple_isp_firmware_retain(isp); + return err; + } isp_disable_irq(isp); isp_firmware_shutdown_stage1(isp); - + WRITE_ONCE(isp->firmware_quiescent, true); isp_firmware_shutdown_stage3(isp); isp_firmware_shutdown_stage2(isp); isp_collect_gc_surface(isp); + apple_isp_release_retained_buffers(isp); + return 0; } /* @@ -970,8 +1071,9 @@ int apple_isp_firmware_boot(struct apple_isp *isp) err = isp_firmware_boot(isp); if (err) { dev_err(isp->dev, "failed to boot firmware: %d\n", err); - pm_runtime_put_sync(isp->dev); - if (isp->hw->resident_fw) + if (READ_ONCE(isp->firmware_quiescent)) + pm_runtime_put_sync(isp->dev); + if (isp->hw->resident_fw || !READ_ONCE(isp->firmware_quiescent)) isp->fw_state = ISP_FW_DEAD; return err; } @@ -991,15 +1093,19 @@ void apple_isp_firmware_shutdown(struct apple_isp *isp) } /* Stop the firmware; resident firmware cannot be started again. */ -void apple_isp_firmware_halt(struct apple_isp *isp) +int apple_isp_firmware_halt(struct apple_isp *isp) { + int err; /* Only a capture services the watchdog, but be sure. */ apple_isp_wdt_stop(isp); if (isp->fw_state != ISP_FW_RUNNING) - return; + return READ_ONCE(isp->firmware_quiescent) ? 0 : -EIO; - isp_firmware_shutdown(isp); + err = isp_firmware_shutdown(isp); + if (err) + return err; pm_runtime_put_sync(isp->dev); isp->fw_state = isp->hw->resident_fw ? ISP_FW_DEAD : ISP_FW_OFF; + return 0; } diff --git a/drivers/media/platform/apple/isp/isp-fw.h b/drivers/media/platform/apple/isp/isp-fw.h index 71e97a77d25c4..79b90301b16bd 100644 --- a/drivers/media/platform/apple/isp/isp-fw.h +++ b/drivers/media/platform/apple/isp/isp-fw.h @@ -11,7 +11,8 @@ void apple_isp_free_firmware_surface(struct apple_isp *isp); int apple_isp_firmware_boot(struct apple_isp *isp); void apple_isp_firmware_shutdown(struct apple_isp *isp); -void apple_isp_firmware_halt(struct apple_isp *isp); +int apple_isp_firmware_halt(struct apple_isp *isp); +void apple_isp_firmware_retain(struct apple_isp *isp); void *apple_isp_translate(struct apple_isp *isp, struct isp_surf *surf, dma_addr_t iova, size_t size); diff --git a/drivers/media/platform/apple/isp/isp-ipc-layout-test.c b/drivers/media/platform/apple/isp/isp-ipc-layout-test.c new file mode 100644 index 0000000000000..3cc522a64e5b1 --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-ipc-layout-test.c @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include + +#include "isp-ipc-layout.h" + +static void isp_ipc_high_address_test(struct kunit *test) +{ + struct isp_ipc_extent ipc = { 0x10002000000ULL, 0x1c000 }; + struct isp_ipc_extent table = { ipc.iova + 0x1000, 0x700 }; + struct isp_ipc_extent ring = { ipc.iova + 0x4000, 0x400 }; + + KUNIT_EXPECT_TRUE(test, isp_ipc_ring_valid(ipc, ring, &table, 1)); + /* Truncating the ring to 32 bits must not identify owned memory. */ + ring.iova = (u32)ring.iova; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, &table, 1)); +} + +static void isp_ipc_extent_test(struct kunit *test) +{ + struct isp_ipc_extent ipc = { 0x10000, 0x4000 }; + struct isp_ipc_extent ring = { 0x13fc0, 0x40 }; + + KUNIT_EXPECT_TRUE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + ring.size += 0x40; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + ring = (struct isp_ipc_extent){ 0xffc0, 0x40 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + ring = (struct isp_ipc_extent){ 0x10000, 0 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); +} + +static void isp_ipc_alignment_test(struct kunit *test) +{ + struct isp_ipc_extent ipc = { 0x10000, 0x4000 }; + struct isp_ipc_extent ring = { 0x10001, 0x40 }; + + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + ring = (struct isp_ipc_extent){ 0x10000, 0x41 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + /* Only the u64 publication alignment is required for older firmware. */ + ring = (struct isp_ipc_extent){ 0x10008, 0x40 }; + KUNIT_EXPECT_TRUE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); +} + +static void isp_ipc_reserved_test(struct kunit *test) +{ + struct isp_ipc_extent ipc = { 0x10000, 0x4000 }; + struct isp_ipc_extent reserved[] = { { 0x10100, 0x700 }, + { 0x11000, 0x400 } }; + struct isp_ipc_extent ring = { 0x100c0, 0x80 }; + + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); + ring = (struct isp_ipc_extent){ 0x10800, 0x40 }; + KUNIT_EXPECT_TRUE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); + ring = (struct isp_ipc_extent){ 0x10fc0, 0x40 }; + KUNIT_EXPECT_TRUE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); + ring = reserved[1]; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); + ring = (struct isp_ipc_extent){ 0x11100, 0x40 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); + ring = (struct isp_ipc_extent){ 0x10fc0, 0x80 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, reserved, 2)); +} + +static void isp_ipc_overflow_test(struct kunit *test) +{ + struct isp_ipc_extent ipc = { (u64)-1 - 0x3fff, 0x4000 }; + struct isp_ipc_extent ring = { ipc.iova, 0x40 }; + + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); + ipc = (struct isp_ipc_extent){ 0, (u64)-1 }; + ring = (struct isp_ipc_extent){ (u64)-1 - 7, 0x40 }; + KUNIT_EXPECT_FALSE(test, isp_ipc_ring_valid(ipc, ring, NULL, 0)); +} + +static void isp_ipc_captured_layout_test(struct kunit *test) +{ + /* Parsed ring geometry from the J616s 25G76 channel table. */ + struct isp_ipc_extent ipc = { 0x10001ff0000ULL, 0x1c000 }; + struct isp_ipc_extent reserved[9] = { + { 0x10001ff0000ULL, 0x700 }, + { 0x10001ffef80ULL, 0x290 + 0x40 + 0x400 }, + { 0x10001ff0700ULL, 768 * 64 }, + { 0x10001ffc700ULL, 8 * 64 }, + { 0x10001ffc900ULL, 8 * 64 }, + { 0x10001ffcb00ULL, 64 * 64 }, + { 0x10001ffdb00ULL, 64 * 64 }, + { 0x10001ffeb00ULL, 8 * 64 }, + { 0x10001ffed00ULL, 8 * 64 }, + }; + + for (unsigned int i = 2; i < ARRAY_SIZE(reserved); i++) + KUNIT_EXPECT_TRUE(test, + isp_ipc_ring_valid(ipc, reserved[i], reserved, i)); + + /* A ring over the command storage must not be admitted. */ + reserved[8] = (struct isp_ipc_extent){ 0x10001fff280ULL, 0x40 }; + KUNIT_EXPECT_FALSE(test, + isp_ipc_ring_valid(ipc, reserved[8], reserved, 8)); +} + +static struct kunit_case isp_ipc_layout_cases[] = { + KUNIT_CASE(isp_ipc_high_address_test), + KUNIT_CASE(isp_ipc_extent_test), + KUNIT_CASE(isp_ipc_alignment_test), + KUNIT_CASE(isp_ipc_reserved_test), + KUNIT_CASE(isp_ipc_overflow_test), + KUNIT_CASE(isp_ipc_captured_layout_test), + {} +}; + +static struct kunit_suite isp_ipc_layout_suite = { + .name = "apple-isp-ipc-layout", + .test_cases = isp_ipc_layout_cases, +}; +kunit_test_suite(isp_ipc_layout_suite); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Apple ISP IPC layout KUnit tests"); diff --git a/drivers/media/platform/apple/isp/isp-ipc-layout.h b/drivers/media/platform/apple/isp/isp-ipc-layout.h new file mode 100644 index 0000000000000..44f06f94c0cee --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-ipc-layout.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __ISP_IPC_LAYOUT_H__ +#define __ISP_IPC_LAYOUT_H__ + +#include + +struct isp_ipc_extent { + u64 iova; + u64 size; +}; + +/* Subtraction keeps firmware-selected extents from wrapping at U64_MAX. */ +static inline bool isp_ipc_extent_contains(struct isp_ipc_extent outer, + struct isp_ipc_extent inner) +{ + return outer.size <= (u64)-1 - outer.iova && + inner.size && inner.iova >= outer.iova && + inner.size <= outer.size && + inner.iova - outer.iova <= outer.size - inner.size; +} + +static inline bool isp_ipc_extents_overlap(struct isp_ipc_extent a, + struct isp_ipc_extent b) +{ + if (!a.size || !b.size) + return false; + if (a.iova <= b.iova) + return b.iova - a.iova < a.size; + return a.iova - b.iova < b.size; +} + +static inline bool isp_ipc_ring_valid(struct isp_ipc_extent ipc, + struct isp_ipc_extent ring, + const struct isp_ipc_extent *reserved, + unsigned int count) +{ + /* Messages are 64 bytes and arg0 is published with a u64 store. */ + if ((ring.iova & 7) || (ring.size & 63) || + !isp_ipc_extent_contains(ipc, ring)) + return false; + + for (unsigned int i = 0; i < count; i++) + if (isp_ipc_extents_overlap(ring, reserved[i])) + return false; + + return true; +} + +#endif /* __ISP_IPC_LAYOUT_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-ipc.c b/drivers/media/platform/apple/isp/isp-ipc.c index 82e1907dbf68c..590b088b484e3 100644 --- a/drivers/media/platform/apple/isp/isp-ipc.c +++ b/drivers/media/platform/apple/isp/isp-ipc.c @@ -66,7 +66,11 @@ static inline void chan_read_msg_index(struct apple_isp *isp, struct isp_channel *chan, struct isp_message *msg, u32 index) { - memcpy(msg, chan_msg_virt(chan, index), sizeof(*msg)); + u64 *source = chan_msg_virt(chan, index); + + msg->arg0 = READ_ONCE(*source); + dma_rmb(); + memcpy(&msg->arg1, source + 1, sizeof(*msg) - sizeof(msg->arg0)); } static inline void chan_read_msg(struct apple_isp *isp, @@ -124,6 +128,8 @@ static int chan_handle_once(struct apple_isp *isp, struct isp_channel *chan) isp_mbox2_write32(isp, ISP_MBOX2_IRQ_DOORBELL, chan->doorbell); chan_update_cursor(chan); + if (chan->ops->acknowledged) + chan->ops->acknowledged(isp, chan); return 0; } @@ -176,6 +182,10 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, { long t; + lockdep_assert_held(&chan->lock); + if (chan->tx_poisoned) + return -EIO; + chan_write_msg(isp, chan, &chan->req); dma_wmb(); @@ -186,6 +196,10 @@ int ipc_chan_send(struct apple_isp *isp, struct isp_channel *chan, t = wait_event_timeout(isp->wait, chan_tx_done(isp, chan), timeout); if (t == 0) { + chan->tx_poisoned = true; + WRITE_ONCE(isp->capture_failed, true); + if (isp->video_registered) + vb2_queue_error(&isp->vbq); dev_err(isp->dev, "%s: timed out on request [0x%llx, 0x%llx, 0x%llx]\n", chan->name, chan->req.arg0, chan->req.arg1, diff --git a/drivers/media/platform/apple/isp/isp-profile.c b/drivers/media/platform/apple/isp/isp-profile.c new file mode 100644 index 0000000000000..b1198d34b9978 --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-profile.c @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include + +#include "isp-profile.h" + +/* Own 25G76 image SHA256: + * 4990fa8e00c31be970166238d7953246ee5b9db3ab0085aca578c4198dbad433. + * The ADT UUID is the runtime selector; this hash is source provenance. + */ +const struct isp_profile isp_profile_t6040_25g76 = { + .name = ISP_T6040_PROFILE, + .uuid = ISP_T6040_UUID, + .chip = 0x6040, .board = 6, .platform = 5, .scheme = 16, + .stream = 0, .alias_tcr = 0x80, .aliases = { 1, 6, 7, 10, 11 }, + .meta_count = 8, .output_count = 2, .meta_size = 0x8000, + .start_argument = 0x11, +}; + +const struct isp_profile *isp_profile_select(u32 chip, u32 board, + const char *name, const char *uuid) +{ + const struct isp_profile *profile = &isp_profile_t6040_25g76; + + if (!name || !uuid || chip != profile->chip || board != profile->board || + strcmp(name, profile->name) || strcmp(uuid, profile->uuid)) + return NULL; + return profile; +} + +int isp_profile_p010_geometry(u32 width, u32 height, + struct isp_profile_geometry *geometry) +{ + struct isp_profile_geometry result; + + if (!geometry || !((width == 1280 && height == 720) || + (width == 1920 && height == 1080))) + return -EINVAL; + result.width = width; + result.height = height; + result.stride = ALIGN(width * 2, 64); + result.luma_size = ALIGN(result.stride * height, ISP_T6040_PAGE_SIZE); + result.chroma_size = ALIGN(result.stride * height / 2, ISP_T6040_PAGE_SIZE); + result.total_size = result.luma_size + result.chroma_size; + *geometry = result; + return 0; +} + +int isp_profile_t6040_boot(u64 ipc_iova, u32 args_offset, u64 extra_iova, + u64 extra_size, struct isp_profile_boot *boot) +{ + struct isp_profile_boot result = {}; + u64 args_end; + + /* Fixed segment sizes, fresh physical backing: never historical PAs. */ + if (!boot || ipc_iova != ISP_T6040_IOVA_BASE + 0x1ff0000 || + args_offset % 0x40 || !extra_size || extra_size > 0x7000000 || + extra_size % ISP_T6040_PAGE_SIZE || + extra_iova != ipc_iova + ISP_T6040_IPC_SIZE + ISP_T6040_PAGE_SIZE) + return -EINVAL; + result.ipc_iova = ipc_iova; + result.args_offset = args_offset; + result.args_iova = ipc_iova + (u64)args_offset + 0x40; + result.command_iova = result.args_iova + ISP_T6040_BOOT_SIZE + 0x40; + args_end = result.command_iova + 0x400; + if (args_end > ipc_iova + ISP_T6040_IPC_SIZE || + extra_iova + extra_size > ISP_T6040_IOVA_LIMIT) + return -EINVAL; + result.extra_iova = extra_iova; + result.extra_size = extra_size; + result.shared_base = 0x1fec000; + result.shared_size = 0x10000000 - result.shared_base; + *boot = result; + return 0; +} + +int isp_profile_t6040_bootargs(void *target, const struct isp_profile_boot *boot) +{ + struct isp_profile_boot checked = {}; + u8 *bytes = target; + + if (!target || !boot) + return -EINVAL; + if (isp_profile_t6040_boot(boot->ipc_iova, boot->args_offset, + boot->extra_iova, boot->extra_size, &checked) || + boot->args_iova != checked.args_iova || + boot->command_iova != checked.command_iova || + boot->shared_base != checked.shared_base || + boot->shared_size != checked.shared_size) + return -EINVAL; + memset(bytes, 0, ISP_T6040_BOOT_SIZE); + put_unaligned_le64(boot->ipc_iova, bytes + 8); + put_unaligned_le64(boot->shared_base, bytes + 0x10); + put_unaligned_le64(boot->shared_size, bytes + 0x18); + put_unaligned_le64(boot->extra_iova, bytes + 0x20); + put_unaligned_le64(boot->extra_size, bytes + 0x28); + put_unaligned_le32(5, bytes + 0x30); + put_unaligned_le64((u64)boot->args_offset + 1, bytes + 0x50); + put_unaligned_le32(0x40, bytes + 0x68); + put_unaligned_le64(16, bytes + 0xd0); + put_unaligned_le64(1, bytes + 0x120); + return 0; +} + +int isp_profile_t6040_dsid(u32 active_mcc_mask, + struct isp_t6040_dsid_command *command) +{ + static_assert(sizeof(*command) == 0x50); + if (!command || active_mcc_mask != 15) + return -EINVAL; + memset(command, 0, sizeof(*command)); + command->opcode = 0x3206ULL << 32; + command->count = 8; + command->range = 0x2fc; + for (unsigned int i = 0; i < 8; i++) + command->bases[i] = i < 4 ? 0x2201d4000ULL + i * 0x2000000ULL : U64_MAX; + return 0; +} diff --git a/drivers/media/platform/apple/isp/isp-profile.h b/drivers/media/platform/apple/isp/isp-profile.h new file mode 100644 index 0000000000000..25d55f1a9b6fc --- /dev/null +++ b/drivers/media/platform/apple/isp/isp-profile.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __ISP_PROFILE_H__ +#define __ISP_PROFILE_H__ + +#include +#include + +#define ISP_T6040_PROFILE "t6040-25G76" +#define ISP_T6040_UUID "4C8FBA5E-45CC-308E-9BE5-053518C19D76" +#define ISP_T6040_PAGE_SIZE 0x4000U +#define ISP_T6040_IOVA_BASE BIT_ULL(40) +#define ISP_T6040_IOVA_LIMIT BIT_ULL(42) +#define ISP_T6040_IPC_SIZE 0x1c000U +#define ISP_T6040_BOOT_SIZE 0x290U + +struct isp_profile { + const char *name; + const char *uuid; + u32 chip, board, platform, scheme, stream, alias_tcr; + u32 aliases[5]; + u32 meta_count, output_count, meta_size, start_argument; +}; + +struct isp_profile_geometry { + u32 width, height, stride, luma_size, chroma_size, total_size; +}; + +struct isp_profile_boot { + u64 ipc_iova, args_iova, command_iova, extra_iova, extra_size; + u64 shared_base, shared_size; + u32 args_offset; +}; + +struct isp_t6040_dsid_command { + u64 opcode; + u32 count, range; + u64 bases[8]; +} __packed; + +extern const struct isp_profile isp_profile_t6040_25g76; +const struct isp_profile *isp_profile_select(u32 chip, u32 board, + const char *name, const char *uuid); +int isp_profile_p010_geometry(u32 width, u32 height, + struct isp_profile_geometry *geometry); +int isp_profile_t6040_boot(u64 ipc_iova, u32 args_offset, u64 extra_iova, + u64 extra_size, struct isp_profile_boot *boot); +int isp_profile_t6040_bootargs(void *target, const struct isp_profile_boot *boot); +int isp_profile_t6040_dsid(u32 active_mcc_mask, + struct isp_t6040_dsid_command *command); + +#endif /* __ISP_PROFILE_H__ */ diff --git a/drivers/media/platform/apple/isp/isp-v4l2.c b/drivers/media/platform/apple/isp/isp-v4l2.c index 299177b9075cf..6ce1f6177dd99 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.c +++ b/drivers/media/platform/apple/isp/isp-v4l2.c @@ -20,7 +20,7 @@ #include "isp-v4l2.h" #define ISP_MIN_FRAMES 2 -#define ISP_MAX_PLANES 4 +#define ISP_MAX_PLANES ISP_BUFFER_PLANES #define ISP_MAX_PIX_FORMATS 2 #define ISP_BUFFER_TIMEOUT msecs_to_jiffies(1500) #define ISP_STRIDE_ALIGNMENT 64 @@ -32,14 +32,6 @@ static bool multiplanar = false; module_param(multiplanar, bool, 0644); MODULE_PARM_DESC(multiplanar, "Enable multiplanar API"); -struct isp_buflist_buffer { - u64 iovas[ISP_MAX_PLANES]; - u32 flags[ISP_MAX_PLANES]; - u32 num_planes; - u32 pool_type; - u32 tag; - u32 pad; -} __packed; static_assert(sizeof(struct isp_buflist_buffer) == ISP_BUFLIST_DESC_SIZE); struct isp_buflist { @@ -47,6 +39,17 @@ struct isp_buflist { u64 num_buffers; struct isp_buflist_buffer buffers[]; } __packed; + +static void isp_prepare_buffer_descriptor(struct apple_isp *isp, + struct isp_buffer_lease *lease, + struct isp_buflist_buffer *descriptor) +{ + u64 tag = isp->next_buffer_tag++; + + descriptor->tag = tag; + descriptor->pad = isp->profile ? tag >> 32 : 0; + isp_buffer_submit_tag(lease, descriptor, !!isp->profile); +} static_assert(sizeof(struct isp_buflist) == ISP_BUFLIST_HDR_SIZE); /* the firmware reads at least ISP_IPC_BUFEXC_STAT_SIZE bytes of a batch */ static_assert(ISP_CMD_AREA_SIZE(0) >= ISP_IPC_BUFEXC_STAT_SIZE); @@ -57,91 +60,140 @@ static_assert(ISP_CMD_AREA_SIZE(0) >= ISP_IPC_BUFEXC_STAT_SIZE); */ #define ISP_BUFLIST_POOL_CAPTURE_META 2 +static void isp_capture_failed(struct apple_isp *isp) +{ + WRITE_ONCE(isp->capture_failed, true); + if (isp->video_registered) + vb2_queue_error(&isp->vbq); +} + +/* buf_lock protects both the descriptor snapshot and its ownership phase. */ +static struct isp_buffer_lease *isp_find_returned_lease(struct apple_isp *isp, + const struct isp_buflist_buffer *report) +{ + struct isp_buffer_match match = {}; + struct isp_buffer *buf; + + for (unsigned int i = 0; i < isp_num_meta(isp); i++) { + struct isp_surf *surf = isp->meta_surfs[i]; + + if (surf) + isp_buffer_match_candidate(&match, &surf->lease, report); + } + for (unsigned int i = 0; i < isp_num_capmeta(isp); i++) { + struct isp_surf *surf = isp->capmeta_surfs[i]; + + if (surf) + isp_buffer_match_candidate(&match, &surf->lease, report); + } + list_for_each_entry(buf, &isp->bufs_submitted, link) { + isp_buffer_match_candidate(&match, &buf->lease, report); + } + return isp_buffer_unique_match(&match); +} + int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan) { struct isp_message *req = &chan->req, *rsp = &chan->rsp; - struct isp_buffer *tmp, *buf; + struct isp_buffer_lease **matches; + struct isp_buffer *buf, *tmp; struct isp_buflist *bl; + unsigned long flags; u64 count; - int err = 0; - - /* printk("H2T: 0x%llx 0x%llx 0x%llx\n", (long long)req->arg0, - (long long)req->arg1, (long long)req->arg2); */ + int err = -EIO; if (!isp->bt_surf || req->arg1 < sizeof(*bl) || - req->arg1 > isp->bt_surf->size) { - dev_err_ratelimited(isp->dev, "%s: Bad length 0x%llx\n", - chan->name, req->arg1); - return -EIO; - } + req->arg1 > isp->bt_surf->size) + goto invalid; bl = apple_isp_translate(isp, isp->bt_surf, isp_fw_iova(isp, req->arg0), req->arg1); if (!bl) - return -EIO; - + goto invalid; count = bl->num_buffers; - if (count > (req->arg1 - sizeof(*bl)) / - sizeof(struct isp_buflist_buffer)) { - dev_err_ratelimited(isp->dev, "%s: Bad length 0x%llx\n", - chan->name, req->arg1); - return -EIO; - } + if (count > (req->arg1 - sizeof(*bl)) / sizeof(*bl->buffers)) + goto invalid; - spin_lock(&isp->buf_lock); + matches = kcalloc(count, sizeof(*matches), GFP_KERNEL); + if (count && !matches) { + err = -ENOMEM; + goto invalid; + } + spin_lock_irqsave(&isp->buf_lock, flags); + /* Validate the whole batch before changing any lease. A malformed or + * duplicate record must not partially release an earlier buffer. + */ for (u64 i = 0; i < count; i++) { - struct isp_buflist_buffer *bufd = &bl->buffers[i]; - - /* printk("Return: 0x%llx (%d)\n", bufd->iovas[0], - bufd->pool_type); */ - - if (bufd->pool_type == 0) { - for (int j = 0; j < ARRAY_SIZE(isp->meta_surfs); j++) { - struct isp_surf *meta = isp->meta_surfs[j]; - if ((u32)bufd->iovas[0] == (u32)meta->iova) { - WARN_ON(!meta->submitted); - meta->submitted = false; - } - } - } else if (bufd->pool_type == ISP_BUFLIST_POOL_CAPTURE_META && - isp_num_capmeta(isp)) { - for (int j = 0; j < isp_num_capmeta(isp); j++) { - struct isp_surf *meta = isp->capmeta_surfs[j]; - - if (meta && (u32)bufd->iovas[0] == (u32)meta->iova) - meta->submitted = false; - } - } else { - list_for_each_entry_safe_reverse( - buf, tmp, &isp->bufs_submitted, link) { - if ((u32)buf->surfs[0].iova == - (u32)bufd->iovas[0]) { - enum vb2_buffer_state state = - VB2_BUF_STATE_ERROR; - - buf->vb.vb2_buf.timestamp = - ktime_get_ns(); - buf->vb.sequence = isp->sequence++; - buf->vb.field = V4L2_FIELD_NONE; - if (req->arg2 == - ISP_IPC_BUFEXC_FLAG_RENDER) - state = VB2_BUF_STATE_DONE; - vb2_buffer_done(&buf->vb.vb2_buf, - state); - list_del(&buf->link); - } - } - } + matches[i] = isp_find_returned_lease(isp, &bl->buffers[i]); + if (!matches[i]) + goto unlock_invalid; + for (u64 j = 0; j < i; j++) + if (matches[i] == matches[j]) + goto unlock_invalid; + } + for (u64 i = 0; i < count; i++) + isp_buffer_return(matches[i], &bl->buffers[i]); + list_for_each_entry_safe(buf, tmp, &isp->bufs_submitted, link) { + if (buf->lease.owner != ISP_BUFFER_REPORT_PENDING) + continue; + buf->rendered = req->arg2 == ISP_IPC_BUFEXC_FLAG_RENDER && + !READ_ONCE(isp->capture_failed); + list_move_tail(&buf->link, &isp->bufs_retiring); } - spin_unlock(&isp->buf_lock); + spin_unlock_irqrestore(&isp->buf_lock, flags); + kfree(matches); rsp->arg0 = req->arg0 | ISP_IPC_FLAG_ACK; - rsp->arg1 = 0x0; + rsp->arg1 = 0; rsp->arg2 = ISP_IPC_BUFEXC_FLAG_ACK; + return 0; +unlock_invalid: + spin_unlock_irqrestore(&isp->buf_lock, flags); + kfree(matches); +invalid: + dev_err_ratelimited(isp->dev, "%s: invalid or unowned buffer return\n", chan->name); + isp_capture_failed(isp); return err; } +/* Called only after the reply validity word, doorbell and cursor publication. */ +void ipc_bt_acknowledged(struct apple_isp *isp, struct isp_channel *chan) +{ + struct isp_buffer *buf, *tmp; + unsigned long flags; + + spin_lock_irqsave(&isp->buf_lock, flags); + for (unsigned int i = 0; i < isp_num_meta(isp); i++) { + struct isp_surf *surf = isp->meta_surfs[i]; + + if (surf) + isp_buffer_acknowledge(&surf->lease); + } + for (unsigned int i = 0; i < isp_num_capmeta(isp); i++) { + struct isp_surf *surf = isp->capmeta_surfs[i]; + + if (surf) + isp_buffer_acknowledge(&surf->lease); + } + list_for_each_entry_safe(buf, tmp, &isp->bufs_retiring, link) { + if (WARN_ON(!isp_buffer_acknowledge(&buf->lease))) + continue; + if (test_bit(ISP_STATE_SLEEPING, &isp->state)) { + list_move_tail(&buf->link, &isp->bufs_pending); + continue; + } + list_del_init(&buf->link); + buf->vb.vb2_buf.timestamp = ktime_get_ns(); + buf->vb.sequence = isp->sequence++; + buf->vb.field = V4L2_FIELD_NONE; + vb2_buffer_done(&buf->vb.vb2_buf, + buf->rendered ? VB2_BUF_STATE_DONE : VB2_BUF_STATE_ERROR); + } + spin_unlock_irqrestore(&isp->buf_lock, flags); + wake_up_all(&isp->wait); +} + /* * With a capture metadata pool, the firmware takes the metadata buffers * before CH_START and the capture metadata and capture buffers after it, @@ -152,134 +204,112 @@ static int isp_submit_buffers(struct apple_isp *isp, bool pre_start) struct isp_format *fmt = isp_get_current_format(isp); struct isp_channel *chan = isp->chan_bh; struct isp_message *req = &chan->req; - struct isp_buffer *buf, *tmp; - unsigned int num_rendered = 0; + struct isp_buffer *buf; + struct isp_buflist *bl = isp->buflist_surf->virt; + struct isp_buflist_buffer *bufd; + unsigned int needed = 0, rendered = 0; unsigned long flags; size_t offset; - int err; + int err = 0; - struct isp_buflist *bl = isp->cmd_virt; - struct isp_buflist_buffer *bufd = &bl->buffers[0]; + mutex_lock(&chan->lock); + if (READ_ONCE(isp->capture_failed) || chan->tx_poisoned) { + err = -EIO; + goto unlock; + } + spin_lock_irqsave(&isp->buf_lock, flags); + for (unsigned int i = 0; i < isp_num_meta(isp); i++) + needed += isp->meta_surfs[i]->lease.owner == ISP_BUFFER_HOST; + if (!(isp_num_capmeta(isp) && pre_start)) { + for (unsigned int i = 0; i < isp_num_capmeta(isp); i++) + needed += isp->capmeta_surfs[i]->lease.owner == ISP_BUFFER_HOST; + list_for_each_entry(buf, &isp->bufs_pending, link) { + if (rendered++ == isp_max_capture_buffers(isp)) + break; + needed++; + } + } + /* Leave the final value unused instead of wrapping into an older tag. */ + if (needed > (isp->profile ? U64_MAX : U32_MAX) - isp->next_buffer_tag) { + err = -EOVERFLOW; + goto unlock_buffers; + } + if (!needed) + goto unlock_buffers; - /* Clear what earlier commands and batches left in the reserved fields. */ - memset(bl, 0, ISP_CMD_AREA_SIZE(isp_num_capmeta(isp))); + /* A separate batch area is immutable until BUF_H2T acknowledges it. + * Control commands must never overwrite an ambiguously submitted batch. + */ + memset(bl, 0, isp->buflist_surf->size); bl->type = 1; + bufd = bl->buffers; + for (unsigned int i = 0; i < isp_num_meta(isp); i++) { + struct isp_surf *surf = isp->meta_surfs[i]; - spin_lock_irqsave(&isp->buf_lock, flags); - for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) { - struct isp_surf *meta = isp->meta_surfs[i]; - - if (meta->submitted) + if (surf->lease.owner != ISP_BUFFER_HOST) continue; - - /* printk("Submit: 0x%llx .. 0x%llx (meta)\n", meta->iova, - meta->iova + meta->size); */ - bufd->num_planes = 1; bufd->pool_type = 0; - bufd->iovas[0] = meta->iova; + bufd->iovas[0] = surf->iova; bufd->flags[0] = 0x40000000; - bufd++; + isp_prepare_buffer_descriptor(isp, &surf->lease, bufd++); bl->num_buffers++; - - meta->submitted = true; } - if (isp_num_capmeta(isp) && pre_start) goto send; - for (int i = 0; i < isp_num_capmeta(isp); i++) { - struct isp_surf *meta = isp->capmeta_surfs[i]; + for (unsigned int i = 0; i < isp_num_capmeta(isp); i++) { + struct isp_surf *surf = isp->capmeta_surfs[i]; - if (meta->submitted) + if (surf->lease.owner != ISP_BUFFER_HOST) continue; - bufd->num_planes = 1; bufd->pool_type = ISP_BUFLIST_POOL_CAPTURE_META; - bufd->iovas[0] = meta->iova; + bufd->iovas[0] = surf->iova; bufd->flags[0] = 0x40000000; - bufd++; + isp_prepare_buffer_descriptor(isp, &surf->lease, bufd++); bl->num_buffers++; - - meta->submitted = true; } - - /* One rendered pool per batch; the rest stays pending for the next. */ - while (num_rendered < ISP_MAX_BUFFERS && - (buf = list_first_entry_or_null(&isp->bufs_pending, - struct isp_buffer, link))) { + for (rendered = 0; rendered < isp_max_capture_buffers(isp); rendered++) { + buf = list_first_entry_or_null(&isp->bufs_pending, + struct isp_buffer, link); + if (!buf) + break; bufd->num_planes = fmt->num_planes; - bufd->pool_type = isp->hw->scl1 ? CISP_POOL_TYPE_RENDERED_SCL1 : + bufd->pool_type = (isp->profile || isp->hw->scl1) ? CISP_POOL_TYPE_RENDERED_SCL1 : CISP_POOL_TYPE_RENDERED; offset = 0; - for (int j = 0; j < fmt->num_planes; j++) { - bufd->iovas[j] = buf->surfs[0].iova + offset; - bufd->flags[j] = 0x40000000; - offset += fmt->plane_size[j]; + for (unsigned int i = 0; i < fmt->num_planes; i++) { + bufd->iovas[i] = buf->memory->surfs[0].iova + offset; + bufd->flags[i] = 0x40000000; + offset += fmt->plane_size[i]; } - - /* printk("Submit: 0x%llx .. 0x%llx (render)\n", - buf->surfs[0].iova, - buf->surfs[0].iova + buf->surfs[0].size); */ - bufd++; + isp_prepare_buffer_descriptor(isp, &buf->lease, bufd++); bl->num_buffers++; - num_rendered++; - - /* - * Queue the buffer as submitted and release the lock for now. - * We need to do this before actually submitting to avoid a - * race with the buffer return codepath. - */ + /* Ownership is recorded before the first hardware publication. */ list_move_tail(&buf->link, &isp->bufs_submitted); } send: spin_unlock_irqrestore(&isp->buf_lock, flags); - - req->arg0 = isp->cmd_iova; + req->arg0 = isp->buflist_surf->iova; req->arg1 = max_t(u64, ISP_IPC_BUFEXC_STAT_SIZE, - ((uintptr_t)bufd - (uintptr_t)bl)); + sizeof(*bl) + bl->num_buffers * sizeof(*bufd)); req->arg2 = ISP_IPC_BUFEXC_FLAG_COMMAND; - err = ipc_chan_send(isp, chan, ISP_BUFFER_TIMEOUT); if (err) { - /* If we fail, consider the buffer not submitted. */ - dev_err(isp->dev, - "%s: failed to send bufs: [0x%llx, 0x%llx, 0x%llx]\n", - chan->name, req->arg0, req->arg1, req->arg2); - - /* - * Try to find the buffer in the list, and if it's - * still there, move it back to the pending list. + dev_err(isp->dev, "%s: buffer submission uncertain: %d\n", chan->name, err); + /* The descriptor area and all leases stay owned until their reports + * retire or a qualified full firmware shutdown stops every DMA user. */ - spin_lock_irqsave(&isp->buf_lock, flags); - - bufd = &bl->buffers[0]; - for (int i = 0; i < bl->num_buffers; i++, bufd++) { - list_for_each_entry_safe_reverse( - buf, tmp, &isp->bufs_submitted, link) { - if (bufd->iovas[0] == buf->surfs[0].iova) { - list_move_tail(&buf->link, - &isp->bufs_pending); - } - } - for (int j = 0; j < ARRAY_SIZE(isp->meta_surfs); j++) { - struct isp_surf *meta = isp->meta_surfs[j]; - if (bufd->iovas[0] == meta->iova) { - meta->submitted = false; - } - } - for (int j = 0; j < isp_num_capmeta(isp); j++) { - struct isp_surf *meta = isp->capmeta_surfs[j]; - - if (bufd->iovas[0] == meta->iova) - meta->submitted = false; - } - } - - spin_unlock_irqrestore(&isp->buf_lock, flags); + isp_capture_failed(isp); } + goto unlock; +unlock_buffers: + spin_unlock_irqrestore(&isp->buf_lock, flags); +unlock: + mutex_unlock(&chan->lock); return err; } @@ -298,7 +328,7 @@ static int isp_vb2_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers, * times out if more buffers are submitted than set in the buffer pool * config before streaming is started. */ - *nbuffers = min_t(unsigned int, *nbuffers, ISP_MAX_BUFFERS); + *nbuffers = min_t(unsigned int, *nbuffers, isp_max_capture_buffers(isp)); if (*num_planes) { if (sizes[0] < fmt->total_size) @@ -313,41 +343,91 @@ static int isp_vb2_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers, return 0; } -static void __isp_vb2_buf_cleanup(struct vb2_buffer *vb, unsigned int i) +static void isp_free_buffer_memory(struct apple_isp *isp, + struct isp_buffer_memory *memory) { - struct apple_isp *isp = vb2_get_drv_priv(vb->vb2_queue); - struct isp_buffer *buf = - container_of(vb, struct isp_buffer, vb.vb2_buf); + for (unsigned int i = 0; i < memory->num_planes; i++) { + if (memory->surfs[i].mm) + apple_isp_iommu_unmap_sgt(isp, &memory->surfs[i]); + isp_unpin_buffer_pages(&memory->pins[i]); + } + kfree(memory); +} - while (i--) - apple_isp_iommu_unmap_sgt(isp, &buf->surfs[i]); +void apple_isp_release_retained_buffers(struct apple_isp *isp) +{ + struct isp_buffer_memory *memory, *tmp; + unsigned long flags; + LIST_HEAD(released); + + if (!READ_ONCE(isp->firmware_quiescent)) + return; + spin_lock_irqsave(&isp->buf_lock, flags); + list_splice_init(&isp->retained_buffers, &released); + spin_unlock_irqrestore(&isp->buf_lock, flags); + list_for_each_entry_safe(memory, tmp, &released, link) { + list_del(&memory->link); + isp_free_buffer_memory(isp, memory); + } } static void isp_vb2_buf_cleanup(struct vb2_buffer *vb) { - __isp_vb2_buf_cleanup(vb, vb->num_planes); + struct apple_isp *isp = vb2_get_drv_priv(vb->vb2_queue); + struct isp_buffer *buf = to_isp_buffer(to_vb2_v4l2_buffer(vb)); + struct isp_buffer_memory *memory = buf->memory; + unsigned long flags; + bool reclaimable; + + if (!memory) + return; + spin_lock_irqsave(&isp->buf_lock, flags); + list_del_init(&buf->link); + reclaimable = isp_buffer_reclaimable(&buf->lease, + READ_ONCE(isp->firmware_quiescent)); + if (!reclaimable) + list_add_tail(&memory->link, &isp->retained_buffers); + buf->memory = NULL; + spin_unlock_irqrestore(&isp->buf_lock, flags); + if (reclaimable) + isp_free_buffer_memory(isp, memory); + else + dev_warn(isp->dev, "retaining unretired capture backing and IOVA\n"); } static int isp_vb2_buf_init(struct vb2_buffer *vb) { struct apple_isp *isp = vb2_get_drv_priv(vb->vb2_queue); - struct isp_buffer *buf = - container_of(vb, struct isp_buffer, vb.vb2_buf); - unsigned int i; + struct isp_buffer *buf = to_isp_buffer(to_vb2_v4l2_buffer(vb)); + struct isp_buffer_memory *memory; int err; - for (i = 0; i < vb->num_planes; i++) { + memory = kzalloc_obj(*memory); + if (!memory) + return -ENOMEM; + INIT_LIST_HEAD(&memory->link); + INIT_LIST_HEAD(&buf->link); + buf->memory = memory; + for (unsigned int i = 0; i < vb->num_planes; i++) { struct sg_table *sgt = vb2_dma_sg_plane_desc(vb, i); - err = apple_isp_iommu_map_sgt(isp, &buf->surfs[i], sgt, + + /* Independent page references keep physical backing alive after + * vb2 metadata cleanup without changing vb2's num_users count. + */ + err = isp_pin_buffer_pages(sgt, &memory->pins[i]); + if (err) + goto cleanup; + memory->num_planes = i + 1; + err = apple_isp_iommu_map_sgt(isp, &memory->surfs[i], sgt, vb2_plane_size(vb, i)); if (err) goto cleanup; } - return 0; cleanup: - __isp_vb2_buf_cleanup(vb, i); + buf->memory = NULL; + isp_free_buffer_memory(isp, memory); return err; } @@ -364,19 +444,31 @@ static int isp_vb2_buf_prepare(struct vb2_buffer *vb) return 0; } +static void isp_vb2_release_list(struct apple_isp *isp, struct list_head *head, + enum vb2_buffer_state state) +{ + struct isp_buffer *buf, *tmp; + + list_for_each_entry_safe(buf, tmp, head, link) { + list_del_init(&buf->link); + if (READ_ONCE(isp->firmware_quiescent)) + buf->lease.owner = ISP_BUFFER_HOST; + /* A vb2 error return is not a DMA-release acknowledgment. An + * unretired lease keeps its independent backing and map on cleanup. + */ + vb2_buffer_done(&buf->vb.vb2_buf, state); + } +} + static void isp_vb2_release_buffers(struct apple_isp *isp, enum vb2_buffer_state state) { - struct isp_buffer *buf; unsigned long flags; spin_lock_irqsave(&isp->buf_lock, flags); - list_for_each_entry(buf, &isp->bufs_submitted, link) - vb2_buffer_done(&buf->vb.vb2_buf, state); - INIT_LIST_HEAD(&isp->bufs_submitted); - list_for_each_entry(buf, &isp->bufs_pending, link) - vb2_buffer_done(&buf->vb.vb2_buf, state); - INIT_LIST_HEAD(&isp->bufs_pending); + isp_vb2_release_list(isp, &isp->bufs_submitted, state); + isp_vb2_release_list(isp, &isp->bufs_retiring, state); + isp_vb2_release_list(isp, &isp->bufs_pending, state); spin_unlock_irqrestore(&isp->buf_lock, flags); } @@ -388,6 +480,11 @@ static void isp_vb2_buf_queue(struct vb2_buffer *vb) unsigned long flags; bool empty; + if (READ_ONCE(isp->capture_failed)) { + vb2_buffer_done(vb, VB2_BUF_STATE_ERROR); + return; + } + spin_lock_irqsave(&isp->buf_lock, flags); empty = list_empty(&isp->bufs_pending) && list_empty(&isp->bufs_submitted); @@ -403,6 +500,9 @@ static int apple_isp_start_streaming(struct apple_isp *isp) unsigned long flags; int err; + if (READ_ONCE(isp->capture_failed)) + return -EIO; + err = apple_isp_start_camera(isp); if (err) { dev_err(isp->dev, "failed to start camera: %d\n", err); @@ -415,12 +515,14 @@ static int apple_isp_start_streaming(struct apple_isp *isp) * as submitted to the previous pools would never reach the new ones. */ if (isp->hw->resident_fw) { + mutex_lock(&isp->chan_bt->lock); spin_lock_irqsave(&isp->buf_lock, flags); - for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) - isp->meta_surfs[i]->submitted = false; + for (int i = 0; i < isp_num_meta(isp); i++) + isp->meta_surfs[i]->lease.owner = ISP_BUFFER_HOST; for (int i = 0; i < isp_num_capmeta(isp); i++) - isp->capmeta_surfs[i]->submitted = false; + isp->capmeta_surfs[i]->lease.owner = ISP_BUFFER_HOST; spin_unlock_irqrestore(&isp->buf_lock, flags); + mutex_unlock(&isp->chan_bt->lock); } err = isp_submit_buffers(isp, true); @@ -450,18 +552,50 @@ static int apple_isp_start_streaming(struct apple_isp *isp) return 0; stop_camera: - apple_isp_stop_camera(isp); + if (READ_ONCE(isp->capture_failed)) + apple_isp_firmware_halt(isp); + else + apple_isp_stop_camera(isp); return err; } -static void apple_isp_stop_streaming(struct apple_isp *isp) +static bool isp_capture_buffers_retired(struct apple_isp *isp) { - /* Not running if restarting it after system sleep failed. */ + unsigned long flags; + bool retired; + + spin_lock_irqsave(&isp->buf_lock, flags); + retired = list_empty(&isp->bufs_submitted) && list_empty(&isp->bufs_retiring); + spin_unlock_irqrestore(&isp->buf_lock, flags); + return retired; +} + +static int apple_isp_stop_streaming(struct apple_isp *isp) +{ + int err; + if (!test_and_clear_bit(ISP_STATE_STREAMING, &isp->state)) - return; + return READ_ONCE(isp->capture_failed) ? -EIO : 0; - apple_isp_stop_capture(isp); + err = apple_isp_stop_capture(isp); + if (err) + isp_capture_failed(isp); apple_isp_stop_camera(isp); + if (READ_ONCE(isp->capture_failed)) + return apple_isp_firmware_halt(isp) ?: -EIO; + if (READ_ONCE(isp->firmware_quiescent)) + return 0; + + /* Resident firmware may stay alive, but a stop command ACK alone + * does not retire user-buffer leases. Wait for their report ACKs. + */ + if (!wait_event_timeout(isp->wait, isp_capture_buffers_retired(isp), + ISP_BUFFER_TIMEOUT)) { + isp_capture_failed(isp); + apple_isp_firmware_halt(isp); + return -ETIME; + } + return 0; } static int isp_vb2_start_streaming(struct vb2_queue *q, unsigned int count) @@ -472,11 +606,13 @@ static int isp_vb2_start_streaming(struct vb2_queue *q, unsigned int count) isp->sequence = 0; /* The exposure priority is applied when the stream starts. */ - v4l2_ctrl_grab(isp->exposure_priority, true); + if (isp->exposure_priority) + v4l2_ctrl_grab(isp->exposure_priority, true); err = apple_isp_start_streaming(isp); if (err) { - v4l2_ctrl_grab(isp->exposure_priority, false); + if (isp->exposure_priority) + v4l2_ctrl_grab(isp->exposure_priority, false); isp_vb2_release_buffers(isp, VB2_BUF_STATE_QUEUED); } @@ -489,34 +625,45 @@ static void isp_vb2_stop_streaming(struct vb2_queue *q) apple_isp_stop_streaming(isp); isp_vb2_release_buffers(isp, VB2_BUF_STATE_ERROR); - v4l2_ctrl_grab(isp->exposure_priority, false); + if (isp->exposure_priority) + v4l2_ctrl_grab(isp->exposure_priority, false); } int apple_isp_video_suspend(struct apple_isp *isp) { + struct isp_buffer *buf; unsigned long flags; + int err = 0; mutex_lock(&isp->video_lock); /* - * Stop the stream but keep its buffers queued to the driver. First - * move the buffers given to the firmware to the pending list, so that - * the buffer return for the stop does not complete them; they are - * submitted again when the stream restarts on resume. + * Keep returned buffers queued while sleeping, but only after their + * reports have retired. Moving submitted buffers before the return + * would hide firmware ownership and prevent completion validation. * isp_vb2_buf_queue() does not submit while the stream is stopped. */ if (test_bit(ISP_STATE_STREAMING, &isp->state)) { - spin_lock_irqsave(&isp->buf_lock, flags); - list_splice_init(&isp->bufs_submitted, &isp->bufs_pending); - spin_unlock_irqrestore(&isp->buf_lock, flags); - - apple_isp_stop_streaming(isp); set_bit(ISP_STATE_SLEEPING, &isp->state); + err = apple_isp_stop_streaming(isp); + /* Nonresident firmware can stop without returning every buffer. + * Only a completed full shutdown permits requeuing those leases. + */ + if (!err && READ_ONCE(isp->firmware_quiescent)) { + spin_lock_irqsave(&isp->buf_lock, flags); + list_for_each_entry(buf, &isp->bufs_submitted, link) + buf->lease.owner = ISP_BUFFER_HOST; + list_for_each_entry(buf, &isp->bufs_retiring, link) + buf->lease.owner = ISP_BUFFER_HOST; + list_splice_tail_init(&isp->bufs_submitted, &isp->bufs_pending); + list_splice_tail_init(&isp->bufs_retiring, &isp->bufs_pending); + spin_unlock_irqrestore(&isp->buf_lock, flags); + } } mutex_unlock(&isp->video_lock); - return 0; + return err; } int apple_isp_video_resume(struct apple_isp *isp) @@ -557,6 +704,22 @@ static int isp_set_preset(struct apple_isp *isp, struct isp_format *fmt, int i; size_t total_size; + if (isp->profile) { + struct isp_profile_geometry geometry; + int err = isp_profile_p010_geometry(preset->output_dim.x, + preset->output_dim.y, &geometry); + + if (err || preset->index != 5) + return -EINVAL; + fmt->preset = preset; + fmt->num_planes = 2; + fmt->strides[0] = geometry.stride; + fmt->strides[1] = geometry.stride; + fmt->plane_size[0] = geometry.luma_size; + fmt->plane_size[1] = geometry.chroma_size; + fmt->total_size = geometry.total_size; + return 0; + } fmt->preset = preset; /* I really fucking hope they all use NV12. */ @@ -615,6 +778,12 @@ static int isp_vidioc_enum_format(struct file *file, void *fh, { struct apple_isp *isp = video_drvdata(file); + if (isp->profile) { + if (f->index) + return -EINVAL; + f->pixelformat = V4L2_PIX_FMT_P010; + return 0; + } if (f->index >= ISP_MAX_PIX_FORMATS) return -EINVAL; @@ -642,8 +811,8 @@ static int isp_vidioc_enum_framesizes(struct file *file, void *fh, if (f->index >= isp->num_presets) return -EINVAL; - if ((f->pixel_format != V4L2_PIX_FMT_NV12) && - (f->pixel_format != V4L2_PIX_FMT_NV12M)) + if (isp->profile ? f->pixel_format != V4L2_PIX_FMT_P010 : + f->pixel_format != V4L2_PIX_FMT_NV12 && f->pixel_format != V4L2_PIX_FMT_NV12M) return -EINVAL; f->discrete.width = isp->presets[f->index].output_dim.x; @@ -659,6 +828,9 @@ static int isp_vidioc_enum_frameintervals(struct file *file, void *fh, struct apple_isp *isp = video_drvdata(file); int i; + /* The recorded AE settings are not a measured frame-rate guarantee. */ + if (isp->profile) + return -EINVAL; if (interval->index >= ARRAY_SIZE(isp_frame_rates)) return -EINVAL; @@ -713,7 +885,7 @@ static inline void isp_get_sp_pix_format(struct apple_isp *isp, f->fmt.pix.sizeimage = fmt->total_size; f->fmt.pix.field = V4L2_FIELD_NONE; - f->fmt.pix.pixelformat = V4L2_PIX_FMT_NV12; + f->fmt.pix.pixelformat = isp->profile ? V4L2_PIX_FMT_P010 : V4L2_PIX_FMT_NV12; f->fmt.pix.colorspace = V4L2_COLORSPACE_REC709; f->fmt.pix.ycbcr_enc = V4L2_YCBCR_ENC_709; f->fmt.pix.xfer_func = V4L2_XFER_FUNC_709; @@ -879,6 +1051,9 @@ static int isp_vidioc_get_param(struct file *file, void *fh, { struct apple_isp *isp = video_drvdata(file); + if (isp->profile) + return -EINVAL; + if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && (!isp->multiplanar || a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) @@ -898,6 +1073,9 @@ static int isp_vidioc_set_param(struct file *file, void *fh, struct apple_isp *isp = video_drvdata(file); struct v4l2_fract *tpf = &a->parm.capture.timeperframe; + if (isp->profile) + return -EINVAL; + if (a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE && (!isp->multiplanar || a->type != V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)) @@ -969,19 +1147,29 @@ static const struct media_device_ops isp_media_device_ops = { .link_notify = v4l2_pipeline_link_notify, }; -static void isp_free_meta_surfaces(struct apple_isp *isp) +static void isp_free_meta_surface(struct apple_isp *isp, struct isp_surf **slot) { - for (int i = 0; i < ARRAY_SIZE(isp->capmeta_surfs); i++) { - if (isp->capmeta_surfs[i]) - isp_free_surface(isp, isp->capmeta_surfs[i]); - isp->capmeta_surfs[i] = NULL; - } + struct isp_surf *surf; + unsigned long flags; - for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) { - if (isp->meta_surfs[i]) - isp_free_surface(isp, isp->meta_surfs[i]); - isp->meta_surfs[i] = NULL; - } + spin_lock_irqsave(&isp->buf_lock, flags); + surf = *slot; + if (surf && !isp_buffer_reclaimable(&surf->lease, + READ_ONCE(isp->firmware_quiescent))) + surf = NULL; + else + *slot = NULL; + spin_unlock_irqrestore(&isp->buf_lock, flags); + if (surf) + isp_free_surface(isp, surf); +} + +static void isp_free_meta_surfaces(struct apple_isp *isp) +{ + for (unsigned int i = 0; i < ARRAY_SIZE(isp->capmeta_surfs); i++) + isp_free_meta_surface(isp, &isp->capmeta_surfs[i]); + for (unsigned int i = 0; i < isp_num_meta(isp); i++) + isp_free_meta_surface(isp, &isp->meta_surfs[i]); } int apple_isp_setup_video(struct apple_isp *isp) @@ -999,9 +1187,10 @@ int apple_isp_setup_video(struct apple_isp *isp) isp->frame_rate = ISP_FRAME_RATE_DEFAULT; - for (int i = 0; i < ARRAY_SIZE(isp->meta_surfs); i++) { + for (int i = 0; i < isp_num_meta(isp); i++) { isp->meta_surfs[i] = - isp_alloc_surface_vmap(isp, isp->hw->meta_size); + isp_alloc_surface_vmap(isp, isp->profile ? + isp->profile->meta_size : isp->hw->meta_size); if (!isp->meta_surfs[i]) { isp_err(isp, "failed to alloc meta surface\n"); err = -ENOMEM; @@ -1033,16 +1222,17 @@ int apple_isp_setup_video(struct apple_isp *isp) goto media_cleanup; } - isp->multiplanar = multiplanar; + isp->multiplanar = multiplanar && !isp->profile; /* * Auto exposure is always on. By default it holds the frame rate, as * V4L2 specifies; with exposure priority it may slow down in low light. */ v4l2_ctrl_handler_init(&isp->ctrl_handler, 1); - isp->exposure_priority = - v4l2_ctrl_new_std(&isp->ctrl_handler, NULL, - V4L2_CID_EXPOSURE_AUTO_PRIORITY, 0, 1, 1, 0); + if (!isp->profile) + isp->exposure_priority = + v4l2_ctrl_new_std(&isp->ctrl_handler, NULL, + V4L2_CID_EXPOSURE_AUTO_PRIORITY, 0, 1, 1, 0); if (isp->ctrl_handler.error) { err = isp->ctrl_handler.error; dev_err(isp->dev, "failed to create controls: %d\n", err); @@ -1065,6 +1255,7 @@ int apple_isp_setup_video(struct apple_isp *isp) vbq->buf_struct_size = sizeof(struct isp_buffer); vbq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; vbq->min_queued_buffers = ISP_MIN_FRAMES; + vbq->max_num_buffers = isp_max_capture_buffers(isp); vbq->lock = &isp->video_lock; err = vb2_queue_init(vbq); @@ -1093,6 +1284,7 @@ int apple_isp_setup_video(struct apple_isp *isp) goto v4l2_unregister; } + isp->video_registered = true; return 0; v4l2_unregister: @@ -1110,6 +1302,7 @@ int apple_isp_setup_video(struct apple_isp *isp) void apple_isp_remove_video(struct apple_isp *isp) { + isp->video_registered = false; vb2_video_unregister_device(&isp->vdev); v4l2_device_unregister(&isp->v4l2_dev); v4l2_ctrl_handler_free(&isp->ctrl_handler); @@ -1121,4 +1314,5 @@ void apple_isp_remove_video(struct apple_isp *isp) void apple_isp_free_video(struct apple_isp *isp) { isp_free_meta_surfaces(isp); + apple_isp_release_retained_buffers(isp); } diff --git a/drivers/media/platform/apple/isp/isp-v4l2.h b/drivers/media/platform/apple/isp/isp-v4l2.h index 622288296aaae..244d0b5c58920 100644 --- a/drivers/media/platform/apple/isp/isp-v4l2.h +++ b/drivers/media/platform/apple/isp/isp-v4l2.h @@ -10,6 +10,8 @@ int apple_isp_setup_video(struct apple_isp *isp); void apple_isp_remove_video(struct apple_isp *isp); void apple_isp_free_video(struct apple_isp *isp); int ipc_bt_handle(struct apple_isp *isp, struct isp_channel *chan); +void ipc_bt_acknowledged(struct apple_isp *isp, struct isp_channel *chan); +void apple_isp_release_retained_buffers(struct apple_isp *isp); int apple_isp_video_suspend(struct apple_isp *isp); int apple_isp_video_resume(struct apple_isp *isp); diff --git a/drivers/phy/apple/Kconfig b/drivers/phy/apple/Kconfig index 8409b67f6d1ec..801980489440d 100644 --- a/drivers/phy/apple/Kconfig +++ b/drivers/phy/apple/Kconfig @@ -1,4 +1,14 @@ # SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause +config PHY_APPLE_T6040_USB2_INHERITED + tristate "J616s inherited USB2 PHY for RAM experiments" + depends on ARCH_APPLE && OF + select GENERIC_PHY + help + Verify bootloader-initialized USB2 device mode on J616s without + programming its PHY or PIPE. The caller must transfer controller/DART + ownership and retain PHY clocks and a physical recovery watchdog. + Host mode, hotplug, suspend and SuperSpeed are unsupported. + config PHY_APPLE_ATC tristate "Apple Type-C PHY" depends on (ARM64 && ARCH_APPLE) || (COMPILE_TEST && !GENERIC_ATOMIC64) diff --git a/drivers/phy/apple/Makefile b/drivers/phy/apple/Makefile index b9e7bf3e4ac17..7a6e3ebf89304 100644 --- a/drivers/phy/apple/Makefile +++ b/drivers/phy/apple/Makefile @@ -5,3 +5,4 @@ phy-apple-atc-y := atc.o obj-$(CONFIG_PHY_APPLE_DPTX) += phy-apple-dptx.o phy-apple-dptx-y += dptx.o +obj-$(CONFIG_PHY_APPLE_T6040_USB2_INHERITED) += usb2-t6040-inherited.o diff --git a/drivers/phy/apple/usb2-t6040-inherited.c b/drivers/phy/apple/usb2-t6040-inherited.c new file mode 100644 index 0000000000000..c17dc24408edd --- /dev/null +++ b/drivers/phy/apple/usb2-t6040-inherited.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * J616s RAM-only inherited USB2 PHY admission. + * + * The bootloader owns PHY power, clock, calibration and PIPE setup for the + * entire experiment. This driver only verifies the device-mode state left by + * m1n1 usb_phy_bringup() at fd360ef69054e42724b975d7a4c61e0242b6b341, + * with the exact post-init SIG readback captured on source-checked recovery + * ea52da04a1fa0e7f648812de23ac5cd8f5000ab3 on 2026-10-09 at 14:52 UTC. + * The difference from the initialization write has no inferred bit meaning. + * It performs no PHY/PIPE writes and cannot provide host, hotplug, suspend or + * SuperSpeed support. Controller/DART retirement and watchdog recovery belong + * to the caller; the inherited-handoff property does not perform a handoff. + */ +#include +#include +#include +#include +#include + +#define USB2PHY_USBCTL 0x00 +#define USB2PHY_USBCTL_MODE GENMASK(2, 0) +#define USB2PHY_USBCTL_RUN 2 +#define USB2PHY_CTL 0x04 +#define USB2PHY_CTL_STOPPED (BIT(0) | BIT(1) | BIT(3)) +#define USB2PHY_SIG 0x08 +#define USB2PHY_SIG_INHERITED 0x01c0000f +#define USB2PHY_MISCTUNE 0x1c +#define USB2PHY_MISCTUNE_INHERITED 0x008c0813 +#define PIPE_MUX_CTRL 0x0c +#define PIPE_MUX_CTRL_MASK GENMASK(5, 0) +#define PIPE_MUX_CTRL_DUMMY 0x22 +#define PIPE_AON_GEN 0x1c +#define PIPE_AON_GEN_MASK (BIT(4) | BIT(0)) +#define PIPE_AON_GEN_RESET_N BIT(0) +#define PIPE_NONSELECTED_OVERRIDE 0x20 +#define PIPE_NONSELECTED_INHERITED 0x9332 + +struct apple_t6040_usb2 { + struct device *dev; + void __iomem *usb2; + void __iomem *pipe; +}; + +static int apple_t6040_usb2_check(struct phy *phy) +{ + struct apple_t6040_usb2 *usb2 = phy_get_drvdata(phy); + + if ((readl(usb2->usb2 + USB2PHY_USBCTL) & USB2PHY_USBCTL_MODE) != + USB2PHY_USBCTL_RUN || + readl(usb2->usb2 + USB2PHY_CTL) & USB2PHY_CTL_STOPPED || + readl(usb2->usb2 + USB2PHY_SIG) != USB2PHY_SIG_INHERITED || + readl(usb2->usb2 + USB2PHY_MISCTUNE) != USB2PHY_MISCTUNE_INHERITED || + (readl(usb2->pipe + PIPE_MUX_CTRL) & PIPE_MUX_CTRL_MASK) != PIPE_MUX_CTRL_DUMMY || + (readl(usb2->pipe + PIPE_AON_GEN) & PIPE_AON_GEN_MASK) != PIPE_AON_GEN_RESET_N || + readl(usb2->pipe + PIPE_NONSELECTED_OVERRIDE) != PIPE_NONSELECTED_INHERITED) { + dev_err(usb2->dev, "Inherited USB2 device state is unavailable\n"); + return -EIO; + } + + return 0; +} + +static int apple_t6040_usb2_set_mode(struct phy *phy, enum phy_mode mode, int submode) +{ + /* No T6040 host-role mask or transition is inferred from T8132. */ + if (mode != PHY_MODE_USB_DEVICE || submode) + return -EOPNOTSUPP; + + return apple_t6040_usb2_check(phy); +} + +static const struct phy_ops apple_t6040_usb2_ops = { + .owner = THIS_MODULE, + .init = apple_t6040_usb2_check, + .power_on = apple_t6040_usb2_check, + .set_mode = apple_t6040_usb2_set_mode, +}; + +static int apple_t6040_usb2_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct apple_t6040_usb2 *usb2; + struct phy_provider *provider; + struct resource *res; + struct phy *phy; + + if (!of_machine_is_compatible("apple,j616s") || + !of_property_read_bool(dev->of_node, "apple,inherited-handoff")) + return -EOPNOTSUPP; + + usb2 = devm_kzalloc(dev, sizeof(*usb2), GFP_KERNEL); + if (!usb2) + return -ENOMEM; + usb2->dev = dev; + + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "usb2phy"); + if (!res || resource_size(res) < USB2PHY_MISCTUNE + sizeof(u32)) + return -EINVAL; + usb2->usb2 = devm_ioremap_resource(dev, res); + if (IS_ERR(usb2->usb2)) + return PTR_ERR(usb2->usb2); + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "pipehandler"); + if (!res || resource_size(res) < PIPE_NONSELECTED_OVERRIDE + sizeof(u32)) + return -EINVAL; + usb2->pipe = devm_ioremap_resource(dev, res); + if (IS_ERR(usb2->pipe)) + return PTR_ERR(usb2->pipe); + + phy = devm_phy_create(dev, NULL, &apple_t6040_usb2_ops); + if (IS_ERR(phy)) + return PTR_ERR(phy); + phy_set_drvdata(phy, usb2); + provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate); + return PTR_ERR_OR_ZERO(provider); +} + +static const struct of_device_id apple_t6040_usb2_match[] = { + { .compatible = "apple,t6040-usb2-inherited" }, + {} +}; +MODULE_DEVICE_TABLE(of, apple_t6040_usb2_match); + +static struct platform_driver apple_t6040_usb2_driver = { + .probe = apple_t6040_usb2_probe, + .driver = { + .name = "apple-t6040-usb2-inherited", + .of_match_table = apple_t6040_usb2_match, + }, +}; +module_platform_driver(apple_t6040_usb2_driver); + +MODULE_DESCRIPTION("J616s RAM-only inherited USB2 PHY admission"); +MODULE_LICENSE("Dual MIT/GPL"); diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index cf79092c1fa7b..4ab9e14328c16 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -5,6 +5,11 @@ //! //! Copyright (C) The Asahi Linux Contributors +#[path = "aop_bootargs.rs"] +mod bootargs; +#[path = "aop_shutdown.rs"] +mod shutdown; + use core::{ arch::asm, cmp, @@ -46,6 +51,10 @@ use kernel::{ EPICService, FakehidListener, ReportListener, + ServiceContext, + ServiceGate, + ServicePlatformData, + ServiceABI, SourceRing, AOP, // }, @@ -415,6 +424,9 @@ impl AFKEndpoint { rtkit.send_message(self.index, AFK_MSG_INIT_ACK)?; } AFK_OPC_GET_BUF => { + if client.transport_closing.load(Acquire) { + return Err(ENODEV); + } self.recv_get_buf(client.dev.clone(), rtkit, msg)?; } AFK_OPC_INIT_UNK => {} // no-op @@ -1088,10 +1100,14 @@ impl WorkItem for SetupRxWork { /// A service platform device registered by this driver. It is unregistered /// explicitly by [`AopData::remove`]; nothing else touches the pointer. -struct ChildDevice(NonNull); +struct ChildDevice( + NonNull, + Option, + Option>>, +); -// SAFETY: The only operation on the pointer is `platform_device_unregister()`, -// which may be called from any thread. +// SAFETY: Registered-device lifecycle/override operations are thread-safe; +// the retirement allocation remains uniquely owned until it is installed. unsafe impl Send for ChildDevice {} /// A driver override that no driver matches: keeps a service device from @@ -1103,29 +1119,65 @@ const RETIRED_DRIVER_OVERRIDE: &CStr = c_str!("apple-aop-retired"); /// before a reboot. static RETIRED: Atomic = Atomic::new(false); -impl ChildDevice { - /// Keeps the child from binding to any driver again; it stays registered. - fn retire(&self) { - // SAFETY: The device is registered, so its embedded `struct device` is - // valid, and the override string is NUL-terminated. - let ret = unsafe { - bindings::__device_set_driver_override( - ptr::addr_of_mut!((*self.0.as_ptr()).dev), +/// An owned, preallocated retirement marker. No failure-time allocation is +/// needed to transfer it into the device, where it survives module reload. +struct PreparedOverride(NonNull); + +// SAFETY: This uniquely owns the allocation; it is transferred or freed once. +unsafe impl Send for PreparedOverride {} + +impl PreparedOverride { + fn new() -> Result { + // SAFETY: The name is NUL-terminated. The returned allocation is + // uniquely owned here until install transfers it to the device. + let pointer = unsafe { + from_err_ptr(bindings::__device_prepare_driver_override( RETIRED_DRIVER_OVERRIDE.as_char_ptr(), RETIRED_DRIVER_OVERRIDE.to_bytes().len(), - ) - }; - // Only an allocation failure; the device is unbound either way and - // the retired flag keeps the parent from coming back. - let _ = ret; + ).cast_mut()) + }?; + Ok(Self(NonNull::new(pointer).ok_or(EINVAL)?)) + } + + unsafe fn install(self, dev: *mut bindings::device) { + // SAFETY: The caller supplies a registered device. This transfers + // the prepared allocation without allocating, and cannot fail. + unsafe { bindings::__device_install_driver_override(dev, self.0.as_ptr()) }; + mem::forget(self); } + unsafe fn retire_driver(self, dev: *mut bindings::device) { + // SAFETY: The child's context was closed and dev is registered. + // The core takes attach's lock, installs and unbinds without allocation. + unsafe { bindings::__device_retire_driver(dev, self.0.as_ptr()) }; + mem::forget(self); + } +} + +impl Drop for PreparedOverride { + fn drop(&mut self) { + // SAFETY: Uninstalled allocations are still uniquely owned here. + unsafe { bindings::kfree(self.0.as_ptr().cast()) }; + } +} + +impl ChildDevice { /// Unbinds the child's driver while the child stays registered. - fn release_driver(&self) { + fn release_driver(&mut self) { // SAFETY: The pointer came from a successful // `platform_device_register_full()` and the device is still // registered, so its embedded `struct device` is valid. - unsafe { bindings::device_release_driver(ptr::addr_of_mut!((*self.0.as_ptr()).dev)) }; + let marker = self + .1 + .take() + .expect("retirement reserved before child registration"); + unsafe { marker.retire_driver(ptr::addr_of_mut!((*self.0.as_ptr()).dev)) }; + } + + fn retain_context(&mut self) { + // Its closed lookup gate must remain available even if a match was + // queued before the persistent override was installed. + mem::forget(self.2.take().expect("child-owned context")); } fn unregister(self) { @@ -1141,6 +1193,8 @@ struct AopData { dev: ARef, /// The firmware speaks EPIC with version 4 sub-headers. epic_v4: bool, + /// Qualified child protocols; framing version is not a child ABI. + service_abi: ServiceABI, /// Runs the service registrations one at a time; drained on removal. registration_queue: OwnedQueue, /// Serializes queueing a registration with the start of removal, so that @@ -1154,6 +1208,14 @@ struct AopData { /// The co-processor was started; only then is there anything to shut /// down and to retain. cpu_started: Atomic, + /// Probe finished handing startup to the firmware. A failed probe may + /// already have exposed buffers even if no AFK endpoint started. + probe_complete: Atomic, + /// Reserved before the CPU or any endpoint can learn a host address. + #[pin] + retirement: Mutex>, + /// Core-owned admission survives parent drvdata and closes pending lookups. + service_gate: Arc, /// Shut the co-processor down on removal, and if that cannot be /// confirmed, retain everything it may still DMA to. quiesce_on_unbind: bool, @@ -1229,14 +1291,23 @@ impl WorkItem for AopServiceRegisterWork { .dev .fwnode() .and_then(|x| x.get_child_by_name(this.name)); + if this.data.service_abi == ServiceABI::J616sJack { + let Some(node) = fwnode.as_ref() else { + return; + }; + // SAFETY: This reference holds a valid fwnode during registration. + if !unsafe { bindings::fwnode_device_is_available(node.as_raw()) } { + return; + } + } let info = bindings::platform_device_info { parent: this.data.dev.as_raw(), name: this.name.as_ptr() as *const _, id: bindings::PLATFORM_DEVID_AUTO, res: ptr::null_mut(), num_res: 0, - data: &this.service as *const EPICService as *const _, - size_data: mem::size_of::(), + data: ptr::null(), + size_data: 0, dma_mask: 0, fwnode: fwnode .as_ref() @@ -1259,6 +1330,35 @@ impl WorkItem for AopServiceRegisterWork { ); return; } + // Registration can synchronously probe the child and publish DMA. + // Reserve its persistent barrier first; ENOMEM exposes no child. + let retirement = match PreparedOverride::new() { + Ok(marker) => marker, + Err(e) => { + dev_err!(this.data.dev, "Failed to reserve service retirement {:?}", e); + return; + } + }; + let context = match ServiceContext::new( + this.data.clone() as Arc, + this.data.service_gate.clone(), + ) { + Ok(context) => context, + Err(e) => { + dev_err!(this.data.dev, "Failed to allocate service context {:?}", e); + return; + } + }; + let payload = ServicePlatformData { + service: this.service, + context: context.as_ref().get_ref() as *const _, + abi: this.data.service_abi, + }; + let info = bindings::platform_device_info { + data: ptr::addr_of!(payload).cast(), + size_data: mem::size_of::(), + ..info + }; // SAFETY: `info` is a valid, fully initialized `platform_device_info` // whose pointers outlive the call. let pdev = unsafe { from_err_ptr(bindings::platform_device_register_full(&info)) }; @@ -1273,7 +1373,8 @@ impl WorkItem for AopServiceRegisterWork { ); } Ok(pdev) => { - if let Err(child) = subdevices.push_within_capacity(ChildDevice(pdev)) { + if let Err(child) = subdevices + .push_within_capacity(ChildDevice(pdev, Some(retirement), Some(context))) { // Cannot happen after the reservation above; never leave // a registered device untracked. child.0.unregister(); @@ -1285,6 +1386,13 @@ impl WorkItem for AopServiceRegisterWork { impl AopData { fn new(dev: &platform::Device, cfg: &AopHwConfig) -> Result> { + if cfg.service_abi == ServiceABI::J616sJack && (cfg.epic_v4 || cfg.setup_port) { + return Err(EINVAL); + } + // Abort on ENOMEM before RTKit construction/CPU handoff can publish + // buffers. The reserve is unused and freed after confirmed shutdown. + let retirement = PreparedOverride::new()?; + let service_gate = ServiceGate::new()?; let registration_queue = OwnedQueue::new_ordered(c_str!("apple-aop"))?; let setup_queue = if cfg.setup_port { Some(OwnedQueue::new_ordered(c_str!("apple-aop-setup"))?) @@ -1296,11 +1404,15 @@ impl AopData { AopData { dev: dev.as_ref().into(), epic_v4: cfg.epic_v4, + service_abi: cfg.service_abi, registration_queue, registration_gate <- new_mutex!(()), removing: Atomic::new(false), transport_closing: Atomic::new(false), cpu_started: Atomic::new(false), + probe_complete: Atomic::new(false), + retirement <- new_mutex!(Some(retirement)), + service_gate, quiesce_on_unbind: cfg.quiesce_on_unbind, required_endpoints: cfg.required_endpoints, setup_queue, @@ -1371,6 +1483,21 @@ impl AopData { channel: u32, name: &[u8], ) -> Result<()> { + if !self.service_abi.publishes_children() { + // No sensor/audio child may mutate a control-only firmware profile. + dev_dbg!( + self.dev, "Discovered {:?} at endpoint {:#x}, channel {}", name, ep.index, channel + ); + return Ok(()); + } + if self.service_abi == ServiceABI::J616sJack { + if name != b"aop-audio" { + return Ok(()); + } + if ep.index != 0x22 { + return Err(EINVAL); + } + } let svc = EPICService { channel, endpoint: ep.index, @@ -1427,16 +1554,33 @@ impl AopData { /// Shuts down every started endpoint, waiting a bounded time for each /// acknowledgment. Returns the first error but still tries the rest. - fn stop(&self) -> Result<()> { + fn stop(&self) -> Result { let mut ret = Ok(()); + let mut stopped = 0; for ep in 0..AFK_ENDPOINT_COUNT as usize { { let mut guard = self.rtkit.lock(); - let mut rtk = guard.as_mut().as_pin_mut().ok_or(ENODEV)?; + let Some(mut rtk) = guard.as_mut().as_pin_mut() else { + if !self.cpu_started.load(Acquire) + && self.endpoints.iter().all(|endpoint| { + let endpoint = endpoint.lock(); + !endpoint.started && endpoint.iomem.is_none() + }) + { + return Ok(0); + } + return Err(ENODEV); + }; let mut ep_guard = self.endpoints[ep].lock(); if !ep_guard.started { + if ep_guard.iomem.is_some() { + // A partial endpoint startup published an address + // without reaching the tracked stop state. + ret = ret.and(Err(EIO)); + } continue; } + stopped += 1; // Whatever happens below, the endpoint is not started again. ep_guard.started = false; self.ep_shutdown[ep].reset(); @@ -1457,7 +1601,7 @@ impl AopData { ret = ret.and(Err(ETIMEDOUT)); } } - ret + ret.map(|()| stopped) } fn patch_bootargs( @@ -1468,22 +1612,16 @@ impl AopData { let aop_mmio = aop_mmio.relaxed(); let offset = aop_mmio.read32(BOOTARGS_OFFSET) as usize; let size = aop_mmio.read32(BOOTARGS_SIZE) as usize; + let end = offset.checked_add(size).ok_or(EINVAL)?; + if end > AOP_MMIO_SIZE { + return Err(EINVAL); + } let mut arg_bytes = KVec::::from_elem(0, size, GFP_KERNEL)?; aop_mmio.try_memcpy_fromio(&mut arg_bytes, offset)?; - let mut idx = 0; - while idx < size { - let key = u32::from_le_bytes(arg_bytes[idx..idx + 4].try_into().unwrap()); - let size = u32::from_le_bytes(arg_bytes[idx + 4..idx + 8].try_into().unwrap()) as usize; - idx += 8; - for (k, v) in patches.iter() { - if *k != key { - continue; - } - arg_bytes[idx..idx + size].copy_from_slice(&(*v as u64).to_le_bytes()[..size]); - break; - } - idx += size; - } + bootargs::patch(&mut arg_bytes, patches).map_err(|error| { + dev_err!(self.dev, "Invalid AOP boot arguments: {:?}\n", error); + EINVAL + })?; aop_mmio.try_memcpy_toio(offset, &arg_bytes) } @@ -1548,6 +1686,14 @@ impl AopData { } impl AOP for AopData { + fn protocol_version(&self) -> Result { + if self.removing.load(Acquire) || self.transport_closing.load(Acquire) { + return Err(ENODEV); + } + let guard = self.rtkit.lock(); + Ok(guard.as_ref().ok_or(ENODEV)?.protocol_version()) + } + fn epic_call(&self, svc: &EPICService, subtype: u16, msg_bytes: &[u8]) -> Result { if self.transport_closing.load(Acquire) { return Err(ENODEV); @@ -1652,7 +1798,13 @@ impl AOP for AopData { false } fn source_ring(&self, dev: &device::Device, size: usize) -> Result> { - if self.setup_queue.is_none() || self.transport_closing.load(Acquire) { + // Serialize address publication with the beginning of removal. A + // retained service device must not publish a new ring after closing. + let _gate = self.registration_gate.lock(); + if self.removing.load(Acquire) || self.transport_closing.load(Acquire) { + return Err(ENODEV); + } + if self.setup_queue.is_none() { return Err(ENODEV); } let mut guard = self.source_ring.lock(); @@ -1690,44 +1842,54 @@ impl AOP for AopData { return; } } + // Close before draining queued registrations: those children share + // this gate too. Pending lookups finish cloning under its lock. + self.service_gate.close(); // No registration is queued after this point, so once the queue is // empty the list of children is complete. self.registration_queue.drain(); // Unbind the children while the transport still works, so that their // unbind can talk to their services. Then drop any listener a child // left behind: no report may reach a driver that is going away. - let children = mem::take(&mut *self.subdevices.lock()); - for child in &children { + let mut children = mem::take(&mut *self.subdevices.lock()); + // An already admitted probe owns its provider Arc; attach's device + // lock serializes its completion before driver release. + for child in &mut children { child.release_driver(); } self.hid_listeners.lock().clear(); self.report_listeners.lock().clear(); self.transport_closing.store(true, Release); - if let Err(e) = self.stop() { - dev_err!(self.dev, "Failed to stop AOP {:?}", e); - } + let stopped = self + .stop() + .map_err(|e| { + dev_err!(self.dev, "Failed to stop AOP {:?}", e); + e + }) + .ok(); // Take the handle out of the shared state before dropping it: the // drop waits for the RTKit receive worker, which takes the same lock. // After it, no callback runs and the device may be unbound. let rtkit = self.rtkit.lock().take(); - let mut quiesced = true; - if let Some(mut rtkit) = rtkit { - if self.quiesce_on_unbind && self.cpu_started.load(Acquire) { - // The co-processor DMAs into the shared buffers until it has - // acknowledged the shutdown. If it does not, nothing it may - // still write to can be freed. - if let Err(e) = Pin::new(&mut rtkit).shutdown() { + let quiesced = shutdown::release_rtkit( + rtkit, + self.cpu_started.load(Acquire), + self.probe_complete.load(Acquire), + stopped, + self.quiesce_on_unbind, + |rtkit| { + if let Err(e) = Pin::new(rtkit).shutdown() { dev_err!( self.dev, "AOP shutdown unconfirmed ({:?}); retaining its buffers until reboot", e ); - rtkit.retain_shared_buffers_on_drop(); - quiesced = false; + return false; } - } - drop(rtkit); - } + true + }, + |rtkit| rtkit.retain_shared_buffers_on_drop(), + ); // Close the setup port: dropping the mailbox stops its interrupt, and // draining the queue finishes the messages that were already taken. // Neither may happen under the setup lock, which the queue's work @@ -1742,8 +1904,13 @@ impl AOP for AopData { // mappings the firmware may still use. Keep them from binding // again, and this device from probing again, until a reboot. self.retain_dma_buffers(); - for child in &children { - child.retire(); + // The parent stays registered and keeps its IOMMU domain. The + // override survives module unload, unlike the in-module latch. + let marker = self.retirement.lock().take() + .expect("retirement reserved before CPU handoff"); + unsafe { marker.install(self.dev.as_raw()) }; + for child in &mut children { + child.retain_context(); } RETIRED.store(true, Release); dev_err!( @@ -2187,20 +2354,18 @@ impl rtkit::Operations for AopData { } } -/// The driver data of the AOP. Children reach the `Arc` through -/// `AOP::from_child()`, which relies on this being `repr(transparent)` over -/// it. +/// The provider's owned driver data. Children use their own service context. #[repr(transparent)] struct AopDriver(Arc); struct AopHwConfig { - ec0p: u64, - alig: u64, - aopt: u64, - /// Complete the firmware's boot arguments before starting it. - patch_bootargs: bool, + /// Required scalar overrides before startup; an empty profile leaves + /// the firmware's boot arguments untouched. + bootarg_overrides: &'static [(u32, u64)], /// The firmware speaks EPIC with version 4 sub-headers. epic_v4: bool, + /// Child configuration ABI, qualified separately from EPIC framing. + service_abi: ServiceABI, /// The firmware boots through a second, "setup", mailbox as well. setup_port: bool, /// Shut the co-processor down on unbind and retain its buffers if the @@ -2211,41 +2376,53 @@ struct AopHwConfig { } const HW_CFG_T8103: AopHwConfig = AopHwConfig { - ec0p: 0x020000, - aopt: 1, - alig: 128, - patch_bootargs: true, + bootarg_overrides: &[ + (from_fourcc(b"EC0p"), 0x020000), + (from_fourcc(b"nCal"), 0), + (from_fourcc(b"alig"), 128), + (from_fourcc(b"AOPt"), 1), + ], epic_v4: false, + service_abi: ServiceABI::Legacy, setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], }; const HW_CFG_T8112: AopHwConfig = AopHwConfig { - ec0p: 0x020000, - aopt: 0, - alig: 128, - patch_bootargs: true, + bootarg_overrides: &[ + (from_fourcc(b"EC0p"), 0x020000), + (from_fourcc(b"nCal"), 0), + (from_fourcc(b"alig"), 128), + (from_fourcc(b"AOPt"), 0), + ], epic_v4: false, + service_abi: ServiceABI::Legacy, setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], }; const HW_CFG_T6000: AopHwConfig = AopHwConfig { - ec0p: 0x020000, - aopt: 0, - alig: 64, - patch_bootargs: true, + bootarg_overrides: &[ + (from_fourcc(b"EC0p"), 0x020000), + (from_fourcc(b"nCal"), 0), + (from_fourcc(b"alig"), 64), + (from_fourcc(b"AOPt"), 0), + ], epic_v4: false, + service_abi: ServiceABI::Legacy, setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], }; const HW_CFG_T6020: AopHwConfig = AopHwConfig { - ec0p: 0x0100_00000000, - aopt: 0, - alig: 64, - patch_bootargs: true, + bootarg_overrides: &[ + (from_fourcc(b"EC0p"), 0x0100_00000000), + (from_fourcc(b"nCal"), 0), + (from_fourcc(b"alig"), 64), + (from_fourcc(b"AOPt"), 0), + ], epic_v4: false, + service_abi: ServiceABI::Legacy, setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], @@ -2256,11 +2433,9 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { /// aop-audio, 0x22 aop-voicetrigger, 0x23 als and 0x2b aop-audprov; the rest /// are started if they will. const HW_CFG_T8140: AopHwConfig = AopHwConfig { - ec0p: 0, - aopt: 0, - alig: 0, - patch_bootargs: false, + bootarg_overrides: &[], epic_v4: true, + service_abi: ServiceABI::T8140, setup_port: true, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], @@ -2289,6 +2464,15 @@ impl platform::Driver for AopDriver { info: Option<&Self::IdInfo>, ) -> impl PinInit { let cfg = info.ok_or(ENODEV)?; + // A legacy fallback compatible must not authorize the incompatible + // PDM/channel writers, setup port or EPIC-v4 transport on J616s. + // SAFETY: The machine-compatible string is static and NUL-terminated. + if unsafe { bindings::of_machine_is_compatible(c_str!("apple,j616s").as_char_ptr()) } != 0 + && (!matches!(cfg.service_abi, ServiceABI::ControlOnly | ServiceABI::J616sJack) + || cfg.epic_v4 || cfg.setup_port) + { + return Err(ENODEV); + } if RETIRED.load(Acquire) { dev_err!( pdev.as_ref(), @@ -2306,16 +2490,8 @@ impl platform::Driver for AopDriver { // 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.access(pdev.as_ref())?; - if cfg.patch_bootargs { - data.patch_bootargs( - aop_mmio, - &[ - (from_fourcc(b"EC0p"), cfg.ec0p), - (from_fourcc(b"nCal"), 0x0), - (from_fourcc(b"alig"), cfg.alig), - (from_fourcc(b"AOPt"), cfg.aopt), - ], - )?; + if !cfg.bootarg_overrides.is_empty() { + data.patch_bootargs(aop_mmio, cfg.bootarg_overrides)?; } let rtkit = rtkit::RtKit::::new(pdev.as_ref(), None, 0, data.clone())?; *data.rtkit.lock() = Some(rtkit); @@ -2339,6 +2515,8 @@ impl platform::Driver for AopDriver { } else { data.start()?; } + data.probe_complete.store(true, Release); + data.service_gate.ready(); probe_guard.dismiss(); let data = data as Arc; Ok(Self(data)) diff --git a/drivers/soc/apple/aop_bootargs.rs b/drivers/soc/apple/aop_bootargs.rs new file mode 100644 index 0000000000000..a283d436330d1 --- /dev/null +++ b/drivers/soc/apple/aop_bootargs.rs @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Validation and scalar overrides for the AOP's boot-argument records. + +use core::ops::Range; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum Error { + TruncatedHeader, + TruncatedValue, + DuplicateOverride(u32), + MissingField(u32), + DuplicateField(u32), + InvalidScalarSize(u32), + ValueTooLarge(u32), +} + +fn entry(bytes: &[u8], offset: usize) -> Result<(u32, Range), Error> { + let data_start = offset.checked_add(8).ok_or(Error::TruncatedHeader)?; + let header = bytes + .get(offset..data_start) + .ok_or(Error::TruncatedHeader)?; + let key = u32::from_le_bytes(header[..4].try_into().unwrap()); + let size = u32::from_le_bytes(header[4..].try_into().unwrap()) as usize; + let data_end = data_start.checked_add(size).ok_or(Error::TruncatedValue)?; + bytes + .get(data_start..data_end) + .ok_or(Error::TruncatedValue)?; + Ok((key, data_start..data_end)) +} + +/// Validate the complete record layout and every required override first. +/// +/// Unknown fields retain their original bytes. Each overridden field must +/// appear exactly once and hold a nonempty scalar of at most eight bytes. +/// The value must fit its recorded width, rather than silently truncating. +/// An error leaves the buffer unchanged. +pub(super) fn patch(bytes: &mut [u8], overrides: &[(u32, u64)]) -> Result<(), Error> { + if overrides.is_empty() { + return Ok(()); + } + + for (index, &(key, value)) in overrides.iter().enumerate() { + if overrides[..index].iter().any(|&(other, _)| other == key) { + return Err(Error::DuplicateOverride(key)); + } + + let mut offset = 0; + let mut found = false; + while offset < bytes.len() { + let (field, data) = entry(bytes, offset)?; + offset = data.end; + if field != key { + continue; + } + if found { + return Err(Error::DuplicateField(key)); + } + if !(1..=8).contains(&data.len()) { + return Err(Error::InvalidScalarSize(key)); + } + if value.to_le_bytes()[data.len()..] + .iter() + .any(|&byte| byte != 0) + { + return Err(Error::ValueTooLarge(key)); + } + found = true; + } + if !found { + return Err(Error::MissingField(key)); + } + } + + let mut offset = 0; + while offset < bytes.len() { + // The immutable validation passes checked every record; writes below + // modify only payload bytes, leaving the keys and lengths unchanged. + let (key, data) = entry(bytes, offset).unwrap(); + offset = data.end; + if let Some(&(_, value)) = overrides.iter().find(|&&(field, _)| field == key) { + bytes[data.clone()].copy_from_slice(&value.to_le_bytes()[..data.len()]); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const KEY: u32 = 0x45433070; + const OTHER: u32 = 0x616c6967; + + fn record(key: u32, data: &[u8]) -> Vec { + let mut bytes = key.to_le_bytes().to_vec(); + bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); + bytes.extend_from_slice(data); + bytes + } + + fn rejects_without_mutation(mut bytes: Vec, overrides: &[(u32, u64)], error: Error) { + let original = bytes.clone(); + assert_eq!(patch(&mut bytes, overrides), Err(error)); + assert_eq!(bytes, original); + } + + #[test] + fn one_override_preserves_other_payloads_and_record_headers() { + let mut bytes = record(KEY, &[0xaa; 8]); + let other = record(OTHER, &[0xbb; 12]); + bytes.extend_from_slice(&other); + let expected_header = bytes[..8].to_vec(); + patch(&mut bytes, &[(KEY, 0x0100_00000000)]).unwrap(); + assert_eq!(&bytes[..8], expected_header); + assert_eq!(&bytes[8..16], &0x0100_00000000u64.to_le_bytes()); + assert_eq!(&bytes[16..], other); + } + + #[test] + fn late_truncated_record_does_not_patch_an_earlier_field() { + let mut bytes = record(KEY, &[0xaa; 8]); + bytes.extend_from_slice(&[1, 2, 3]); + rejects_without_mutation(bytes, &[(KEY, 1)], Error::TruncatedHeader); + } + + #[test] + fn oversized_record_length_is_rejected() { + let mut bytes = KEY.to_le_bytes().to_vec(); + bytes.extend_from_slice(&u32::MAX.to_le_bytes()); + rejects_without_mutation(bytes, &[(KEY, 1)], Error::TruncatedValue); + } + + #[test] + fn missing_later_override_does_not_patch_an_earlier_field() { + rejects_without_mutation( + record(KEY, &[0xaa; 8]), + &[(KEY, 1), (OTHER, 2)], + Error::MissingField(OTHER), + ); + } + + #[test] + fn duplicate_fields_and_overrides_are_rejected() { + let mut bytes = record(KEY, &[0xaa; 8]); + bytes.extend_from_slice(&record(KEY, &[0xbb; 8])); + rejects_without_mutation(bytes, &[(KEY, 1)], Error::DuplicateField(KEY)); + rejects_without_mutation( + record(KEY, &[0xaa; 8]), + &[(KEY, 1), (KEY, 2)], + Error::DuplicateOverride(KEY), + ); + } + + #[test] + fn overridden_scalar_width_is_checked() { + for size in [0, 9] { + rejects_without_mutation( + record(KEY, &vec![0xaa; size]), + &[(KEY, 1)], + Error::InvalidScalarSize(KEY), + ); + } + } + + #[test] + fn value_cannot_be_truncated_to_a_narrow_field() { + rejects_without_mutation( + record(KEY, &[0xaa; 4]), + &[(KEY, 0x0100_00000000)], + Error::ValueTooLarge(KEY), + ); + } + + #[test] + fn scalar_widths_encode_little_endian() { + for size in 1..=8 { + let value = if size == 8 { + u64::MAX + } else { + (1u64 << (size * 8)) - 1 + }; + let mut bytes = record(KEY, &vec![0xaa; size]); + patch(&mut bytes, &[(KEY, value)]).unwrap(); + assert_eq!(&bytes[8..], &value.to_le_bytes()[..size]); + } + } + + #[test] + fn unknown_empty_or_large_fields_are_unchanged() { + let mut bytes = record(OTHER, &[]); + bytes.extend_from_slice(&record(OTHER, &[0xbb; 16])); + let prefix = bytes.clone(); + bytes.extend_from_slice(&record(KEY, &[0xaa; 8])); + patch(&mut bytes, &[(KEY, 1)]).unwrap(); + assert_eq!(&bytes[..prefix.len()], prefix); + } + + #[test] + fn empty_profile_does_not_touch_or_parse_bootargs() { + let mut bytes = vec![0xaa; 3]; + patch(&mut bytes, &[]).unwrap(); + assert_eq!(bytes, vec![0xaa; 3]); + } +} diff --git a/drivers/soc/apple/aop_shutdown.rs b/drivers/soc/apple/aop_shutdown.rs new file mode 100644 index 0000000000000..2917dc22f339c --- /dev/null +++ b/drivers/soc/apple/aop_shutdown.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Shared-buffer release requires a completed probe and a confirmed stop. + +/// Dispose of the RTKit handle, retaining its buffers on missing stop proof. +/// +/// A failed/partial startup must not call shutdown as a fallback: successful +/// RTKit shutdown runs reinit, which frees shared buffers before handle drop. +/// The callbacks close/drain the handle even when its shared buffers survive. +pub(super) fn release_rtkit( + rtkit: Option, + cpu_started: bool, + probe_complete: bool, + stopped_endpoints: Option, + require_power_ack: bool, + shutdown: impl FnOnce(&mut R) -> bool, + retain: impl FnOnce(&mut R), +) -> bool { + let mut confirmed = + stopped_endpoints.is_some_and(|count| !cpu_started || (probe_complete && count != 0)); + // AFK ACKs alone do not stop the RTKit system endpoints. On legacy + // profiles which do not request power quiescence, their shared buffers + // remain firmware-visible until reboot even after the apps have stopped. + if cpu_started && !require_power_ack { + confirmed = false; + } + if let Some(mut rtkit) = rtkit { + if confirmed && cpu_started && require_power_ack { + confirmed = shutdown(&mut rtkit); + } + if !confirmed { + retain(&mut rtkit); + } + drop(rtkit); + } else if cpu_started { + // The CPU was handed ownership, but there is no handle to confirm + // the stop or close its callbacks. Keep the caller's buffers too. + confirmed = false; + } + confirmed +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{cell::RefCell, rc::Rc}; + + struct Peer { + events: Rc>>, + live: bool, + retained: bool, + power_ack: bool, + } + + impl Drop for Peer { + fn drop(&mut self) { + self.events.borrow_mut().push("callbacks closed"); + if self.live { + self.events.borrow_mut().push(if self.retained { + "buffers retained" + } else { + "buffers freed" + }); + } + } + } + + fn run( + started: bool, + complete: bool, + endpoints: Option, + power: bool, + ack: bool, + ) -> (bool, Vec<&'static str>) { + let events = Rc::new(RefCell::new(Vec::new())); + let peer = Peer { + events: events.clone(), + live: true, + retained: false, + power_ack: ack, + }; + let release = release_rtkit( + Some(peer), + started, + complete, + endpoints, + power, + |peer| { + peer.events.borrow_mut().push("power shutdown"); + if peer.power_ack { + // This models the C shutdown's successful reinit. + peer.events.borrow_mut().push("reinit freed buffers"); + peer.live = false; + true + } else { + false + } + }, + |peer| { + peer.retained = true; + }, + ); + let trace = events.borrow().clone(); + (release, trace) + } + + #[test] + fn failed_afk_stop_never_calls_freeing_power_fallback() { + let (release, events) = run(true, true, None, true, true); + assert!(!release); + assert_eq!(events, ["callbacks closed", "buffers retained"]); + } + + #[test] + fn partial_probe_retains_even_if_some_endpoints_acknowledged() { + let (release, events) = run(true, false, Some(2), true, true); + assert!(!release); + assert_eq!(events, ["callbacks closed", "buffers retained"]); + } + + #[test] + fn no_endpoint_proof_cannot_release_a_running_processor() { + let (release, events) = run(true, true, Some(0), false, false); + assert!(!release); + assert_eq!(events, ["callbacks closed", "buffers retained"]); + } + + #[test] + fn failed_power_ack_retains_before_handle_drop() { + let (release, events) = run(true, true, Some(2), true, false); + assert!(!release); + assert_eq!( + events, + ["power shutdown", "callbacks closed", "buffers retained"] + ); + } + + #[test] + fn confirmed_power_shutdown_preserves_the_reinit_release_path() { + let (release, events) = run(true, true, Some(2), true, true); + assert!(release); + assert_eq!( + events, + ["power shutdown", "reinit freed buffers", "callbacks closed"] + ); + } + + #[test] + fn legacy_afk_stop_without_power_proof_keeps_system_buffers() { + let (release, events) = run(true, true, Some(2), false, false); + assert!(!release); + assert_eq!(events, ["callbacks closed", "buffers retained"]); + } + + #[test] + fn failure_before_cpu_handoff_releases_unpublished_buffers() { + let (release, events) = run(false, false, Some(0), true, false); + assert!(release); + assert_eq!(events, ["callbacks closed", "buffers freed"]); + } + + #[test] + fn missing_handle_retains_caller_buffers_after_cpu_handoff() { + assert!(!release_rtkit::<()>( + None, + true, + true, + Some(2), + false, + |_| true, + |_| {} + )); + assert!(release_rtkit::<()>( + None, + false, + false, + Some(0), + false, + |_| true, + |_| {} + )); + } +} diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index eb0da50d092a2..fac737ae952f7 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -94,6 +94,12 @@ bool apple_rtkit_is_running(struct apple_rtkit *rtk) } EXPORT_SYMBOL_GPL(apple_rtkit_is_running); +unsigned int apple_rtkit_protocol_version(struct apple_rtkit *rtk) +{ + return READ_ONCE(rtk->version); +} +EXPORT_SYMBOL_GPL(apple_rtkit_protocol_version); + bool apple_rtkit_is_crashed(struct apple_rtkit *rtk) { return rtk->crashed; diff --git a/include/linux/device.h b/include/linux/device.h index 79a4a6549be8e..6c4348be45537 100644 --- a/include/linux/device.h +++ b/include/linux/device.h @@ -800,6 +800,40 @@ struct device_link { int __device_set_driver_override(struct device *dev, const char *s, size_t len); +/** + * __device_prepare_driver_override() - Reserve a driver override before exposure + * @s: NUL-terminated driver name, or an empty string to clear an override + * @len: Length limit, as for __device_set_driver_override() + * + * Returns an owned kstrndup allocation, NULL for an empty override, or an + * ERR_PTR. Install the prepared value with __device_install_driver_override() + * or release it with kfree(). This permits failure paths to retire DMA owners + * without depending on an allocation after the device has started DMA. + */ +const char *__device_prepare_driver_override(const char *s, size_t len); + +/** + * __device_install_driver_override() - Transfer a prepared override to a device + * @dev: Device whose override is replaced + * @name: Owned allocation from __device_prepare_driver_override(), or NULL + * + * Takes ownership of @name and frees the previous override. Installation uses + * the override lock and cannot fail or allocate. Do not pass an ERR_PTR. + */ +void __device_install_driver_override(struct device *dev, const char *name); + +/** + * __device_retire_driver() - Install a reserved barrier and unbind under one lock + * @dev: Registered device, with consumer lookup admission already closed + * @name: Owned allocation from __device_prepare_driver_override() + * + * Takes the locks used by driver attach, installs @name without allocation, + * and releases the current driver before unlocking. A previously matched + * attach can still enter probe after this helper; its lookup must reject + * closed admission rather than borrowing released provider driver data. + */ +void __device_retire_driver(struct device *dev, const char *name); + /** * device_set_driver_override() - Helper to set or clear driver override. * @dev: Device to change diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index e4b38a8f6e1ba..62b2d8230caf2 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -202,6 +202,9 @@ int apple_rtkit_idle(struct apple_rtkit *rtk); */ bool apple_rtkit_is_running(struct apple_rtkit *rtk); +/* Negotiated protocol version; zero until a HELLO has been accepted. */ +unsigned int apple_rtkit_protocol_version(struct apple_rtkit *rtk); + /* * Checks if RTKit has crashed. */ diff --git a/rust/helpers/of.c b/rust/helpers/of.c index da4ae35163c6f..d28cf9b838bf1 100644 --- a/rust/helpers/of.c +++ b/rust/helpers/of.c @@ -3,6 +3,11 @@ #include #include +__rust_helper int rust_helper_of_machine_is_compatible(const char *compatible) +{ + return of_machine_is_compatible(compatible); +} + __rust_helper bool rust_helper_is_of_node(const struct fwnode_handle *fwnode) { return is_of_node(fwnode); diff --git a/rust/kernel/soc/apple/aop.rs b/rust/kernel/soc/apple/aop.rs index d49146db2c71a..9b28f03ae7c80 100644 --- a/rust/kernel/soc/apple/aop.rs +++ b/rust/kernel/soc/apple/aop.rs @@ -5,12 +5,12 @@ use kernel::{ device::{Bound, Device}, dma::Coherent, + new_mutex, prelude::*, sync::{ atomic::{Atomic, Relaxed}, - Arc, // + Arc, Mutex, // }, - types::ForeignOwnable, }; /// Representation of an "EPIC" service. @@ -23,6 +23,107 @@ pub struct EPICService { pub endpoint: u8, } +/// Child protocols qualified by a hardware profile, independently of EPIC framing. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum ServiceABI { + /// Discover firmware services without exposing a child that can configure them. + ControlOnly, + /// The existing PDM-upload microphone child. + Legacy, + /// T8140 setup-port and version-4 audio child. + T8140, + /// J616s EPIC-v2 jack control with retained firmware MCA profiles. + J616sJack, +} + +impl ServiceABI { + /// Control discovery is independent of child configuration qualification. + pub fn publishes_children(self) -> bool { + self != Self::ControlOnly + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ServiceAdmission { + Probing, + Ready, + Closed, +} + +/// Shared admission gate; closing covers existing and not-yet-registered children. +#[pin_data] +pub struct ServiceGate { + #[pin] + admission: Mutex, +} + +impl ServiceGate { + /// Allocate before the provider hands ownership to hardware. + pub fn new() -> Result> { + Arc::pin_init( + pin_init!(Self { + admission <- new_mutex!(ServiceAdmission::Probing), + }), + GFP_KERNEL, + ) + } + + /// Clone a safely owned provider while lookup admission remains open. + fn acquire(&self, provider: &Arc) -> Result> { + let admission = self.admission.lock(); + match *admission { + ServiceAdmission::Probing => Err(EPROBE_DEFER), + ServiceAdmission::Closed => Err(ENODEV), + ServiceAdmission::Ready => Ok(provider.clone()), + } + } + + /// The provider completed startup; do not reopen a retired context. + pub fn ready(&self) { + let mut admission = self.admission.lock(); + if *admission == ServiceAdmission::Probing { + *admission = ServiceAdmission::Ready; + } + } + + /// Wait for pending lookups to finish cloning, then refuse all new ones. + pub fn close(&self) { + *self.admission.lock() = ServiceAdmission::Closed; + } +} + +/// Stable child-owned context, independent of the parent's driver-data box. +#[pin_data] +pub struct ServiceContext { + provider: Arc, + gate: Arc, +} + +impl ServiceContext { + /// Allocate before child registration can probe or publish a DMA address. + pub fn new(provider: Arc, gate: Arc) -> Result>> { + KBox::pin_init(pin_init!(Self { provider, gate }), GFP_KERNEL) + } + + /// Cloning is synchronized with provider closing; returned references are owned. + pub fn acquire(&self) -> Result> { + self.gate.acquire(&self.provider) + } +} + +/// Copied into a platform child's data. The service prefix preserves the +/// existing service reader layout; context storage is owned by that child. +#[repr(C)] +pub struct ServicePlatformData { + /// Firmware service, not a provider driver-data pointer. + pub service: EPICService, + /// Stable until unregister; retained closed on unconfirmed shutdown. + pub context: *const ServiceContext, + /// The hardware profile's child protocol, not inferred from DT or EPIC framing. + pub abi: ServiceABI, +} + /// Listener for the "HID" events sent by aop. /// /// The AOP driver keeps the listener in an [`Arc`] shared with the child driver that registered @@ -93,6 +194,10 @@ impl SourceRing { /// AOP communications manager. pub trait AOP: Send + Sync { + /// Negotiated RTKit protocol, after startup; no register access is performed. + fn protocol_version(&self) -> Result { + Err(ENODEV) + } /// Calls a method on a specified service fn epic_call(&self, svc: &EPICService, subtype: u16, msg_bytes: &[u8]) -> Result; /// Just like epic_call, but also returns a value @@ -136,29 +241,45 @@ pub trait AOP: Send + Sync { } impl dyn AOP { + /// Looks up an audio provider only when its hardware profile allows this ABI. + /// + /// Checking this before any attach/property write also protects against a + /// stale or incorrect child DT compatible. EPIC v2 alone does not qualify + /// the legacy PDM upload and low-power channel setters. + /// + /// # Safety + /// + /// The same platform-child and lookup lifetime contract as [`Self::from_child`]. + pub unsafe fn audio_from_child(child: &Device, abi: ServiceABI) -> Result> { + // SAFETY: The core owns the copied immutable platform payload during probe/unbind. + let data = unsafe { (*child.as_raw()).platform_data.cast::().as_ref() } + .ok_or(ENODEV)?; + if abi == ServiceABI::ControlOnly || data.abi != abi { + return Err(ENODEV); + } + // SAFETY: Caller supplies the same registered AOP child as above. + unsafe { Self::from_child(child) } + } + /// Returns the AOP core that registered the service device `child`. /// - /// The core registers its service devices from a workqueue while it is still probing, and the - /// driver core publishes a driver's data only once its probe has returned, so a child can be - /// probed before the core's data exists. This returns `EPROBE_DEFER` in that case; the child's - /// probe is retried once the core has bound. + /// Initial lookups defer while the provider is probing. Closing synchronizes + /// pending lookups before unbind; admitted callers own an Arc independent + /// of parent drvdata, and retained children refuse all later lookups. /// /// # Safety /// /// `child` must be a platform device that the AOP core driver registered for one of its - /// services, and the caller must be probing it or be bound to it. The core unregisters its - /// children before it releases its driver data, so that data is valid for the lookup. + /// services, and the caller must be probing it or bound to it. Its platform + /// data/context remain alive until unregister serializes all child probes. pub unsafe fn from_child(child: &Device) -> Result> { - let parent = child.parent().ok_or(ENODEV)?; - let ptr = parent.get_drvdata::(); - if ptr.is_null() { - return Err(EPROBE_DEFER); - } - // SAFETY: By this function's contract `ptr` is the AOP core's driver data: a pinned box - // holding the core's driver type, which is `#[repr(transparent)]` over an `Arc`, - // and it stays valid while `child` is registered. - let aop = unsafe { Pin::>>::borrow(ptr) }; - Ok((*aop).clone()) + // SAFETY: The platform data has the layout/lifetime in this contract. + let data = unsafe { (*child.as_raw()).platform_data.cast::().as_ref() } + .ok_or(ENODEV)?; + // SAFETY: The child owns this stable context until unregister, or + // retains it permanently closed together with its DMA domain. + let context = unsafe { data.context.as_ref() }.ok_or(ENODEV)?; + context.acquire() } } diff --git a/rust/kernel/soc/apple/rtkit.rs b/rust/kernel/soc/apple/rtkit.rs index ab3fc6c33fe5d..07174b1c1a062 100644 --- a/rust/kernel/soc/apple/rtkit.rs +++ b/rust/kernel/soc/apple/rtkit.rs @@ -282,6 +282,12 @@ impl RtKit { self.retain_shared_buffers = true; } + /// The accepted HELLO version, or zero before negotiation. + pub fn protocol_version(&self) -> u32 { + // SAFETY: The handle is valid and the C accessor uses READ_ONCE. + unsafe { bindings::apple_rtkit_protocol_version(self.rtk) } + } + /// Boots (wakes up) the RTKit coprocessor. pub fn wake(self: Pin<&mut Self>) -> Result { // SAFETY: `rtk` is valid per the type invariant. diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index a57eadd07a1ba..9b7bebe6316fa 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -1,6 +1,17 @@ # SPDX-License-Identifier: GPL-2.0-only menu "Apple" +config SND_SOC_APPLE_J616S_AOP_JACK + tristate "J616s retained-profile AOP jack control probe" + depends on ARCH_APPLE || COMPILE_TEST + depends on RUST + select APPLE_AOP + help + Inspect the qualified J616s jack service and retained MCA profiles. + Accept only the matching board, firmware and EPIC transport identity. + This control probe does not power an audio path or expose a PCM. + Streaming requires separate DMA and shared-clock lifecycle admission. + config SND_SOC_APPLE_AOP_AUDIO tristate "AOP audio driver" depends on ARCH_APPLE || COMPILE_TEST diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index df3c2264f6279..2b9c081debdeb 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -6,6 +6,9 @@ obj-$(CONFIG_SND_SOC_APPLE_AOP_AUDIO) += snd-soc-aop.o snd-soc-t8140-aop-audio-y := t8140_aop_audio.o obj-$(CONFIG_SND_SOC_APPLE_T8140_AOP_AUDIO) += snd-soc-t8140-aop-audio.o +snd-soc-j616s-aop-jack-y := j616s_aop_jack.o +obj-$(CONFIG_SND_SOC_APPLE_J616S_AOP_JACK) += snd-soc-j616s-aop-jack.o + snd-soc-apple-mca-y := mca.o obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o diff --git a/sound/soc/apple/aop_audio.rs b/sound/soc/apple/aop_audio.rs index 6cb974a82e785..2f13596b30341 100644 --- a/sound/soc/apple/aop_audio.rs +++ b/sound/soc/apple/aop_audio.rs @@ -971,9 +971,12 @@ impl platform::Driver for SndSocAopDriver { let info = info.ok_or(ENODEV)?; let dev = ARef::::from(pdev.as_ref()); let parent = pdev.as_ref().parent().unwrap(); - // SAFETY: our parent is AOP, and AopDriver is repr(transparent) for Arc - let adata_ptr = unsafe { Pin::>>::borrow(parent.get_drvdata()) }; - let adata = (&*adata_ptr).clone(); + // SAFETY: This is a service child registered by the AOP provider. + // A retired child can remain registered after its parent unbinds; + // the checked lookup defers rather than borrowing null driver data. + let adata = unsafe { + ::audio_from_child(dev.as_ref(), kernel::soc::apple::aop::ServiceABI::Legacy)? + }; // SAFETY: AOP sets the platform data correctly let svc = unsafe { *((*dev.as_raw()).platform_data as *const EPICService) }; let parent_fwnode = parent.fwnode().ok_or(ENOENT)?; diff --git a/sound/soc/apple/j616s_aop_jack.rs b/sound/soc/apple/j616s_aop_jack.rs new file mode 100644 index 0000000000000..b9216dee53b56 --- /dev/null +++ b/sound/soc/apple/j616s_aop_jack.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! J616s retained-profile jack control probe. +//! +//! This child reads the qualified EPIC-v2 service without attaching a device, +//! uploading coefficients, powering a path, allocating DMA or registering a +//! PCM. Its provider and DMA lifecycle must be qualified before streaming. + +#[path = "j616s_jack_profile.rs"] +mod profile; + +use kernel::{ + bindings, c_str, + device::Core, + module_platform_driver, of, platform, + prelude::*, + soc::apple::aop::{EPICService, ServiceABI, ServicePlatformData, AOP}, + str::CString, + sync::Arc, +}; +use profile::Route; + +struct J616sJack { + // Owned independently of provider drvdata; no teardown request is needed + // because this child never changes firmware or publishes a DMA address. + _provider: Arc, + _service: EPICService, +} + +fn get_property( + provider: &dyn AOP, + service: &EPICService, + route: Route, + property: u32, + bytes: usize, +) -> Result> { + let request = profile::get_request(route, property); + // Capture one extra byte so transport truncation cannot hide a long reply. + let (retcode, reply) = provider.epic_call_ret(service, 0x20, &request, bytes + 5)?; + if retcode != 0 || profile::property_data(&reply, bytes).is_none() { + return Err(EIO); + } + Ok(reply) +} + +kernel::of_device_table!( + OF_TABLE, + MODULE_OF_TABLE, + ::IdInfo, + [(of::DeviceId::new(c_str!("apple,j616s-aop-jack")), ())] +); + +impl platform::Driver for J616sJack { + type IdInfo = (); + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe(pdev: &platform::Device, _info: Option<&()>) -> impl PinInit { + // SAFETY: The compatible string is static and NUL-terminated. + if unsafe { bindings::of_machine_is_compatible(c_str!("apple,j616s").as_char_ptr()) } == 0 { + return Err(ENODEV); + } + let node = pdev.as_ref().fwnode().ok_or(ENODEV)?; + // SAFETY: The node reference stays alive for the call. Manual bind + // cannot bypass the loader's explicit inactive child status. + if !unsafe { bindings::fwnode_device_is_available(node.as_raw()) } { + return Err(ENODEV); + } + let uuid = node + .property_read::(c_str!("apple,firmware-uuid")) + .required_by(pdev.as_ref())?; + let name = node + .property_read::(c_str!("apple,firmware-name")) + .required_by(pdev.as_ref())?; + // SAFETY: Only the AOP provider creates this platform child. ABI and + // admission are checked before any firmware request is issued. + let provider = + unsafe { ::audio_from_child(pdev.as_ref(), ServiceABI::J616sJack) }?; + // SAFETY: The provider owns the copied platform data during probe. + let payload = unsafe { + (*pdev.as_ref().as_raw()) + .platform_data + .cast::() + .as_ref() + } + .ok_or(ENODEV)?; + let service = payload.service; + if !profile::identity_matches( + uuid.to_bytes(), + name.to_bytes(), + provider.protocol_version()?, + service.endpoint, + ) { + return Err(ENODEV); + } + for route in [Route::Cout, Route::Cin] { + let state = get_property(provider.as_ref(), &service, route, 200, 4)?; + if !profile::idle_reply(&state) { + return Err(EBUSY); + } + let mca = get_property(provider.as_ref(), &service, route, 701, 109)?; + if !profile::profile_reply(route, &mca) { + return Err(ENODEV); + } + } + // Recheck both shared-clock users after all profile reads. This is + // admission for control inspection, not concurrent PCM qualification. + for route in [Route::Cout, Route::Cin] { + let state = get_property(provider.as_ref(), &service, route, 200, 4)?; + if !profile::idle_reply(&state) { + return Err(EBUSY); + } + dev_info!( + pdev.as_ref(), + "{:?}: retained profile, nominal 48000 Hz, {} channels, ADMAC channel {}", + route.device(), + route.channels(), + route.dma_channel() + ); + } + Ok(Self { + _provider: provider, + _service: service, + }) + } +} + +module_platform_driver! { + type: J616sJack, + name: "snd_soc_j616s_aop_jack", + description: "J616s retained-profile AOP jack control probe", + license: "Dual MIT/GPL", +} diff --git a/sound/soc/apple/j616s_jack_profile.rs b/sound/soc/apple/j616s_jack_profile.rs new file mode 100644 index 0000000000000..a485336e7825f --- /dev/null +++ b/sound/soc/apple/j616s_jack_profile.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Exact retained J616s jack profiles; none of these bytes are uploaded. + +pub(crate) const FIRMWARE_UUID: &[u8] = b"5FCD028B-6AAA-3181-8735-3924DB85AD23"; +pub(crate) const FIRMWARE_NAME: &[u8] = b"mac16jaop"; + +pub(crate) fn identity_matches(uuid: &[u8], name: &[u8], version: u32, endpoint: u8) -> bool { + uuid == FIRMWARE_UUID && name == FIRMWARE_NAME && version == 12 && endpoint == 0x22 +} + +pub(crate) const COUT: [u8; 109] = [ + 0x20, 0x53, 0x6f, 0x63, 0x32, 0x6b, 0x63, 0x6d, 0x6f, 0x63, 0x6e, 0x61, 0x00, 0xe0, 0x2e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x32, 0x67, 0x6b, 0x63, 0x32, 0x6b, 0x63, 0x6d, 0x01, 0x04, 0x00, 0x00, + 0x00, 0x32, 0x6e, 0x79, 0x73, 0x32, 0x67, 0x6b, 0x63, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x3f, + 0x00, 0x00, 0x00, 0x32, 0x30, 0x73, 0x6d, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6c, 0x6c, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub(crate) const CIN: [u8; 109] = [ + 0x20, 0x53, 0x69, 0x63, 0x32, 0x6b, 0x63, 0x6d, 0x6f, 0x63, 0x6e, 0x61, 0x00, 0xe0, 0x2e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x32, 0x67, 0x6b, 0x63, 0x32, 0x6b, 0x63, 0x6d, 0x01, 0x04, 0x00, 0x00, + 0x00, 0x32, 0x6e, 0x79, 0x73, 0x32, 0x67, 0x6b, 0x63, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x3f, + 0x00, 0x00, 0x00, 0x6c, 0x6c, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x32, 0x30, 0x73, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Route { + Cout, + Cin, +} + +impl Route { + pub(crate) fn device(self) -> [u8; 4] { + match self { + Self::Cout => *b"cout", + Self::Cin => *b"cin ", + } + } + pub(crate) fn profile(self) -> &'static [u8; 109] { + match self { + Self::Cout => &COUT, + Self::Cin => &CIN, + } + } + pub(crate) fn channels(self) -> u32 { + match self { + Self::Cout => 2, + Self::Cin => 1, + } + } + pub(crate) fn dma_channel(self) -> u32 { + match self { + Self::Cout => 4, + Self::Cin => 5, + } + } +} + +/// Exact GetDeviceProp wire record from the qualified EPIC-v2 service. +pub(crate) fn get_request(route: Route, property: u32) -> [u8; 52] { + let mut request = [0; 52]; + request[4..8].copy_from_slice(&u32::MAX.to_le_bytes()); + request[8..12].copy_from_slice(&0xc3000004u32.to_le_bytes()); + request[32..40].copy_from_slice(&0x30u64.to_le_bytes()); + let mut device = route.device(); + device.reverse(); + request[40..44].copy_from_slice(&device); + request[44..48].copy_from_slice(&property.to_le_bytes()); + request[48..52].copy_from_slice(&1u32.to_le_bytes()); + request +} + +/// The transport has removed the retcode. Refuse short, long or inconsistent replies. +pub(crate) fn property_data(reply: &[u8], expected: usize) -> Option<&[u8]> { + if reply.len() != expected.checked_add(4)? { + return None; + } + let length = u32::from_le_bytes(reply[..4].try_into().ok()?) as usize; + (length == expected).then_some(&reply[4..]) +} + +/// Idle admission never powers or reconfigures the shared jack clock. +pub(crate) fn idle_reply(reply: &[u8]) -> bool { + property_data(reply, 4) == Some(b"eldi".as_slice()) +} + +/// Retained coS/cin profiles are compared byte for byte, including unknown fields. +pub(crate) fn profile_reply(route: Route, reply: &[u8]) -> bool { + property_data(reply, 109) == Some(route.profile().as_slice()) +} diff --git a/sound/soc/apple/t8140_aop_audio.rs b/sound/soc/apple/t8140_aop_audio.rs index 069656e5e6d98..d2814c88fb8ac 100644 --- a/sound/soc/apple/t8140_aop_audio.rs +++ b/sound/soc/apple/t8140_aop_audio.rs @@ -2680,7 +2680,9 @@ impl platform::Driver for SndSocT8140AopDriver { // SAFETY: this is a service device the AOP core registered, being // probed here; the lookup defers the probe until the core has // published its driver data. - let adata = unsafe { ::from_child(pdev.as_ref()) }?; + let adata = unsafe { + ::audio_from_child(pdev.as_ref(), kernel::soc::apple::aop::ServiceABI::T8140) + }?; // SAFETY: the AOP core sets this child's platform data to the EPIC // service it created the child for; it lives as long as the child. let service = unsafe { (*dev.as_raw()).platform_data as *const EPICService }; diff --git a/tools/testing/selftests/drivers/apple/Makefile b/tools/testing/selftests/drivers/apple/Makefile index 76bca086dde7a..21832e960017e 100644 --- a/tools/testing/selftests/drivers/apple/Makefile +++ b/tools/testing/selftests/drivers/apple/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 # Manual physical test: requires user interaction and changes LED brightness. TEST_FILES := j813-peripherals.sh +TEST_PROGS := aop-bootargs.py aop-shutdown.py aop-retirement.py aop-service-race.py firmware-permissions.py j616s-jack-profile.py admac-descriptor-range.py include ../../lib.mk diff --git a/tools/testing/selftests/drivers/apple/admac-descriptor-range.py b/tools/testing/selftests/drivers/apple/admac-descriptor-range.py new file mode 100755 index 0000000000000..eddea06831f7f --- /dev/null +++ b/tools/testing/selftests/drivers/apple/admac-descriptor-range.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Production ADMAC cyclic preparation rejects malformed ranges before allocation.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def function(source, name): + at = source.index(name + "(") + start = source.rfind("\n", 0, at) + 1 + opening = source.index("{", at) + depth, end = 1, opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + cc = os.environ.get("CC", "cc") + if not shutil.which(cc): + print("SKIP: C compiler required") + return 4 + source = (ROOT / "drivers/dma/apple-admac.c").read_text() + functions = "\n".join(function(source, name) for name in ( + "admac_cyclic_range_valid", "admac_prep_dma_cyclic")) + harness = r''' +#include +#include +#include +#include +#include +#define U32_MAX UINT32_MAX +#define DMA_BIT_MASK(n) ((UINT64_C(1) << (n)) - 1) +#define GFP_NOWAIT 0 +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +typedef uint64_t dma_addr_t; +enum dma_transfer_direction { DMA_MEM_TO_DEV, DMA_DEV_TO_MEM }; +struct dma_chan { int unused; }; +struct dma_async_tx_descriptor { + struct dma_chan *chan; + int (*tx_submit)(struct dma_async_tx_descriptor *); + int (*desc_free)(struct dma_async_tx_descriptor *); +}; +struct admac_chan { struct dma_chan chan; int no; }; +struct admac_tx { + struct dma_async_tx_descriptor tx; + bool cyclic; + dma_addr_t buf_addr, buf_end; + size_t buf_len, period_len, submitted_pos, reclaimed_pos; +}; +static int allocations; +static void *allocate(size_t bytes) { allocations++; return calloc(1, bytes); } +#define kzalloc_obj(object, flags) allocate(sizeof(object)) +static enum dma_transfer_direction admac_chan_direction(int channel) +{ return channel & 1 ? DMA_DEV_TO_MEM : DMA_MEM_TO_DEV; } +static int admac_tx_submit(struct dma_async_tx_descriptor *tx) { (void)tx; return 0; } +static int admac_desc_free(struct dma_async_tx_descriptor *tx) { free(tx); return 0; } +static void dma_async_tx_descriptor_init(struct dma_async_tx_descriptor *tx, struct dma_chan *chan) +{ tx->chan = chan; } +FUNCTIONS +int main(void) +{ + struct admac_chan channel = {.no = 4}; /* Qualified base-ns TX2 numbering. */ + const uint64_t edge = UINT64_C(1) << 42; + struct { uint64_t address; size_t bytes, period; bool valid; } cases[] = { + {0x4000, 768000, 384000, true}, + {0x4000, 0x200000, 0x10000, true}, + {edge - 0x4000, 0x4000, 0x4000, true}, + {0x4000, 0, 1, false}, {0x4000, 1, 0, false}, + {0x4000, 10, 3, false}, {0x4000, 4, 8, false}, + {0x4000, UINT64_C(1) << 32, UINT64_C(1) << 32, false}, + {edge, 1, 1, false}, {edge - 4, 8, 4, false}, + {UINT64_MAX - 1, 8, 4, false}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int before = allocations; + struct dma_async_tx_descriptor *descriptor = admac_prep_dma_cyclic( + &channel.chan, cases[i].address, cases[i].bytes, cases[i].period, + DMA_MEM_TO_DEV, 0); + assert(!!descriptor == cases[i].valid); + assert(allocations == before + cases[i].valid); + if (descriptor) { + struct admac_tx *tx = container_of(descriptor, struct admac_tx, tx); + assert(tx->cyclic && tx->buf_addr == cases[i].address); + assert(tx->buf_end == cases[i].address + cases[i].bytes); + assert(tx->period_len == cases[i].period && !tx->submitted_pos && !tx->reclaimed_pos); + descriptor->desc_free(descriptor); + } + } + assert(!admac_prep_dma_cyclic(&channel.chan, 0x4000, 0x4000, 0x4000, DMA_DEV_TO_MEM, 0)); + printf("12 production cyclic preparation cases passed; invalid ranges allocate nothing\n"); + return 0; +} +'''.replace("FUNCTIONS", functions).replace("#include ", "#include \n#include ") + with tempfile.TemporaryDirectory(prefix="admac-descriptor-range-") as directory: + directory = Path(directory) + c, binary = directory / "fixtures.c", directory / "fixtures" + c.write_text(harness) + subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", + str(c), "-o", str(binary)], check=True) + return subprocess.run([str(binary)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/aop-bootargs.py b/tools/testing/selftests/drivers/apple/aop-bootargs.py new file mode 100755 index 0000000000000..e532a1ee25d66 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/aop-bootargs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Host fixtures for the actual AOP bootarg parser and hardware profiles.""" +import os +import json +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def main(): + compiler = os.environ.get("RUSTC", "rustc") + if shutil.which(compiler) is None: + print("SKIP: rustc is required") + return 4 + driver = ROOT / "drivers/soc/apple/aop.rs" + helper = ROOT / "drivers/soc/apple/aop_bootargs.rs" + source = driver.read_text() + start = source.index("struct AopHwConfig {") + end = source.index("kernel::of_device_table!", start) + profiles = source[start:end] + # Use the same FourCC conversion as the kernel provider. + transport = (ROOT / "rust/kernel/soc/apple/aop.rs").read_text() + abi_start = transport.index("pub enum ServiceABI {") + abi_end = transport.index("\n}", abi_start) + 2 + abi = "#[derive(Clone, Copy, PartialEq, Eq)]\n" + transport[abi_start:abi_end] + start = transport.index("pub const fn from_fourcc(") + end = transport.index("\n}", start) + 2 + fourcc = transport[start:end] + + harness = """#![allow(dead_code)] +#[path = {helper}] +mod bootargs; + +{fourcc} +{abi} +{profiles} + +fn record(key: u32, data: &[u8]) -> Vec {{ + let mut bytes = key.to_le_bytes().to_vec(); + bytes.extend_from_slice(&(data.len() as u32).to_le_bytes()); + bytes.extend_from_slice(data); + bytes +}} + +#[test] +fn current_profiles_preserve_exact_legacy_values_and_unknown_fields() {{ + let cases = [ + (&HW_CFG_T8103, [0x020000u64, 0, 128, 1]), + (&HW_CFG_T8112, [0x020000u64, 0, 128, 0]), + (&HW_CFG_T6000, [0x020000u64, 0, 64, 0]), + (&HW_CFG_T6020, [0x0100_00000000u64, 0, 64, 0]), + ]; + let keys = [from_fourcc(b"EC0p"), from_fourcc(b"nCal"), + from_fourcc(b"alig"), from_fourcc(b"AOPt")]; + for (profile, expected) in cases {{ + assert_eq!(profile.bootarg_overrides, + keys.iter().copied().zip(expected).collect::>()); + let mut bytes = record(from_fourcc(b"keep"), &[0xbb; 12]); + let prefix = bytes.clone(); + for (i, key) in keys.iter().enumerate() {{ + let size = if i == 0 {{ 8 }} else {{ 4 }}; + bytes.extend_from_slice(&record(*key, &vec![0xaa; size])); + }} + bootargs::patch(&mut bytes, profile.bootarg_overrides).unwrap(); + assert_eq!(&bytes[..prefix.len()], prefix); + let mut offset = prefix.len(); + for (i, value) in expected.iter().enumerate() {{ + let size = if i == 0 {{ 8 }} else {{ 4 }}; + assert_eq!(&bytes[offset..offset + 4], &keys[i].to_le_bytes()); + assert_eq!(&bytes[offset + 4..offset + 8], &(size as u32).to_le_bytes()); + assert_eq!(&bytes[offset + 8..offset + 8 + size], &value.to_le_bytes()[..size]); + offset += 8 + size; + }} + }} +}} + +#[test] +fn j700_keeps_firmware_bootargs_untouched() {{ + assert!(HW_CFG_T8140.bootarg_overrides.is_empty()); + let mut bytes = vec![0xaa; 3]; + bootargs::patch(&mut bytes, HW_CFG_T8140.bootarg_overrides).unwrap(); + assert_eq!(bytes, vec![0xaa; 3]); +}} + +#[test] +fn ec0p_only_override_preserves_all_other_firmware_fields() {{ + let mut bytes = record(from_fourcc(b"EC0p"), &[0xaa; 8]); + for key in [from_fourcc(b"nCal"), from_fourcc(b"alig"), from_fourcc(b"AOPt")] {{ + bytes.extend_from_slice(&record(key, &[0xbb; 4])); + }} + let mut expected = bytes.clone(); + expected[8..16].copy_from_slice(&0x0100_00000000u64.to_le_bytes()); + bootargs::patch(&mut bytes, &[(from_fourcc(b"EC0p"), 0x0100_00000000)]).unwrap(); + assert_eq!(bytes, expected); +}} +""".format(helper=json.dumps(str(helper), ensure_ascii=False), profiles=profiles, fourcc=fourcc, abi=abi) + + with tempfile.TemporaryDirectory(prefix="apple-aop-bootargs-") as directory: + directory = Path(directory) + crate = directory / "fixtures.rs" + binary = directory / "fixtures" + crate.write_text(harness) + subprocess.run([compiler, "--edition=2021", "--test", str(crate), + "-o", str(binary)], check=True) + return subprocess.run([str(binary)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/aop-retirement.py b/tools/testing/selftests/drivers/apple/aop-retirement.py new file mode 100755 index 0000000000000..3d20648181b37 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/aop-retirement.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Real override allocation/installation and AOP token lifetime fault fixtures.""" +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def extract(source, name): + at = source.index(name + "(") + start = source.rfind("\n", 0, at) + 1 + opening = source.index("{", at) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + cc, rustc = os.environ.get("CC", "cc"), os.environ.get("RUSTC", "rustc") + if not shutil.which(cc) or not shutil.which(rustc): + print("SKIP: host C and Rust compilers required") + return 4 + driver = (ROOT / "drivers/soc/apple/aop.rs").read_text() + begin = driver.index("struct PreparedOverride(") + end = driver.index("\nimpl ChildDevice", begin) + token = driver[begin:end] + core = (ROOT / "drivers/base/dd.c").read_text() + functions = "\n".join(extract(core, name) for name in ( + "__device_prepare_driver_override", "__device_install_driver_override", + "__device_set_driver_override", "__device_retire_driver")) + c = r''' +#include +#include +#include +#include +#include +#include +#define PAGE_SIZE 4096 +#define GFP_KERNEL 0 +#define EINVAL 22 +#define ENOMEM 12 +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define IS_ERR(p) ((uintptr_t)(p) >= (uintptr_t)-4095) +#define PTR_ERR(p) ((intptr_t)(p)) +#define scoped_guard(kind, lock) for (int once = 1; once; once = 0) +struct device { + struct { const char *name; int lock; } driver_override; + struct device *parent; +}; +static void __device_driver_lock(struct device *dev, struct device *parent) +{ (void)dev; (void)parent; } +static void __device_driver_unlock(struct device *dev, struct device *parent) +{ (void)dev; (void)parent; } +static void __device_release_driver(struct device *dev, struct device *parent) +{ (void)dev; (void)parent; } +static bool fail_alloc; +static int allocated, freed; +static char *strnchr(const char *s, size_t len, int ch) { return (char *)memchr(s, ch, len); } +static char *kstrndup(const char *s, size_t len, int flags) +{ + (void)flags; + if (fail_alloc) return NULL; + char *copy = malloc(len + 1); assert(copy); + memcpy(copy, s, len); copy[len] = 0; allocated++; return copy; +} +void kfree(const void *p) { if (p) { freed++; free((void *)p); } } +FUNCTIONS +void fixture_fail(bool fail) { fail_alloc = fail; } +int fixture_live(void) { return allocated - freed; } +struct device *fixture_device(void) { return calloc(1, sizeof(struct device)); } +bool fixture_matches(struct device *d, const char *name) +{ + return !d->driver_override.name || !strcmp(d->driver_override.name, name); +} +void fixture_destroy(struct device *d) { kfree(d->driver_override.name); free(d); } +'''.replace("FUNCTIONS", functions) + rust = r''' +use std::{mem, ptr::NonNull, ffi::CStr}; +type Result = std::result::Result; +const EINVAL: isize = -22; +const RETIRED_DRIVER_OVERRIDE: &CStr = c"apple-aop-retired"; +trait KernelCStr { fn as_char_ptr(&self) -> *const core::ffi::c_char; } +impl KernelCStr for CStr { + fn as_char_ptr(&self) -> *const core::ffi::c_char { self.as_ptr() } +} +unsafe fn from_err_ptr(pointer: *mut T) -> Result<*mut T> { + if pointer as usize >= usize::MAX - 4094 { Err(pointer as isize) } else { Ok(pointer) } +} +mod bindings { + pub type device = core::ffi::c_void; + unsafe extern "C" { + pub fn __device_prepare_driver_override(s: *const core::ffi::c_char, len: usize) -> *const core::ffi::c_char; + pub fn __device_install_driver_override(dev: *mut device, s: *const core::ffi::c_char); + pub fn __device_retire_driver(dev: *mut device, s: *const core::ffi::c_char); + pub fn __device_set_driver_override(dev: *mut device, s: *const core::ffi::c_char, len: usize) -> i32; + pub fn kfree(pointer: *const core::ffi::c_void); + pub fn fixture_fail(fail: bool); + pub fn fixture_live() -> i32; + pub fn fixture_device() -> *mut device; + pub fn fixture_matches(dev: *mut device, name: *const core::ffi::c_char) -> bool; + pub fn fixture_destroy(dev: *mut device); + } +} +TOKEN +#[test] +fn parent_child_oom_and_reload_preserve_the_device_owned_barrier() { + unsafe { + // Parent reserve fails before its handoff point can be reached. + bindings::fixture_fail(true); + let mut cpu_exposed = false; + let result = (|| -> Result { + let marker = PreparedOverride::new()?; + cpu_exposed = true; + Ok(marker) + })(); + assert!(matches!(result, Err(-12))); + assert!(!cpu_exposed && bindings::fixture_live() == 0); + + // Parent succeeds. A child reserve fails before child registration/ + // publication; the parent already owns its guaranteed retire reserve. + bindings::fixture_fail(false); + let parent = PreparedOverride::new().unwrap(); + bindings::fixture_fail(true); + let mut child_exposed = false; + let result = (|| -> Result { + let marker = PreparedOverride::new()?; + child_exposed = true; + Ok(marker) + })(); + assert!(matches!(result, Err(-12))); + assert!(!child_exposed && bindings::fixture_live() == 1); + let device = bindings::fixture_device(); + parent.install(device); // Allocation still forced to fail: install cannot allocate. + assert!(!bindings::fixture_matches(device, c"apple-aop".as_ptr())); + let mut module_local_retired = true; + assert!(module_local_retired); + module_local_retired = false; // Simulated module unload/reload. + assert!(!module_local_retired); + assert!(!bindings::fixture_matches(device, c"apple-aop".as_ptr())); + assert!(bindings::fixture_live() == 1); + bindings::fixture_destroy(device); + assert!(bindings::fixture_live() == 0); + + // A child marker also transfers without allocation at retirement. + bindings::fixture_fail(false); + let child = PreparedOverride::new().unwrap(); + let device = bindings::fixture_device(); + bindings::fixture_fail(true); + child.retire_driver(device); + assert!(!bindings::fixture_matches(device, c"snd_soc_apple_aop".as_ptr())); + bindings::fixture_destroy(device); + assert!(bindings::fixture_live() == 0); + + // Confirmed shutdown discards the unused reserve, preserving normal + // matching/reprobe and any pre-existing override. + bindings::fixture_fail(false); + let device = bindings::fixture_device(); + assert_eq!(bindings::__device_set_driver_override(device, c"old-driver".as_ptr(), 10), 0); + let reserved = PreparedOverride::new().unwrap(); + drop(reserved); + assert!(bindings::fixture_matches(device, c"old-driver".as_ptr())); + assert!(!bindings::fixture_matches(device, c"another".as_ptr())); + // The existing setter retains its old override on allocation error. + bindings::fixture_fail(true); + assert_eq!(bindings::__device_set_driver_override(device, c"replacement".as_ptr(), 11), -12); + assert!(bindings::fixture_matches(device, c"old-driver".as_ptr())); + assert_eq!(bindings::__device_set_driver_override(device, c"".as_ptr(), 0), 0); + assert!(bindings::fixture_matches(device, c"any-driver".as_ptr())); + bindings::fixture_destroy(device); + assert!(bindings::fixture_live() == 0); + } +} +'''.replace("TOKEN", token) + with tempfile.TemporaryDirectory(prefix="aop-retirement-") as directory: + directory = Path(directory) + cfile, obj = directory / "core.c", directory / "core.o" + rs, binary = directory / "fixtures.rs", directory / "fixtures" + cfile.write_text(c) + rs.write_text("#![allow(non_camel_case_types)]\n" + rust) + subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-c", str(cfile), + "-o", str(obj)], check=True) + subprocess.run([rustc, "--edition=2021", "--test", str(rs), + "-C", "link-arg=" + str(obj), "-o", str(binary)], check=True) + return subprocess.run([str(binary)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/aop-service-race.py b/tools/testing/selftests/drivers/apple/aop-service-race.py new file mode 100755 index 0000000000000..c3970e0597c5e --- /dev/null +++ b/tools/testing/selftests/drivers/apple/aop-service-race.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise real AOP admission and device-core attach/retire functions. + +Only allocation, PM, bus callbacks and device storage are host fixtures. The +admission methods, device lock helpers, probe admission, attach and retirement +functions are compiled directly from the production sources. +""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def extract(source, name): + declaration = re.search( + r"(?m)^(?:[ \t]*(?:pub )?(?:unsafe )?fn |" + r"(?:static )?(?:int |void |const char \*))" + re.escape(name) + r"\(", source) + if declaration is None: + raise ValueError("missing definition: " + name) + start, at = declaration.start(), declaration.end() + opening = source.index("{", at) + end, depth = opening + 1, 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def structure(source, name): + start = source.index("pub struct " + name + " {") + end = source.index("\n}", start) + 2 + return "#[repr(C)]\n" + source[start:end] + + +def main(): + cc, rustc = os.environ.get("CC", "cc"), os.environ.get("RUSTC", "rustc") + if not shutil.which(cc) or not shutil.which(rustc): + print("SKIP: host C and Rust compilers required") + return 4 + core = (ROOT / "drivers/base/dd.c").read_text() + methods = (ROOT / "rust/kernel/soc/apple/aop.rs").read_text() + gate = methods[methods.index("impl ServiceGate {"):methods.index("/// Stable child-owned")] + gate = "\n".join(extract(gate, name) for name in ("acquire", "ready", "close")) + context = extract(methods[methods.index("impl ServiceContext {"):], "acquire") + lookup = extract(methods, "from_child") + audio_lookup = extract(methods, "audio_from_child") + abi_start = methods.index("pub enum ServiceABI {") + abi_end = methods.index("\n}", abi_start) + 2 + abi = "#[derive(Clone, Copy, PartialEq, Eq)]\n" + methods[abi_start:abi_end] + abi += "\nimpl ServiceABI {\n" + extract(methods, "publishes_children") + "\n}" + payload = "\n".join(structure(methods, name) for name in ( + "EPICService", "ServicePlatformData")) + functions = "\n".join(extract(core, name) for name in ( + "__device_prepare_driver_override", "__device_install_driver_override", + "__device_driver_lock", "__device_driver_unlock", "__driver_probe_device", + "device_driver_attach", "__device_retire_driver")) + c = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PAGE_SIZE 4096 +#define GFP_KERNEL 0 +#define EPROBE_DEFER 517 +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define IS_ERR(p) ((uintptr_t)(p) >= (uintptr_t)-4095) +#define PTR_ERR(p) ((intptr_t)(p)) +#define scoped_guard(kind, lock) for (int once = 1; once; once = 0) +#define dev_dbg(...) ((void)0) +#define dev_err_probe(dev, err, msg) (err) +#define pm_runtime_get_suppliers(dev) ((void)0) +#define pm_runtime_get_sync(dev) ((void)0) +#define pm_runtime_barrier(dev) ((void)0) +#define pm_request_idle(dev) ((void)0) +#define pm_runtime_put(dev) ((void)0) +#define pm_runtime_put_suppliers(dev) ((void)0) +struct bus_type { bool need_parent_lock; }; +struct device_driver { int unused; }; +struct device_private { bool dead; }; +struct device { + pthread_mutex_t lock; + struct { const char *name; int lock; } driver_override; + struct device *parent; + struct bus_type *bus; + struct device_private *p; + const struct device_driver *driver; + void *context; + bool registered, can_match; + int released; + atomic_bool notify_retire; +}; +static bool initcall_debug; +static struct bus_type bus; /* Platform bus does not take parent lock. */ +static const struct device_driver driver; +extern int fixture_probe(void *context); +extern void fixture_retire_wait(void *context); +static void device_lock(struct device *d) +{ + if (atomic_exchange(&d->notify_retire, false)) + fixture_retire_wait(d->context); + assert(!pthread_mutex_lock(&d->lock)); +} +static void device_unlock(struct device *d) { assert(!pthread_mutex_unlock(&d->lock)); } +static bool device_is_registered(struct device *d) { return d->registered; } +static bool dev_ready_to_probe(struct device *d) { (void)d; return true; } +static int really_probe(struct device *d, const struct device_driver *drv) +{ + int ret = fixture_probe(d->context); + if (!ret) d->driver = drv; + return ret < 0 ? -ret : ret; +} +static int really_probe_debug(struct device *d, const struct device_driver *drv) +{ return really_probe(d, drv); } +static char *strnchr(const char *s, size_t len, int ch) { return (char *)memchr(s, ch, len); } +static char *kstrndup(const char *s, size_t len, int flags) +{ + (void)flags; + char *copy = malloc(len + 1); assert(copy); + memcpy(copy, s, len); copy[len] = 0; return copy; +} +static void kfree(const void *p) { free((void *)p); } +static void __device_release_driver(struct device *d, struct device *parent) +{ + (void)parent; + if (d->driver) { d->driver = NULL; d->released++; } +} +FUNCTIONS +struct device *fixture_device(void *context) +{ + struct device *d = calloc(1, sizeof(*d)); assert(d); + d->p = calloc(1, sizeof(*d->p)); assert(d->p); + assert(!pthread_mutex_init(&d->lock, NULL)); + d->bus = &bus; d->context = context; d->registered = true; + return d; +} +bool fixture_match(struct device *d) +{ return !d->driver_override.name || !strcmp(d->driver_override.name, "aop-child"); } +int fixture_attach(struct device *d) { return device_driver_attach(&driver, d); } +void fixture_retire(struct device *d) +{ + const char *name = __device_prepare_driver_override("apple-aop-retired", 17); + assert(!IS_ERR(name)); + atomic_store(&d->notify_retire, true); + __device_retire_driver(d, name); +} +bool fixture_bound(struct device *d) { return d->driver != NULL; } +int fixture_released(struct device *d) { return d->released; } +void fixture_unregister(struct device *d) +{ + device_lock(d); d->p->dead = true; d->registered = false; + __device_release_driver(d, NULL); device_unlock(d); +} +void fixture_destroy(struct device *d) +{ + kfree(d->driver_override.name); free(d->p); + assert(!pthread_mutex_destroy(&d->lock)); free(d); +} +'''.replace("FUNCTIONS", functions) + rust = r''' +use std::{ffi::c_void, sync::{Arc, Mutex as StdMutex, Condvar, + atomic::{AtomicUsize, Ordering}}, thread}; +type Result = std::result::Result; +const EPROBE_DEFER: i32 = -517; +const ENODEV: i32 = -19; +struct Mutex(StdMutex); +impl Mutex { + fn lock(&self) -> std::sync::MutexGuard<'_, T> { self.0.lock().unwrap() } +} +pub trait AOP: Send + Sync {} +struct Provider(Arc); +impl AOP for Provider {} +impl Drop for Provider { + fn drop(&mut self) { self.0.fetch_add(1, Ordering::SeqCst); } +} +#[derive(Clone, Copy, PartialEq, Eq)] +enum ServiceAdmission { Probing, Ready, Closed } +struct ServiceGate { admission: Mutex } +impl ServiceGate { + fn new() -> Arc { + Arc::new(Self { admission: Mutex(StdMutex::new(ServiceAdmission::Probing)) }) + } + GATE +} +pub struct ServiceContext { provider: Arc, gate: Arc } +impl ServiceContext { CONTEXT } +ABI +PAYLOAD +struct DeviceRaw { platform_data: *const c_void } +pub struct Device(DeviceRaw); +impl Device { fn as_raw(&self) -> *const DeviceRaw { &self.0 } } +impl dyn AOP { LOOKUP AUDIO_LOOKUP } +struct ProbeState { entered: bool, finish: bool, retiring: bool } +struct Fixture { + context: ServiceContext, + payload: ServicePlatformData, + child: Device, + state: StdMutex, + changed: Condvar, + hold: bool, + lookups: AtomicUsize, +} +impl Fixture { + fn new(hold: bool) -> (Box, Arc, Arc) { + let drops = Arc::new(AtomicUsize::new(0)); + let parent: Arc = Arc::new(Provider(drops.clone())); + let mut fixture = Box::new(Self { + context: ServiceContext { provider: parent.clone(), gate: ServiceGate::new() }, + payload: ServicePlatformData { + service: EPICService { channel: 0, endpoint: 0x22 }, + context: std::ptr::null(), + abi: ServiceABI::Legacy, + }, + child: Device(DeviceRaw { platform_data: std::ptr::null() }), + state: StdMutex::new(ProbeState { entered: false, finish: false, retiring: false }), + changed: Condvar::new(), hold, lookups: AtomicUsize::new(0), + }); + fixture.payload.context = &fixture.context; + fixture.child.0.platform_data = (&fixture.payload as *const ServicePlatformData).cast(); + (fixture, parent, drops) + } +} +#[no_mangle] +unsafe extern "C" fn fixture_probe(context: *mut c_void) -> i32 { + let fixture = unsafe { &*context.cast::() }; + fixture.lookups.fetch_add(1, Ordering::SeqCst); + let provider = match unsafe { ::from_child(&fixture.child) } { + Ok(p) => p, Err(e) => return e, + }; + if fixture.hold { + let mut state = fixture.state.lock().unwrap(); + state.entered = true; fixture.changed.notify_all(); + while !state.finish { state = fixture.changed.wait(state).unwrap(); } + } + drop(provider); + 0 +} +#[no_mangle] +unsafe extern "C" fn fixture_retire_wait(context: *mut c_void) { + let fixture = unsafe { &*context.cast::() }; + if fixture.hold { + let mut state = fixture.state.lock().unwrap(); + state.retiring = true; fixture.changed.notify_all(); + } +} +unsafe extern "C" { + fn fixture_device(context: *mut c_void) -> *mut c_void; + fn fixture_match(device: *mut c_void) -> bool; + fn fixture_attach(device: *mut c_void) -> i32; + fn fixture_retire(device: *mut c_void); + fn fixture_bound(device: *mut c_void) -> bool; + fn fixture_released(device: *mut c_void) -> i32; + fn fixture_unregister(device: *mut c_void); + fn fixture_destroy(device: *mut c_void); +} + +#[test] +fn probing_defers_then_ready_and_closed_context_owns_provider() { + let (fixture, parent, drops) = Fixture::new(false); + unsafe { + let dev = fixture_device((&*fixture as *const Fixture).cast_mut().cast()); + assert_eq!(fixture_attach(dev), -11); // Real attach translates DEFER to EAGAIN. + fixture.context.gate.ready(); + let owned = fixture.context.acquire().unwrap(); + drop(parent); // Driver-data ownership can go away without a borrowed pointer. + assert_eq!(drops.load(Ordering::SeqCst), 0); + fixture.context.gate.close(); + fixture.context.gate.ready(); // A late startup completion cannot reopen it. + assert!(matches!(fixture.context.acquire(), Err(ENODEV))); + drop(owned); + fixture_unregister(dev); fixture_destroy(dev); + } + drop(fixture); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn matched_before_override_still_probes_but_closed_gate_rejects_after_reload() { + let (fixture, parent, drops) = Fixture::new(false); + fixture.context.gate.ready(); + unsafe { + let dev = fixture_device((&*fixture as *const Fixture).cast_mut().cast()); + assert!(fixture_match(dev)); // bus.c match can precede attach's lock. + fixture.context.gate.close(); + fixture_retire(dev); + assert!(!fixture_match(dev)); + drop(parent); // Simulate parent drvdata release/module-local latch loss. + let attempts = fixture.lookups.load(Ordering::SeqCst); + assert_eq!(fixture_attach(dev), ENODEV); // Real attach has no rematch. + assert_eq!(fixture.lookups.load(Ordering::SeqCst), attempts + 1); + assert!(!fixture_bound(dev)); + assert_eq!(drops.load(Ordering::SeqCst), 0); // Retained context still owns it. + fixture_unregister(dev); fixture_destroy(dev); + } + drop(fixture); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn admitted_probe_holds_owned_provider_and_retirement_waits_then_unbinds() { + let (fixture, parent, drops) = Fixture::new(true); + fixture.context.gate.ready(); + unsafe { + let dev = fixture_device((&*fixture as *const Fixture).cast_mut().cast()); + let address = dev as usize; + let probing = thread::spawn(move || fixture_attach(address as *mut c_void)); + let mut state = fixture.state.lock().unwrap(); + while !state.entered { state = fixture.changed.wait(state).unwrap(); } + fixture.context.gate.close(); + drop(parent); + assert_eq!(drops.load(Ordering::SeqCst), 0); + let done = Arc::new(AtomicUsize::new(0)); + let completed = done.clone(); + let retiring = thread::spawn(move || { + fixture_retire(address as *mut c_void); + completed.store(1, Ordering::SeqCst); + }); + while !state.retiring { state = fixture.changed.wait(state).unwrap(); } + assert_eq!(done.load(Ordering::SeqCst), 0); + state.finish = true; fixture.changed.notify_all(); drop(state); + assert_eq!(probing.join().unwrap(), 0); + retiring.join().unwrap(); + assert!(!fixture_bound(dev)); assert_eq!(fixture_released(dev), 1); + assert_eq!(fixture_attach(dev), ENODEV); + fixture_unregister(dev); fixture_destroy(dev); + } + drop(fixture); + assert_eq!(drops.load(Ordering::SeqCst), 1); +} + +#[test] +fn close_serializes_lookup_and_unregistered_child_never_dereferences_context() { + let (fixture, parent, drops) = Fixture::new(false); + fixture.context.gate.ready(); + let admission = fixture.context.gate.admission.lock(); + let gate = fixture.context.gate.clone(); + let closing = thread::spawn(move || gate.close()); + // The same lock used by acquire keeps close waiting until an owned clone + // has been made. This reference remains valid after parent drvdata drops. + let pending = fixture.context.provider.clone(); + drop(admission); closing.join().unwrap(); + assert!(matches!(fixture.context.acquire(), Err(ENODEV))); + unsafe { + let dev = fixture_device((&*fixture as *const Fixture).cast_mut().cast()); + fixture_unregister(dev); + drop(parent); drop(fixture); drop(pending); + assert_eq!(drops.load(Ordering::SeqCst), 1); + assert_eq!(fixture_attach(dev), ENODEV); // Real __driver_probe_device guard. + fixture_destroy(dev); + } +} + +#[test] +fn audio_abi_is_checked_before_provider_admission_or_any_configuration_call() { + for provided in [ServiceABI::ControlOnly, ServiceABI::Legacy, ServiceABI::T8140, ServiceABI::J616sJack] { + let (mut fixture, parent, drops) = Fixture::new(false); + fixture.payload.abi = provided; + fixture.context.gate.ready(); + assert_eq!(provided.publishes_children(), provided != ServiceABI::ControlOnly); + for requested in [ServiceABI::ControlOnly, ServiceABI::Legacy, ServiceABI::T8140, ServiceABI::J616sJack] { + let lookup = unsafe { ::audio_from_child(&fixture.child, requested) }; + if requested != ServiceABI::ControlOnly && requested == provided { + drop(lookup.unwrap()); + } else { + assert!(matches!(lookup, Err(ENODEV))); + } + } + fixture.context.gate.close(); + assert!(matches!(unsafe { + ::audio_from_child(&fixture.child, provided) + }, Err(ENODEV))); + drop(parent); drop(fixture); + assert_eq!(drops.load(Ordering::SeqCst), 1); + } +} +'''.replace("GATE", gate).replace("CONTEXT", context).replace("PAYLOAD", payload).replace( + "AUDIO_LOOKUP", audio_lookup).replace("LOOKUP", lookup).replace("ABI\n", abi + "\n") + with tempfile.TemporaryDirectory(prefix="aop-service-race-") as directory: + directory = Path(directory) + cfile, obj = directory / "core.c", directory / "core.o" + rs, binary = directory / "fixtures.rs", directory / "fixtures" + cfile.write_text(c) + rs.write_text(rust) + subprocess.run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread", + "-c", str(cfile), "-o", str(obj)], check=True) + subprocess.run([rustc, "--edition=2021", "--test", str(rs), + "-C", "link-arg=" + str(obj), "-C", "link-arg=-pthread", + "-o", str(binary)], check=True) + return subprocess.run([str(binary)], timeout=30).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/aop-shutdown.py b/tools/testing/selftests/drivers/apple/aop-shutdown.py new file mode 100755 index 0000000000000..74610acd407b9 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/aop-shutdown.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile/run the production RTKit release policy with faulting/drop peers.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + +def main(): + compiler = os.environ.get("RUSTC", "rustc") + if shutil.which(compiler) is None: + print("SKIP: rustc is required") + return 4 + helper = ROOT / "drivers/soc/apple/aop_shutdown.rs" + with tempfile.TemporaryDirectory(prefix="apple-aop-shutdown-") as directory: + directory = Path(directory) + source = directory / "fixtures.rs" + binary = directory / "fixtures" + source.write_text("#[path = " + json.dumps(str(helper), ensure_ascii=False) + + "]\nmod shutdown;\n") + subprocess.run([compiler, "--edition=2021", "--test", str(source), + "-o", str(binary)], check=True) + return subprocess.run([str(binary)]).returncode + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/firmware-permissions.py b/tools/testing/selftests/drivers/apple/firmware-permissions.py new file mode 100755 index 0000000000000..67fcd8634b7d2 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/firmware-permissions.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise actual reserved-region allocation and DART PTE permission code.""" +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def function(source, name): + start = source.index("static dart_iopte " + name) + opening = source.index("{", start) + depth = 1 + end = opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + compiler = os.environ.get("CC", "cc") + if shutil.which(compiler) is None: + print("SKIP: a host C compiler is required") + return 4 + iommu = (ROOT / "include/linux/iommu.h").read_text() + dart = (ROOT / "drivers/iommu/io-pgtable-dart.c").read_text() + flags = "\n".join(line for line in iommu.splitlines() + if re.match(r"#define IOMMU_(READ|WRITE|CACHE)\s", line)) + flags += "\n" + "\n".join(line for line in dart.splitlines() + if re.match(r"#define APPLE_DART[12]_PTE_PROT_", line)) + enum = re.search(r"enum iommu_resv_type\s*\{.*?\n\};", iommu, re.S).group() + fmt = re.search(r"enum io_pgtable_fmt\s*\{.*?\n\};", + (ROOT / "include/linux/io-pgtable.h").read_text(), re.S).group() + header = json.dumps(str(ROOT / "drivers/iommu/of_iommu_firmware.h")) + source = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#define BIT(n) (1UL << (n)) +#define GFP_KERNEL 0 +typedef uint64_t phys_addr_t; +typedef uint64_t dart_iopte; +FLAGS +ENUM +FMT +struct iommu_resv_region { phys_addr_t phys, iova; size_t length; int prot; enum iommu_resv_type type; }; +static struct iommu_resv_region recorded; +static struct iommu_resv_region *iommu_alloc_resv_region_tr( + phys_addr_t phys, phys_addr_t iova, size_t length, int prot, enum iommu_resv_type type, int gfp) +{ + (void)gfp; + recorded = (struct iommu_resv_region){phys, iova, length, prot, type}; + return &recorded; +} +static struct iommu_resv_region *iommu_alloc_resv_region( + phys_addr_t iova, size_t length, int prot, enum iommu_resv_type type, int gfp) +{ + return iommu_alloc_resv_region_tr(iova, iova, length, prot, type, gfp); +} +struct device_node { bool asc, read_only; }; +static bool of_device_is_compatible(struct device_node *node, const char *name) +{ + assert(!strcmp(name, "apple,asc-mem")); + return node->asc; +} +static bool of_property_read_bool(struct device_node *node, const char *name) +{ + assert(!strcmp(name, "apple,firmware-read-only")); + return node->read_only; +} +#include HEADER +struct dart_io_pgtable { struct { enum io_pgtable_fmt fmt; } iop; }; +DART_FUNCTION +int main(int argc, char **argv) +{ + unsigned count = 0; + for (int asc = 0; asc < 2; asc++) + for (int ro = 0; ro < 2; ro++) + for (int coherent = 0; coherent < 2; coherent++) + for (int translated = 0; translated < 2; translated++) { + struct device_node node = {asc, ro}; + enum iommu_resv_type type = translated ? IOMMU_RESV_TRANSLATED : IOMMU_RESV_DIRECT; + struct iommu_resv_region *region = of_iommu_alloc_firmware_region( + 0x12340000, 0x10000000000ULL, 0x4000, type, coherent, + of_iommu_firmware_read_only(&node)); + assert(region->type == type && region->length == 0x4000); + assert(region->iova == 0x10000000000ULL); + assert(region->phys == (translated ? 0x12340000 : region->iova)); + assert(region->prot & IOMMU_READ); + assert(!!(region->prot & IOMMU_WRITE) == !(asc && ro)); + assert(!!(region->prot & IOMMU_CACHE) == !!coherent); + for (int version = 0; version < 2; version++) { + struct dart_io_pgtable data = {.iop.fmt = version ? APPLE_DART2 : APPLE_DART}; + dart_iopte pte = dart_prot_to_pte(&data, region->prot); + dart_iopte no_write = version ? APPLE_DART2_PTE_PROT_NO_WRITE : APPLE_DART1_PTE_PROT_NO_WRITE; + dart_iopte no_read = version ? APPLE_DART2_PTE_PROT_NO_READ : APPLE_DART1_PTE_PROT_NO_READ; + assert(!!(pte & no_write) == !!(asc && ro)); + assert(!(pte & no_read)); + if (version) + assert(!!(pte & APPLE_DART2_PTE_PROT_NO_CACHE) == !coherent); + count++; + } + } + printf("%u allocator/PTE permission paths passed\n", count); + if (argc == 2) { + FILE *input = fopen(argv[1], "rb"); assert(input); + unsigned char blob[32768]; + size_t bytes = fread(blob, 1, sizeof(blob), input); fclose(input); + assert(bytes > 0 && !fdt_check_header(blob)); + int memory = fdt_path_offset(blob, "/reserved-memory"); assert(memory >= 0); + int node, found = 0; + fdt_for_each_subnode(node, blob, memory) { + int length; + const fdt64_t *reg = fdt_getprop(blob, node, "reg", &length); + assert(reg && length == 16); + const fdt32_t *mapping = fdt_getprop(blob, node, "iommu-addresses", &length); + assert(mapping && length == 20); + phys_addr_t phys = fdt64_to_cpu(reg[0]); + phys_addr_t iova = (uint64_t)fdt32_to_cpu(mapping[1]) << 32 | + fdt32_to_cpu(mapping[2]); + size_t size = fdt64_to_cpu(reg[1]); + struct device_node ofnode = { + !fdt_node_check_compatible(blob, node, "apple,asc-mem"), + fdt_getprop(blob, node, "apple,firmware-read-only", NULL) != NULL, + }; + enum iommu_resv_type type = phys == iova ? IOMMU_RESV_DIRECT : IOMMU_RESV_TRANSLATED; + struct iommu_resv_region *region = of_iommu_alloc_firmware_region( + phys, iova, size, type, false, of_iommu_firmware_read_only(&ofnode)); + struct dart_io_pgtable data = {.iop.fmt = APPLE_DART2}; + dart_iopte pte = dart_prot_to_pte(&data, region->prot); + assert(!!(pte & APPLE_DART2_PTE_PROT_NO_WRITE) == ofnode.read_only); + assert(!(pte & APPLE_DART2_PTE_PROT_NO_READ)); + found++; + } + assert(found == 2); + printf("actual producer DT: %d DART2 text/data mapping permissions passed\n", found); + } + return 0; +} +'''.replace("FLAGS", flags).replace("ENUM", enum).replace("FMT", fmt).replace( + "HEADER", header).replace("DART_FUNCTION", function(dart, "dart_prot_to_pte")) + with tempfile.TemporaryDirectory(prefix="apple-firmware-permissions-") as folder: + folder = Path(folder) + c = folder / "fixtures.c" + binary = folder / "fixtures" + c.write_text(source) + subprocess.run([compiler, "-std=c11", "-Wall", "-Wextra", "-Werror", + str(c), "-lfdt", "-o", str(binary)], check=True) + return subprocess.run([str(binary), *sys.argv[1:]]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/testing/selftests/drivers/apple/j616s-jack-profile.py b/tools/testing/selftests/drivers/apple/j616s-jack-profile.py new file mode 100755 index 0000000000000..65b069a0013c2 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/j616s-jack-profile.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Production J616s getter/identity/profile gates, with read-only IPC fixtures.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[5] + + +def main(): + compiler = os.environ.get("RUSTC", "rustc") + if not shutil.which(compiler): + print("SKIP: rustc required") + return 4 + source = (ROOT / "sound/soc/apple/j616s_aop_jack.rs").read_text() + first = source.index("fn get_property(") + last = source.index("\n}\n", first) + 2 + getter = source[first:last] + helper = ROOT / "sound/soc/apple/j616s_jack_profile.rs" + rust = r''' +use std::cell::Cell; +#[path = HELPER] +mod profile; +use profile::Route; +type KVec = Vec; +type Result = std::result::Result; +const EIO: i32 = -5; +struct EPICService; +trait AOP { + fn epic_call_ret(&self, service: &EPICService, subtype: u16, + request: &[u8], capacity: usize) -> Result<(u32, KVec)>; +} +GETTER +struct ReadOnly { route: Route, retcode: u32, reply: Vec, calls: Cell } +impl AOP for ReadOnly { + fn epic_call_ret(&self, _: &EPICService, subtype: u16, + request: &[u8], capacity: usize) -> Result<(u32, Vec)> { + self.calls.set(self.calls.get() + 1); + assert_eq!(subtype, 0x20); + assert_eq!(request.len(), 52); + assert_eq!(&request[8..12], &0xc3000004u32.to_le_bytes()); + assert_eq!(request, profile::get_request(self.route, 701)); + assert_eq!(capacity, 114); // One extra byte exposes a too-long reply. + Ok((self.retcode, self.reply.clone())) + } +} +fn framed(bytes: &[u8]) -> Vec { + let mut reply = (bytes.len() as u32).to_le_bytes().to_vec(); + reply.extend_from_slice(bytes); reply +} +#[test] +fn getter_uses_exact_native_record_and_refuses_bad_replies_without_retry() { + for route in [Route::Cout, Route::Cin] { + let good = framed(route.profile()); + let mut wrong_length = good.clone(); wrong_length[0] = 108; + let mut extra = good.clone(); extra.push(0); + for (code, reply, accepted) in [ + (0, good.clone(), true), (1, good.clone(), false), + (0, good[..112].to_vec(), false), (0, wrong_length, false), + (0, extra, false), (0, vec![0; 3], false), + ] { + let ipc = ReadOnly { route, retcode: code, reply, calls: Cell::new(0) }; + assert_eq!(get_property(&ipc, &EPICService, route, 701, 109).is_ok(), accepted); + assert_eq!(ipc.calls.get(), 1); + } + } +} +#[test] +fn both_profiles_are_exact_including_unknown_fields() { + for route in [Route::Cout, Route::Cin] { + let good = framed(route.profile()); + assert!(profile::profile_reply(route, &good)); + assert!(!profile::profile_reply( + if route == Route::Cout { Route::Cin } else { Route::Cout }, &good)); + for byte in 4..good.len() { + let mut changed = good.clone(); changed[byte] ^= 1; + assert!(!profile::profile_reply(route, &changed)); + } + } +} +#[test] +fn idle_is_required_and_state_payload_lengths_are_strict() { + assert!(profile::idle_reply(&framed(b"eldi"))); + for bytes in [b"drwp", b" 1wp", b"nnur", b"0000"] { + assert!(!profile::idle_reply(&framed(bytes))); + } + assert!(profile::property_data(&[0; 3], 0).is_none()); + assert!(profile::property_data(&[0; 4], usize::MAX).is_none()); +} +#[test] +fn firmware_protocol_and_endpoint_are_independently_pinned() { + let uuid = profile::FIRMWARE_UUID; let name = profile::FIRMWARE_NAME; + assert!(profile::identity_matches(uuid, name, 12, 0x22)); + for version in [0, 11, 13] { assert!(!profile::identity_matches(uuid, name, version, 0x22)); } + for endpoint in [0x20, 0x21, 0x23] { assert!(!profile::identity_matches(uuid, name, 12, endpoint)); } + assert!(!profile::identity_matches(b"unknown", name, 12, 0x22)); + assert!(!profile::identity_matches(uuid, b"j700", 12, 0x22)); +} +#[test] +fn getter_encoding_retains_native_fourcc_and_32_bit_slot_route_metadata() { + for (route, device, channels, channel) in [ + (Route::Cout, b"tuoc", 2, 4), (Route::Cin, b" nic", 1, 5), + ] { + let request = profile::get_request(route, 200); + assert_eq!(&request[..4], &[0; 4]); + assert_eq!(&request[4..8], &[255; 4]); + assert_eq!(&request[12..32], &[0; 20]); + assert_eq!(&request[32..40], &0x30u64.to_le_bytes()); + assert_eq!(&request[40..44], device); + assert_eq!(&request[44..48], &200u32.to_le_bytes()); + assert_eq!(&request[48..52], &1u32.to_le_bytes()); + assert_eq!(route.channels(), channels); assert_eq!(route.dma_channel(), channel); + } +} +'''.replace("HELPER", json.dumps(str(helper))).replace("GETTER", getter) + with tempfile.TemporaryDirectory(prefix="j616s-jack-profile-") as directory: + directory = Path(directory) + rs, binary = directory / "fixtures.rs", directory / "fixtures" + rs.write_text(rust) + subprocess.run([compiler, "--edition=2021", "--test", str(rs), "-o", str(binary)], check=True) + return subprocess.run([str(binary)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) From d545839b35a05e55c911963c0e30cd3679b0fe0d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 10 Oct 2026 20:12:15 +1100 Subject: [PATCH 2/4] iommu: apple-dart: update locked-root lifecycle tests Pass the strict synchronization mode explicitly in root handoff tests. Verify that pre-attach synchronization defers roots until their ownership shadow exists, while strict synchronization rejects incomplete ownership without changing the live root. Stream registration does not map the locked roots. Expect the deferred synchronization path to succeed and retain the firmware TTBR and software mapping, rather than expecting an attachment error. Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/iommu/apple-dart-test.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/drivers/iommu/apple-dart-test.c b/drivers/iommu/apple-dart-test.c index 0eba2eca17474..05387c663c0c2 100644 --- a/drivers/iommu/apple-dart-test.c +++ b/drivers/iommu/apple-dart-test.c @@ -47,9 +47,14 @@ static void apple_dart_test_before_attach(struct kunit *test) KUNIT_EXPECT_EQ(test, atomic_long_read(&domain->stream_maps[0].sidmap[0]), 0L); KUNIT_EXPECT_EQ(test, readl(dart->regs + DART_T8020_STREAM_COMMAND), 0U); - /* Once attached, missing root mappings must still report an error. */ + /* Adding streams defers publication until attach maps the locked roots. */ KUNIT_EXPECT_EQ(test, apple_dart_domain_add_streams(domain, &master), 0); - KUNIT_EXPECT_EQ(test, apple_dart_iotlb_sync_map(&domain->domain, 0, PAGE_SIZE), -EIO); + KUNIT_EXPECT_EQ(test, apple_dart_iotlb_sync_map(&domain->domain, 0, PAGE_SIZE), 0); + KUNIT_EXPECT_EQ(test, readl(dart->regs + DART_T8020_STREAM_COMMAND), + (u32)DART_T8020_STREAM_COMMAND_INVALIDATE); + KUNIT_EXPECT_EQ(test, readl(dart->regs + DART_TTBR(dart, 0, 0)), + dart->hw->ttbr_valid); + KUNIT_EXPECT_EQ(test, ops->iova_to_phys(ops, 0), (phys_addr_t)SZ_1M); pm_runtime_disable(dart->dev); free_io_pgtable_ops(ops); @@ -77,8 +82,16 @@ static void apple_dart_test_locked_handoff(struct kunit *test) cfg.apple_dart_cfg.n_ttbrs = 1; cfg.apple_dart_cfg.ttbr[0] = ours; + /* Pre-attach synchronization defers a root without its owner shadow. */ + dart->locked_owned[0][0] = NULL; + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, true), 0); + KUNIT_EXPECT_EQ(test, live[0], 0x1001ULL); + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), -EIO); + KUNIT_EXPECT_EQ(test, live[0], 0x1001ULL); + dart->locked_owned[0][0] = owned; + /* Legacy firmware mappings are rebuilt by the IOMMU core at handoff. */ - KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream), 0); + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), 0); KUNIT_EXPECT_MEMEQ(test, live, ours, sizeof(live)); KUNIT_EXPECT_MEMEQ(test, owned, ours, sizeof(owned)); apple_dart_retire_root(live, owned, ARRAY_SIZE(live)); @@ -89,7 +102,7 @@ static void apple_dart_test_locked_handoff(struct kunit *test) dart->hw = &apple_dart_hw_t8110; dart->version = 0x0200; live[0] = 0x1001; - KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream), 0); + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), 0); KUNIT_EXPECT_MEMEQ(test, live, ours, sizeof(live)); } @@ -116,13 +129,13 @@ static void apple_dart_test_firmware_roots(struct kunit *test) cfg.apple_dart_cfg.n_ttbrs = 1; cfg.apple_dart_cfg.ttbr[0] = ours; - KUNIT_ASSERT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream), 0); + KUNIT_ASSERT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), 0); KUNIT_EXPECT_EQ(test, live[0], 0x1001ULL); KUNIT_EXPECT_EQ(test, live[1], 0x2001ULL); ours[0] = 0x3001; - KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream), -EBUSY); + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), -EBUSY); dart->version = 0x0203; - KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream), -EBUSY); + KUNIT_EXPECT_EQ(test, apple_dart_hw_sync_locked(&cfg, &stream, false), -EBUSY); KUNIT_EXPECT_EQ(test, live[0], 0x1001ULL); /* Teardown must preserve a root which firmware has since replaced. */ live[1] = 0x4001; From 5d32235fc684749ddaff5e7515b10b80df8c560b Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 11 Oct 2026 08:03:35 +1100 Subject: [PATCH 3/4] dt-bindings: phy: describe T6040 dual-DART USB handoff Signed-off-by: Ryan Murray Assisted-by: LLM --- .../phy/apple,t6040-usb2-inherited.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml b/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml index 4be48381b41d2..21240adb967af 100644 --- a/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml +++ b/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml @@ -14,7 +14,17 @@ description: | The provider reads its USB2/PIPE state and performs no power, reset or calibration writes. Its caller owns controller/DART handoff and must retain the bootloader's PHY clocks and physical watchdog until cold recovery. - This does not describe complete T6040 Type-C, host or SuperSpeed support. + + The USB0 DWC3 consumer references SID 0 of the DART at 0x382f00000 and + SID 1 of the DART at 0x382f80000. Both DART providers share fault interrupt + 1623 and use the same DMA aperture. The native handoff uses the low 32-bit + aperture, from zero through 0xffffffff, for both providers. The caller must + transfer both banks before either ordinary DART driver probe; an idle state + snapshot does not establish exclusive ownership. + + Notification-suppressed USB2 bulk device operation has been observed with + this two-bank model. Normal ACM notifications remain unqualified, and this + does not describe complete T6040 Type-C, host or SuperSpeed support. properties: compatible: @@ -35,9 +45,9 @@ properties: apple,inherited-handoff: type: boolean description: | - The bootloader has retired its DWC3/DART consumers, transferred DMA - ownership and retained initialized USB2 PHY clocks and a physical - watchdog. This assertion does not perform those operations. + The bootloader has retired its DWC3 consumers, transferred DMA ownership + of both USB DART banks and retained initialized USB2 PHY clocks and a + physical watchdog. This assertion does not perform those operations. required: - compatible From a4e7ee86bc5eed8ff9dab61ef9656adfe000b207 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 11 Oct 2026 12:44:39 +1100 Subject: [PATCH 4/4] watchdog: apple: initialize parent and nowayout policy Signed-off-by: Ryan Murray Assisted-by: LLM --- drivers/watchdog/apple_wdt.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/watchdog/apple_wdt.c b/drivers/watchdog/apple_wdt.c index 6b9b0f9b05ced..b31a016a56452 100644 --- a/drivers/watchdog/apple_wdt.c +++ b/drivers/watchdog/apple_wdt.c @@ -187,8 +187,10 @@ static int apple_wdt_probe(struct platform_device *pdev) if (wdt_ctrl & APPLE_WDT_CTRL_RESET_EN) set_bit(WDOG_HW_RUNNING, &wdt->wdd.status); + wdt->wdd.parent = dev; watchdog_init_timeout(&wdt->wdd, 0, dev); apple_wdt_set_timeout(&wdt->wdd, wdt->wdd.timeout); + watchdog_set_nowayout(&wdt->wdd, WATCHDOG_NOWAYOUT); watchdog_stop_on_unregister(&wdt->wdd); watchdog_set_restart_priority(&wdt->wdd, 128);