Repository navigation
drm/asahi: m3: one GPU firmware start per boot, and quarantine only published packets - #236
Conversation
A second start of the T8122 runtime in one boot (unbind and rebind, a module reload, or a retry after a failed start) hands the firmware freed tables. Runtime::drop and the failed-start arm free the adopted lower root once ASC has stopped, but TTBAT context 0 still names it, and the next start sets RUN before adopt_uat rewrites context 0. stop_asc only clears RUN, so there is no firmware state to restart from anyway. Refuse every start after the first one of the boot instead. The latch is claimed just before the first ASC start and is never released, and claiming it pins the module, so a reload can't forget it. An unbind and rebind now ends in a clean refusal rather than a second firmware boot, and a reboot is the only way to start the GPU again. Pinning the module also means it can't be unloaded right after an unbind while a completion work item is still queued, which was the reason for draining that work in Backend::stop. Reported-by: Marcelo Alcantara <maralc@gmail.com> Generated-by: LLM Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott Jones <scottajones@gmail.com>
Since a failed job's VM is quarantined before its fence signals, Inner::fail_pending quarantines every pending packet's VM, including packets the firmware never saw: it finishes them without releasing their job guards. A check against the active batches can't tell them apart, because a pending packet can be partly published and fail_active drains the active batches before it calls fail_pending. Give each packet a flag that is set before the first firmware-visible change of any batch that includes it: before the producer state of a batch changes, so partial ring appends that overlapping firmware may consume count as published. fail_pending releases the guard only for a packet whose flag is unset. Everything else keeps quarantining as before, including on ECANCELED: cancellation only withdraws a packet's unpublished remainder, and its published commands may still be running. Reported-by: Marcelo Alcantara <maralc@gmail.com> Generated-by: LLM Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott Jones <scottajones@gmail.com>
chriskearney
left a comment
There was a problem hiding this comment.
One startup lifetime fix is needed before merging 6246daa: the permanent reference must be acquired with try_module_get, failing closed, before the latch/start.
A first deferred, async or sysfs probe can run while the module is LIVE. The Rust platform probe wrapper and C bind/probe path do not take a module reference. An ordinary rmmod can therefore drop the base reference and commit GOING before this probe reaches the unconditional __module_get.
The relevant ordering is source-verifiable:
- try_stop_module checks/releases the reference and marks GOING. The subsequent exit/free path does not recheck it.
- __module_get unconditionally increments; it can resurrect the count after removal has been committed. Driver unregister drains the in-flight bind/probe, so the probe can finish starting ASC before being unbound and unloaded.
- The next module load gets a fresh static latch and can start the firmware again against the stale adopted context. FORCE_UNLOAD=n does not protect this ordinary-unload case. The synchronous module-init probe is protected by COMING; the later first-probe paths are the issue.
Please take try_module_get(THIS_MODULE) before claiming, return without claiming/starting on failure, and module_put only the temporary reference when another start already claimed the latch. Keep the winning reference permanently. The kernel's module-reference documentation describes this removal check.
The packet-publication/guard ordering checks out, including partial append errors, active-list drain and ECANCELED quarantine. Validation: 11 production policy tests pass, independent source-order controls pass, and a small exhaustive source-derived unload model admits the bad interleaving as written and rejects it with the proposed pin ordering. That model is not a kernel/hardware reproduction. Native M3-enabled software builds also succeed; no T8122 hardware result is claimed.
CI coverage note: this PR's package config leaves M3_T8122 disabled, so its green package job excludes the changed runtime/submit paths. The separate M3-enabled builds supply that compilation coverage; the final #236+#230 production composition is building separately.
| return Err(EBUSY); | ||
| } | ||
| // SAFETY: never released: the latch must outlive this instance of the module. | ||
| unsafe { kernel::bindings::__module_get(crate::THIS_MODULE.as_ptr()) }; |
There was a problem hiding this comment.
The unconditional reference can be acquired after ordinary delete_module has already committed GOING. A racing first deferred/sysfs/async probe can then start, finish while unregister drains it, and unload; reloading loses this static latch. Use a fallible live-module pin before claiming/start, release only the extra reference when claim fails, and otherwise retain it permanently. The packet flag side looks sound.
There was a problem hiding this comment.
A minimal replacement for the claim/pin block, preserving the existing fail-closed placement:
// A deferred, async or sysfs probe may race an ordinary module removal.
// Pin before claiming so a reload cannot forget an already-started firmware.
// SAFETY: THIS_MODULE names this module, or is null when built in.
if !unsafe { kernel::bindings::try_module_get(crate::THIS_MODULE.as_ptr()) } {
dev_err!(pdev.as_ref(), "M3: the driver is being removed; GPU not started\n");
return Err(ENODEV);
}
if !FIRMWARE_START.claim() {
// SAFETY: release only this attempt's reference; the first start keeps its own.
unsafe { kernel::bindings::module_put(crate::THIS_MODULE.as_ptr()) };
dev_err!(pdev.as_ref(), "M3: the GPU firmware already started once this boot and can't start again; reboot to start the GPU\n");
return Err(EBUSY);
}
// Keep the winning reference permanently so the latch lasts until reboot.The unload interleaving model rejects both bad schedules with this ordering. This proposed replacement has not yet been natively compiled; the completed builds apply to the original6246 source. No branch changes from me.
There was a problem hiding this comment.
Agreed, thanks for catching this. Fixed in 80958cd5a4. The reference is now taken with try_module_get(THIS_MODULE) before the latch is claimed, and nothing is claimed or started if removal has begun. A start that loses the claim drops only its own reference, and the winner keeps its reference, so an ordinary unload stays blocked.
The order lives in the batch policy (claim_start), so the host test covers it:
- a failed pin claims nothing;
- a lost claim gives back its reference;
- the winner keeps its reference.
Four mutations of that order each fail the test. The two commits you reviewed are unchanged.
The one-start-per-boot latch pinned the module with __module_get after claiming it. A first probe that isn't the module-init one (deferred, async or a sysfs bind) can run while an ordinary rmmod is under way: once try_stop_module has dropped the reference and marked the module GOING, __module_get revives the count unconditionally, the probe starts ASC, and the module still unloads. The next load then gets a fresh latch and starts the firmware a second time. Take the reference with try_module_get before claiming the latch, and start nothing when the module is already being removed. When another start already holds the latch, drop only the reference this probe just took; the winning claim keeps its reference, so an ordinary unload stays blocked. A forced unload, or a module init that fails after the probe started ASC, still frees the module regardless of references; neither is new here. The order lives in the batch policy (claim_start), so the host test checks it: a pin that fails claims nothing, a start that loses the claim gives back only its own reference, and the winner keeps its reference. Reported-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Generated-by: LLM Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott Jones <scottajones@gmail.com>
chriskearney
left a comment
There was a problem hiding this comment.
Re-reviewed 80958cd: the requested unload-race fix is correct. try_module_get now precedes the latch claim; a failed pin claims nothing, a losing claim releases only its temporary reference, and the winner retains its pin. The two remaining __module_get calls are reached only after that winning reference is held. Packet publication and guard handling are unchanged from the earlier review.
All 12 production policy tests pass with warnings denied on Rust 1.85.0, 1.93.1 and 1.98.1. Five independent mutations of the pin/claim/release ordering are rejected by the new regression test. The source-derived unload interleaving model also rejects the previously identified double-start paths with this ordering; this is host/source evidence, not a hardware reproduction.
Approving the source change. The updated M3-enabled production composition with #230 is being compiled separately; the earlier 6246 build does not cover this revision. No T8122 hardware qualification is claimed.
|
The updated native production composition of #236 at 80958cd with #230 at fc53911 built successfully. The actual Image and headers configs match, with Rust/G15/T8122/16KiB and module unloading enabled. The AArch64 Asahi module contains the new removing-module refusal, the first-start latch and This validates the corrected runtime compilation; it is not a T8122 hardware result. Nonfatal rustfmt and header-helper strip diagnostics remain, and DKMS/header-tool execution was not tested. |
|
Hardware result, as promised. We ran this on our 13" M3 Air (J613), on kernel 7.2.11 built from 80958cd (this PR's head, so the merged content) with G15 and M3_T8122 on, booted with
Three things we noticed along the way:
|
Summary
These are the two fixes Marcelo asked for in #187, which need to land in or before the M3 Air switch-on (#230):
61eccdbbee). A second start (unbind and rebind, a module reload, a retry after a failed start) would set RUN while TTBAT context 0 still names the freed lower root of the first one. Sincestop_asconly clears RUN, we refuse it instead of trying to make a restart work. The latch is claimed just before the first ASC start, and claiming it takes a module reference that is never dropped, so a reload can't forget it. An unbind and rebind now ends in "M3: the GPU firmware already started once this boot and can't start again; reboot to start the GPU".6246daaee9). Each packet gets a flag that is set before any producer-state change of a batch that includes it, so a partial ring append counts.fail_pendingreleases the guard only when the flag is unset. ECANCELED still quarantines, as you asked.This also covers the
Backend::stopcompletion drain: a completion work item only exists once the runtime has started, and by then the module is pinned, so it can't be unloaded right after an unbind.Marcelo, would you rather have these as one PR or two? They're independent commits, so splitting is easy.
Evidence
tools/asahi/m3-batching-tests.rs, Rust 1.85.0 as in the CI manifest): 11 pass, including two new ones.fail_activehas drained the active batch, a partly published pending packet keeps its guard and a never-published one releases it.19f4965180, M3 only and M3 + G15: 0 errors in both. M3 only has 0 Rust warnings, the same as the base. M3 + G15 has 28, the same 28g15/sites as the base, none inm3/.Merge Danger
Door: two-way
Blast Radius: M3 Air runtime
Both changes are in
m3/, built only withCONFIG_DRM_ASAHI_M3_T8122. After the first T8122 start,rmmod asahiis refused until reboot.🤖 Generated with Claude Code