Re: [PATCH] Bluetooth: request LE data length for affected MT7922 firmware

From: Luiz Augusto von Dentz

Date: Fri Oct 16 2026 - 14:08:35 EST


Hi Kyle,

On Fri, Oct 16, 2026 at 1:01 PM Kyle Pepper <cochcoder@xxxxxxxxx> wrote:
>
> On an ASUS G614PR, the MT7922 USB controller 0489:e0f6 accepts maximum
> LE suggested default data lengths, but ASHA playback remains severely
> choppy until the host sends an explicit LE Set Data Length request.
>
> With the firmware image dated 2026-07-24 14:38:15, a request on the same
> Cochlear Nucleus 8 connection produces a Data Length Change event with
> TX 251 octets/2120 us. Delivery rises from 21.5 to 50 audio SDUs/s and
> the peak outstanding packet count falls from 32 to 3. Wi-Fi power save
> remains enabled. Playback also works with the automatic request in the
> local 7.2.9 kernel build.
>
> Keep the existing suggested-default initialization and add a driver-set
> quirk for the tested USB ID, chip and firmware image only. After a
> successful remote feature exchange, queue a request using the current
> host defaults if the central-role link and controller support DLE and
> the controller advertises LE Set Data Length.
>
> Hold a connection reference while queued and recheck its membership,
> state, role and capabilities before constructing the command. Release
> the reference on completion, cancellation or queue failure. Failure of
> this optional request must not reject connection setup. Other controller
> matches, PHY fallback and suggested defaults are unchanged.
>
> Assisted-by: OpenCode:gpt-6.1-sol
> Signed-off-by: Kyle Pepper <cochcoder@xxxxxxxxx>
> ---
> This is the first submission of this MediaTek-specific fix. It follows
> Luiz's suggestion, in review of a separate generic DLE RFC on October 13,
> to use a driver-set quirk if a controller needs explicit DLE. It is not
> a revision or resend of that generic request.
>
> Differences from the separate generic RFC:
> - Require the exact tested USB ID, chip and firmware image build.
> - Keep the existing suggested-default write unchanged.
> - Require a central-role link, local/peer DLE and command support.
> - Use current host defaults, not hard-coded transmit limits.
> - Keep referenced queued work, cancellation and locked revalidation.
>
> Tested on ASUS G614PR with MT7922 and a Cochlear Nucleus 8:
> - Manual same-connection request: 21.5 -> 50 SDUs/s, peak queue 32 -> 3.
> - Automatic request: local 7.2.9-arch1-2-asha-mediatek boots and playback
> is reported working; the matching firmware and quirk log are present.
> This local kernel/userspace stack has other ASHA patches. This is not
> a vanilla upstream-kernel hardware test or a post-boot throughput trace.
> - Broader suspend/resume, battery, other-profile and native concurrency
> coverage is not claimed. An earlier brief quality drop is not assigned
> a cause or declared independently resolved.
>
> Validation on Bluetooth upstream base 185c11232d5f:
> - Four native Bluetooth objects compile with W=1 on an x86_64 diagnostic
> configuration (Rust disabled); this is not a full upstream kernel build.
> - 37 source-extracted ASan/UBSan controls pass, with unpatched baseline
> and negative controls. These are not in-kernel concurrency tests.
> - Equivalent backports pass the same controls on pinned 7.2.y, 6.18.y
> and 6.12.y snapshots. The 6.18.y and 6.12.y variants also compile the
> four native objects with W=1. No older-kernel hardware boot is claimed.
>
> The firmware gate uses the selected image header. If setup reuses an
> already-resident image, this is not an independent read of that image's
> identity; the observed boot log matches the tested image. Please review
> this limitation before widening the match to other firmware or devices.
>
> Stable consideration: this fixes a real firmware interoperability issue,
> but the change exceeds the usual 100-line-with-context stable guideline.
> Please assess stable suitability after upstream review. No introducing
> kernel commit has been established, so no speculative Fixes tag is added.
> The 6.18.y backport uses the older connection-feature layout. The 6.12.y
> backport also uses scalar quirks and checks queued duplicates before
> taking a reference, since queue_once() there returns zero for duplicates.
> Backport application/API differences and checks are documented locally;
> no stable acceptance, older-kernel hardware test or public issue closure
> is claimed. Please do not queue an unreviewed backport directly.
>
> drivers/bluetooth/btmtk.c | 29 ++++++++++++++++
> include/net/bluetooth/hci.h | 6 ++++
> include/net/bluetooth/hci_sync.h | 1 +
> net/bluetooth/hci_event.c | 8 +++++
> net/bluetooth/hci_sync.c | 58 ++++++++++++++++++++++++++++++++
> 5 files changed, 102 insertions(+)
>
> diff --git a/drivers/bluetooth/btmtk.c b/drivers/bluetooth/btmtk.c
> index 75483d823..cab5c4c36 100644
> --- a/drivers/bluetooth/btmtk.c
> +++ b/drivers/bluetooth/btmtk.c
> @@ -47,6 +47,24 @@ struct btmtk_global_desc {
> __le32 section_num;
> } __packed;
>
> +static bool btmtk_needs_manual_dle(struct hci_dev *hdev, u32 dev_id,
> + const struct btmtk_patch_header *hdr)
> +{
> + static const u8 affected_build[16] = "20260724143815";
> + struct btmtk_data *data;
> +
> + if (hdev->bus != HCI_USB || dev_id != 0x7922)
> + return false;
> +
> + data = hci_get_priv(hdev);
> + if (!data->udev)
> + return false;
> +
> + return le16_to_cpu(data->udev->descriptor.idVendor) == 0x0489 &&
> + le16_to_cpu(data->udev->descriptor.idProduct) == 0xe0f6 &&
> + !memcmp(hdr->datetime, affected_build, sizeof(affected_build));
> +}
> +
> struct btmtk_section_map {
> __le32 sectype;
> __le32 secoffset;
> @@ -219,6 +237,9 @@ int btmtk_setup_firmware_79xx(struct hci_dev *hdev, const char *fwname,
> u32 section_num, dl_size, section_offset;
> u8 cmd[64];
>
> + /* Re-evaluate the firmware-specific workaround on every setup. */
> + hci_clear_quirk(hdev, HCI_QUIRK_NEEDS_LE_SET_DATA_LEN);
> +
> err = request_firmware(&fw, fwname, &hdev->dev);
> if (err < 0) {
> bt_dev_err(hdev, "Failed to load firmware file (%d)", err);
> @@ -347,6 +368,14 @@ int btmtk_setup_firmware_79xx(struct hci_dev *hdev, const char *fwname,
> /* Wait a few moments for firmware activation done */
> usleep_range(100000, 120000);
>
> + /* This firmware accepts maximum suggested defaults but needs an
> + * explicit per-link request to negotiate a larger TX data length.
> + */
> + if (btmtk_needs_manual_dle(hdev, dev_id, hdr)) {
> + hci_set_quirk(hdev, HCI_QUIRK_NEEDS_LE_SET_DATA_LEN);
> + bt_dev_info(hdev, "Using explicit LE data length requests");
> + }
> +
> err_release_fw:
> release_firmware(fw);
>
> diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h
> index 1c992e875..513ca226d 100644
> --- a/include/net/bluetooth/hci.h
> +++ b/include/net/bluetooth/hci.h
> @@ -340,6 +340,12 @@ enum {
> */
> HCI_QUIRK_BROKEN_LE_CODED,
>
> + /* The controller needs an explicit LE Set Data Length request for
> + * each central-role connection despite accepting suggested defaults.
> + * Set during the vendor setup callback for affected firmware only.
> + */
> + HCI_QUIRK_NEEDS_LE_SET_DATA_LEN,
> +
> /*
> * When this quirk is set, the HCI_OP_READ_ENC_KEY_SIZE command is
> * skipped during an HCI_EV_ENCRYPT_CHANGE event. This is required
> diff --git a/include/net/bluetooth/hci_sync.h b/include/net/bluetooth/hci_sync.h
> index 59cb918d1..5a37d3e50 100644
> --- a/include/net/bluetooth/hci_sync.h
> +++ b/include/net/bluetooth/hci_sync.h
> @@ -194,3 +194,4 @@ int hci_le_read_remote_features(struct hci_conn *conn);
>
> int hci_acl_change_pkt_type(struct hci_conn *conn, u16 pkt_type);
> int hci_le_set_phy(struct hci_conn *conn, u8 tx_phys, u8 rx_phys);
> +int hci_le_set_data_len(struct hci_conn *conn);
> diff --git a/net/bluetooth/hci_event.c b/net/bluetooth/hci_event.c
> index 56ad10ab9..d78f330e3 100644
> --- a/net/bluetooth/hci_event.c
> +++ b/net/bluetooth/hci_event.c
> @@ -6875,6 +6875,14 @@ static void hci_le_remote_feat_complete_evt(struct hci_dev *hdev, void *data,
> conn->le_tx_def_phys &= ~HCI_LE_SET_PHY_CODED;
> conn->le_rx_def_phys &= ~HCI_LE_SET_PHY_CODED;
> }
> +
> + if (hci_test_quirk(hdev, HCI_QUIRK_NEEDS_LE_SET_DATA_LEN) &&
> + conn->role == HCI_ROLE_MASTER &&
> + (hdev->commands[33] & BIT(6)) &&
> + (hdev->le_features[0] & HCI_LE_DATA_LEN_EXT) &&
> + (conn->le_features[0] & HCI_LE_DATA_LEN_EXT) &&
> + hdev->le_def_tx_len > 27)
> + hci_le_set_data_len(conn);
> }
>
> if (conn->state == BT_CONFIG) {
> diff --git a/net/bluetooth/hci_sync.c b/net/bluetooth/hci_sync.c
> index 7ac26daba..f919249e9 100644
> --- a/net/bluetooth/hci_sync.c
> +++ b/net/bluetooth/hci_sync.c
> @@ -8152,3 +8152,61 @@ int hci_le_set_phy(struct hci_conn *conn, u8 tx_phys, u8 rx_phys)
>
> return (err == -EEXIST) ? 0 : err;
> }
> +
> +static void le_set_data_len_complete(struct hci_dev *hdev, void *data, int err)
> +{
> + struct hci_conn *conn = data;
> +
> + bt_dev_dbg(hdev, "err %d", err);
> + if (err && err != -ECANCELED && err != -ENODEV)
> + bt_dev_warn(hdev, "Failed to set LE data length (%d)", err);
> + hci_conn_put(conn);
> +}
> +
> +static int hci_le_set_data_len_sync(struct hci_dev *hdev, void *data)
> +{
> + struct hci_conn *conn = data;
> + struct hci_cp_le_set_data_len cp;
> +
> + hci_dev_lock(hdev);
> +
> + if (!hci_conn_valid(hdev, conn) || conn->type != LE_LINK ||
> + conn->role != HCI_ROLE_MASTER ||
> + (conn->state != BT_CONNECTED && conn->state != BT_CONFIG)) {
> + hci_dev_unlock(hdev);
> + return -ECANCELED;
> + }
> +
> + /* Use the current suggested defaults, including later host changes. */
> + if (!hci_test_quirk(hdev, HCI_QUIRK_NEEDS_LE_SET_DATA_LEN) ||
> + !(hdev->commands[33] & BIT(6)) ||
> + !(hdev->le_features[0] & HCI_LE_DATA_LEN_EXT) ||
> + !(conn->le_features[0] & HCI_LE_DATA_LEN_EXT) ||
> + hdev->le_def_tx_len <= 27) {
> + hci_dev_unlock(hdev);
> + return 0;
> + }
> +
> + cp.handle = cpu_to_le16(conn->handle);
> + cp.tx_len = cpu_to_le16(hdev->le_def_tx_len);
> + cp.tx_time = cpu_to_le16(hdev->le_def_tx_time);
> +
> + hci_dev_unlock(hdev);
> +
> + return __hci_cmd_sync_status(hdev, HCI_OP_LE_SET_DATA_LEN,
> + sizeof(cp), &cp, HCI_CMD_TIMEOUT);
> +}
> +
> +int hci_le_set_data_len(struct hci_conn *conn)
> +{
> + struct hci_dev *hdev = conn->hdev;
> + int err;
> +
> + err = hci_cmd_sync_queue_once(hdev, hci_le_set_data_len_sync,
> + hci_conn_get(conn),
> + le_set_data_len_complete);
> + if (err)
> + hci_conn_put(conn);
> +
> + return (err == -EEXIST) ? 0 : err;
> +}
>
> base-commit: 185c11232d5f586489bb3b74e6bcc7a6560ddd40

Perhaps before we go into the whole trouble of adding a quirk we
should check with MediaTek folks if there is a fix planned for this,
since this is actually a firmware bug. If there isn't one and a fix
would take time to be validated, I'd proceed with the quirk but I
don't think gating it on a specific build is a good idea; there might
be other firmware releases affected by this, unless MediaTek can
confirm this is exclusive to a single image in which case we should
treat it as a regression.

--
Luiz Augusto von Dentz