From 21edfe36ce1d47fb9269a7b7bd33550cc1ab4a4d Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Sun, 1 Nov 2026 19:04:19 -0500 Subject: [PATCH 1/5] iommu: apple-dart: preserve the live J613 MTP bypass stream Keep the boot loader DMA context for a running J613 MTP session. Admit only the T8122 MTP DART at 0x2fa808000, exactly stream 1, and verify that its control register still selects bypass before skipping reset. Select an identity domain and reject paging, blocked-domain attachment and runtime suspend while the inherited session runs. Retain the clock and bypass state at removal. This permits the existing mtp-live-v1 RTKit adoption path to keep serving input without another INIT. The C-diag11 implementation passed owner input testing on J613 with firmware 26.6.2. The rebased series requires a separate owner boot test. Signed-off-by: Satya Benson --- drivers/iommu/apple-dart.c | 42 ++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 65d2ac48c4958..240c6e140bb4d 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -292,6 +292,8 @@ struct apple_dart { u32 pgsize; u32 num_streams; u32 supports_bypass : 1; + /* J613 MTP keeps issuing DMA in the boot loader identity domain. */ + bool live_mtp; u32 four_level : 1; u32 locked : 1; u32 tunneled : 1; @@ -2593,6 +2595,10 @@ static int apple_dart_attach_dev_paging(struct iommu_domain *domain, struct apple_dart_domain *dart_domain = to_dart_domain(domain); bool streams_added = false; + for_each_stream_map(i, cfg, stream_map) + if (stream_map->dart->live_mtp) + return -EBUSY; + for_each_stream_map(i, cfg, stream_map) WARN_ON(pm_runtime_get_sync(stream_map->dart->dev) < 0); @@ -2686,6 +2692,10 @@ static int apple_dart_attach_dev_blocked(struct iommu_domain *domain, struct apple_dart_stream_map *stream_map; int i; + for_each_stream_map(i, cfg, stream_map) + if (stream_map->dart->live_mtp) + return -EBUSY; + if (cfg->locked) return -EINVAL; @@ -3377,7 +3387,8 @@ static int apple_dart_def_domain_type(struct device *dev) if (cfg->stream_maps[0].dart->managed_sids || apple_dart_has_borrowed(cfg->stream_maps[0].dart)) return IOMMU_DOMAIN_DMA; - if (cfg->stream_maps[0].dart->retained_mtp) + if (cfg->stream_maps[0].dart->retained_mtp || + cfg->stream_maps[0].dart->live_mtp) return IOMMU_DOMAIN_IDENTITY; if (cfg->stream_maps[0].dart->pgsize > PAGE_SIZE) return IOMMU_DOMAIN_IDENTITY; @@ -4302,6 +4313,25 @@ static int apple_dart_probe(struct platform_device *pdev) goto err_clk_disable; } + /* A live MTP epoch must never lose its firmware DMA addresses. */ + if (of_property_present(dev->of_node, "apple,bypass-handoff-streams")) { + u32 stream; + + if (!of_machine_is_compatible("apple,j613") || + !of_device_is_compatible(dev->of_node, "apple,t8122-dart") || + res->start != 0x2fa808000ULL || !dart->supports_bypass || + of_property_count_u32_elems(dev->of_node, + "apple,bypass-handoff-streams") != 1 || + of_property_read_u32(dev->of_node, "apple,bypass-handoff-streams", + &stream) || + stream != 1 || stream >= dart->num_streams || + apple_dart_readl(dart, DART_TCR(dart, stream)) != dart->hw->tcr_bypass) { + ret = -EINVAL; + goto err_clk_disable; + } + dart->live_mtp = true; + } + ret = apple_dart_read_remaps(dart); if (ret) goto err_clk_disable; @@ -4411,7 +4441,7 @@ static int apple_dart_probe(struct platform_device *pdev) } /* ENABLE is write-one-to-set; no foreign stream changes state. */ apple_dart_writel(dart, dart->managed_sids, dart->hw->enable_streams); - } else if (!dart->locked && !dart->retained_mtp && !dart->preserve_display) { + } else if (!dart->locked && !dart->retained_mtp && !dart->live_mtp && !dart->preserve_display) { ret = apple_dart_hw_reset(dart); if (ret) goto err_clk_disable; @@ -4650,7 +4680,7 @@ static void apple_dart_remove(struct platform_device *pdev) * A command issued then never completes. The next probe resets the * block after that clock is running again. */ - if (!dart->locked && !dart->retained_mtp && + if (!dart->locked && !dart->retained_mtp && !dart->live_mtp && !READ_ONCE(dart->commands_gated)) apple_dart_hw_reset(dart); @@ -4660,7 +4690,8 @@ static void apple_dart_remove(struct platform_device *pdev) iommu_device_unregister(&dart->iommu); iommu_device_sysfs_remove(&dart->iommu); - clk_bulk_disable_unprepare(dart->num_clks, dart->clks); + if (!dart->live_mtp) + clk_bulk_disable_unprepare(dart->num_clks, dart->clks); } static void apple_dart_shutdown(struct platform_device *pdev) @@ -4808,6 +4839,9 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); + if (dart->live_mtp) + return -EBUSY; + dart->tunnel_state_restored = false; if (READ_ONCE(dart->commands_gated)) return 0; From 739fc9d5a83b9cf9987614c997b62677fe25818a Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Mon, 2 Nov 2026 12:56:58 -0500 Subject: [PATCH 2/5] soc: apple: rtkit-helper: match the T8122 helper compatible The J613 and J615 25G83 device trees list the MTP as "apple,t8122-mtp", "apple,t8122-rtk-helper-asc4", "apple,mtp" without the generic "apple,rtk-helper-asc4" fallback, and say that Linux binds the SoC-specific compatible. The helper only matches the generic one, so on those boards it never probes, DockChannel HID defers forever and the built-in keyboard and trackpad stay absent. Match "apple,t8122-rtk-helper-asc4" as well. Signed-off-by: Satya Benson --- drivers/soc/apple/rtkit-helper.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/soc/apple/rtkit-helper.c b/drivers/soc/apple/rtkit-helper.c index cbb4be1ebfbaf..c7d9832aa0f67 100644 --- a/drivers/soc/apple/rtkit-helper.c +++ b/drivers/soc/apple/rtkit-helper.c @@ -405,6 +405,8 @@ static const struct dev_pm_ops apple_rtkit_helper_pm_ops = { static const struct of_device_id apple_rtkit_helper_of_match[] = { { .compatible = "apple,rtk-helper-asc4" }, + /* J613/J615 25G83 DTs omit the generic fallback (see t8122-j613-25g83.dts). */ + { .compatible = "apple,t8122-rtk-helper-asc4" }, {}, }; MODULE_DEVICE_TABLE(of, apple_rtkit_helper_of_match); From 0fed74715329352c7d00cf28c68cc063c65a057d Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 11:27:59 -0500 Subject: [PATCH 3/5] soc: apple: retain the live J613 MTP and DART suppliers through s2idle Keep the adopted MTP firmware session and identity DMA stream powered through s2idle. Check ASC RUN and bypass before and after sleep, reject deeper sleep, and refuse to reset inherited DMA on loss. Leave non-J613 helpers and ordinary DART runtime PM unchanged. Exercise the real PM callbacks with host fixtures, including abort unwinding and non-J613 peers. Keep successful retention messages at debug level. Signed-off-by: Satya Benson --- drivers/iommu/apple-dart.c | 43 +++++++++++++++++++- drivers/soc/apple/rtkit-helper.c | 27 +++++++++++- tools/asahi/j613-mtp-s2idle-tests.py | 61 ++++++++++++++++++++++++++++ tools/asahi/run-host-tests.py | 1 + 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tools/asahi/j613-mtp-s2idle-tests.py diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 240c6e140bb4d..07630cdd23d75 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -38,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -294,6 +296,7 @@ struct apple_dart { u32 supports_bypass : 1; /* J613 MTP keeps issuing DMA in the boot loader identity domain. */ bool live_mtp; + bool live_mtp_sleep; u32 four_level : 1; u32 locked : 1; u32 tunneled : 1; @@ -5031,7 +5034,45 @@ static __maybe_unused int apple_dart_resume(struct device *dev) return ret; } -static DEFINE_RUNTIME_DEV_PM_OPS(apple_dart_pm_ops, apple_dart_suspend, apple_dart_resume, NULL); +/* The adopted MTP stays live: system s2idle must retain, never reset it. */ +static int apple_dart_system_suspend(struct device *dev) +{ + struct apple_dart *dart = dev_get_drvdata(dev); + + if (!dart->live_mtp) + return pm_runtime_force_suspend(dev); + if (pm_suspend_target_state != PM_SUSPEND_TO_IDLE) + return -EBUSY; + if (apple_dart_readl(dart, DART_TCR(dart, 1)) != dart->hw->tcr_bypass) + return -EIO; + /* PMGR's ACTIVE_WAKEUP domains and their ancestors must stay powered. */ + device_set_awake_path(dev); + dart->live_mtp_sleep = true; + dev_dbg(dev, "live MTP s2idle: retaining clocks, supplier power and bypass stream 1\n"); + return 0; +} + +static int apple_dart_system_resume(struct device *dev) +{ + struct apple_dart *dart = dev_get_drvdata(dev); + + if (!dart->live_mtp) + return pm_runtime_force_resume(dev); + if (!dart->live_mtp_sleep) + return 0; + dart->live_mtp_sleep = false; + if (apple_dart_readl(dart, DART_TCR(dart, 1)) != dart->hw->tcr_bypass) { + dev_err(dev, "live MTP bypass lost across s2idle; refusing reset of inherited DMA\n"); + return -EIO; + } + dev_dbg(dev, "live MTP s2idle: bypass retained; no reset or invalidate\n"); + return 0; +} + +static const struct dev_pm_ops apple_dart_pm_ops = { + RUNTIME_PM_OPS(apple_dart_suspend, apple_dart_resume, NULL) + SYSTEM_SLEEP_PM_OPS(apple_dart_system_suspend, apple_dart_system_resume) +}; static const struct of_device_id apple_dart_of_match[] = { { .compatible = "apple,t8103-dart", .data = &apple_dart_hw_t8103 }, diff --git a/drivers/soc/apple/rtkit-helper.c b/drivers/soc/apple/rtkit-helper.c index c7d9832aa0f67..602da220a3de2 100644 --- a/drivers/soc/apple/rtkit-helper.c +++ b/drivers/soc/apple/rtkit-helper.c @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -383,9 +385,29 @@ static int __maybe_unused apple_rtkit_helper_suspend(struct device *dev) { struct apple_rtkit_helper *helper = dev_get_drvdata(dev); - /* Only suspend-to-idle has a verified live-session retention contract. */ if (helper->adopted && pm_suspend_target_state != PM_SUSPEND_TO_IDLE) return -EBUSY; + if (!helper->adopted || !of_machine_is_compatible("apple,j613")) + return 0; + if (!(readl(helper->asc_base + APPLE_ASC_CPU_CONTROL) & APPLE_ASC_CPU_CONTROL_RUN)) + return -EHOSTDOWN; + /* mtp-live-v1 cannot be restarted: keep its ASC suppliers awake. */ + device_set_awake_path(dev); + dev_dbg(dev, "adopted MTP s2idle: retaining ASC RUN and firmware session\n"); + return 0; +} + +static int __maybe_unused apple_rtkit_helper_resume(struct device *dev) +{ + struct apple_rtkit_helper *helper = dev_get_drvdata(dev); + + if (!helper->adopted || !of_machine_is_compatible("apple,j613")) + return 0; + if (!(readl(helper->asc_base + APPLE_ASC_CPU_CONTROL) & APPLE_ASC_CPU_CONTROL_RUN)) { + dev_err(dev, "adopted MTP lost ASC RUN; refusing firmware restart\n"); + return -EHOSTDOWN; + } + dev_dbg(dev, "adopted MTP s2idle: ASC RUN retained, no INIT\n"); return 0; } @@ -393,12 +415,12 @@ static int __maybe_unused apple_rtkit_helper_freeze(struct device *dev) { struct apple_rtkit_helper *helper = dev_get_drvdata(dev); - /* A hibernation image cannot recreate the firmware's live DMA ownership. */ return helper->adopted ? -EBUSY : 0; } static const struct dev_pm_ops apple_rtkit_helper_pm_ops = { .suspend = pm_sleep_ptr(apple_rtkit_helper_suspend), + .resume = pm_sleep_ptr(apple_rtkit_helper_resume), .freeze = pm_sleep_ptr(apple_rtkit_helper_freeze), .poweroff = pm_sleep_ptr(apple_rtkit_helper_freeze), }; @@ -414,6 +436,7 @@ MODULE_DEVICE_TABLE(of, apple_rtkit_helper_of_match); static struct platform_driver apple_rtkit_helper_driver = { .driver = { .name = "rtkit-helper", + .pm = pm_sleep_ptr(&apple_rtkit_helper_pm_ops), .of_match_table = apple_rtkit_helper_of_match, /* The live MTP input session is announced only once per boot. */ .suppress_bind_attrs = true, diff --git a/tools/asahi/j613-mtp-s2idle-tests.py b/tools/asahi/j613-mtp-s2idle-tests.py new file mode 100644 index 0000000000000..a235677ca6efc --- /dev/null +++ b/tools/asahi/j613-mtp-s2idle-tests.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Actual system PM callbacks, synthetic register/PM boundaries; no device access.""" +from pathlib import Path +import subprocess,tempfile +R=Path(__file__).resolve().parents[2] +def fn(s,n): + a=s.index('static int '+n+'(');b=s.index('\n}',a)+2;return s[a:b] +s=(R/'drivers/iommu/apple-dart.c').read_text();h=(R/'drivers/soc/apple/rtkit-helper.c').read_text() +assert 'if (dart->live_mtp)\n\t\treturn -EBUSY;' in s +pre=r''' +#include +#include +#include +#include +#define dev_dbg(...) ((void)0) +#define dev_err(...) ((void)0) +#define DART_TCR(d,s) (s) +#define APPLE_ASC_CPU_CONTROL 0x44 +#define APPLE_ASC_CPU_CONTROL_RUN 16 +#define PM_SUSPEND_TO_IDLE 1 +static int pm_suspend_target_state=1; +struct device {void *data;bool awake;}; +struct hw {unsigned tcr_bypass;}; +struct apple_dart {bool live_mtp,live_mtp_sleep;struct hw *hw;unsigned tcr;}; +struct apple_rtkit_helper {bool adopted;unsigned char *asc_base;}; +static void *dev_get_drvdata(struct device *d){return d->data;} +static unsigned apple_dart_readl(struct apple_dart *d,unsigned sid){assert(sid==1);return d->tcr;} +static void device_set_awake_path(struct device *d){d->awake=true;} +static unsigned forced_suspend,forced_resume; +static int pm_runtime_force_suspend(struct device *d){forced_suspend++;return 7;} +static int pm_runtime_force_resume(struct device *d){forced_resume++;return 8;} +static bool of_machine_is_compatible(const char *s){return true;} +static unsigned readl(void *p){return *(unsigned *)p;} +''' +body=fn(s,'apple_dart_system_suspend')+fn(s,'apple_dart_system_resume')+fn(h,'apple_rtkit_helper_suspend')+fn(h,'apple_rtkit_helper_resume') +for forbidden in ['writel','hw_reset','invalidate_tlb','apple_rtkit_wake','apple_rtkit_boot','apple_rtkit_quiesce']: + assert forbidden not in body,forbidden +checks=r''' +int main(void){ + struct hw hw={.tcr_bypass=3};struct apple_dart a={.live_mtp=true,.hw=&hw,.tcr=3};struct device d={.data=&a}; + assert(!apple_dart_system_suspend(&d)&&d.awake&&a.live_mtp_sleep); + assert(!apple_dart_system_resume(&d)&&!a.live_mtp_sleep&&a.tcr==3&&!forced_suspend&&!forced_resume); + // Abort unwind is the same retained transaction, and can be repeated. + assert(!apple_dart_system_suspend(&d));assert(!apple_dart_system_resume(&d)); + pm_suspend_target_state=2;assert(apple_dart_system_suspend(&d)==-EBUSY);pm_suspend_target_state=1; + a.tcr=0;assert(apple_dart_system_suspend(&d)==-EIO);a.tcr=3; + assert(!apple_dart_system_suspend(&d));a.tcr=0;assert(apple_dart_system_resume(&d)==-EIO&&a.tcr==0); + a.live_mtp=false;assert(apple_dart_system_suspend(&d)==7&&apple_dart_system_resume(&d)==8); + unsigned regs[32]={0};struct apple_rtkit_helper h={.adopted=true,.asc_base=(void *)regs};d.data=&h;d.awake=false; + regs[0x44/4]=16;assert(!apple_rtkit_helper_suspend(&d)&&d.awake);assert(!apple_rtkit_helper_resume(&d)); + regs[0x44/4]=0;assert(apple_rtkit_helper_resume(&d)==-EHOSTDOWN&&!regs[0x44/4]); + assert(apple_rtkit_helper_suspend(&d)==-EHOSTDOWN); + h.adopted=false;assert(!apple_rtkit_helper_suspend(&d)&&!apple_rtkit_helper_resume(&d)); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t)/'pm.c';p.write_text(pre+body+checks) + subprocess.run(['clang','-std=gnu11','-Wall','-Werror','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + subprocess.run([str(p.with_suffix(''))],check=True) +print('PASS actual MTP/DART system PM: retained normal/abort cycles, runtime delegation, non-s2idle refusal, lost bypass/RUN refuses without reset/INIT') diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py index 5c5ca93c68594..92f7b23022239 100755 --- a/tools/asahi/run-host-tests.py +++ b/tools/asahi/run-host-tests.py @@ -53,6 +53,7 @@ def mutations(flag, names, base, expect=KILL): # test file -> [(arguments, expectation)]. '{out}' is a fresh directory. ASAHI_TESTS = { + 'j613-mtp-s2idle-tests.py': [([], PASS)], 'aop-als-channel-tests.py': [([], PASS)], 'mtp-pm-tests.py': [(out(), PASS), (out() + ['--old-transport', '4db3a5a22c43'], PASS)], From 3e505615668ce2668b70f6e4a078006a962886be Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Wed, 4 Nov 2026 21:17:32 -0500 Subject: [PATCH 4/5] selftests: apple: follow the 7.3 MTP callback annotations Signed-off-by: Satya Benson --- tools/asahi/j613-mtp-s2idle-tests.py | 2 +- tools/asahi/run-host-tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/asahi/j613-mtp-s2idle-tests.py b/tools/asahi/j613-mtp-s2idle-tests.py index a235677ca6efc..7aa413645303a 100644 --- a/tools/asahi/j613-mtp-s2idle-tests.py +++ b/tools/asahi/j613-mtp-s2idle-tests.py @@ -5,7 +5,7 @@ import subprocess,tempfile R=Path(__file__).resolve().parents[2] def fn(s,n): - a=s.index('static int '+n+'(');b=s.index('\n}',a)+2;return s[a:b] + normalized=s.replace('static int __maybe_unused ', 'static int ');a=normalized.index('static int '+n+'(');b=normalized.index('\n}',a)+2;return normalized[a:b] s=(R/'drivers/iommu/apple-dart.c').read_text();h=(R/'drivers/soc/apple/rtkit-helper.c').read_text() assert 'if (dart->live_mtp)\n\t\treturn -EBUSY;' in s pre=r''' diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py index 92f7b23022239..b39229b1bcb19 100755 --- a/tools/asahi/run-host-tests.py +++ b/tools/asahi/run-host-tests.py @@ -189,7 +189,7 @@ def jobs(work, conf, dtc): if re.search(r'-tests\.(py|rs|sh)$', p.name) and p.name != Path(__file__).name} known = set(ASAHI_TESTS) | set(ASAHI_RUST) | set(ASAHI_SHELL) missing = sorted(found - known) - gone = sorted(known - found) + gone = sorted(name for name in known if not (ASAHI / name).is_file()) if missing or gone: for name in missing: print(f'not registered in {Path(__file__).name}: tools/asahi/{name}', file=sys.stderr) From 4d7ab9478312c97a38a0c33206fa62ec415f87d3 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Thu, 5 Nov 2026 22:47:51 -0500 Subject: [PATCH 5/5] HID: dockchannel-hid: write each frame to the FIFO under one lock dchid_send() writes a frame as up to four dockchannel_send() calls: header, body, tail word and checksum. Only the interface's out_mutex serializes it, so frames for two interfaces can interleave in the shared TX FIFO. On J613 (M3 MacBook Air, T8122, 26.6.2 firmware) this crashes the MTP firmware, and the internal keyboard and trackpad stay dead until reboot. The firmware's crash log reads RTKit: Message (id=1): [dock ch tl]: xsum err [comp=0xfff2ec9d; rcvd=0xc1108] rcvd holds the bytes 08 11 0c 00: the header of a 12-byte command frame, an interface enable, where another frame's checksum belonged. In the three crashed boots, the actuator enable (0xb4 on comm) and magicmouse's dimension query (0xd9 on multi-touch, sent when multi-touch reports ready) both timed out at the same moment. Hold a transport-wide mutex across the writes of one frame. dchid_send() is the only path that writes to the FIFO. The lock order is the interface's out_mutex, then tx_mutex. The host test sends frames from two interfaces in two threads through the production dchid_send() and parses the FIFO with dchid_verify_checksum(); the driver before this change fails it. Signed-off-by: Satya Benson --- drivers/hid/dockchannel-hid/dockchannel-hid.c | 23 ++- tools/asahi/mtp-tx-frame-tests.py | 194 ++++++++++++++++++ tools/asahi/run-host-tests.py | 3 + 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100755 tools/asahi/mtp-tx-frame-tests.py diff --git a/drivers/hid/dockchannel-hid/dockchannel-hid.c b/drivers/hid/dockchannel-hid/dockchannel-hid.c index da0f2ab1403d9..5e1b46a0c87f3 100644 --- a/drivers/hid/dockchannel-hid/dockchannel-hid.c +++ b/drivers/hid/dockchannel-hid/dockchannel-hid.c @@ -224,6 +224,9 @@ struct dockchannel_hid { /* Workqueue to asynchronously create HID devices */ struct workqueue_struct *new_iface_wq; + + /* Held while one frame is written to the shared TX FIFO */ + struct mutex tx_mutex; }; static ssize_t apple_layout_id_show(struct device *dev, @@ -342,7 +345,7 @@ static bool dchid_verify_checksum(struct dchid_hdr *hdr, void *data, u32 *checks return *checksum == U32_MAX; } -static int dchid_send(struct dchid_iface *iface, u32 flags, void *msg, size_t size) +static int dchid_send_frame(struct dchid_iface *iface, u32 flags, void *msg, size_t size) { u32 checksum = 0xffffffff; size_t wsize = round_down(size, 4); @@ -393,6 +396,23 @@ static int dchid_send(struct dchid_iface *iface, u32 flags, void *msg, size_t si return 0; } +/* + * A frame goes out as several FIFO writes, and each interface has its own + * output lock, so frames for two interfaces could interleave in the shared + * FIFO. The MTP firmware then fails the frame checksum and crashes + * ("[dock ch tl]: xsum err"). Write whole frames only. + */ +static int dchid_send(struct dchid_iface *iface, u32 flags, void *msg, size_t size) +{ + struct dockchannel_hid *dchid = iface->dchid; + int ret; + + mutex_lock(&dchid->tx_mutex); + ret = dchid_send_frame(iface, flags, msg, size); + mutex_unlock(&dchid->tx_mutex); + return ret; +} + static int dchid_cmd(struct dchid_iface *iface, u32 type, u32 req, void *data, size_t size, void *resp_buf, size_t resp_size) { @@ -1566,6 +1586,7 @@ static int dockchannel_hid_probe(struct platform_device *pdev) } dchid->dev = dev; + mutex_init(&dchid->tx_mutex); stm = device_get_named_child_node(dev, "stm"); dchid->id_ready = !stm || !fwnode_device_is_available(stm); fwnode_handle_put(stm); diff --git a/tools/asahi/mtp-tx-frame-tests.py b/tools/asahi/mtp-tx-frame-tests.py new file mode 100755 index 0000000000000..f33b298c42ae8 --- /dev/null +++ b/tools/asahi/mtp-tx-frame-tests.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Send DockChannel HID frames from two interfaces at once and parse the FIFO. + +Runs the production dchid_send() (and dchid_send_frame()) in two threads, one +per interface, as when the actuator enable (0xb4 on comm) and the trackpad's +dimension query (0xd9 on multi-touch) go out together after multi-touch +reports ready. The stub FIFO records every dockchannel_send() write and +yields between writes. Parsing it back with the production +dchid_verify_checksum() must give whole, checksum-valid frames, all of them, +in per-interface order. The old transport must compile and fail this at +runtime, as the J613 MTP firmware did ("[dock ch tl]: xsum err"). +""" +import argparse +import os +from pathlib import Path +import re +import resource +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +TRANSPORT = 'drivers/hid/dockchannel-hid/dockchannel-hid.c' + + +def function(source, name): + for kind in ('int', 'void', 'u32', 'bool'): + prefix = 'static ' + kind + ' ' + name + '(' + if prefix in source: + start = source.index(prefix) + return source[start:source.index('\n}', start)+2] + raise ValueError(name) + + +def struct(source, name): + m = re.search(r'struct ' + name + r' \{.*?\} __packed;', source, re.S) + if not m: + raise ValueError(name) + return m.group(0) + + +PREFIX = r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +#define __packed __attribute__((packed)) +#define U16_MAX 0xffff +#define U32_MAX 0xffffffffu +#define round_down(x, y) ((x) & ~((y) - 1)) +#define round_up(x, y) ((((x) + (y) - 1) / (y)) * (y)) +#define IS_ALIGNED(x, a) (((x) & ((a) - 1)) == 0) +#define WARN_ON_ONCE(x) assert(!(x)) +#define DCHID_CHANNEL_CMD 0x11 +static u32 get_unaligned_le32(const void *p) { u32 v; memcpy(&v, p, 4); return v; } +struct mutex { pthread_mutex_t m; }; +static void mutex_lock(struct mutex *l) { assert(!pthread_mutex_lock(&l->m)); } +static void mutex_unlock(struct mutex *l) { assert(!pthread_mutex_unlock(&l->m)); } + +/* The shared TX FIFO: each dockchannel_send() lands whole, as its words do. */ +static u8 fifo[1 << 20]; +static size_t fifo_len; +static pthread_mutex_t fifo_lock = PTHREAD_MUTEX_INITIALIZER; +struct dockchannel { int unused; }; +static int dockchannel_send(struct dockchannel *dc, const void *buf, size_t count) { + pthread_mutex_lock(&fifo_lock); + assert(fifo_len + count <= sizeof(fifo)); + memcpy(fifo + fifo_len, buf, count); + fifo_len += count; + pthread_mutex_unlock(&fifo_lock); + usleep(200); /* the next write of this frame comes later */ + return (int)count; +} +struct dockchannel_hid { struct dockchannel *dc; struct mutex tx_mutex; }; +struct dchid_iface { struct dockchannel_hid *dchid; int index; u8 tx_seq; }; +''' + +CASES = r''' +#define FRAMES 40 +static struct dockchannel dc; +static struct dockchannel_hid dchid = { .dc = &dc }; +static struct dchid_iface comm = { .dchid = &dchid, .index = 0 }, mt = { .dchid = &dchid, .index = 1 }; +static volatile int go; + +struct sender { struct dchid_iface *iface; u8 msg[16]; size_t size; u32 flags; int sent; }; +static void *run(void *p) { + struct sender *s = p; + while (!__atomic_load_n(&go, __ATOMIC_ACQUIRE)) + sched_yield(); + for (int i = 0; i < FRAMES; i++) { + s->msg[s->size - 1] = (u8)i; + assert(dchid_send(s->iface, s->flags, s->msg, s->size) == 0); + s->iface->tx_seq++; + s->sent++; + sched_yield(); + } + return NULL; +} + +int main(void) { + pthread_mutex_init(&dchid.tx_mutex.m, NULL); + /* 0xb4 iface: enable (2 bytes, a tail word) vs 0xd9 GET_REPORT (1 byte) and a 9-byte frame. */ + struct sender a = { .iface = &comm, .msg = { 0xb4, 4 }, .size = 2, .flags = 0x80 }; + struct sender b = { .iface = &mt, .msg = { 0xd9, 1, 2, 3, 4, 5, 6, 7, 0 }, .size = 9, .flags = 0x41 }; + pthread_t ta, tb; + assert(!pthread_create(&ta, NULL, run, &a) && !pthread_create(&tb, NULL, run, &b)); + __atomic_store_n(&go, 1, __ATOMIC_RELEASE); + assert(!pthread_join(ta, NULL) && !pthread_join(tb, NULL)); + + int seen[2] = { 0, 0 }; + size_t o = 0; + while (o < fifo_len) { + struct dchid_hdr hdr; + assert(o + sizeof(hdr) <= fifo_len); + memcpy(&hdr, fifo + o, sizeof(hdr)); + assert(hdr.hdr_len == sizeof(hdr) && hdr.channel == DCHID_CHANNEL_CMD && "frame header"); + assert(hdr.iface <= 1); + size_t body = hdr.length + 4; + assert(o + sizeof(hdr) + body <= fifo_len); + u32 sum; + assert(dchid_verify_checksum(&hdr, fifo + o + sizeof(hdr), &sum) && "frame checksum (firmware: xsum err)"); + struct dchid_subhdr sub; + memcpy(&sub, fifo + o + sizeof(hdr), sizeof(sub)); + struct sender *s = hdr.iface ? &b : &a; + assert(sub.length == s->size && sub.flags == s->flags); + assert(hdr.seq == (u8)seen[hdr.iface] && "per-interface frame order"); + assert(fifo[o + sizeof(hdr) + sizeof(sub) + s->size - 1] == (u8)seen[hdr.iface]); + seen[hdr.iface]++; + o += sizeof(hdr) + body; + } + assert(seen[0] == FRAMES && seen[1] == FRAMES); + printf("MTP TX frames: %d + %d frames from two interfaces, all whole and checksum-valid\n", seen[0], seen[1]); + return 0; +} +''' + + +def check_callers(source): + """Every FIFO write is inside dchid_send_frame(), which only dchid_send() calls.""" + frame = function(source, 'dchid_send_frame') + if source.count('dockchannel_send(') != frame.count('dockchannel_send('): + raise SystemExit('dockchannel_send() is called outside dchid_send_frame()') + if len(re.findall(r'\bdchid_send_frame\(', source)) != 2: + raise SystemExit('dchid_send_frame() has a caller other than dchid_send()') + if 'mutex_init(&dchid->tx_mutex)' not in source: + raise SystemExit('tx_mutex is never initialised') + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--out') + p.add_argument('--old-transport') + args = p.parse_args() + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + source = ((ROOT / TRANSPORT).read_text() if not args.old_transport else subprocess.check_output( + ['git', 'show', args.old_transport + ':' + TRANSPORT], cwd=ROOT, text=True)) + body = (struct(source, 'dchid_hdr') + '\n' + struct(source, 'dchid_subhdr') + '\n' + + function(source, 'dchid_checksum') + '\n' + function(source, 'dchid_verify_checksum') + '\n') + if args.old_transport: + body += function(source, 'dchid_send') + '\n' + else: + check_callers(source) + body += function(source, 'dchid_send_frame') + '\n' + function(source, 'dchid_send') + '\n' + with tempfile.TemporaryDirectory(prefix='mtp-tx-frame-') as tmp: + out = Path(args.out or tmp) + out.mkdir(parents=True, exist_ok=True) + c, exe = out/'mtp-tx-frame.c', out/'mtp-tx-frame' + c.write_text(PREFIX + body + CASES) + subprocess.run([os.environ.get('CC', 'cc'), '-std=gnu11', '-Wall', '-Wextra', + '-Wno-unused-parameter', '-Wno-unused-function', '-pthread', + str(c), '-o', str(exe)], check=True) + r = subprocess.run([str(exe)], capture_output=True, text=True, timeout=60) + (out/'run.log').write_text(r.stdout+r.stderr) + if args.old_transport: + if r.returncode != -6 or 'Assertion' not in r.stderr: + raise SystemExit('old transport did not fail its runtime assertion:\n' + r.stdout + r.stderr) + print('MTP TX frames: old transport compiled and interleaved two interfaces\' frames') + elif r.returncode: + raise SystemExit(r.stdout+r.stderr) + else: + print(r.stdout, end='') + + +if __name__ == '__main__': + main() diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py index 90ae0cc42255f..d97ed9ef2e9e2 100755 --- a/tools/asahi/run-host-tests.py +++ b/tools/asahi/run-host-tests.py @@ -60,6 +60,9 @@ def mutations(flag, names, base, expect=KILL): 'aop-bootargs-tests.py': [([], PASS)], 'mtp-pm-tests.py': [(out(), PASS), (out() + ['--old-transport', '4db3a5a22c43'], PASS)], + # 11eccbedc3ed writes frames without a transport-wide lock. + 'mtp-tx-frame-tests.py': [(out(), PASS), + (out() + ['--old-transport', '11eccbedc3ed'], PASS)], 'ave-pm-tests.py': [(out(), PASS), (out() + ['--old-ref', '4db3a5a22c43'], PASS)], 'ave-probe-pm-tests.py': [(out(), PASS), (out() + ['--negative-control'], PASS)],