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..21240adb967af --- /dev/null +++ b/Documentation/devicetree/bindings/phy/apple,t6040-usb2-inherited.yaml @@ -0,0 +1,59 @@ +# 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. + + 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: + 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 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 + - 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/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; 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/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); 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())