Re: [DISCUSSION] fw_devlink: incorrect ancestor fallback for intermediate bus topologies breaks audio dependency resolution

Ajay Kumar Nandam <[email protected]> Mon, 20 Jul 2026 16:42:21 +0530
Newsgroups gmane.linux.alsa.devel,gmane.linux.kernel,gmane.linux.ports.arm.msm
Message-ID <[email protected]>
Hi All,

Gentle Ping on this discussion.

CC'ing Danilo Krummrich and Andy Shevchenko

Thanks & Regards
Ajay Kumar Nandam

On 7/1/2026 6:47 PM, Ajay Kumar Nandam wrote:
> Hi Saravana Kannan,
> 
> I am Ajay from Qualcomm, and I am working together with Ravi on ADSP
> Subsystem Restart (SSR) support for Qualcomm LPASS audio. During our
> SSR work Ravi observed that restart does not behave as expected — when
> the ADSP restarts, all child nodes of the remoteproc should detach and
> re-attach cleanly once the remoteproc is up. While investigating
> this, Ravi identified an incorrect assumption in fw_devlink's dependency
> resolution logic that causes wrong device links to be created when a
> supplier device lives behind intermediate bus nodes that are populated
> asynchronously. We observed this on a Qualcomm qcs6490-rb3gen2 board
> with LPASS audio drivers, but the assumption is generic and can affect
> any similar topology.
> 
> Problem Summary:
> 
> fw_devlink appears to incorrectly collapse pending dependencies to
> an ancestor device when supplier devices exist behind asynchronously
> created intermediate bus nodes.
> 
> In our topology, q6prmcc is instantiated as a descendant of the ADSP
> remoteproc through asynchronously created glink/gpr bus devices.
> When the remoteproc device binds earlier, fw_devlink appears to assume
> that these intermediate nodes will never become devices, collapses the
> dependency to the remoteproc ancestor, and deletes the pending links.
> As a result, the intended dependency:
>      lpass_va_macro -> q6prmcc
> is replaced by:
>      lpass_va_macro -> remoteproc
> 
> This results in incorrect teardown ordering during ADSP SSR because
> codec drivers are no longer linked to their actual clock supplier.
> 
> Could you please suggest how we can resolve this issue? Is there
> a specific reason for this assumption, as it looks incorrect in our case?
> 
> 
> -------------------------------------------
> 1. AUDIO SYSTEM TOPOLOGY ON qcs6490-rb3gen2
> 
> On qcs6490-rb3gen2 the LPASS audio clock supply chain spans a
> four-level device hierarchy rooted at the ADSP remoteproc. The
> intermediate nodes (glink-edge, gpr) only get their struct devices
> after the ADSP firmware has booted, which happens well after the
> remoteproc driver's own probe() returns.
> 
> The Device Tree hierarchy (from qcs6490-audioreach.dtsi / kodiak.dtsi):
> https://elixir.bootlin.com/linux/v7.1/source/arch/arm64/boot/dts/qcom/qcs6490-audioreach.dtsi#L113
> 
>    remoteproc_adsp                         (ADSP remoteproc)
>        └── remoteproc_adsp_glink           (GLink transport edge)
>                └── gpr                     (GPR bus, compatible="qcom,gpr")
>                        └── service@2       (PRM service, compatible="qcom,q6prm")
>                                └── clock-controller  (q6prmcc)
>                                        compatible = "qcom,q6prm-lpass-clocks"
> 
> The LPASS codec drivers (VA macro, WSA macro, RX macro, TX macro) are
> platform devices under soc@0 that reference q6prmcc as their clock
> supplier via DT phandles:
> 
>    From qcs6490-audioreach.dtsi:
>    ------------------------------
>    &lpass_va_macro {
>        clocks = <&q6prmcc LPASS_CLK_ID_VA_CORE_MCLK ...>,
>                 <&q6prmcc LPASS_HW_MACRO_VOTE ...>,
>                 <&q6prmcc LPASS_HW_DCODEC_VOTE ...>;
>        clock-names = "mclk", "macro", "dcodec";
>    };
> 
>    &lpass_wsa_macro {
>        clocks = <&q6prmcc LPASS_CLK_ID_TX_CORE_MCLK ...>,
>                 ...;
>    };
> 
>    (similarly for lpass_rx_macro and lpass_tx_macro)
> 
> The intended fw_devlink dependency is therefore:
> 
>    lpass_va_macro  (consumer)  ──►  q6prmcc / clock-controller (supplier)
>    lpass_wsa_macro (consumer)  ──►  q6prmcc / clock-controller (supplier)
> 
> This dependency is critical for correct ADSP SSR (Subsystem Restart)
> teardown: all codec drivers must be torn down BEFORE q6prmcc is
> removed, so that no driver calls into a clock controller that no longer
> exists.
> 
> -----------------------------------------
> 2. THE INCORRECT ASSUMPTION IN fw_devlink
> 
> fw_devlink correctly identifies the intended supplier at boot time via
> fwnode links. However, the links are never converted to device links.
> Instead, fallback device links to the remoteproc device are created.
> 
> The sequence of events:
> 
>    1. At boot (~t=0.085s), fw_devlink resolves the DT clock phandles
>       and creates fwnode links:
>         lpass_va_macro  ──fwnode──►  gpr/service@2/clock-controller
>         lpass_wsa_macro ──fwnode──►  gpr/service@2/clock-controller
>       q6prmcc does not exist yet → EAGAIN → fwnode links kept pending.
>       This is correct.
> 
>    2. At ~t=7.597s, remoteproc_adsp probe() completes. This triggers
>       device_links_driver_bound() which calls
>       fw_devlink_pickup_dangling_consumers(remoteproc_dev).
> 
>    3. fw_devlink_pickup_dangling_consumers() walks remoteproc's child
>       fwnodes. It finds the gpr fwnode with no struct device attached.
> 
>    4. Here is the incorrect assumption:
>       __fw_devlink_pickup_dangling_consumers() concludes that because
>       gpr has no struct device at this moment, it will never get one.
>       It marks gpr as FWNODE_FLAG_NOT_DEVICE and moves all of gpr's
>       consumers (including the codec → q6prmcc fwnode links) up to
>       remoteproc.
> 
>    5. The correct fwnode links (codec → q6prmcc) are permanently
>       deleted. Wrong device links (codec → remoteproc) are created.
> 
>    6. At ~t=8.364s, the GPR bus probes and q6prmcc eventually appears.
>       But there are no pending fwnode links left for it — the correct
>       dependency is lost forever.
> 
> The assumption that is wrong:
> 
>    "If a child fwnode has no struct device when its ancestor device
>     binds, it will never get one."
> 
> This assumption does not hold when intermediate bus nodes (glink-edge,
> gpr) are populated asynchronously after their parent device boots remote
> firmware. At the time remoteproc binds, glink-edge and gpr have no
> struct device not because they will never exist, but because they are
> waiting for the ADSP firmware to load — which only happens after
> remoteproc's own probe() returns.
> 
> https://elixir.bootlin.com/linux/v7.1/source/drivers/base/core.c#L1300
> 
> ------------------------
> 3. EVIDENCE — DEBUG LOGS
> 
> We added debug instrumentation to fw_devlink's key decision points
> (filtered to audio devices only) to capture the exact sequence.
> 
> --- t=0.085s: Correct fwnode links identified, q6prmcc absent ---
> 
>    [0.085060] debug: fw_devlink_create_devlink:2356 audio supplier
>      /soc@0/remoteproc@3700000/glink-edge/gpr/service@2/clock-controller
>      not yet a device, deferring link from consumer=3240000.codec (EAGAIN)
> 
>    [0.086178] debug: fw_devlink_create_devlink:2356 audio supplier
>      /soc@0/remoteproc@3700000/glink-edge/gpr/service@2/clock-controller
>      not yet a device, deferring link from consumer=3370000.codec (EAGAIN)
> 
>    Both codecs correctly identify q6prmcc as their supplier. q6prmcc
>    does not exist yet → EAGAIN → fwnode links kept pending. Correct.
> 
> --- t=7.597s: remoteproc probe completes ---
> 
>    [7.555169] qcom_q6v5_pas 3700000.remoteproc: debug: qcom_pas_probe:745
>      qcom_pas: probe start, parent=soc@0
>    [7.597126] qcom_q6v5_pas 3700000.remoteproc: debug: qcom_pas_probe:865
>      qcom_pas: probe complete, parent=soc@0
> 
> --- t=7.597s: POINT OF NO RETURN ---
> 
>    device_links_driver_bound() fires for remoteproc. It calls
>    fw_devlink_pickup_dangling_consumers() which finds gpr with no
>    struct device and moves ALL of gpr's consumers to remoteproc:
> 
>    [7.597160] debug: __fw_devlink_pickup_dangling_consumers:276
>      audio dangling consumers: moving consumers of
>      /soc@0/remoteproc@3700000/glink-edge/gpr
>      to ancestor /soc@0/remoteproc@3700000 (no device yet)
> 
> --- t=7.597s: Correct fwnode links permanently deleted ---
> 
>    [7.597205] debug: __fwnode_link_del:155 audio fwnode link deleted:
>      consumer=/soc@0/codec@3370000
>      supplier=/soc@0/remoteproc@3700000/glink-edge/gpr/service@2/clock-controller
> 
>    [7.609265] debug: __fwnode_link_del:155 audio fwnode link deleted:
>      consumer=/soc@0/codec@3240000
>      supplier=/soc@0/remoteproc@3700000/glink-edge/gpr/service@2/clock-controller
> 
> --- t=7.637s: Wrong device links created ---
> 
>    [7.637952] debug: fw_devlink_create_devlink:2331 audio device link created:
>      consumer=3240000.codec supplier=3700000.remoteproc flags=0x124
>      (consumer_fwnode=/soc@0/codec@3240000)
> 
>    [7.674145] debug: fw_devlink_create_devlink:2331 audio device link created:
>      consumer=3370000.codec supplier=3700000.remoteproc flags=0x124
>      (consumer_fwnode=/soc@0/codec@3370000)
> 
> --- t=8.364s: GPR bus probes — 770ms too late ---
> 
>    [8.364221] qcom,apr ...: debug: apr_probe:597
>      apr/gpr: probe start, parent=3700000.remoteproc:glink-edge
>    [8.364446] qcom,apr ...: debug: apr_probe:648
>      apr/gpr: probe complete, parent=3700000.remoteproc:glink-edge
> 
>    GPR bus probes 770ms after the fwnode links were deleted. q6prmcc's
>    consumers have already been stolen. No pending links remain.
> 
> --- t=15.409s: VA macro binds — wrong supplier confirmed ---
> 
>    [15.389975] va_macro 3370000.codec: debug: va_macro_probe:1538
>      va_macro: probe start, parent=soc@0
>    [15.399678] va_macro 3370000.codec: debug: va_macro_probe:1683
>      va_macro: probe complete, parent=soc@0
> 
>    [15.409155] debug: device_links_driver_bound:1420
>      ENTRY dev=3370000.codec (struct parent=soc@0)
>    [15.418101] debug: device_links_driver_bound:1426
>      ENTRY supplier link: dev=3370000.codec
>      supplier=3700000.remoteproc (fwnode=/soc@0/remoteproc@3700000)
>      flags=0x164 status=2
>    [15.434076] debug: device_links_driver_bound:1426
>      ENTRY supplier link: dev=3370000.codec
>      supplier=33c0000.pinctrl (fwnode=/soc@0/pinctrl@33c0000)
>      flags=0x164 status=2
> 
>    q6prmcc is completely absent from the supplier list. The device link
>    to remoteproc is wrong. The correct dependency is permanently lost.
> 
> ------------------------------------
> 4. IMPACT — ADSP SSR TEARDOWN BROKEN
> 
> With the wrong device link in place (codec → remoteproc instead of
> codec → q6prmcc), the teardown order during ADSP stop is incorrect.
> q6prmcc is removed while codec drivers still depend on it, causing
> them to call into a clock controller that no longer exists. Audio
> drivers are left in an invalid state and proper cleanup does not happen.
> 
> ----------------------------------------------------------
> 5. WORKAROUND — MANUAL DEVICE LINK IN q6dsp-lpass-clocks.c
> 
> To confirm the hypothesis and restore correct teardown ordering, we
> manually created the device link from q6prmcc to lpass_va_macro inside
> q6dsp_clock_dev_probe() in sound/soc/qcom/qdsp6/q6dsp-lpass-clocks.c.
> 
> With this workaround in place:
>    - The correct device link (lpass_va_macro → q6prmcc) is established
>      when q6prmcc probes.
>    - On ADSP stop, lpass_va_macro is torn down before q6prmcc.
>    - Audio driver cleanup happens correctly and completely.
>    - No dangling state is observed after ADSP SSR.
> 
> This confirms that the dependency model itself is correct — the problem
> is purely that fw_devlink fails to establish it automatically due to
> the incorrect assumption described above.
> 
> The workaround patch is included below for reference.
> 
> --- Workaround patch (reference only, not proposed for merge) ---
> 
> diff --git a/sound/soc/qcom/qdsp6/q6dsp-lpass-clocks.c b/sound/soc/qcom/qdsp6/q6dsp-lpass-clocks.c
> index 03838582aead..3884d3e96e33 100644
> --- a/sound/soc/qcom/qdsp6/q6dsp-lpass-clocks.c
> +++ b/sound/soc/qcom/qdsp6/q6dsp-lpass-clocks.c
> @@ -8,6 +8,7 @@
> #include <linux/device.h>
> #include <linux/platform_device.h>
> #include <linux/of.h>
> +#include <linux/of_platform.h>
> #include <linux/slab.h>
> #include <dt-bindings/sound/qcom,q6dsp-lpass-ports.h>
> #include "q6dsp-lpass-clocks.h"
> @@ -126,6 +127,52 @@ static struct clk_hw *q6dsp_of_clk_hw_get(struct of_phandle_args *clkspec,
>   return ERR_PTR(-ENOENT);
> }
> 
> +/*
> + * va_macro_compat - compatible string for the va macro codec device.
> + *
> + * Used to locate the consumer device for the manual device link.
> + */
> +#define LPASS_VA_MACRO_COMPAT  "qcom,sc7280-lpass-va-macro"
> +
> +/**
> + * q6dsp_clock_link_va_macro - create a manual device link from va_macro
> + *                              to this clock-controller (q6prmcc).
> + *
> + * fw_devlink cannot establish this link automatically because q6prmcc is
> + * a dynamically created device (child of remoteproc/glink/gpr) and its
> + * fwnode link is purged by __fw_devlink_pickup_dangling_consumers() when
> + * the remoteproc parent binds. This manual link ensures va_macro is torn
> + * down before q6prmcc on ADSP SSR.
> + */
> +static int q6dsp_clock_link_va_macro(struct device *clk_dev)
> +{
> +  struct device_node *va_np;
> +  struct platform_device *va_pdev;
> +  struct device_link *link;
> +
> +  va_np = of_find_compatible_node(NULL, NULL, LPASS_VA_MACRO_COMPAT);
> +  if (!va_np) {
> +    dev_dbg(clk_dev, "va_macro node not found, skip devlink\n");
> +    return 0;
> +  }
> +
> +  va_pdev = of_find_device_by_node(va_np);
> +  of_node_put(va_np);
> +  if (!va_pdev) {
> +    dev_dbg(clk_dev, "va_macro device not yet registered\n");
> +    return 0;
> +  }
> +
> +  link = device_link_add(&va_pdev->dev, clk_dev,
> +             DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOPROBE_CONSUMER);
> +  put_device(&va_pdev->dev);
> +  if (!link) {
> +    dev_err(clk_dev, "failed to add device link to va_macro\n");
> +    return -EINVAL;
> +  }
> +
> +  return 0;
> +}
> +
> int q6dsp_clock_dev_probe(struct platform_device *pdev)
> {
>   struct q6dsp_cc *cc;
> @@ -180,6 +227,10 @@ int q6dsp_clock_dev_probe(struct platform_device *pdev)
> 
>   dev_set_drvdata(dev, cc);
> 
> +  ret = q6dsp_clock_link_va_macro(dev);
> +  if (ret)
> +    dev_err(dev, "Failed to create devlink between prmcc and va macro\n");
> +
>   return 0;
> }
> EXPORT_SYMBOL_GPL(q6dsp_clock_dev_probe);
> 
> --- End of workaround patch ---
> 
> Tested on: Qualcomm qcs6490-rb3gen2, Qualcomm sm8750-mtp
> 
> Thanks,
> Ajay Kumar Nandam