diff --git a/.gitattributes b/.gitattributes index c96967b..4c3b043 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,8 @@ +/.gitattributes text eol=lf tests/issue63-btusb-zlp/*.patch -whitespace tests/issue63-btusb-zlp/*.sh text eol=lf +tests/issue63-zlp-quirk/*.c text eol=lf +tests/issue63-zlp-quirk/*.conf text eol=lf +tests/issue63-zlp-quirk/*.md text eol=lf +tests/issue63-zlp-quirk/*.sh text eol=lf +tests/issue63-zlp-quirk/Makefile text eol=lf diff --git a/tests/issue63-zlp-quirk/Makefile b/tests/issue63-zlp-quirk/Makefile new file mode 100644 index 0000000..26ad7b0 --- /dev/null +++ b/tests/issue63-zlp-quirk/Makefile @@ -0,0 +1,12 @@ +KVER ?= $(shell uname -r) +KDIR ?= /lib/modules/$(KVER)/build + +obj-m += aic_zlp_quirk.o + +.PHONY: all clean + +all: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules + +clean: + $(MAKE) -C $(KDIR) M=$(CURDIR) clean diff --git a/tests/issue63-zlp-quirk/README.md b/tests/issue63-zlp-quirk/README.md new file mode 100644 index 0000000..444cc6a --- /dev/null +++ b/tests/issue63-zlp-quirk/README.md @@ -0,0 +1,115 @@ +# Issue #63: standard `btusb` ZLP companion-module test + +This experiment keeps the normal `aic_load_fw + system btusb` architecture. +It does not install or bind the vendor `aic_btusb` transport, and it does not +replace the distribution's `btusb.ko`. + +The underlying ZLP behavior was confirmed in +[issue #63](https://github.com/shenmintao/aic8800d80/issues/63): AAC playback +that previously stalled after about one minute remained stable for more than +one hour with the standard `btusb` ZLP patch, including after the obsolete +`aic_btusb` module was removed. + +The small `aic_zlp_quirk.ko` module first tries to attach a kretprobe to the +standard `btusb` function that allocates Bluetooth ACL bulk OUT URBs. The +target is module-qualified as `btusb:alloc_bulk_urb`, avoiding similarly named +symbols in unrelated USB drivers. + +Some distribution builds inline that private `btusb` function. When the +preferred hook is unavailable, the same module falls back to the stable +`usb_submit_urb` entry point. The fallback requires all of the following before +changing an URB: + +- USB device `368b:8d81`; +- bulk OUT transfer; +- endpoint declared by Bluetooth interface 0 (`e0/01/01`). + +Both paths add `URB_ZERO_PACKET` before the USB core submits the transfer. The +Wi-Fi interface and unrelated USB devices are left unchanged. + +The hook fails closed: if neither probe can be installed or kprobes are +disabled, the companion module refuses to load and the system `btusb` remains +unchanged. + +## Before installing + +First remove the earlier patched-`btusb` diagnostic build, if installed: + +```bash +cd ../issue63-btusb-zlp +sudo ./btusb-zlp-test.sh remove +``` + +An obsolete `aic_btusb` installation must also be unloaded and removed before +this test. Verify that the AIC Bluetooth interfaces use the system driver: + +```bash +lsusb -t +lsmod | grep -E '^(btusb|aic_btusb)\b' +``` + +## Install + +Install the normal build dependencies. For Fedora: + +```bash +sudo dnf install dkms gcc make kernel-devel-$(uname -r) +``` + +Then install and load the companion module: + +```bash +cd tests/issue63-zlp-quirk +sudo ./aic-zlp-quirk-test.sh install +./aic-zlp-quirk-test.sh status +``` + +Expected status includes: + +```text +quirk loaded: yes +active hook: btusb:alloc_bulk_urb +legacy aic_btusb: not loaded +AIC BT driver: btusb +``` + +`active hook: usb_submit_urb` is also valid when the distribution compiler +inlined the preferred private `btusb` function. + +Select AAC and play audio for at least one hour: + +```bash +bluetoothctl scan off +pactl set-card-profile bluez_card.28_6F_40_46_AB_B1 a2dp-sink-aac +``` + +While audio is playing, the `ZLP injections` counter should increase: + +```bash +./aic-zlp-quirk-test.sh status +sudo dmesg | grep -i aic_zlp_quirk +``` + +## Remove + +```bash +sudo ./aic-zlp-quirk-test.sh remove +./aic-zlp-quirk-test.sh status +``` + +Removal unloads only `aic_zlp_quirk.ko`. The distribution `btusb.ko` was never +overwritten and remains the Bluetooth transport throughout the test. + +## Kernel requirements + +- matching kernel headers; +- `CONFIG_KPROBES=y` and kretprobe support; +- standard `btusb` plus either its `alloc_bulk_urb` symbol or the USB core + `usb_submit_urb` symbol; +- a valid module signature when Secure Boot policy requires one. + +The `alloc_bulk_urb` function is present with the same signature in Linux +5.15, 6.1, 6.6, 6.12, 6.18, and 7.1.3, although compilers may inline it. The +fallback avoids depending on that implementation detail. DKMS compiles a +separate binary for each installed kernel while the repository maintains one +small source file. diff --git a/tests/issue63-zlp-quirk/aic-zlp-quirk-test.sh b/tests/issue63-zlp-quirk/aic-zlp-quirk-test.sh new file mode 100755 index 0000000..acd64ac --- /dev/null +++ b/tests/issue63-zlp-quirk/aic-zlp-quirk-test.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACKAGE_NAME="aic-zlp-quirk" +PACKAGE_VERSION="0.1" +MODULE_NAME="aic_zlp_quirk" +KERNEL_RELEASE="${KERNEL_RELEASE:-$(uname -r)}" +KERNEL_BUILD_DIR="/lib/modules/${KERNEL_RELEASE}/build" +SOURCE_DIR="/usr/src/${PACKAGE_NAME}-${PACKAGE_VERSION}" +MODULES_LOAD_FILE="/etc/modules-load.d/${PACKAGE_NAME}.conf" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +log() { + printf '[aic-zlp-quirk-test] %s\n' "$*" +} + +die() { + printf '[aic-zlp-quirk-test] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_root() { + [ "$(id -u)" -eq 0 ] || die "run this command as root" +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +stop_bluetooth() { + if command -v systemctl >/dev/null 2>&1; then + systemctl stop bluetooth.service 2>/dev/null || true + fi +} + +start_bluetooth() { + if command -v systemctl >/dev/null 2>&1; then + systemctl start bluetooth.service 2>/dev/null || true + fi +} + +module_loaded() { + [ -d "/sys/module/$1" ] +} + +legacy_btusb_loaded() { + module_loaded aic_btusb +} + +patched_btusb_installed() { + dkms status -m btusb-aic-zlp -v 0.1 2>/dev/null | grep -q . +} + +copy_sources() { + install -d "${SOURCE_DIR}" + install -m 0644 "${SCRIPT_DIR}/aic_zlp_quirk.c" "${SOURCE_DIR}/" + install -m 0644 "${SCRIPT_DIR}/Makefile" "${SOURCE_DIR}/" + install -m 0644 "${SCRIPT_DIR}/dkms.conf" "${SOURCE_DIR}/" +} + +rollback_install() { + log "rolling back the ZLP companion module" + rm -f -- "${MODULES_LOAD_FILE}" + modprobe -r "${MODULE_NAME}" >/dev/null 2>&1 || true + dkms remove -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" --all >/dev/null 2>&1 || true + rm -rf -- "${SOURCE_DIR}" + depmod -a "${KERNEL_RELEASE}" >/dev/null 2>&1 || true + start_bluetooth +} + +verify_standard_btusb() { + if legacy_btusb_loaded; then + die "obsolete aic_btusb is loaded; run 'sudo modprobe -r aic_btusb' and remove its old autoload configuration first" + fi + + if patched_btusb_installed; then + die "the patched btusb test package is still installed; remove it before testing the companion module" + fi + + modprobe btusb || die "could not load the system btusb module" +} + +install_module() { + require_root + require_command dkms + require_command make + require_command modprobe + require_command depmod + + [ -r "${KERNEL_BUILD_DIR}/Makefile" ] || \ + die "kernel build tree not found: ${KERNEL_BUILD_DIR}" + + if command -v lsusb >/dev/null 2>&1 && ! lsusb -d 368b:8d81 >/dev/null 2>&1; then + die "this test is restricted to USB device 368b:8d81, which is not connected" + fi + + verify_standard_btusb + + if module_loaded "${MODULE_NAME}"; then + log "unloading the earlier companion module" + if ! modprobe -r "${MODULE_NAME}"; then + die "could not unload the earlier ${MODULE_NAME} module" + fi + fi + + trap rollback_install ERR + + if dkms status -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" 2>/dev/null | grep -q .; then + log "removing an earlier companion-module build" + dkms remove -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" --all + fi + + rm -rf -- "${SOURCE_DIR}" + copy_sources + + log "building the ZLP companion module for ${KERNEL_RELEASE}" + dkms add -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" + dkms build -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" -k "${KERNEL_RELEASE}" + dkms install -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" \ + -k "${KERNEL_RELEASE}" --force + depmod -a "${KERNEL_RELEASE}" + + printf '%s\n' "${MODULE_NAME}" > "${MODULES_LOAD_FILE}" + + stop_bluetooth + if ! modprobe "${MODULE_NAME}"; then + trap - ERR + rollback_install + die "the companion module could not attach; the system btusb was left unchanged" + fi + start_bluetooth + trap - ERR + + log "loaded module: $(modinfo -n "${MODULE_NAME}")" + log "system btusb remains active; test AAC playback for at least one hour" +} + +remove_module() { + require_root + require_command dkms + require_command modprobe + require_command depmod + + stop_bluetooth + rm -f -- "${MODULES_LOAD_FILE}" + if module_loaded "${MODULE_NAME}" && ! modprobe -r "${MODULE_NAME}"; then + start_bluetooth + die "could not unload ${MODULE_NAME}; no files were removed" + fi + if dkms status -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" 2>/dev/null | grep -q .; then + if ! dkms remove -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" --all; then + start_bluetooth + die "DKMS removal failed; source files were left in place" + fi + fi + rm -rf -- "${SOURCE_DIR}" + depmod -a "${KERNEL_RELEASE}" + start_bluetooth + + log "removed the companion module; the distribution btusb was never replaced" +} + +show_aic_binding() { + local device + local driver + + for device in /sys/bus/usb/devices/*; do + [ -r "${device}/idVendor" ] || continue + [ -r "${device}/idProduct" ] || continue + [ "$(cat "${device}/idVendor")" = "368b" ] || continue + [ "$(cat "${device}/idProduct")" = "8d81" ] || continue + + driver="not bound" + if [ -L "${device}:1.0/driver" ]; then + driver="$(basename "$(readlink -f "${device}:1.0/driver")")" + fi + + printf 'AIC BT driver: %s\n' "${driver}" + return + done + + printf 'AIC BT driver: device not found\n' +} + +show_status() { + local loaded="no" + local injections="unavailable" + local hook="unavailable" + local dkms_status + + if module_loaded "${MODULE_NAME}"; then + loaded="yes" + if [ -r "/sys/module/${MODULE_NAME}/parameters/injections" ]; then + injections="$(cat "/sys/module/${MODULE_NAME}/parameters/injections")" + fi + if [ -r "/sys/module/${MODULE_NAME}/parameters/hook" ]; then + hook="$(cat "/sys/module/${MODULE_NAME}/parameters/hook")" + fi + fi + + dkms_status="$(dkms status -m "${PACKAGE_NAME}" -v "${PACKAGE_VERSION}" 2>/dev/null || true)" + if [ -z "${dkms_status}" ]; then + dkms_status="not installed" + fi + + printf 'kernel: %s\n' "${KERNEL_RELEASE}" + printf 'btusb module: %s\n' "$(modinfo -n btusb 2>/dev/null || printf 'not found')" + printf 'quirk module: %s\n' "$(modinfo -n "${MODULE_NAME}" 2>/dev/null || printf 'not found')" + printf 'quirk loaded: %s\n' "${loaded}" + printf 'active hook: %s\n' "${hook}" + printf 'ZLP injections:%s\n' " ${injections}" + printf 'legacy aic_btusb: %s\n' "$(legacy_btusb_loaded && printf 'loaded' || printf 'not loaded')" + printf 'dkms status: %s\n' "${dkms_status}" + show_aic_binding +} + +case "${1:-}" in + install) + install_module + ;; + remove) + remove_module + ;; + status) + show_status + ;; + *) + printf 'Usage: %s {install|remove|status}\n' "$0" >&2 + exit 2 + ;; +esac diff --git a/tests/issue63-zlp-quirk/aic_zlp_quirk.c b/tests/issue63-zlp-quirk/aic_zlp_quirk.c new file mode 100644 index 0000000..f3fc322 --- /dev/null +++ b/tests/issue63-zlp-quirk/aic_zlp_quirk.c @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0 + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include + +#define AIC_USB_VENDOR_ID 0x368b +#define AIC_USB_PRODUCT_ID 0x8d81 +#define USB_BT_SUBCLASS 0x01 +#define USB_BT_PROTOCOL 0x01 + +static atomic64_t injection_count = ATOMIC64_INIT(0); +static const char *hook_name = "none"; + +static int injections_get(char *buffer, const struct kernel_param *kp) +{ + (void)kp; + + return scnprintf(buffer, PAGE_SIZE, "%lld\n", + (long long)atomic64_read(&injection_count)); +} + +static const struct kernel_param_ops injections_ops = { + .get = injections_get, +}; + +module_param_cb(injections, &injections_ops, NULL, 0444); +MODULE_PARM_DESC(injections, "Number of AIC ACL bulk OUT URBs modified"); + +static int hook_get(char *buffer, const struct kernel_param *kp) +{ + (void)kp; + + return scnprintf(buffer, PAGE_SIZE, "%s\n", hook_name); +} + +static const struct kernel_param_ops hook_ops = { + .get = hook_get, +}; + +module_param_cb(hook, &hook_ops, NULL, 0444); +MODULE_PARM_DESC(hook, "Active injection hook"); + +#if IS_ENABLED(CONFIG_KPROBES) + +enum aic_zlp_hook { + AIC_ZLP_HOOK_NONE, + AIC_ZLP_HOOK_BTUSB_RETURN, + AIC_ZLP_HOOK_USB_SUBMIT, +}; + +static enum aic_zlp_hook active_hook; + +static bool is_aic_bulk_out(const struct urb *urb) +{ + const struct usb_device *udev; + + if (!urb) + return false; + + udev = urb->dev; + if (!udev) + return false; + + if (le16_to_cpu(udev->descriptor.idVendor) != AIC_USB_VENDOR_ID || + le16_to_cpu(udev->descriptor.idProduct) != AIC_USB_PRODUCT_ID) + return false; + + return usb_pipetype(urb->pipe) == PIPE_BULK && + usb_pipeout(urb->pipe); +} + +static bool is_bluetooth_acl_endpoint(const struct urb *urb) +{ + const struct usb_endpoint_descriptor *ep; + struct usb_host_interface *alt; + struct usb_interface *intf; + unsigned int endpoint; + int i; + + if (!is_aic_bulk_out(urb)) + return false; + + intf = usb_ifnum_to_if(urb->dev, 0); + if (!intf) + return false; + + alt = READ_ONCE(intf->cur_altsetting); + if (!alt || + alt->desc.bInterfaceClass != USB_CLASS_WIRELESS_CONTROLLER || + alt->desc.bInterfaceSubClass != USB_BT_SUBCLASS || + alt->desc.bInterfaceProtocol != USB_BT_PROTOCOL) + return false; + + endpoint = usb_pipeendpoint(urb->pipe); + for (i = 0; i < alt->desc.bNumEndpoints; i++) { + ep = &alt->endpoint[i].desc; + if (usb_endpoint_is_bulk_out(ep) && + usb_endpoint_num(ep) == endpoint) + return true; + } + + return false; +} + +static void enable_zlp(struct urb *urb) +{ + long long count; + + if (urb->transfer_flags & URB_ZERO_PACKET) + return; + + urb->transfer_flags |= URB_ZERO_PACKET; + count = atomic64_inc_return(&injection_count); + + if (count == 1) + pr_info("enabled ZLP on the first 368b:8d81 ACL bulk OUT URB\n"); +} + +static int alloc_bulk_urb_ret_handler(struct kretprobe_instance *ri, + struct pt_regs *regs) +{ + struct urb *urb; + + (void)ri; + + urb = (struct urb *)regs_return_value(regs); + if (!is_aic_bulk_out(urb)) + return 0; + + enable_zlp(urb); + return 0; +} + +static struct kretprobe alloc_bulk_urb_probe = { + .kp.symbol_name = "btusb:alloc_bulk_urb", + .handler = alloc_bulk_urb_ret_handler, +}; + +static int usb_submit_urb_pre_handler(struct kprobe *p, struct pt_regs *regs) +{ + struct urb *urb; + + (void)p; + + urb = (struct urb *)regs_get_kernel_argument(regs, 0); + if (is_bluetooth_acl_endpoint(urb)) + enable_zlp(urb); + + return 0; +} + +static struct kprobe usb_submit_urb_probe = { + .symbol_name = "usb_submit_urb", + .pre_handler = usb_submit_urb_pre_handler, +}; + +static int __init aic_zlp_quirk_init(void) +{ + int ret; + + ret = register_kretprobe(&alloc_bulk_urb_probe); + if (!ret) { + active_hook = AIC_ZLP_HOOK_BTUSB_RETURN; + hook_name = "btusb:alloc_bulk_urb"; + pr_info("attached to system btusb for USB device 368b:8d81\n"); + return 0; + } + + pr_warn("btusb return hook unavailable (%d), trying USB submit fallback\n", + ret); + + ret = register_kprobe(&usb_submit_urb_probe); + if (ret) { + pr_err("cannot attach to usb_submit_urb: %d\n", ret); + return ret; + } + + active_hook = AIC_ZLP_HOOK_USB_SUBMIT; + hook_name = "usb_submit_urb"; + pr_info("attached USB submit fallback for device 368b:8d81 interface 0\n"); + return 0; +} + +static void __exit aic_zlp_quirk_exit(void) +{ + unsigned long missed = 0; + + if (active_hook == AIC_ZLP_HOOK_BTUSB_RETURN) { + unregister_kretprobe(&alloc_bulk_urb_probe); + missed = alloc_bulk_urb_probe.nmissed; + } else if (active_hook == AIC_ZLP_HOOK_USB_SUBMIT) { + unregister_kprobe(&usb_submit_urb_probe); + missed = usb_submit_urb_probe.nmissed; + } + + pr_info("detached %s after %lld injections (%lu missed hits)\n", + hook_name, (long long)atomic64_read(&injection_count), missed); +} + +#else + +static int __init aic_zlp_quirk_init(void) +{ + pr_err("CONFIG_KPROBES is disabled in this kernel\n"); + return -EOPNOTSUPP; +} + +static void __exit aic_zlp_quirk_exit(void) +{ +} + +#endif + +module_init(aic_zlp_quirk_init); +module_exit(aic_zlp_quirk_exit); + +MODULE_AUTHOR("Shen Mintao "); +MODULE_DESCRIPTION("AIC 8800D80 standard btusb ACL bulk TX ZLP quirk"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("0.1"); +MODULE_SOFTDEP("pre: btusb"); diff --git a/tests/issue63-zlp-quirk/dkms.conf b/tests/issue63-zlp-quirk/dkms.conf new file mode 100644 index 0000000..1795467 --- /dev/null +++ b/tests/issue63-zlp-quirk/dkms.conf @@ -0,0 +1,10 @@ +PACKAGE_NAME="aic-zlp-quirk" +PACKAGE_VERSION="0.1" + +MAKE[0]="make KVER=${kernelver}" +CLEAN="make KVER=${kernelver} clean" + +BUILT_MODULE_NAME[0]="aic_zlp_quirk" +DEST_MODULE_LOCATION[0]="/updates/dkms" + +AUTOINSTALL="yes"