From 9775d96503c0d0321455cdff7cd61e1c7d897699 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:34 +1100 Subject: [PATCH 01/13] tools/asahi: follow the variant table and suspend rollback in the bcm4377-resume test The bcm4377-resume host test stopped compiling after two driver changes. Commit 0877cd766126 ("arm64: apple: enable J713 Bluetooth with scoped radio DMA") made the hardware variant table name its context register through BCM4377_BAR2_CONTEXT_ADDR_HI and BCM4388_BAR2_CONTEXT_ADDR_HI. The test compiles that table unchanged but never had those macros. Commit afd51176986a ("Bluetooth: bcm4377: abort suspend if wake IRQ arming fails") made bcm4377_suspend() call bcm4377_resume() to undo a failed arming, through a forward declaration ahead of suspend. The test pastes suspend before resume, so the call became an implicit declaration that clashed with the later static definition. Take every #define the variant table refers to from the driver source, and take the forward declaration of bcm4377_resume() from the driver as well, so both follow the code instead of being restated in the fixture. The controls, assertions and the five mutations are unchanged, and each mutation is still killed by its control. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/bcm4377-resume-tests.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/asahi/bcm4377-resume-tests.py b/tools/asahi/bcm4377-resume-tests.py index 2eab82a96c1ca..4c7104ff1b458 100755 --- a/tools/asahi/bcm4377-resume-tests.py +++ b/tools/asahi/bcm4377-resume-tests.py @@ -35,6 +35,11 @@ def extract(s,n): hw_enum=re.search(r'^enum bcm4377_chip \{.*?^\};',s,re.M|re.S).group() hw_table=re.search(r'^static const struct bcm4377_hw bcm4377_hw_variants\[\] = \{.*?^\};',s,re.M|re.S).group() fixture=re.sub(r'^struct bcm4377_hw \{.*?\};', 'typedef uint32_t u32;\n'+hw_struct,fixture,count=1,flags=re.M) +# The variant table names register offsets by macro; take their definitions from the driver. +hw_macros='\n'+''.join(m.group()+'\n' for name in sorted(set(re.findall(r'\b[A-Z][A-Z0-9_]+\b',hw_table))) for m in [re.search(r'^#define '+name+r'\b.*$',s,re.M)] if m) +assert 'BAR2_CONTEXT_ADDR_HI' in hw_macros,hw_macros +# bcm4377_suspend() undoes a failed wake arming through bcm4377_resume(), declared ahead of it. +resume_decl=re.search(r'^static int bcm4377_resume\(struct device \*dev\);$',s,re.M).group()+'\n' fixture=fixture.replace('struct bcm4377_hw *hw;', 'const struct bcm4377_hw *hw;') table_callbacks=''' static int send_cal(struct bcm4377_data *); @@ -45,7 +50,7 @@ def extract(s,n): #define bcm4377_send_ptb send_ptb #define bcm4378_send_ptb send_ptb ''' -code=fixture+hw_enum+table_callbacks+hw_table+'\n'.join(bodies.values())+'\n'.join(core_bodies.values())+extract(core,'hci_reset_dev_sync')+(r/'tools/asahi/bcm4377-resume/controls.c').read_text() +code=fixture+hw_enum+hw_macros+table_callbacks+hw_table+resume_decl+'\n'.join(bodies.values())+'\n'.join(core_bodies.values())+extract(core,'hci_reset_dev_sync')+(r/'tools/asahi/bcm4377-resume/controls.c').read_text() (o/'production.c').write_text(code);env=dict(os.environ,TMPDIR=str(o/'tmp')) c=subprocess.run(['cc','-std=gnu11','-pthread','-Wall','-Wextra','-Wno-unused-function','-Wno-unused-parameter','-fsanitize=address,undefined','-fno-pie','-no-pie',str(o/'production.c'),'-o',str(o/'controls')],env=env,capture_output=True,text=True) (o/'compile.log').write_text(c.stdout+c.stderr);assert not c.returncode,c.stderr From c56864aeba7df3b755539cdc1111959a49cb9658 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:42:02 +1100 Subject: [PATCH 02/13] tools/asahi: follow the grown variant table in the bcm4377-id test The bcm4377-id host test compiles the gate from before commit 8935b9f50620 ("Bluetooth: bcm4377: match the hardware table ID for J493 recovery") against the current hardware tables, and it required the hardware struct and the variant table to be byte for byte the same in the old and the current driver. Commit 0877cd766126 ("arm64: apple: enable J713 Bluetooth with scoped radio DMA") added a context register field to both, so that check failed with any pre-fix ref. The new table entries also name BCM4377_BAR2_CONTEXT_ADDR_HI and BCM4388_BAR2_CONTEXT_ADDR_HI, which the test never defined, so even the positive build stopped compiling. Keep the exact match for the chip enum, the PCI ID table, the probe selection and the reset registration, which is what the old gate is paired with. For the struct and the table, check what the gate reads instead, the id field and the same chip to ID mapping in both versions. Take every #define the variant table refers to from the driver source. All five builds keep their expected results, and with the pre-fix ref the original, decimal ID, other chip and board bypass gates still abort on the probe registration check. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/bcm4377-id-tests.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/asahi/bcm4377-id-tests.py b/tools/asahi/bcm4377-id-tests.py index 6d5bdb4b68427..167139cfffbc4 100755 --- a/tools/asahi/bcm4377-id-tests.py +++ b/tools/asahi/bcm4377-id-tests.py @@ -97,15 +97,28 @@ def main(): } pieces = {name: take(current, pattern) for name, pattern in patterns.items()} old_pieces = {name: take(old, pattern) for name, pattern in patterns.items()} + # The old gate is compiled against the current tables. Later commits added + # hardware fields and variant settings, so the struct and table may differ, + # but the old gate must still see the same chip IDs, PCI table and probe code. for name in patterns: - if name != 'gate': + if name not in ('gate', 'hw_struct', 'variants'): assert pieces[name] == old_pieces[name], name + for src in (pieces, old_pieces): + assert re.search(r'^\tunsigned int id;$', src['hw_struct'], re.M), src['hw_struct'] + variant_ids = r'^\t\[(BCM\d+)\] = \{\n\t\t\.id = (0x[0-9a-f]+),' + current_ids = re.findall(variant_ids, pieces['variants'], re.M) + assert len(current_ids) == 4, current_ids + assert current_ids == re.findall(variant_ids, old_pieces['variants'], re.M), 'variant ids' defs = '\n'.join(re.findall(r'^#define BCM\d+_DEVICE_ID [^\n]+', current, re.M)) assert defs == '\n'.join(re.findall(r'^#define BCM\d+_DEVICE_ID [^\n]+', old, re.M)) ids = (root / 'include/linux/pci_ids.h').read_text() pci_defs = '\n'.join(re.findall(r'^#define (?:PCI_VENDOR_ID_BROADCOM|PCI_CLASS_NETWORK_OTHER)\s+[^\n]+', ids, re.M)) any_id = take((root / 'include/linux/device-id/pci.h').read_text(), r'^#define PCI_ANY_ID[^\n]+') - source_prefix = PREFIX + defs + '\n' + pci_defs + '\n' + any_id + '\n' + '\n'.join(pieces[n] for n in ('enum', 'hw_struct', 'variants', 'pci_macro_table')) + # Register offsets the variant table names by macro, as defined in the driver. + table_macros = sorted(set(re.findall(r'\b[A-Z][A-Z0-9_]+\b', pieces['variants']))) + hw_defs = '\n'.join(m.group() for name in table_macros for m in [re.search(r'^#define ' + name + r'\b[^\n]*', current, re.M)] if m) + assert 'BAR2_CONTEXT_ADDR_HI' in hw_defs, hw_defs + source_prefix = PREFIX + defs + '\n' + hw_defs + '\n' + pci_defs + '\n' + any_id + '\n' + '\n'.join(pieces[n] for n in ('enum', 'hw_struct', 'variants', 'pci_macro_table')) suffix = SUFFIX.replace('@SELECTION@', pieces['selection']).replace('@REGISTRATION@', pieces['registration']) gates = {'positive': pieces['gate'], 'published-original': old_pieces['gate']} assert pieces['gate'].count('0x4378') == 1 @@ -124,7 +137,7 @@ def no_core(): (args.out / f'{name}.log').write_text(run.stdout + run.stderr) assert run.returncode == (0 if name == 'positive' else -6), (name, run.returncode) results[name] = {'exit': run.returncode, 'source_sha256': sha(source.read_text()), 'stdout': run.stdout.strip()} - receipt = {'source_sha256': sha(current), 'old_source_sha256': sha(old), 'old_ref': args.old_ref, 'actual_inputs': {name: sha(value) for name, value in pieces.items()}, 'device_ids_sha256': sha(defs), 'pci_defs_sha256': sha(pci_defs + any_id), 'results': results, 'scope': 'Actual variant/PCI tables, hardware struct, probe selection statement, reset registration block and admission function. PCI enumeration, OF board matching and unrelated probe operations are controlled; no full probe or hardware execution.'} + receipt = {'source_sha256': sha(current), 'old_source_sha256': sha(old), 'old_ref': args.old_ref, 'actual_inputs': {name: sha(value) for name, value in pieces.items()}, 'device_ids_sha256': sha(defs), 'table_macros_sha256': sha(hw_defs), 'variant_ids': current_ids, 'pci_defs_sha256': sha(pci_defs + any_id), 'results': results, 'scope': 'Actual variant/PCI tables, hardware struct, probe selection statement, reset registration block and admission function. PCI enumeration, OF board matching and unrelated probe operations are controlled; no full probe or hardware execution.'} (args.out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') print(json.dumps(receipt, indent=2)) From e8cf14bc873206326a1edcf9859f03d02445f067 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:43:01 +1100 Subject: [PATCH 03/13] tools/asahi: follow the max3() conversion in the dma-heap-fd test The dma-heap-fd host test compiles the real dma_heap_ioctl() against a small user space harness that defines max() but not max3(). Commit d59676b3ebad ("dma-buf: heaps: use max3 in dma_heap_ioctl") came in with the 7.3 base and changed the ioctl to size its buffer with max3(), so the positive build stopped compiling with an implicit declaration error. Define max3() in the harness the same way the kernel composes it from max(). The bodies under test, the old ref build and all five mutations are unchanged. With the parent of 30d0aff2c65a ("dma-buf: dma-heap: don't publish fd before copy_to_user() succeeds") as the old ref, the positive build passes at both page sizes and the original and every mutation still abort on their assertions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/dma-heap-fd-tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/asahi/dma-heap-fd-tests.py b/tools/asahi/dma-heap-fd-tests.py index f40d0709d58a8..4bf6770547efe 100755 --- a/tools/asahi/dma-heap-fd-tests.py +++ b/tools/asahi/dma-heap-fd-tests.py @@ -45,6 +45,7 @@ def function(source, name): #define PAGE_ALIGN(x) (((x)+PAGE_SIZE-1)&~(uint64_t)(PAGE_SIZE-1)) #define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0])) #define max(a,b) ((a)>(b)?(a):(b)) +#define max3(a,b,c) max(max(a,b),c) #define IOC_IN (_IOC_WRITE << _IOC_DIRSHIFT) #define IOC_OUT (_IOC_READ << _IOC_DIRSHIFT) #define array_index_nospec(x,n) (x) From b3a3a1b36eb58f8d1e2c1ac7999c568591934e8f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:39:47 +1100 Subject: [PATCH 04/13] tools/asahi: follow the closed-client logging in the g15-input-error test The test copies the real VmStatus::report_failure body into a host prelude. Commit f227c7671c0f ("drm/asahi: log the first GPU work failure reported to each G17 VM") made that body use a warned flag, and commit 769e0b0f6214 ("drm/asahi: g17: log a closed client's teardown failures at debug level") made it call unobserved_teardown and mod_pr_debug. The prelude had none of these, so the control no longer built. Give the host VmStatus the client_closed and warned fields, add ENOENT and a mod_pr_debug stub next to pr_warn, and extract the real unobserved_teardown body with the other status methods. The control and all nine mutations run as before, and the removed-fix control still reproduces the lost input error with --ref ffe8011a479c^. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/g15-input-error-tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/asahi/g15-input-error-tests.py b/tools/asahi/g15-input-error-tests.py index 47971fccfa710..809411963fd17 100644 --- a/tools/asahi/g15-input-error-tests.py +++ b/tools/asahi/g15-input-error-tests.py @@ -32,13 +32,14 @@ def extract(text, marker): impl Error { fn from_errno(n:i32)->Self {assert!(n<0);Self(n)} fn to_errno(self)->i32{self.0} } type Result = std::result::Result; const EIO:Error=Error(-5);const ENODEV:Error=Error(-19);const ETIMEDOUT:Error=Error(-110); -const ECANCELED:Error=Error(-125);const EINVAL:Error=Error(-22);const ENOMEM:Error=Error(-12); +const ECANCELED:Error=Error(-125);const EINVAL:Error=Error(-22);const ENOMEM:Error=Error(-12);const ENOENT:Error=Error(-2); macro_rules! pr_warn { ($($arg:tt)*)=>{{ let _=format_args!($($arg)*); }} } +macro_rules! mod_pr_debug { ($($arg:tt)*)=>{{ let _=format_args!($($arg)*); }} } struct Mutex(StdMutex);impl Mutex{fn lock(&self)->MutexGuard<'_,T>{self.0.lock().unwrap()}} struct Mirror{word:*mut i32} -struct VmStatus{error:AtomicI32,reported_error:AtomicI32,mirror:Mutex>} +struct VmStatus{error:AtomicI32,reported_error:AtomicI32,client_closed:AtomicBool,warned:AtomicBool,mirror:Mutex>} impl VmStatus { - fn new(word:*mut i32)->Self{Self{error:AtomicI32::new(0),reported_error:AtomicI32::new(0),mirror:Mutex(StdMutex::new(Some(Mirror{word})))}} + fn new(word:*mut i32)->Self{Self{error:AtomicI32::new(0),reported_error:AtomicI32::new(0),client_closed:AtomicBool::new(false),warned:AtomicBool::new(false),mirror:Mutex(StdMutex::new(Some(Mirror{word})))}} __STATUS__ } mod g17 {use super::*;__POLICY__} @@ -137,7 +138,8 @@ def main(): bodies = "\n".join(methods) code = PRELUDE.replace("__JOB__", bodies).replace("__STATUS__", "\n".join( extract(status, marker) for marker in [" pub(crate) fn get(", " pub(crate) fn record(", - " pub(crate) fn reported_error(", " pub(crate) fn report_failure("])).replace( + " pub(crate) fn reported_error(", " pub(crate) fn report_failure(", + " fn unobserved_teardown("])).replace( "__POLICY__", extract(policy, "pub(crate) fn inherited_error(")) variants = {"control": code} expected = {} From 17756caf4290ae8382248885fce762ae3affe6e8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:45 +1100 Subject: [PATCH 05/13] tools/asahi: follow the moved queue info setup in the compute-preempt test The test extracts the priority policy from WorkQueue::new as the text between the PRIORITY lookup and the WorkQueueInner literal. Commit ab30807676fb ("drm/asahi: keep work-queue firmware structures until the context is invalidated") moved the QueueInfo allocation out of that literal to just after the policy, so the extracted body pulled in try_init!, alloc, gpu_context and DebugFlags::VerboseFaults and no longer built. End the extracted body at the close of the compute priority override, which is the last statement of the policy in the current driver and in the older ones. All four mutations are unchanged and still fail, and --old-ref c8aed729e9bf^ still fails the same assertions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/compute-preempt-tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/asahi/compute-preempt-tests.py b/tools/asahi/compute-preempt-tests.py index fdab1e9e7406b..ed246a3e69060 100755 --- a/tools/asahi/compute-preempt-tests.py +++ b/tools/asahi/compute-preempt-tests.py @@ -29,7 +29,9 @@ def read(path, ref=None): return (root / path).read_text() def policy(text): start = text.index(' let mut prio = *raw::PRIORITY.get(priority as usize).ok_or(EINVAL)?;') - end = text.index(' let inner = WorkQueueInner::ver {', start) + # The policy ends with the compute priority override; queue allocation follows. + tail = ' prio.5 = 1;\n }\n' + end = text.index(tail, start) + len(tail) return text[start:end] queue = read('drivers/gpu/drm/asahi/workqueue.rs', args.ref) macro = read('rust/macros/versions.rs', args.ref) From 68bca8e1a7e484fafe202196ba0cdadb3bd354f3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:40:19 +1100 Subject: [PATCH 06/13] tools/asahi: follow the IRQ-safe PMP report entries in the T6030 graph test The PMP report entry probe gained an apple,irq-safe opt-in that sets GENPD_FLAG_IRQ_SAFE (540f52fa22b1). The host fixture had no such flag, so the extracted probe stopped compiling. Define the flag, let the fake OF node carry apple,irq-safe and read it from the overlay alongside the other entry properties. Check that each entry gets the ACTIVE_WAKEUP and IRQ_SAFE flags its overlay node asks for, and add a case where only one entry opts in. Two new mutations drop the flag and set it without the property, and both are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/t6030-pmp-graph-tests.c | 27 +++++++++++++++++++++++++-- tools/asahi/t6030-pmp-graph-tests.py | 7 +++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index be106bf7e3d1f..9f018ee94d628 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -22,6 +22,7 @@ typedef uint8_t u8; #define PMP_REPORT_READY 1 #define GFP_KERNEL 0 #define GENPD_FLAG_ACTIVE_WAKEUP 1 +#define GENPD_FLAG_IRQ_SAFE 2 #define MAX_PHANDLE_ARGS 4 #define EPROBE_DEFER 517 #define SZ_4K 4096 @@ -44,7 +45,7 @@ static void host_log(const char *format, ...) #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct device_node { const char *name, *compatible; - bool available, always_on, no_ack, deferred, uuid, soc_record; + bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record; u32 reg; int refs; struct device_node *parent, *child, *sibling, *pmp; @@ -387,6 +388,8 @@ static bool of_property_read_bool(const struct device_node *n, const char *p) return n->no_ack; if (!strcmp(p, "apple,defer-startup-ack")) return n->deferred; + if (!strcmp(p, "apple,irq-safe")) + return n->irq_safe; if (!strcmp(p, "apple,soc-device-record")) return n->soc_record; return false; @@ -620,7 +623,7 @@ static int of_platform_populate(struct device_node *n, void *a, void *b, } struct overlay_entry { u32 reg; - bool always_on, no_ack, deferred; + bool always_on, no_ack, deferred, irq_safe; }; #include "overlay.h" static void cleanup(void) @@ -669,6 +672,7 @@ static struct apple_pmp_report *setup(void) .always_on = o->always_on, .no_ack = o->no_ack, .deferred = o->deferred, + .irq_safe = o->irq_safe, .parent = &report_node, .sibling = i < 3 ? &entries[i + 1] : NULL }; @@ -727,6 +731,14 @@ int main(void) assert(regs[0x1180 / 8] == r->start->seed); assert(((struct apple_pmp_report_entry *)entry_devs[3].dev.data) ->no_ack); + /* genpd flags follow the overlay: always-on wakes, irq-safe opts in. */ + for (unsigned int i = 0; i < 4; i++) { + const struct apple_pmp_report_entry *e = entry_devs[i].dev.data; + assert(!!(e->genpd.flags & GENPD_FLAG_ACTIVE_WAKEUP) == + overlay_entries[i].always_on); + assert(!!(e->genpd.flags & GENPD_FLAG_IRQ_SAFE) == + overlay_entries[i].irq_safe); + } assert(apple_pmp_report_wait_ready(&entries[0], 30000) == 0 && enables == 1 && pmp_node.available); assert(g15_vote() == 0 && commands == 1 && enables == 1); @@ -747,6 +759,17 @@ int main(void) assert(g15_vote() == -EIO && commands == 1); cases++; cleanup(); + /* Only an entry that carries apple,irq-safe becomes an IRQ-safe domain. */ + setup(); + entries[1].irq_safe = true; + assert(apple_pmp_report_probe(&report_dev) == 0); + for (unsigned int i = 0; i < 4; i++) { + const struct apple_pmp_report_entry *e = entry_devs[i].dev.data; + assert(!!(e->genpd.flags & GENPD_FLAG_IRQ_SAFE) == + (i == 1 || overlay_entries[i].irq_safe)); + } + cases++; + cleanup(); /* Missing display acknowledgement cannot release G15, even if bound+READY. */ setup(); assert(apple_pmp_report_probe(&report_dev) == 0); diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index af7c849401d3b..6ba26a09c0176 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -30,9 +30,10 @@ def main(): entries = [] for node in re.findall(r'report@[0-9a-f]+\s*\{(.*?)\n\s*\};', overlay, re.S): reg = int(re.search(r'reg = <(0x[0-9a-f]+|[0-9]+)>', node)[1], 0) - entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node)) + entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node, + 'apple,irq-safe;' in node)) assert len(entries) == 4 - entry_data = ',\n'.join('{%d, %d, %d, %d}' % e for e in entries) + entry_data = ',\n'.join('{%d, %d, %d, %d, %d}' % e for e in entries) names = ['static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', 'static int apple_pmp_start_describe(', 'static void apple_pmp_start_finish(', 'static bool apple_pmp_start_stopping(', 'static void apple_pmp_start_work(', @@ -81,6 +82,8 @@ def main(): ('ignore-startup-ack', '(ack & st->startup_ack) != st->startup_ack', 'false'), ('ignore-entry-ack', 'status == PMP_REPORT_READY && (ack & BIT_ULL(id))', '((void)ack, status == PMP_REPORT_READY)'), ('storage-ack-layout', 'st->ack & ~st->record_seed,', 'st->seed & ~st->record_seed,'), + ('ignore-irq-safe', 'ent->genpd.flags |= GENPD_FLAG_IRQ_SAFE;', '(void)0;'), + ('irq-safe-without-property', 'of_property_read_bool(node, "apple,irq-safe")', 'true'), ('skip-supplier-wait', 'apple_pmp_report_wait_ready(entry, msecs_to_jiffies(timeout_ms))', '((void)timeout_ms, 0)'), ] for name, old, new in controls: From 969fb93c15484af904b602b93b436b3a310a2892 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:43:22 +1100 Subject: [PATCH 07/13] tools/asahi: follow the AV1 segmentation map tail in the AVD test The AV1 capture tail gained a per-frame segmentation map below the CDFs (57b0e7c91643), and the preamble now refuses frames larger than the capture format and out-of-range primary references (9fb5f796fed2). The host test did not extract avd_av1_seg_size() and its fixture had no decoded format or s8, so it stopped compiling. Extract avd_av1_seg_size() and avd_av1_set_seg(), give the fixture buffers a DMA address, and give the context a decoded format. The reservation check now counts the map and places it between the compressed data and the CDFs, and the preamble check covers the map address. New cases cover the size and reference index guards, and the map that set_seg zeroes, copies from a reference with the same MiCols and MiRows, or leaves for the AVD, with the CPU and DMA offsets agreeing. Every mutation now names the text it rewrites and is refused if the driver no longer contains it. Four new ones break the map CPU offset, the MiCols match, the reference index bound and the frame width guard, and all six are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/avd-av1-tail-tests.py | 105 +++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/tools/asahi/avd-av1-tail-tests.py b/tools/asahi/avd-av1-tail-tests.py index 8a36298013af6..d602801b7c0a7 100644 --- a/tools/asahi/avd-av1-tail-tests.py +++ b/tools/asahi/avd-av1-tail-tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""AV1 auxiliary alignment, probability inheritance and reservation controls.""" +"""AV1 auxiliary alignment, probability and segmentation map inheritance, and reservation controls.""" import argparse import hashlib import json @@ -44,20 +44,22 @@ def structure(source, name): #include #include "controls.h" typedef uint8_t u8; +typedef int8_t s8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t dma_addr_t; #define ALIGN(x,a) (((x)+(a)-1)&~((__typeof__(x))(a)-1)) #define ALIGN_DOWN(x,a) ((x)&~((__typeof__(x))(a)-1)) #define WARN_ON(x) (x) +#define DIV_ROUND_UP(n,d) (((n)+(d)-1)/(d)) @ALIGNMENT@ @CDF_HEADER@ @CDF_DEFAULTS@ -struct vb2_buffer {struct {unsigned int length;} planes[1]; unsigned char *memory;}; +struct vb2_buffer {struct {unsigned int length;} planes[1]; unsigned char *memory; dma_addr_t dma;}; struct vb2_v4l2_buffer {struct vb2_buffer vb2_buf;}; struct avd_decoded_buffer { struct {struct vb2_v4l2_buffer vb;} base; - struct {unsigned int color_size;} av1; + struct {unsigned int color_size; unsigned short width,height; size_t seg_size;} av1; }; struct avd_buf {void *cpu; dma_addr_t addr;}; struct avd_run {struct {struct vb2_v4l2_buffer *dst;} bufs; dma_addr_t y_out;}; @@ -65,7 +67,7 @@ def structure(source, name): @CTX_STRUCT@ struct v4l2_ctrl {struct {void *p;} p_cur;}; struct v4l2_pix_format_mplane {unsigned int width,height; struct {unsigned int sizeimage;} plane_fmt[1];}; -struct avd_ctx {void *priv; int ctrl_hdl;}; +struct avd_ctx {void *priv; int ctrl_hdl; struct {struct {struct {unsigned int width,height;} pix_mp;} fmt;} decoded_fmt;}; static struct avd_decoded_buffer *reference; static struct v4l2_ctrl_av1_frame frame; static struct v4l2_ctrl_av1_sequence sequence; @@ -74,6 +76,7 @@ def structure(source, name): static struct v4l2_ctrl controls[4]; static unsigned int lookup_calls, preamble_calls; static void *vb2_plane_vaddr(struct vb2_buffer *b,unsigned int p){assert(p==0);return b->memory;} +static dma_addr_t vb2_dma_contig_plane_dma_addr(struct vb2_buffer *b,unsigned int p){assert(p==0);return b->dma;} static struct avd_decoded_buffer *vb2_to_avd_decoded_buf(struct vb2_buffer *b){return (void *)b;} static struct avd_decoded_buffer *avd_get_ref_buf(struct avd_ctx *ctx,struct vb2_v4l2_buffer *dst,uint64_t timestamp){(void)ctx;(void)dst;assert(timestamp==1000);lookup_calls++;return reference;} static void avd_run_preamble(struct avd_ctx *ctx,struct avd_run *run){(void)ctx;(void)run;preamble_calls++;} @@ -89,11 +92,14 @@ def structure(source, name): TESTS = r''' static size_t aligned_cdf(size_t length,size_t color){return (length&~255UL)-((color+255)&~255UL)-9216;} +/* 3 bits per 4x4 block over 128x128 superblocks, 256-byte aligned. */ +static size_t seg_bytes(size_t w,size_t h){return ALIGN(ALIGN(w,128)*ALIGN(h,128)/128*3,256);} +static void set_frame_ctx(struct avd_ctx *ctx){ctx->decoded_fmt.fmt.pix_mp.width=1920;ctx->decoded_fmt.fmt.pix_mp.height=1080;controls[0].p_cur.p=&sequence;controls[1].p_cur.p=&frame;controls[2].p_cur.p=&tile;controls[3].p_cur.p=&grain;} static void read_exact(const char *path,void *data,size_t n){FILE *f=fopen(path,"rb");assert(f);assert(fread(data,1,n,f)==n);assert(fgetc(f)==EOF);assert(fclose(f)==0);} static void inheritance(unsigned int residue,unsigned int mode,const char *neighbors,bool check_addresses){ const size_t ref_len=6964992+residue,dst_len=7000064+(residue?((residue+79)%256):0); const size_t color=138240,ref_offset=aligned_cdf(ref_len,color),dst_offset=aligned_cdf(dst_len,color); - struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0}; + struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); struct avd_av1_cdfs expected={0};unsigned char probabilities[sizeof(expected)]; src.base.vb.vb2_buf.planes[0].length=ref_len;src.base.vb.vb2_buf.memory=malloc(ref_len);assert(src.base.vb.vb2_buf.memory); dst.base.vb.vb2_buf.planes[0].length=dst_len;dst.base.vb.vb2_buf.memory=malloc(dst_len);assert(dst.base.vb.vb2_buf.memory); @@ -108,7 +114,7 @@ def structure(source, name): controls[0].p_cur.p=&sequence;controls[1].p_cur.p=&frame;controls[2].p_cur.p=&tile;controls[3].p_cur.p=&grain; run.base.bufs.dst=&dst.base.vb;run.base.y_out=0x120000000UL;av1.bufs.probs.cpu=probabilities;lookup_calls=0;preamble_calls=0; assert(avd_av1_run_preamble(&ctx,&run)==0);assert(preamble_calls==1); - if(check_addresses){assert(run.addresses.probs_out==run.base.y_out+dst_offset);assert(run.addresses.color==run.base.y_out+dst_offset+9216);} + if(check_addresses){assert(run.addresses.probs_out==run.base.y_out+dst_offset);assert(run.addresses.color==run.base.y_out+dst_offset+9216);assert(run.addresses.seg==run.base.y_out+dst_offset-seg_bytes(1920,1080));} avd_av1_set_prob(&ctx,&run); assert(lookup_calls==(mode?0:1));assert(memcmp(probabilities,&expected,sizeof(expected))==0); assert(memcmp(dst.base.vb.vb2_buf.memory+dst_offset,&expected,sizeof(expected))==0); @@ -120,18 +126,68 @@ def structure(source, name): struct v4l2_pix_format_mplane pix={.width=width,.height=height};struct avd_ctx ctx={0};pix.plane_fmt[0].sizeimage=compressed; avd_av1_adjust_decoded_fmt(&ctx,&pix); size_t min=pix.plane_fmt[0].sizeimage,color=avd_color_size(width,height),length=min+residue; - assert(min%256==0);assert(min==ALIGN(compressed,256)+9216+ALIGN(color,256)); - size_t cdf=AVD_AV1_CDFS_OFFSET(length,color),co=AVD_AV1_COLOR_OFFSET(length,color); + size_t seg=seg_bytes(width,height); + assert(min%256==0);assert(min==ALIGN(compressed,256)+9216+ALIGN(color,256)+seg); + size_t cdf=AVD_AV1_CDFS_OFFSET(length,color),co=AVD_AV1_COLOR_OFFSET(length,color),sg=AVD_AV1_SEG_OFFSET(length,color,seg); + assert(sg%256==0);assert(sg>=ALIGN(compressed,256));assert(sg+seg==cdf); assert(cdf%256==0&&co%256==0);assert(cdf>=compressed);assert(cdf+sizeof(struct avd_av1_cdfs)<=co);assert(co+color<=length); assert(cdf==aligned_cdf(length,color));assert(co==cdf+9216); } +/* The preamble refuses frames larger than the format and out-of-range reference indices. */ +static int preamble(unsigned int w,unsigned int h,unsigned char primary,signed char idx){ + struct avd_decoded_buffer dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); + dst.base.vb.vb2_buf.planes[0].length=7000064;memset(&frame,0,sizeof(frame)); + frame.frame_width_minus_1=w-1;frame.frame_height_minus_1=h-1;frame.primary_ref_frame=primary;if(primary<7)frame.ref_frame_idx[primary]=idx; + run.base.bufs.dst=&dst.base.vb;run.base.y_out=0x120000000UL;return avd_av1_run_preamble(&ctx,&run); +} +static void guards(void){ + assert(preamble(1920,1080,0,0)==0);assert(preamble(1280,720,0,0)==0); + assert(preamble(1921,1080,0,0)==-EINVAL);assert(preamble(1920,1081,0,0)==-EINVAL); + assert(preamble(1920,1080,7,0)==0);assert(preamble(1920,1080,8,0)==-EINVAL);assert(preamble(1920,1080,255,0)==-EINVAL); + assert(preamble(1920,1080,6,7)==0);assert(preamble(1920,1080,6,8)==-EINVAL);assert(preamble(1920,1080,0,-1)==-EINVAL);assert(preamble(1920,1080,3,-128)==-EINVAL); +} +/* Segmentation map kept with each frame: expect 0 zeroes it, 1 copies the reference's, 2 leaves the AVD's map. */ +static void segmap(unsigned char flags,unsigned char primary,unsigned int ref_w,unsigned int ref_h,bool missing,int expect){ + const size_t len=7000064+79,ref_len=6964992+64+(ref_w*ref_h>1920*1080?65536:0),color=138240,seg=seg_bytes(1920,1080),ref_seg=seg_bytes(ref_w,ref_h); + const size_t off=aligned_cdf(len,color)-seg,roff=aligned_cdf(ref_len,color)-ref_seg;const dma_addr_t none=0x140000000UL; + struct avd_decoded_buffer src={0},dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0};set_frame_ctx(&ctx); + dst.base.vb.vb2_buf.planes[0].length=len;dst.base.vb.vb2_buf.memory=malloc(len);dst.base.vb.vb2_buf.dma=0x120000000UL;assert(dst.base.vb.vb2_buf.memory); + src.base.vb.vb2_buf.planes[0].length=ref_len;src.base.vb.vb2_buf.memory=malloc(ref_len);src.base.vb.vb2_buf.dma=0x130000000UL;assert(src.base.vb.vb2_buf.memory); + dst.av1.color_size=color;dst.av1.seg_size=seg;src.av1.color_size=color;src.av1.seg_size=ref_seg;src.av1.width=ref_w-1;src.av1.height=ref_h-1; + memset(dst.base.vb.vb2_buf.memory,0xa5,len);memset(src.base.vb.vb2_buf.memory,0x3c,ref_len); + for(size_t i=0;i=roff&&i1&&(strcmp(argv[1],"snapshot")==0||strcmp(argv[1],"snapshot-copy")==0)){assert(argc==3);inheritance(64,0,argv[2],strcmp(argv[1],"snapshot")==0);puts("snapshot inheritance PASS");return 0;} if(argc>1&&strcmp(argv[1],"copy")==0){inheritance(64,0,NULL,false);puts("probability inheritance PASS");return 0;} if(argc>1&&strcmp(argv[1],"seed")==0){inheritance(64,1,NULL,false);puts("default destination seed PASS");return 0;} if(argc>1&&strcmp(argv[1],"aligned")==0){for(unsigned int mode=0;mode<=4;mode++)inheritance(0,mode,NULL,true);reservation(1920,1080,6425600,0);puts("aligned compatibility PASS");return 0;} unsigned int cases=0; + guards();cases++;cases+=segmaps(); for(unsigned int residue=0;residue<256;residue++){ inheritance(residue,0,NULL,true);cases++; for(unsigned int mode=1;mode<=4;mode++){inheritance(residue,mode,NULL,true);cases++;} @@ -148,21 +204,36 @@ def main(): parser.add_argument('--out', type=Path, required=True) parser.add_argument('--neighbors', type=Path) parser.add_argument('--case', choices=['all', 'snapshot', 'snapshot-copy', 'copy', 'seed', 'aligned'], default='all') - parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned']) + parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned', 'seg-cpu-unaligned', 'seg-any-size', 'ref-index-upper', 'frame-width-unchecked']) args = parser.parse_args() args.out.mkdir(parents=True, exist_ok=True) paths = {name: args.source / DRIVER / name for name in ['avd-av1.c', 'avd-av1-entropymode.h', 'avd-av1-entropymode.c', 'avd.h']} text = {name: p.read_text() for name, p in paths.items()} src = text['avd-av1.c'] macros = src[src.index('#define AVD_CDFS_SIZE'):src.index('struct avd_av1_run {')] - functions = {n: function(src, n) for n in ['avd_av1_set_prob', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} - if args.mutation == 'round-up': - if 'ALIGN_DOWN((dst), AVD_ALIGN)' not in macros: - parser.error('round-up mutation requires aligned tail macros') - macros = macros.replace('ALIGN_DOWN((dst), AVD_ALIGN)', 'ALIGN((dst), AVD_ALIGN)') - elif args.mutation == 'cpu-unaligned': - functions['avd_av1_set_prob'] = functions['avd_av1_set_prob'].replace('AVD_AV1_CDFS_OFFSET(', 'UNALIGNED_CDFS_OFFSET(') - macros += '\n#define UNALIGNED_CDFS_OFFSET(dst,cl) ((dst)-ALIGN(cl,AVD_ALIGN)-ALIGN(AVD_CDFS_SIZE,AVD_ALIGN))\n' + functions = {n: function(src, n) for n in ['avd_av1_set_prob', 'avd_av1_set_seg', 'avd_av1_seg_size', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} + unaligned = ('\n#define UNALIGNED_CDFS_OFFSET(dst,cl) ((dst)-ALIGN(cl,AVD_ALIGN)-ALIGN(AVD_CDFS_SIZE,AVD_ALIGN))\n' + '#define UNALIGNED_SEG_OFFSET(dst,cl,sl) (UNALIGNED_CDFS_OFFSET(dst,cl)-(sl))\n') + # Each mutation names the extracted text it rewrites; a stale one is refused, never run as a no-op. + mutations = { + 'round-up': (None, 'ALIGN_DOWN((dst), AVD_ALIGN)', 'ALIGN((dst), AVD_ALIGN)'), + 'cpu-unaligned': ('avd_av1_set_prob', 'AVD_AV1_CDFS_OFFSET(', 'UNALIGNED_CDFS_OFFSET('), + 'seg-cpu-unaligned': ('avd_av1_set_seg', 'AVD_AV1_SEG_OFFSET(dst->', 'UNALIGNED_SEG_OFFSET(dst->'), + 'seg-any-size': ('avd_av1_set_seg', 'DIV_ROUND_UP(ref->av1.width + 1, 8)', 'DIV_ROUND_UP(frame->frame_width_minus_1 + 1, 8)'), + 'ref-index-upper': ('avd_av1_run_preamble', 'idx < 0 || idx >= V4L2_AV1_TOTAL_REFS_PER_FRAME', 'idx < 0'), + 'frame-width-unchecked': ('avd_av1_run_preamble', 'run->frame->frame_width_minus_1 + 1 > ctx->decoded_fmt.fmt.pix_mp.width ||', '0 ||'), + } + if args.mutation: + target, old, new = mutations[args.mutation] + body = macros if target is None else functions[target] + if old not in body: + parser.error(args.mutation + ' mutation no longer matches the driver source') + body = body.replace(old, new) + if target is None: + macros = body + else: + functions[target] = body + macros += unaligned header = text['avd-av1-entropymode.h'].replace('#include ', '') defaults = re.sub(r'^#include .*$', '', text['avd-av1-entropymode.c'], flags=re.M) alignment = re.search(r'^#define AVD_ALIGN\s+\d+', text['avd.h'], re.M).group() From 3d70f0e07afa7d3c9e689bf85c8103e4c156e3f6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:44:58 +1100 Subject: [PATCH 08/13] tools/asahi: follow the HT capability log in the MT7932 association test The association capability event gained a dynamic debug line for the generated HT capabilities (b23d29d6f661). It looks the element up with cfg80211_find_elem(), which the host fixture did not provide, so the extracted mt_link_event() stopped compiling. Define WLAN_EID_HT_CAPABILITY, give struct element its real id and length header, and add a cfg80211_find_elem() that walks only the IEs it is given. The capability event had no case before, so add one where a generated HT element is exported and the lookup reads the driver's copy, and one where a malformed element fails the attempt without any lookup. The four existing mutations are unchanged and still killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/mt7932-association-events-tests.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/asahi/mt7932-association-events-tests.py b/tools/asahi/mt7932-association-events-tests.py index 3f352376a1c82..436ab4d00b75f 100644 --- a/tools/asahi/mt7932-association-events-tests.py +++ b/tools/asahi/mt7932-association-events-tests.py @@ -52,6 +52,8 @@ def function(source, name): #define NL80211_CHAN_WIDTH_40 1 #define NL80211_CHAN_WIDTH_80 2 #define WLAN_REASON_UNSPECIFIED 1 +#define WLAN_EID_HT_CAPABILITY 45 +struct element {u8 id,datalen,data[];}; #define WLAN_EID_EXTENSION 255 #define WLAN_EID_EXT_HE_CAPABILITY 35 #define dev_info(...) ((void)0) @@ -74,7 +76,6 @@ def function(source, name): static void complete_all(struct completion*c){c->done=UINT_MAX;} struct ieee80211_channel {int center_freq;}; struct cfg80211_chan_def {struct ieee80211_channel *chan;int width,center_freq1;}; -struct element {u8 data[32];}; struct test_band {struct {bool ht_supported;}ht_cap;}; struct test_wiphy {struct test_band *bands[NUM_NL80211_BANDS];}; static struct test_wiphy wiphy; @@ -108,6 +109,13 @@ def function(source, name): static bool cfg80211_chandef_usable(void*w,const struct cfg80211_chan_def*d,int flags){return usable;} static bool mt7932_assoc_phy_valid(const void*b,const void*p,size_t n){return true;} static const struct element *cfg80211_find_ext_elem(int id,const void*p,size_t n){return NULL;} +/* The HT lookup feeds a debug log; it walks only the exported request IEs. */ +static unsigned ht_lookups;static const void *ht_found; +static const struct element *cfg80211_find_elem(int id,const void*p,size_t n){ + const u8*b=p;size_t o=0;assert(id==WLAN_EID_HT_CAPABILITY&&p&&n<=512);ht_lookups++;ht_found=NULL; + while(n-o>=2&&b[o+1]<=n-o-2){if(b[o]==id)return ht_found=b+o;o+=2+b[o+1];} + return NULL; +} static void netif_carrier_off(void*n){} static void netif_stop_queue(void*n){} static void schedule_work(void*w){scheduled++;} @@ -209,6 +217,12 @@ def function(source, name): reset(&m,SUCCESS);u8 rejected[52]={};put_unaligned_le16(1,rejected); receive(&m,0xee,0,rejected,sizeof(rejected),0x40); assert(m.connect_error==-ECONNREFUSED&&m.assoc_start.done&&m.assoc_done.done&&!m.peer_valid);cases++; + // The generated request IEs are exported once; the HT log reads the copy. + reset(&m,SUCCESS);u8 ies[32]={};put_unaligned_le32(28,ies);ies[4]=WLAN_EID_HT_CAPABILITY;ies[5]=26;for(unsigned i=0;i<26;i++)ies[6+i]=i+1;ht_lookups=0; + receive(&m,0xee,0,ies,sizeof(ies),0x5e); + assert(!m.connect_error&&m.assoc_request_seen&&m.assoc_request_ie_len==28&&!memcmp(m.assoc_request_ies,ies+4,28)&&ht_lookups==1&&ht_found==m.assoc_request_ies);cases++; + reset(&m,SUCCESS);ies[5]=25;ht_lookups=0;receive(&m,0xee,0,ies,sizeof(ies),0x5e); + assert(m.connect_error==-EPROTO&&!m.assoc_request_seen&&!ht_lookups&&m.assoc_start.done&&m.assoc_done.done);cases++; printf("PASS %u association event/wait/calibration ownership controls\n",cases); } ''' From 2c242fb405658efe78a8b21fcd25d0fb080a84de Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 17:49:27 +1100 Subject: [PATCH 09/13] tools/asahi: follow 6 GHz policy, USB runtime PM and RX wake in the Neo stability test Several driver changes left this harness behind. The MT7932 6 GHz station support (12010e055535) made channels band-qualified, added channels6, PSD and VLP handling to the regulatory notifier and a band argument to mt7932_policy_permits(), so the harness stopped compiling. The DWC3 glue gained a runtime PM reference (c8372a0ec637), a host-only role mark (a50abc4f7b41) and a set_role that can fail (3949e3640d94). DockChannel no longer makes the receive IRQ a wake IRQ and requests it with IRQF_NO_SUSPEND instead (f98122b072d6), so the wake case and its mutation tested behavior the driver no longer has. Give the fixture the nl80211 band, 6 GHz channels with frequency, PSD and VLP flags, a recording cfg80211 rule lookup and the missing helpers. The regulatory case now checks band-qualified permission where channel numbers overlap, that only 6 GHz channels consult the rule, that the VLP-client channel stays out of the domain and the others lose NO_IR, that the policy pass lifts NO_IR only under a rule without it, and that the disable bitmap spans every channel. The USB case checks that the runtime PM reference is dropped exactly once per stop and kept while the reset assert fails, that a refused PHY role stops the core before any host or gadget start, and that only a host-only host start marks the host role before the core comes up. The wake case now checks that the device is a wakeup source without a wake IRQ, and runs the RX IRQ request statement from dockchannel_init() to check its flags. The wake-irq mutation now restores the old wake IRQ, and new rx-suspend, usb-rpm, vlp-client and six-band mutations join it. Every mutation is refused if it no longer matches the driver. All seven mutations and both old-code controls are killed. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/asahi/neo-stability-tests.py | 167 ++++++++++++++++++++++------- 1 file changed, 127 insertions(+), 40 deletions(-) diff --git a/tools/asahi/neo-stability-tests.py b/tools/asahi/neo-stability-tests.py index f65cab986c626..ff02d627e5711 100644 --- a/tools/asahi/neo-stability-tests.py +++ b/tools/asahi/neo-stability-tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Host controls for retained domains, wake IRQs, USB roles and country policy.""" +"""Host controls for retained domains, wake IRQs, USB roles and band-qualified country policy.""" import argparse import hashlib import json @@ -34,7 +34,16 @@ def function(source, name): #include #include #define SHA256_DIGEST_SIZE 32 -typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef int32_t s32; +typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef int8_t s8; typedef int32_t s32; +enum nl80211_band {NL80211_BAND_2GHZ,NL80211_BAND_5GHZ,NL80211_BAND_60GHZ,NL80211_BAND_6GHZ,NL80211_BAND_S1GHZ,NL80211_BAND_LC,NUM_NL80211_BANDS}; +#define min(a,b) ((a)<(b)?(a):(b)) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) +#define BIT_ULL(n) (1ULL<<(n)) +#define GENMASK_ULL(h,l) (((~0ULL)>>(63-(h)))&(~0ULL<<(l))) +#define ERR_PTR(e) ((void*)(intptr_t)(e)) +#define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) +#define MHZ_TO_KHZ(f) ((f)*1000) #define READ_ONCE(x) (x) #define WRITE_ONCE(x,v) ((x)=(v)) #define lockdep_assert_held(x) ((void)0) @@ -61,12 +70,24 @@ def function(source, name): static int devm_add_action_or_reset(struct device*d,void(*f)(void*),void*p){if(action_error){f(p);return action_error;}cleanup=f;cleanup_data=p;return 0;} static int devm_device_init_wakeup(struct device*d){wakeup_calls++;if(!wakeup_error)d->wakeup=true;return wakeup_error;} static int devm_pm_set_wake_irq(struct device*d,int i){irq_calls++;if(!irq_error)d->wake_irq=i;return irq_error;} +#define IRQF_NO_SUSPEND 0x4000 +#define IRQF_NO_AUTOEN 0x80000 +static unsigned long rx_flags;static int rx_irq_requested; +static int dockchannel_rx_irq(int i,void*p){return 0;} +static int dockchannel_rx_irq_thread(int i,void*p){return 0;} +static int devm_request_threaded_irq(struct device*d,int irq,int(*h)(int,void*),int(*t)(int,void*),unsigned long f,const char*n,void*p){ + assert(h==dockchannel_rx_irq&&t==dockchannel_rx_irq_thread&&!strcmp(n,"apple-dockchannel-rx"));rx_irq_requested=irq;rx_flags=f;return 0;} enum dwc3_apple_state {DWC3_APPLE_PROBE_PENDING,DWC3_APPLE_NO_CABLE,DWC3_APPLE_HOST,DWC3_APPLE_DEVICE,DWC3_APPLE_SUSPENDED}; -struct dwc3 {int num_usb3_ports,num_usb2_ports;int usb3_generic_phy[2],usb2_generic_phy[2],usb3_phy,usb2_phy;}; -struct dwc3_apple {struct device*dev;struct dwc3 dwc;int lock,reset;enum dwc3_apple_state state;bool fixed_hub,core_active,core_exit_pending,reset_pending;unsigned usb3_power_off_pending,usb2_power_off_pending;}; -static int core_error,host_error,gadget_error,reset_error; -static int dwc3_apple_core_start(struct dwc3_apple*a,enum dwc3_apple_state s){if(core_error)return core_error;a->core_active=true;a->reset_pending=true;return 0;} -static void dwc3_apple_set_role(struct dwc3_apple*a,enum dwc3_apple_state s){} +#define DWC3_GCTL_PRTCAP_HOST 1 +struct dwc3 {int num_usb3_ports,num_usb2_ports;int usb3_generic_phy[2],usb2_generic_phy[2],usb3_phy,usb2_phy;int current_dr_role;}; +struct dwc3_apple {struct device*dev;struct dwc3 dwc;int lock,reset;enum dwc3_apple_state state;bool fixed_hub,host_only,rpm_held,core_active,core_exit_pending,reset_pending;unsigned usb3_power_off_pending,usb2_power_off_pending;}; +static int core_error,host_error,gadget_error,reset_error,role_error,start_role; +static unsigned rpm_puts; +/* A started core holds the glue runtime PM reference until core_stop drops it. */ +static int dwc3_apple_core_start(struct dwc3_apple*a,enum dwc3_apple_state s){start_role=a->dwc.current_dr_role;if(core_error)return core_error;a->core_active=true;a->reset_pending=true;a->rpm_held=true;return 0;} +static int dwc3_apple_set_role(struct dwc3_apple*a,enum dwc3_apple_state s){assert(a->core_active);return role_error;} +static void pm_runtime_mark_last_busy(struct device*d){} +static void pm_runtime_put_autosuspend(struct device*d){rpm_puts++;} static int dwc3_host_init(struct dwc3*d){return host_error;} static int dwc3_gadget_init(struct dwc3*d){return gadget_error;} static void dwc3_host_exit(struct dwc3*d){role_exits++;} @@ -93,16 +114,24 @@ def function(source, name): #define WLAN_REASON_DEAUTH_LEAVING 3 #define __assign_bit(i,b,v) ((b)[(i)/64]=((b)[(i)/64]&~(1ULL<<((i)%64)))|((uint64_t)!!(v)<<((i)%64))) #define for_each_set_bit(i,b,n) for((i)=0;(i)<(n);(i)++) if((b)[(i)/64]&(1ULL<<((i)%64))) -#define bitmap_zero(b,n) memset((b),0,8) -struct ieee80211_channel {unsigned flags,hw_value;int max_power;}; +#define bitmap_zero(b,n) memset((b),0,((n)+63)/64*8) +#define IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT 128 +#define NL80211_RRF_NO_IR (1<<7) +struct ieee80211_channel {unsigned flags,hw_value;int max_power;enum nl80211_band band;int center_freq;s8 psd;}; +static u64 get_unaligned_le64(const u8*p){return get_unaligned_le32(p)|(u64)get_unaligned_le32(p+4)<<32;} +static const void *memchr_inv(const void*p,int c,size_t n){const u8*b=p;for(size_t i=0;i=5955000&&khz<=7115000&&(khz/1000-5955)%20==0);return rule6_absent?ERR_PTR(-ERANGE):&rule6;} static unsigned rtnl_depth,wiphy_depth,schedules; static void rtnl_lock(void){assert(!rtnl_depth++);} static void rtnl_unlock(void){assert(rtnl_depth--==1);} static void wiphy_lock(struct wiphy*w){assert(rtnl_depth&& !wiphy_depth++);} static void wiphy_unlock(struct wiphy*w){assert(wiphy_depth--==1);} -struct mt7932 {struct ieee80211_channel channels[MT7932_CHANNELS_2G],channels5[MT7932_CHANNELS_5G];struct wiphy*wiphy;struct net_device*netdev;struct {struct device dev;}*pdev;struct mt7932_reg_snapshot reg_desired,reg_deferred;unsigned reg_generation,response_lock,startup_work;bool reg_pending,stopping,interface_registered,connecting,connected,disconnecting,reg_deferral,data_ready;int data_lock;int connect_error,assoc_start,assoc_done,discovery_done;uint64_t policy_disabled[1];}; +struct mt7932 {struct ieee80211_channel channels[MT7932_CHANNELS_2G],channels5[MT7932_CHANNELS_5G],channels6[MT7932_CHANNELS_6G];struct wiphy*wiphy;struct net_device*netdev;struct {struct device dev;}*pdev;struct mt7932_reg_snapshot reg_desired,reg_deferred;unsigned reg_generation,response_lock,startup_work;bool reg_pending,stopping,interface_registered,connecting,connected,disconnecting,reg_deferral,data_ready;int data_lock;int connect_error,assoc_start,assoc_done,discovery_done;uint64_t policy_disabled[(MT7932_CHANNELS+63)/64];}; #define spin_lock(l) ((void)(l)) #define spin_unlock(l) ((void)(l)) static void mt_data_kick_locked(struct mt7932*m){assert(0 && "unexpected TX boundary");} @@ -117,9 +146,11 @@ def function(source, name): int main(int argc,char**argv){ assert(argc==2);struct device d={};struct dockchannel dc={.dev=&d,.rx_irq=42}; if(!strcmp(argv[1],"wake")){ - assert(!dockchannel_init_wakeup(&dc)&&d.wakeup&&d.wake_irq==42); - wakeup_error=-ENOMEM;assert(dockchannel_init_wakeup(&dc)==-ENOMEM&&irq_calls==1); - wakeup_error=0;irq_error=-EINVAL;assert(dockchannel_init_wakeup(&dc)==-EINVAL); + /* The FIFO's device is a wakeup source; the receive IRQ is not a wake IRQ. */ + assert(!dockchannel_init_wakeup(&dc)&&d.wakeup&&!d.wake_irq&&!irq_calls&&wakeup_calls==1); + wakeup_error=-ENOMEM;d.wakeup=false;assert(dockchannel_init_wakeup(&dc)==-ENOMEM&&!d.wakeup&&!irq_calls); + /* It keeps receiving through system sleep, so the client filters wake data. */ + assert(!dockchannel_request_rx(&d,&dc)&&rx_irq_requested==42&&rx_flags==(IRQF_NO_AUTOEN|IRQF_NO_SUSPEND)); }else if(!strcmp(argv[1],"sep")){ domain_count=1;assert(!sep_pm_keep_domains(&d)&&!attach_calls); domain_count=2;attach_error=-EAGAIN;assert(sep_pm_keep_domains(&d)==-EAGAIN&&!pd0.syscore); @@ -127,39 +158,78 @@ def function(source, name): cleanup(cleanup_data);assert(!pd0.syscore&&!pd1.syscore); action_error=-ENOMEM;assert(sep_pm_keep_domains(&d)==-ENOMEM&&!pd0.syscore&&!pd1.syscore); }else if(!strcmp(argv[1],"usb")){ - for(unsigned fixed=0;fixed<2;fixed++)for(unsigned role=DWC3_APPLE_HOST;role<=DWC3_APPLE_DEVICE;role++){ - struct dwc3_apple a={.dev=&d,.fixed_hub=fixed,.state=DWC3_APPLE_PROBE_PENDING}; - assert(!dwc3_apple_init(&a,role)&&d.syscore&&a.state==role); - assert(!dwc3_apple_exit(&a)&&!d.syscore&&a.state==DWC3_APPLE_NO_CABLE); - unsigned n=core_exits;assert(!dwc3_apple_exit(&a)&&core_exits==n); - core_error=-EIO;assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore);core_error=0; - host_error=gadget_error=-ENOMEM;n=core_exits;assert(dwc3_apple_init(&a,role)==-ENOMEM&&!d.syscore&&core_exits==n+1);host_error=gadget_error=0; - assert(!dwc3_apple_init(&a,role)&&d.syscore);reset_error=-EIO;assert(dwc3_apple_exit(&a)==-EIO&&d.syscore&&a.reset_pending); - reset_error=0;n=core_exits;unsigned exits=role_exits; - assert(!dwc3_apple_exit(&a)&&!d.syscore&&!a.reset_pending); + for(unsigned fixed=0;fixed<2;fixed++)for(unsigned host_only=0;host_only<2;host_only++)for(unsigned role=DWC3_APPLE_HOST;role<=DWC3_APPLE_DEVICE;role++){ + struct dwc3_apple a={.dev=&d,.fixed_hub=fixed,.host_only=host_only,.state=DWC3_APPLE_PROBE_PENDING}; + unsigned puts=rpm_puts; + assert(!dwc3_apple_init(&a,role)&&d.syscore&&a.state==role&&a.rpm_held); + /* Only a host-only host start marks the role before the core comes up. */ + assert(start_role==(host_only&&role==DWC3_APPLE_HOST?DWC3_GCTL_PRTCAP_HOST:0)); + assert(!dwc3_apple_exit(&a)&&!d.syscore&&a.state==DWC3_APPLE_NO_CABLE&&!a.rpm_held&&rpm_puts==puts+1); + unsigned n=core_exits;assert(!dwc3_apple_exit(&a)&&core_exits==n&&rpm_puts==puts+1); + core_error=-EIO;assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore&&!a.rpm_held);core_error=0; + host_error=gadget_error=-ENOMEM;n=core_exits;assert(dwc3_apple_init(&a,role)==-ENOMEM&&!d.syscore&&core_exits==n+1&&!a.rpm_held&&rpm_puts==puts+2);host_error=gadget_error=0; + /* A refused PHY role stops the core again before any host or gadget start. */ + role_error=-EIO;n=core_exits;unsigned exits=role_exits; + assert(dwc3_apple_init(&a,role)==-EIO&&!d.syscore&&!a.core_active&&!a.rpm_held&&core_exits==n+1&&role_exits==exits&&a.state==DWC3_APPLE_NO_CABLE&&rpm_puts==puts+3);role_error=0; + assert(!dwc3_apple_init(&a,role)&&d.syscore);reset_error=-EIO;assert(dwc3_apple_exit(&a)==-EIO&&d.syscore&&a.reset_pending&&a.rpm_held&&rpm_puts==puts+3); + reset_error=0;n=core_exits;exits=role_exits; + assert(!dwc3_apple_exit(&a)&&!d.syscore&&!a.reset_pending&&!a.rpm_held&&rpm_puts==puts+4); assert(core_exits==n&&role_exits==exits); } }else if(!strcmp(argv[1],"regulatory")){ + enum {N25=MT7932_CHANNELS_2G+MT7932_CHANNELS_5G,SIX13=N25+3,VLP=N25+5}; struct mt7932 m={.reg_generation=7};struct wiphy w={.priv=&m};m.wiphy=&w; struct mt7932_policy p={};u8 tables[9][1020]={}; - for(unsigned i=0;i<9;i++){p.table[i+1]=tables[i];tables[i][4]=0;} - for(unsigned i=0;iband=ihw_value=imax_power=20; memset(tables[t]+44+j*122,0xc4,122);tables[t][44+j*122]=c->hw_value; if(i!=10)tables[t][45+j*122]=1; } - assert(!mt7932_policy_permits(&p,11)&&mt7932_policy_permits(&p,10)&&mt7932_policy_permits(&p,165)&&!mt7932_policy_permits(&p,14)); - struct regulatory_request request={.alpha2={'U','S'}};mt_regulatory_notify(&w,&request);m.reg_pending=false; - unsigned generation=m.reg_generation;mt_policy_disable(&m,&p,generation-1);assert(!m.channels[10].flags&&!m.policy_disabled[0]); - mt_policy_disable(&m,&p,generation);assert(m.channels[10].flags==1&&m.policy_disabled[0]==(1ULL<<10)); - struct mt7932_reg_snapshot reg=m.reg_desired;mt7932_policy_filter(®,&p);assert(reg.length==12+(MT7932_CHANNELS-1)*8&®.domain[8]==12&®.domain[9]==MT7932_CHANNELS_5G); - for(unsigned i=0;iband=NL80211_BAND_6GHZ;c->hw_value=mt7932_channel_6g(k);c->center_freq=5950+5*c->hw_value; + c->max_power=14;c->psd=-1;c->flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD|(N25+k==VLP?IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT:0); + } + assert(!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,11)&&mt7932_policy_permits(&p,NL80211_BAND_2GHZ,10)&&mt7932_policy_permits(&p,NL80211_BAND_5GHZ,165)&&!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,14)); + /* Channel numbers mean nothing without their band. */ + assert(mt7932_policy_permits(&p,NL80211_BAND_2GHZ,13)&&!mt7932_policy_permits(&p,NL80211_BAND_6GHZ,13)&&mt7932_policy_permits(&p,NL80211_BAND_6GHZ,1)); + assert(!mt7932_policy_permits(&p,NL80211_BAND_5GHZ,1)&&!mt7932_policy_permits(&p,NL80211_BAND_2GHZ,36)&&!mt7932_policy_permits(&p,NL80211_BAND_6GHZ,3)&&!mt7932_policy_permits(&p,NUM_NL80211_BANDS,1)); + struct regulatory_request request={.alpha2={'U','S'}};rule_lookups=0;mt_regulatory_notify(&w,&request);m.reg_pending=false; + assert(rule_lookups==MT7932_CHANNELS_6G); + /* The VLP-client channel stays out of the firmware domain; the others lose NO_IR there. */ + assert(m.reg_desired.length==12+(MT7932_CHANNELS-1)*8&&m.reg_desired.domain[8]==13&&m.reg_desired.domain[9]==MT7932_CHANNELS_5G&&m.reg_desired.domain[10]==MT7932_CHANNELS_6G-1); + for(unsigned i=0;ihw_value&&!f); + else{unsigned k=i-N25+(i>=VLP);assert(get_unaligned_le16(e)==mt7932_channel_6g(k)&&f==IEEE80211_CHAN_PSD);} + } + for(unsigned k=0;k=N25-1?c!=13:true);} + /* A rule that keeps NO_IR, or no rule at all, leaves the channel passive. */ + m.channels6[0].flags|=IEEE80211_CHAN_NO_IR;m.channels6[1].flags|=IEEE80211_CHAN_NO_IR; + rule6.flags=NL80211_RRF_NO_IR;mt_policy_disable(&m,&p,generation);assert(m.channels6[0].flags&IEEE80211_CHAN_NO_IR); + rule6.flags=0;rule6_absent=true;mt_policy_disable(&m,&p,generation);assert(m.channels6[1].flags&IEEE80211_CHAN_NO_IR); + rule6_absent=false;mt_policy_disable(&m,&p,generation);assert(!(m.channels6[0].flags&IEEE80211_CHAN_NO_IR)&&!(m.channels6[1].flags&IEEE80211_CHAN_NO_IR)); + m.channels[10].flags=0;m.channels6[3].flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD;mt_regulatory_notify(&w,&request); + assert(m.reg_generation==generation&&m.channels[10].flags==1&&(m.channels6[3].flags&IEEE80211_CHAN_DISABLED)); + m.channels[10].flags=0;m.channels6[3].flags=IEEE80211_CHAN_NO_IR|IEEE80211_CHAN_PSD;request.alpha2[0]='C';request.alpha2[1]='A';mt_regulatory_notify(&w,&request); + assert(m.reg_generation==generation+1&&!m.policy_disabled[0]&&!m.policy_disabled[1]); m.reg_pending=false;m.connected=true;unsigned applied=m.reg_generation; put_unaligned_le32(IEEE80211_CHAN_NO_IR,m.reg_desired.domain+16); mt_regulatory_notify(&w,&request); @@ -178,7 +248,7 @@ def main(): parser.add_argument('--output', type=Path, required=True) parser.add_argument('--old-usb', action='store_true') parser.add_argument('--old-regulatory', action='store_true') - parser.add_argument('--mutation', choices=['sep-syscore', 'wake-irq', 'stale-policy']) + parser.add_argument('--mutation', choices=['sep-syscore', 'wake-irq', 'rx-suspend', 'stale-policy', 'usb-rpm', 'vlp-client', 'six-band']) args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) files = { @@ -199,16 +269,33 @@ def main(): ROOT/'drivers/net/wireless/mediatek/mt7932/regulatory.h'] header = '\n'.join(re.sub(r'^#include .*\n', '', path.read_text(), flags=re.M) for path in headers) + # The receive IRQ request statement from dockchannel_init(), run against a recording stub. + dock = (ROOT/'drivers/soc/apple/dockchannel.c').read_text() + request = re.search(r'\tret = devm_request_threaded_irq\(dev, dockchannel->rx_irq,.*?\);\n', dock, re.S) + if not request: + raise ValueError('dockchannel RX IRQ request') + bodies['dockchannel_request_rx'] = ('static int dockchannel_request_rx(struct device *dev, struct dockchannel *dockchannel)\n' + '{\n\tint ret;\n\n' + request[0] + '\treturn ret;\n}\n') at = PREFIX.index('struct mt7932 {') code = PREFIX[:at] + header + PREFIX[at:] + '\n'.join(bodies.values()) + MAIN if not args.old_usb: code = code.replace(',DWC3_APPLE_SUSPENDED', '') - if args.mutation == 'sep-syscore': - code = code.replace('dev_pm_syscore_device(list->pd_devs[i], true);', 'dev_pm_syscore_device(list->pd_devs[i], false);') - if args.mutation == 'wake-irq': - code = code.replace('return devm_pm_set_wake_irq(dockchannel->dev, dockchannel->rx_irq);', 'return 0;') - if args.mutation == 'stale-policy': - code = code.replace('generation != READ_ONCE(m->reg_generation)', 'false') + mutations = { + 'sep-syscore': ('dev_pm_syscore_device(list->pd_devs[i], true);', 'dev_pm_syscore_device(list->pd_devs[i], false);'), + 'wake-irq': ('return devm_device_init_wakeup(dockchannel->dev);', + 'return devm_device_init_wakeup(dockchannel->dev) ?: devm_pm_set_wake_irq(dockchannel->dev, dockchannel->rx_irq);'), + 'rx-suspend': ('IRQF_NO_AUTOEN | IRQF_NO_SUSPEND', 'IRQF_NO_AUTOEN'), + 'stale-policy': ('generation != READ_ONCE(m->reg_generation)', 'false'), + 'usb-rpm': ('pm_runtime_put_autosuspend(appledwc->dev);', '(void)0;'), + 'vlp-client': ('if (flags & IEEE80211_CHAN_NO_6GHZ_VLP_CLIENT)', 'if (0)'), + 'six-band': ('(policy->six_channels & BIT_ULL((channel - 1) / 4))', 'true'), + } + if args.mutation: + old, new = mutations[args.mutation] + # A mutation that no longer matches the driver would test nothing. + if code.count(old) != 1: + parser.error(args.mutation + ' mutation no longer matches the driver source') + code = code.replace(old, new) source = args.output/'controls.c'; source.write_text(code) binary = args.output/'controls' subprocess.run(['cc', '-std=gnu11', '-g', '-Wall', '-Werror', '-Wno-unused-function', From 937e9b0336e870bff457eba617d0204f394dd65a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:00:01 +1100 Subject: [PATCH 10/13] selftests: apple-afkv2: follow compact EPIC and reply lengths in the HPD, sender and EDID tests Since the HPD reply check (e8f48a64e1b1), driver work reached this tree that the extracted units never followed, so three of them stopped compiling. The 14.7 external role closes its AV client on an HPD low (cadaaeb5e040). Compact EPIC on desktop 26.x endpoints (8e82151640fc, 99870822214e) gave the sender a compact path, the HPD request a status word reply and the legacy EDID reader a 2 KiB request. The sender gained a legacy reply-length interface on top of afk_send_command_common() (e6c10627c55e), and the legacy EDID reader now bounds the EDID by the reported reply length (48cbf951457b). HPD: count dcpavserv_v14_close() calls and check that only an HPD low closes the AV client, before the request goes out. On compact EPIC check that a zero status is accepted without an echo and that any nonzero status is refused. Sender: extract afk_send_command_common(), the reply-length wrapper and the compact status allocator. Legacy reply lengths go through the real completer, and the test checks the bounded copy with zero fill, the oversized refusal, the U32 overflow and NULL guards, and that AFK-v2 refuses the interface without sending. Compact requests check the status echo mapping and that no DMA or tag slot is used. EDID: model the reply-length call and the compact buffer size. The legacy reader keeps its checksum tolerance but now refuses a truncated reply, a wrong max_size echo, partial blocks and a short block count. Its compact request asks for 2 KiB and expects that size echoed. Add EOVERFLOW and EOPNOTSUPP to the host errno header. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .../drivers/gpu/apple-afkv2/extract-edid.py | 46 +++++-- .../drivers/gpu/apple-afkv2/extract-hpd.py | 79 ++++++++++-- .../drivers/gpu/apple-afkv2/extract-send.py | 119 ++++++++++++++---- .../apple-afkv2/host-include/linux/errno.h | 2 + 4 files changed, 202 insertions(+), 44 deletions(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py index 0d554d1c82b5d..bcced2f684cfa 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py @@ -29,12 +29,17 @@ def function(name): #define kzalloc(n,f) calloc(1,n) #define cpu_to_le64(x) (x) #define le64_to_cpu(x) (x) +#define READ_ONCE(x) (x) +#define KUNIT_STATIC_STUB_REDIRECT(...) do {} while (0) +#define SZ_2K 2048U +#define EDID_COMPACT_BUF_SIZE (EDID_LEADING_DATA_SIZE + SZ_2K) #define EDID_EXT_BLOCK_COUNT_OFFSET 0x7e #define ERR_PTR(x) ((void*)(intptr_t)(x)) #define IS_ERR(p) ((uintptr_t)(p)>(uintptr_t)-4096) static void cleanup_free(void *p) { free(*(void**)p); } #define __free(x) __attribute__((cleanup(cleanup_free))) -struct apple_epic_service { int unused; }; +struct ep { bool compact; }; +struct apple_epic_service { struct ep *ep; }; struct drm_edid { size_t size; }; struct dpavserv_copy_edid_cmd { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8]; }; struct dpavserv_copy_edid_resp { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8];u8 data[]; }; @@ -42,26 +47,36 @@ def function(name): static bool truncated; static const struct drm_edid *drm_edid_alloc(const void *p,size_t n) { struct drm_edid *e=malloc(sizeof(*e));(void)p;e->size=n;return e; } -static int afk_service_call(struct apple_epic_service *s,u16 group,u32 command, +/* The request asks for the buffer the transport can carry: 2 KiB on compact EPIC. */ +static size_t request_size(const struct apple_epic_service *s, bool inline_call) +{ return !inline_call && s->ep->compact ? EDID_COMPACT_BUF_SIZE : EDID_BUF_SIZE; } +static int edid_call(struct apple_epic_service *s,bool inline_call,u16 group,u32 command, const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len,size_t output_pad) { const struct dpavserv_copy_edid_cmd *cmd=data; - (void)s;(void)data_pad;(void)output_pad; - assert(group==1 && command==7 && data_len==48 && cmd->max_size==EDID_BUF_SIZE); + size_t want=request_size(s,inline_call); + assert(group==1 && command==7 && data_len==48 && cmd->max_size==want && data_pad==want); + assert(output_len==48+want && output_pad==0); assert(output_len>=sizeof(fixture));memcpy(output,fixture,sizeof(fixture));return 0; } static int afk_service_call_len(struct apple_epic_service *s,u16 group,u32 command, const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len, size_t output_pad,size_t *received) -{ int ret=afk_service_call(s,group,command,data,data_len,data_pad,output,output_len,output_pad); +{ int ret=edid_call(s,true,group,command,data,data_len,data_pad,output,output_len,output_pad); *received=sizeof(fixture)-(truncated?1:0);return ret; } +/* Legacy EPIC: the reply length the firmware reported. */ +static int afk_service_call_with_reply_len(struct apple_epic_service *s,u16 group,u32 command, + const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len, + size_t output_pad,size_t *reply_len) +{ int ret=edid_call(s,false,group,command,data,data_len,data_pad,output,output_len,output_pad); + *reply_len=sizeof(fixture)-(truncated?1:0);return ret; } static void fix_checksum(u8 *p) { unsigned int i;u8 sum=0;p[127]=0;for(i=0;i<127;i++)sum+=p[i];p[127]=-sum; } ''' suffix = r''' int main(void) { - struct apple_epic_service service={0};const struct drm_edid *e; + struct ep ep={0};struct apple_epic_service service={.ep=&ep};const struct drm_edid *e; u8 *base=fixture+56,*extension=base+128; afkv2_put64(fixture,EDID_BUF_SIZE);afkv2_put64(fixture+32,8+256); memset(base+1,255,6);base[126]=1;fix_checksum(base); @@ -72,8 +87,23 @@ def function(name): e=dcpavserv_read_edid_inline(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); truncated=true; e=dcpavserv_read_edid_inline(&service);assert(IS_ERR(e)); - e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e));free((void*)e); - puts("PASS: actual legacy EDID checksum tolerance preserved; inline checksum/actual-length validation enforced"); + /* The legacy reader now bounds the EDID by the reported reply length too. */ + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + truncated=false; + /* Its max_size echo must match the request, and the EDID must be whole blocks. */ + afkv2_put64(fixture,EDID_BUF_SIZE-1); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture,EDID_BUF_SIZE);afkv2_put64(fixture+32,8+200); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture+32,8+128); + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); /* base block announces one extension */ + afkv2_put64(fixture+32,8+256); + /* Compact EPIC asks for 2 KiB and expects that size echoed. */ + ep.compact=true; + e=dcpavserv_read_edid_legacy(&service);assert(IS_ERR(e)); + afkv2_put64(fixture,EDID_COMPACT_BUF_SIZE); + e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); + puts("PASS: actual legacy EDID checksum tolerance preserved with reply-length, echo and block bounds (legacy and compact); inline checksum/actual-length validation enforced"); } ''' pathlib.Path(sys.argv[2]).write_text(prefix + function("dcpavserv_read_edid_legacy") + function("dcpavserv_read_edid_inline") + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py index 6957d92f390e2..b21cba6c7665c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Exercise the production DPTX HPD request with mocked service-call replies.""" +"""Exercise the production DPTX HPD request with mocked service-call replies. + +Covers the legacy, AFK-v2 and compact EPIC replies and the 14.7 AV client close. +""" import pathlib import re import sys @@ -44,12 +47,19 @@ def function(source, name): #define EPIC_SUBTYPE_STD_SERVICE 0xc0 #define dev_warn(...) do {} while (0) #define dev_dbg(...) do {} while (0) +#define READ_ONCE(x) (x) +static u32 get_unaligned_le32(const void *p) +{ + const u8 *b = p; + + return b[0] | b[1] << 8 | b[2] << 16 | (u32)b[3] << 24; +} struct epic_service_call { u8 pad0[2]; u16 group; u32 command, data_len, magic; u8 pad1[48]; }; struct dcp { void *dev; }; struct ops { const char *name; }; -struct ep { struct dcp *dcp; int endpoint; bool inline_v2; }; +struct ep { struct dcp *dcp; int endpoint; bool inline_v2; bool compact; }; struct apple_epic_service { struct ep *ep; struct ops *ops; u32 channel; }; int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t data_len, size_t data_pad, void *output, @@ -67,7 +77,16 @@ def function(source, name): */ static bool wire_v2; static size_t declared, arrived; -static u32 echo_flip; +static u32 echo_flip, compact_status; +/* An HPD low closes the 14.7 external AV client first; the stub counts it. */ +static struct dcp *expected_dcp; +static unsigned int v14_closes, closes_at_send; +/* Kept even if a mutant drops the call, so the count check is what fails. */ +__attribute__((unused)) static void dcpavserv_v14_close(struct dcp *dcp) +{ + assert(dcp == expected_dcp); + v14_closes++; +} static int afk_send_command_timeout_len(struct apple_epic_service *s, u8 type, const void *payload, size_t payload_len, void *output, size_t output_len, u32 *retcode, unsigned int timeout_ms, size_t *received) @@ -76,15 +95,21 @@ def function(source, name): u8 *body = (u8 *)output + sizeof(*call); u32 echo; - (void)s; (void)payload; (void)timeout_ms; + (void)payload; (void)timeout_ms; assert(type == EPIC_SUBTYPE_STD_SERVICE && payload_len == output_len); assert(output_len == sizeof(*call) + 32 && call->data_len == 32); assert(call->group == 8 && call->command == 8); + closes_at_send = v14_closes; if (wire_v2 && !received && arrived < output_len - sizeof(*call)) return -EMSGSIZE; memcpy(&echo, body + 16, sizeof(echo)); echo ^= echo_flip; memcpy(body + 16, &echo, sizeof(echo)); + /* Compact EPIC answers with an IOReturn status in the first word. */ + if (s->ep->compact) { + for (int i = 0; i < 4; i++) + body[i] = compact_status >> (8 * i); + } call->data_len = declared; *retcode = 0; if (received) @@ -94,12 +119,24 @@ def function(source, name): ''' suffix = r''' static int failures; +static struct ep *the_ep; static void expect(bool ok, const char *what, bool hpd, int ret) { - printf("%s %s: %s hpd=%d declared=%zu arrived=%zu ret=%d\n", ok ? "ok" : "FAIL", - wire_v2 ? "afk-v2" : "legacy", what, hpd, declared, arrived, ret); + printf("%s %s%s: %s hpd=%d declared=%zu arrived=%zu ret=%d\n", ok ? "ok" : "FAIL", + wire_v2 ? "afk-v2" : "legacy", the_ep->compact ? " compact" : "", what, hpd, + declared, arrived, ret); failures += !ok; } +/* Every request closes the AV client exactly when it lowers HPD, and before it is sent. */ +static int set_hpd(struct apple_epic_service *service, int hpd) +{ + unsigned int before = v14_closes; + int ret = dptxport_set_hpd_timeout(service, hpd, 1000); + + expect(v14_closes == before + !hpd && closes_at_send == before + !hpd, + "AV client closed before an HPD low only", hpd, ret); + return ret; +} int main(void) { static const size_t lengths[] = { 0, 4, 8, 12, 16, 20, 32, 36, 65535 }; @@ -108,6 +145,9 @@ def function(source, name): struct ep ep = { .dcp = &dcp }; struct apple_epic_service service = { .ep = &ep, .ops = &ops, .channel = 1 }; + the_ep = &ep; + expected_dcp = &dcp; + for (int v2 = 0; v2 < 2; v2++) { wire_v2 = v2; for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) { @@ -115,7 +155,7 @@ def function(source, name): int ret; declared = lengths[i]; arrived = 32; echo_flip = 0; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); if (declared < sizeof(struct dcpdptx_hotplug_cmd)) expect(ret < 0, "short reply refused", hpd, ret); else if (declared > 32) @@ -128,19 +168,38 @@ def function(source, name): int ret; declared = 20; arrived = 32; echo_flip = 1; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); expect(ret == -EINVAL, "wrong echo refused", hpd, ret); if (!wire_v2) continue; declared = 20; arrived = 16; echo_flip = 0; - ret = dptxport_set_hpd_timeout(&service, hpd, 1000); + ret = set_hpd(&service, hpd); expect(ret < 0, "reply cut short in transit refused", hpd, ret); } + /* Compact EPIC: the status word decides, there is no echo to compare. */ + ep.compact = true; + for (int hpd = 0; hpd < 2; hpd++) { + int ret; + + declared = 20; arrived = 32; echo_flip = 0; compact_status = 0; + ret = set_hpd(&service, hpd); + expect(ret == 0, "zero status accepted", hpd, ret); + echo_flip = 1; + ret = set_hpd(&service, hpd); + expect(ret == 0, "echo word ignored", hpd, ret); + echo_flip = 0; compact_status = 0xe00002c2; + ret = set_hpd(&service, hpd); + expect(ret == -EIO, "firmware error status refused", hpd, ret); + compact_status = 0x00000100; + ret = set_hpd(&service, hpd); + expect(ret == -EIO, "any nonzero status byte refused", hpd, ret); + } + ep.compact = false; compact_status = 0; } printf("%d HPD reply checks failed\n", failures); if (failures) return 1; - puts("PASS: extracted production DPTX HPD request: short, oversized, wrong-echo and complete replies, legacy and AFK-v2"); + puts("PASS: extracted production DPTX HPD request: short, oversized, wrong-echo and complete replies, legacy, AFK-v2 and compact status, AV client close on HPD low"); return 0; } ''' diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py index 70dbd5d3a69f4..eb55dd281456f 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only OR MIT -"""Compile the real command sender against synthetic ring and DMA operations.""" +"""Compile the real command sender against synthetic ring and DMA operations. + +Covers AFK-v2, legacy reply-length and compact EPIC requests. +""" import pathlib import re import sys source = pathlib.Path(sys.argv[1]).read_text() -match = re.search(r"static int afk_send_command_timeout_len\([^;]*?\)\n\{", source) -if not match: - raise RuntimeError("production command sender not found") -start, end, depth = match.start(), match.end(), 1 -while depth: - depth += (source[end] == "{") - (source[end] == "}") - end += 1 -reply_match = re.search(r"static void afk_recv_handle_reply\([^;]*?\)\n\{", source) -if not reply_match: - raise RuntimeError("production reply completer not found") -reply_end, reply_depth = reply_match.end(), 1 -while reply_depth: - reply_depth += (source[reply_end] == "{") - (source[reply_end] == "}") - reply_end += 1 + + +def function(pattern, what): + match = re.search(r"^" + pattern + r"\([^;]*?\)\n\{", source, re.M) + if not match: + raise RuntimeError("production " + what + " not found") + end, depth = match.end(), 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + "\n" + + prefix = r''' #undef NDEBUG #include @@ -27,7 +29,10 @@ #include #include #include "afkv2-codec.h" +#include "afk_reply.h" #define SZ_1M 1048576U +#define U32_MAX 0xffffffffU +#define MSEC_PER_SEC 1000U #define GFP_KERNEL 0 #define MAX_PENDING_CMDS 16 #define AFKV2_INTERFACES (1U << 16) @@ -55,10 +60,13 @@ struct cmd_info { u16 tag; u8 type; size_t reply_length; void *rxbuf,*txbuf; dma_addr_t rxbuf_dma,txbuf_dma; size_t rxlen,txlen; u32 retcode; + size_t reply_len; bool reply_len_valid; bool done,free_on_ack; int *completion; }; -struct dcp { void *dev; bool quiescing; }; -struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2; int endpoint,lock;u8 *command_tags; }; +struct dcp { void *dev; bool quiescing,external; void *rtk; }; +struct afk_ringbuffer { bool ready; }; +struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2,compact; int endpoint,lock;u8 *command_tags; + struct afk_ringbuffer txbfr,rxbfr; }; struct ops { const char *name; }; struct apple_epic_service { struct apple_dcp_afkep *ep; struct ops *ops; unsigned int channel; unsigned long cmd_map[1]; @@ -68,6 +76,25 @@ static unsigned int frees, sends; static int wire_fail, timeout, reply_status; static size_t owned_reply_size=32888; +/* Legacy replies can go through the real completer, as from the ring. */ +static bool real_reply; +static u32 legacy_rxlen; +static void afk_recv_handle_reply(struct apple_dcp_afkep *ep,u32 channel,u16 tag,void *payload,size_t payload_size); +/* Compact EPIC goes inline through the ring; this mock stands for that transport. */ +static bool compact_echo; +static u32 compact_reply,compact_request; +static size_t compact_copied; +static unsigned int compact_sends; +static int afk_send_compact_command(struct apple_epic_service *s,u8 type,const void *payload,size_t payload_len, + void *output,size_t output_len,u32 request_status,u32 *retcode,size_t *copied,unsigned long timeout) +{ + (void)timeout; + assert(s==active && type==0xc0 && payload_len==112 && ((const u8 *)payload)[0]==0x5a && output_len==32888); + compact_sends++;compact_request=request_status; + *retcode=compact_echo?request_status:compact_reply; + memset(output,0x33,compact_copied);*copied=compact_copied; + return 0; +} static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) { if(!s->enabled) return NULL; s->users++; return s; } static void afk_service_put(struct apple_epic_service *s) { assert(s->users); s->users--; } @@ -100,6 +127,14 @@ } if(wire_fail)return -ENOSPC; if(timeout)return 0; + if(real_reply) { + struct epic_cmd reply={.retcode=reply_status,.rxlen=legacy_rxlen}; + assert(!ep->wire_v2); + memset(c->rxbuf,0x77,legacy_rxlenrxlen?legacy_rxlen:c->rxlen); + afk_recv_handle_reply(ep,channel,tag,&reply,sizeof(reply)); + assert(c->done && c->reply_len_valid && c->reply_len==legacy_rxlen && *c->completion==1); + return 0; + } c->reply_length=ep->inline_v2?4:owned_reply_size;c->retcode=reply_status; memset(c->rxbuf,0x77,c->reply_length);c->done=true;*c->completion=1; return 0; @@ -139,6 +174,39 @@ ep.wire_v2=false;owned_reply_size=4; assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); assert(received==32888 && s.users==1); + /* Legacy EPIC reply lengths: bounded by the buffer, rest of the output zeroed. */ + real_reply=true;legacy_rxlen=4;size_t rlen=99;memset(output,0x19,32888); + assert(!afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)); + assert(rlen==4 && status==0 && output[0]==0x77 && output[3]==0x77 && !output[4] && !output[32887]); + assert(!s.cmd_map[0] && s.users==1); + legacy_rxlen=32889;rlen=99; + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)==-EMSGSIZE); + assert(rlen==0 && !s.cmd_map[0] && s.users==1); + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,NULL)==-EINVAL); + unsigned int before_sends=sends; + assert(afk_send_command_with_reply_len(&s,0xc0,request,(size_t)U32_MAX+1,output,32888,&status,&rlen)==-EOVERFLOW); + /* AFK-v2 reports reply lengths its own way and refuses this interface. */ + ep.wire_v2=true; + assert(afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen)==-EOPNOTSUPP); + assert(sends==before_sends && s.users==1 && !s.cmd_map[0]); + real_reply=false; + /* Compact EPIC: success is the reply echoing the request's own status. */ + ep.compact=true;unsigned int before_frees=frees;status=99;received=99; + compact_echo=true;compact_copied=20; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(status==0 && received==20 && output[0]==0x33 && compact_sends==1); + u32 first=compact_request; + compact_echo=false;compact_reply=0xe00002c2; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(status==0xe00002c2 && compact_request==((((first>>8)+1)&0xff)<<8)); + compact_reply=0; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(compact_request && status==~0U); + compact_echo=true;compact_copied=8;rlen=99; + assert(!afk_send_command_with_reply_len(&s,0xc0,request,112,output,32888,&status,&rlen) && rlen==8 && !status); + assert(compact_sends==4 && sends==before_sends && frees==before_frees && s.users==1 && !s.cmd_map[0]); + ep.compact=false; + dcp.external=true;ep.txbfr.ready=ep.rxbfr.ready=true; ep.wire_v2=true;owned_reply_size=32888;timeout=1; assert(afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)==-ETIMEDOUT); assert(s.cmd_map[0] && s.cmds[0].free_on_ack && !s.cmds[0].completion); @@ -155,14 +223,13 @@ s.enabled=true;s.torndown=false;timeout=0; assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); assert(!s.cmd_map[0] && s.users==1);free(output); - puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, short OOB, strict/legacy callers and actual late-ACK ownership"); + puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, short OOB, strict/legacy callers, legacy reply lengths, compact status and actual late-ACK ownership"); } ''' -tag_match = re.search(r"static u16 afk_command_tag\([^;]*?\)\n\{", source) -if not tag_match: - raise RuntimeError("production interface tag allocator not found") -tag_end, tag_depth = tag_match.end(), 1 -while tag_depth: - tag_depth += (source[tag_end] == "{") - (source[tag_end] == "}") - tag_end += 1 -pathlib.Path(sys.argv[2]).write_text(prefix + source[tag_match.start():tag_end] + source[start:end] + source[reply_match.start():reply_end] + suffix) +bodies = [function(r"static u32 afk_compact_next_status", "compact status allocator"), + function(r"static u16 afk_command_tag", "interface tag allocator"), + function(r"static int afk_send_command_common", "command sender"), + function(r"static int afk_send_command_timeout_len", "length-aware sender"), + function(r"int afk_send_command_with_reply_len", "legacy reply-length sender"), + function(r"static void afk_recv_handle_reply", "reply completer")] +pathlib.Path(sys.argv[2]).write_text(prefix + "".join(bodies) + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h index 23e11801f1e76..e7a1050305b68 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h @@ -8,3 +8,5 @@ #define ENODEV 19 #define ETIMEDOUT 110 #define ENOMEM 12 +#define EOVERFLOW 75 +#define EOPNOTSUPP 95 From 76378dd6ec7af7beae4a0ae11e15089703ff9564 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:01:17 +1100 Subject: [PATCH 11/13] ci: run every host test on each push and pull request The host tests extract production code from this tree and build it on the host. Nothing ran them automatically, so a driver change could break one while the kernel build stayed green, and by 2026-10-30 seventeen of them had stopped passing. Add tools/asahi/run-host-tests.py. It runs every tools/asahi test under python3 -I in its own directory, together with its mutation and old-code controls, plus the Rust and shell tests there and the apple-dcp, apple- afkv2 (gcc and clang, with sanitizers), asahi-g15 and asahi-m4 selftests. A mutation only counts as killed when the built control reports the failure, so a mutant that just stops compiling fails the run. A tools/asahi test that is not registered in the runner also fails it, so new tests cannot be skipped by accident. Add a workflow that runs it on ubuntu-24.04 with pinned Python 3.14, rustc 1.96.0, clang 18 and gcc 13, for every push and pull request on integration/aurora and aurora-wip. The checkout keeps the commit history without trees so the old-code controls can read their revisions. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .github/workflows/host-tests.yml | 87 +++++++++++ tools/asahi/run-host-tests.py | 250 +++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 .github/workflows/host-tests.yml create mode 100755 tools/asahi/run-host-tests.py diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml new file mode 100644 index 0000000000000..73a3c67c1842b --- /dev/null +++ b/.github/workflows/host-tests.yml @@ -0,0 +1,87 @@ +name: Host tests + +# Every host-side test of the Aurora drivers: tools/asahi (with their mutation +# and old-code controls) and the apple-dcp, apple-afkv2, asahi-g15 and asahi-m4 +# selftests. They extract production code from this tree and build it on the +# host, so a driver change can break them while the kernel build stays green. + +on: + push: + branches: [integration/aurora, aurora-wip] + pull_request: + branches: [integration/aurora, aurora-wip] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: host-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + RUST_VERSION: '1.96.0' + CLANG_VERSION: '18' + GCC_VERSION: '13' + +jobs: + host-tests: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + # Negative controls read older revisions with `git show`, so keep the + # whole history. Trees and blobs of old commits load on demand. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Cache the Rust toolchain + uses: actions/cache@v4 + with: + path: ~/.rustup/toolchains/${{ env.RUST_VERSION }}-x86_64-unknown-linux-gnu + key: rustup-${{ runner.os }}-${{ env.RUST_VERSION }} + + - name: Pin the host toolchains + run: | + rustup toolchain install "$RUST_VERSION" --profile minimal --no-self-update + echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> "$GITHUB_ENV" + need= + for p in clang-$CLANG_VERSION gcc-$GCC_VERSION flex bison bc; do + command -v "$p" >/dev/null || need="$need $p" + done + dpkg -s "libclang-rt-$CLANG_VERSION-dev" >/dev/null 2>&1 || need="$need libclang-rt-$CLANG_VERSION-dev" + if [ -n "$need" ]; then + sudo apt-get update -q + sudo apt-get install -y -q $need + fi + # The tests call clang, cc, gcc and rustc by name. + bin="$RUNNER_TEMP/toolchain" + mkdir -p "$bin" + ln -sf "$(command -v clang-$CLANG_VERSION)" "$bin/clang" + ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/gcc" + ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/cc" + echo "$bin" >> "$GITHUB_PATH" + + - name: Show the toolchains + run: | + python3 --version + rustc --version + clang --version | head -1 + cc --version | head -1 + git --version + + - name: Run the host tests + run: python3 -I tools/asahi/run-host-tests.py --jobs "$(nproc)" --work "$RUNNER_TEMP/host-tests" + + - name: Keep the logs of a failed run + if: failure() + uses: actions/upload-artifact@v4 + with: + name: host-test-logs + path: ${{ runner.temp }}/host-tests/**/log + retention-days: 14 diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py new file mode 100755 index 0000000000000..bcc42fe042c0f --- /dev/null +++ b/tools/asahi/run-host-tests.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Run every Aurora host test: tools/asahi and the GPU/display selftests. + +Each tools/asahi test runs under `python3 -I` with its own output directory, +once as a control and once per mutation or old-code negative control listed +below. A mutation run is only accepted as a kill when the test reports one +(an assertion, an abort or a panic of the built control), so a mutant that +merely stops compiling is a failure, not a pass. + +Every tools/asahi/*-tests.{py,rs,sh} file must appear here. A new test that +is not registered fails the run, so it cannot be silently skipped. + +Negative controls read older revisions with `git show`, so the checkout needs +the commits (a full or treeless clone, not a depth-1 one). + +Usage: tools/asahi/run-host-tests.py [--jobs N] [--work DIR] [--only SUBSTR] +""" +import argparse +import concurrent.futures +import os +import re +import shlex +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +ASAHI = ROOT / 'tools/asahi' +GPU = ROOT / 'tools/testing/selftests/drivers/gpu' + +PASS = 'pass' +KILL = 'kill' + +# A killed mutant shows the failing control: a C assertion or abort (-6), or +# a Rust panic (exit 101). A Python traceback means the harness itself broke. +KILL_SIGN = re.compile(r'Assertion|": -6\b|"exit": 101\b|panicked at') +BROKEN_SIGN = re.compile(r'Traceback \(most recent call last\)') + + +def out(flag='--out'): + return [flag, '{out}'] + + +def mutations(flag, names, base, expect=KILL): + return [(base + [flag, n], expect) for n in names] + + +# test file -> [(arguments, expectation)]. '{out}' is a fresh directory. +ASAHI_TESTS = { + 'apple-dart-fw-tests.py': [(out(), PASS)], + 'apple-dart-range-tests.py': [([], PASS)], + 'atc-usb3-tests.py': [(out(), PASS), (out() + ['--mutations'], PASS)], + 'avd-av1-tail-tests.py': [(out(), PASS)] + + [(out() + ['--case', c], PASS) for c in ('copy', 'seed', 'aligned')] + + mutations('--mutation', ('round-up', 'cpu-unaligned', 'seg-cpu-unaligned', + 'seg-any-size', 'ref-index-upper', 'frame-width-unchecked'), out()), + 'avd-codec-tail-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + # The old revision is the one before the J493 hardware-table ID match. + 'bcm4377-id-tests.py': [(out() + ['--old-ref', '87ddae1397f6f70817cb57df76477a86bdf96424'], PASS)], + # These mutations are caught inside the test, which then exits 0. + 'bcm4377-resume-tests.py': [(out(), PASS)] + + mutations('--mutation', ('epoch', 'close', 'producer-fence', 'active-fence', 'board'), out(), PASS), + 'brcmf-assoc-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('short-ie', 'header-length', 'skb-capacity', 'cmplt-header'), out('--output')), + 'brcmf-bss-tests.py': [(out(), PASS)], + 'brcmf-pcie-dma-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('narrow-stream', 'ignore-aperture', 'revision-wide'), out('--output')), + 'brcmf-roam-tests.py': [(out(), PASS)], + 'brcmfmac-scan-timeout-tests.py': [(out('--output'), PASS)], + 'clpc-gpu-demand-tests.py': [([], PASS)], + # The old policy always disabled compute preemption. + 'compute-preempt-tests.py': [(out(), PASS), + (out() + ['--old-ref', '6dbfbcb21eb09d07ae59037a1ceca8cc3500841f'], PASS)], + 'dcp14-backlight-tests.py': [([], PASS)], + 'dcp14-panel-tests.py': [([], PASS)], + 'devcoredump-init-tests.py': [([], PASS)], + 'display26-tests.py': [([], PASS)], + # The old revision publishes the fd before copy_to_user() succeeds. + 'dma-heap-fd-tests.py': [(out() + ['--old-ref', 'cb732d027aa18e1fcf9d2797f47d20b179ebc59c'], PASS)], + 'g15-input-error-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS), + (out('--output') + ['--ref', 'ede99b3c1990b4a4a8c045fdf627a2bd0f14446a'], PASS)], + 'g15-maintained-handoff-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-pb-growth-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-probe-init-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-queue-credit-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-retention-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-stats-drain-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'g15-zero-words-tests.py': [([], PASS), (['--mutations'], PASS)], + 'g17-admission-deadline-tests.py': [([], PASS)], + 'g17-admission-order-tests.py': [([], PASS)], + 'g17-capacity-fault-tests.py': [([], PASS)], + 'g17-compute-constructor-tests.py': [([], PASS)], + 'g17-compute-deadline-tests.py': [([], PASS)], + 'g17-compute-growth-deadline-tests.py': [([], PASS)], + 'g17-firmware-selection-tests.py': [([], PASS)], + 'g17-growth-gate-tests.py': [([], PASS)], + 'g17-growth-tests.py': [([], PASS)], + 'g17-owner-release-order-tests.py': [([], PASS)], + 'g17-pool-release-tests.py': [([], PASS)], + 'g17-resource-tests.py': [([], PASS)], + 'g17-shared-pool-tests.py': [([], PASS)], + 'g17-uninstalled-cleanup-tests.py': [([], PASS)], + 'g17-unpublished-tests.py': [([], PASS)], + 'mt7932-association-events-tests.py': [(out(), PASS)], + 'mt7932-connect-result-tests.py': [(out('--output'), PASS)], + 'mt7932-owe-retry-tests.py': [(out(), PASS)], + 'mt7932-policy-6ghz-tests.py': [([], PASS)], + 'neo-auxiliary-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutation'], KILL), + (out('--output') + ['--old'], KILL)], + 'neo-stability-tests.py': [(out('--output'), PASS)] + + mutations('--mutation', ('sep-syscore', 'wake-irq', 'rx-suspend', 'stale-policy', + 'usb-rpm', 'vlp-client', 'six-band'), out('--output')) + + [(out('--output') + ['--old-usb'], KILL), (out('--output') + ['--old-regulatory'], KILL)], + 'pmp-j613-hwmon-tests.py': [([], PASS)], + 'qspi-nvram-tests.py': [(out('--output'), PASS)], + 'rtkit-oslog-tests.py': [(out(), PASS)] + + mutations('--mutate', ('admission', 'channel', 'counter', 'reset', 'buffer'), out()), + 't6030-clock-tests.py': [(out(), PASS)], + 't6030-display-admission-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-display-linkage-tests.py': [(out('--output') + ['--conf', '{conf}'], PASS)], + 't6030-pmp-graph-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-pmp-supplier-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 't6030-wiring-tests.py': [(out(), PASS), (out() + ['--old-classifier', 'tb'], PASS), + (out() + ['--old-classifier', 'dcp'], PASS)], + 't6040-calibration-tests.py': [([], PASS)], + 'tb-dp-repair-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], + 'tbnet-refill-tests.py': [(out(), PASS)] + + mutations('--mutant', ('refill-count', 'no-retry', 'no-cancel', 'worker-allocation'), out()), + 'tiled-split-tests.py': [(out(), PASS), (out() + ['--mutations'], PASS)], + 'tunnel-route-tests.py': [(out(), PASS)], +} + +# Rust unit-test files built with `rustc --test`, and shell drivers. +ASAHI_RUST = ['m3-batching-tests.rs', 'm3-runtime-tests.rs', 'pmp-bootargs-tests.rs', 'pmp-power-tests.rs'] +ASAHI_SHELL = ['g17-ray-query-timeout-tests.sh'] + +# Python that runs on the host but imports its neighbours, so it keeps the +# script directory on sys.path (-E -s instead of -I). +SUITES = [ + ('apple-dcp', GPU / 'apple-dcp', ['python3', '-E', '-s', '-B', 'run.py', '--output', '{out}']), + ('asahi-g15', GPU / 'asahi-g15', ['python3', '-E', '-s', '-B', 'run.py', '--output', '{out}']), + ('asahi-m4', GPU / 'asahi-m4', ['python3', '-E', '-s', '-B', 'run-host-tests.py']), + ('apple-afkv2-gcc', GPU / 'apple-afkv2', ['sh', 'run-afkv2-tests.sh']), + ('apple-afkv2-clang', GPU / 'apple-afkv2', ['sh', 'run-afkv2-tests.sh']), + ('t8140-external-bw', GPU, ['python3', '-I', '-B', 'test_t8140_external_bw.py']), +] + + +def jobs(work, conf): + found = {p.name for p in ASAHI.iterdir() + 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) + if missing or gone: + for name in missing: + print(f'not registered in {Path(__file__).name}: tools/asahi/{name}', file=sys.stderr) + for name in gone: + print(f'registered but missing: tools/asahi/{name}', file=sys.stderr) + sys.exit(2) + result = [] + # The suites take longest, so start them first. + for name, cwd, cmd in SUITES: + d = work / name + # Only ASan catches an unbounded legacy reply copy, so keep it on. + env = {'AFKV2_SANITIZERS': '1', 'HOSTCC': name.rsplit('-', 1)[1]} if name.startswith('apple-afkv2') else {} + result.append((name, [a.replace('{out}', str(d / 'out')) for a in cmd], cwd, PASS, d, env)) + for name, runs in ASAHI_TESTS.items(): + for index, (args, expect) in enumerate(runs): + label = name.removesuffix('.py') + ('' if not args or args == out() or args == out('--output') + else ' ' + ' '.join(a for a in args if a not in ('{out}', '--out', '--output', '{conf}', '--conf'))) + d = work / f'{name}.{index}' + cmd = ['python3', '-I', '-B', str(ASAHI / name)] + [a.replace('{out}', str(d / 'out')).replace('{conf}', str(conf)) for a in args] + result.append((label, cmd, ASAHI, expect, d, {})) + for name in ASAHI_RUST: + d = work / name + binary = d / 'tests' + cmd = ['sh', '-c', f'{os.environ.get("RUSTC", "rustc")} --edition=2021 --test {shlex.quote(str(ASAHI / name))} ' + f'-o {shlex.quote(str(binary))} && {shlex.quote(str(binary))}'] + result.append((name.removesuffix('.rs'), cmd, ASAHI, PASS, d, {})) + for name in ASAHI_SHELL: + d = work / name + result.append((name.removesuffix('.sh'), ['sh', str(ASAHI / name), str(d / 'out')], ROOT, PASS, d, {})) + return result + + +def run(job, timeout): + label, cmd, cwd, expect, d, extra = job + (d / 'tmp').mkdir(parents=True, exist_ok=True) + env = dict(os.environ, TMPDIR=str(d / 'tmp'), **extra) + start = time.monotonic() + try: + p = subprocess.run(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=timeout) + rc, text = p.returncode, p.stdout + except subprocess.TimeoutExpired as e: + rc, text = 'timeout', (e.stdout or b'').decode(errors='replace') if isinstance(e.stdout, bytes) else (e.stdout or '') + took = time.monotonic() - start + (d / 'log').write_text(f'$ {shlex.join(cmd)}\n{text}\nexit: {rc}\n') + if expect == PASS: + ok = rc == 0 + why = '' if ok else f'exit {rc}' + else: + ok = rc not in (0, 'timeout') and KILL_SIGN.search(text) and not BROKEN_SIGN.search(text) + why = '' if ok else (f'mutant survived (exit {rc})' if rc == 0 else f'exit {rc} without a control failure') + return label, bool(ok), why, took, d / 'log' + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0]) + ap.add_argument('--jobs', type=int, default=os.cpu_count() or 2) + ap.add_argument('--work', type=Path, help='keep logs and build products here (default: a temporary directory)') + ap.add_argument('--conf', type=Path, help='a built scripts/kconfig/conf (default: build one)') + ap.add_argument('--only', help='run only jobs whose name contains this text') + ap.add_argument('--timeout', type=int, default=900) + args = ap.parse_args() + + work = args.work or Path(tempfile.mkdtemp(prefix='aurora-host-tests-')) + work = work.resolve() + work.mkdir(parents=True, exist_ok=True) + conf = args.conf + if conf is None: + build = work / 'kconfig' + subprocess.run(['make', '-s', '-C', str(ROOT), f'O={build}', 'ARCH=arm64', 'allnoconfig'], + check=True, stdout=subprocess.DEVNULL) + conf = build / 'scripts/kconfig/conf' + todo = jobs(work, conf.resolve()) + if args.only: + todo = [j for j in todo if args.only in j[0]] + print(f'{len(todo)} runs, {args.jobs} at a time, logs in {work}', flush=True) + failed = [] + start = time.monotonic() + with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool: + for label, ok, why, took, log in pool.map(lambda j: run(j, args.timeout), todo): + print(f'{"ok" if ok else "FAIL"} {label} ({took:.1f}s){"" if ok else ": " + why}', flush=True) + if not ok: + failed.append((label, log)) + print(f'{len(todo) - len(failed)}/{len(todo)} passed in {time.monotonic() - start:.0f}s') + for label, log in failed: + text = log.read_text(errors='replace').splitlines() + print(f'\n=== {label}: last lines of {log}') + print('\n'.join(text[-40:])) + return 1 if failed else 0 + + +if __name__ == '__main__': + sys.exit(main()) From 08c62cc8d71a6b66fb086b46a76dedebf9a96270 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:08:58 +1100 Subject: [PATCH 12/13] selftests: asahi-m4: find newer UAPI headers in the tree for the layout probe The C layout probe includes the tree's asahi_drm.h, which pulls in drm_mode.h and so . That header only exists in recent kernel UAPI exports. On a host with older ones, such as Ubuntu 24.04's 6.8 linux-libc-dev, the probe stopped compiling and test-uapi failed. Search the tree's include/uapi after the system headers. Headers the host already has still come from the host, and only the missing ones come from the tree. The measured layout is unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- tools/testing/selftests/drivers/gpu/asahi-m4/abi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py index 38c6d7806eabd..7b9bbce635e6e 100644 --- a/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py +++ b/tools/testing/selftests/drivers/gpu/asahi-m4/abi.py @@ -104,7 +104,9 @@ def c_layout(header, bindings, output, tag): lines += [f'printf("{key}=%llu\\n", (unsigned long long)({expr}));' for key, expr in keys] source.write_text('\n'.join(lines + ['return 0;', '}']) + '\n') return run_measurements(shlex.split(os.environ.get('CC', 'cc')) + - ['-std=gnu11', '-Wall', '-Werror', '-D__user='], + ['-std=gnu11', '-Wall', '-Werror', '-D__user=', + # Fall back to the tree for UAPI headers the host lacks. + '-idirafter', str(header.parents[1])], source, output / tag) From 2c5ba39fa5f1ccc0317169f03e4ee96fab88f235 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 18:08:58 +1100 Subject: [PATCH 13/13] ci: host tests: provide ld.lld and use the pinned rustc everywhere The first run on ubuntu-24.04 found two gaps in the toolchain setup. The apple-dcp usb-kconfig case configures the tree with LLVM=1, which needs ld.lld. The g15-pb-growth and g15-stats-drain tests ask rustup for their own toolchain unless HOSTRUSTC names a compiler. Install lld 18 next to clang 18, and point HOSTRUSTC at the pinned rustc. Assisted-by: LLM Signed-off-by: Ryan Murray Co-Authored-By: Claude Opus 5.5 --- .github/workflows/host-tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 73a3c67c1842b..f16fb33953f7c 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -50,19 +50,23 @@ jobs: run: | rustup toolchain install "$RUST_VERSION" --profile minimal --no-self-update echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> "$GITHUB_ENV" + # Tests that would pick their own rustup toolchain use this one. + echo "HOSTRUSTC=rustc" >> "$GITHUB_ENV" need= for p in clang-$CLANG_VERSION gcc-$GCC_VERSION flex bison bc; do command -v "$p" >/dev/null || need="$need $p" done + command -v "ld.lld-$CLANG_VERSION" >/dev/null || need="$need lld-$CLANG_VERSION" dpkg -s "libclang-rt-$CLANG_VERSION-dev" >/dev/null 2>&1 || need="$need libclang-rt-$CLANG_VERSION-dev" if [ -n "$need" ]; then sudo apt-get update -q sudo apt-get install -y -q $need fi - # The tests call clang, cc, gcc and rustc by name. + # The tests call clang, ld.lld, cc and gcc by name. bin="$RUNNER_TEMP/toolchain" mkdir -p "$bin" ln -sf "$(command -v clang-$CLANG_VERSION)" "$bin/clang" + ln -sf "$(command -v ld.lld-$CLANG_VERSION)" "$bin/ld.lld" ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/gcc" ln -sf "$(command -v gcc-$GCC_VERSION)" "$bin/cc" echo "$bin" >> "$GITHUB_PATH" @@ -72,6 +76,7 @@ jobs: python3 --version rustc --version clang --version | head -1 + ld.lld --version cc --version | head -1 git --version