test: add standalone btusb ZLP quirk module

This commit is contained in:
Shen Mintao
2026-07-25 15:40:59 +08:00
parent 268c4f7122
commit 6550131e18
6 changed files with 604 additions and 0 deletions
+8
View File
@@ -0,0 +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
+12
View File
@@ -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
+115
View File
@@ -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.
+231
View File
@@ -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
+228
View File
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: GPL-2.0
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/atomic.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/ptrace.h>
#include <linux/usb.h>
#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 <cx330.shen@autocore.ai>");
MODULE_DESCRIPTION("AIC 8800D80 standard btusb ACL bulk TX ZLP quirk");
MODULE_LICENSE("GPL");
MODULE_VERSION("0.1");
MODULE_SOFTDEP("pre: btusb");
+10
View File
@@ -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"