Conversation
… is 0
l2c_fcr_chk_chan_modes() reports whether ertm_info.allowed_modes is
non-zero; forcing it to return 1 does not put BASIC back into the
bitmask. On stacks where the mask ends up 0 the channel still dies,
because l2c_fcr_process_peer_cfg_req() re-tests it when the peer sends
its config request:
l2c_fcr_process_peer_cfg_req() CFG fcr_present:0 fcr.mode:0 \
CCB FCR mode:0 preferred:3 allowed:0
L2CAP - incompatible configurations disconnect
PSM 0x1001 is accepted by the AirPods and then dropped by us, so the
socket surfaces as "read failed, socket might closed or timeout,
read ret: -1" and looks like a refused connection.
Hook l2c_fcr_process_peer_cfg_req() as well and only rewrite the verdict
when the original asks for a disconnect, leaving mode negotiation on
every other L2CAP channel untouched. Patching the verdict rather than
allowed_modes avoids hardcoding a t_l2c_ccb field offset.
Verified on a Xiaomi 14 Pro (HyperOS 2.0.45, Android 14, Snapdragon
8 Gen 3, libbluetooth_qti.so) with AirPods Pro 3.
|
The red check here is not from this patch — the job dies in
Opened #786 for that separately rather than folding it in here. Once it lands this branch should go green on a re-run. Built and verified locally in the meantime: |
|
Could you run the Action on your repo and link it here? I’ll merge the fix for |
|
Ran it on my fork with this branch plus the https://github.com/SAGIRIxr/librepods/actions/runs/35490704019 — (Branch: Two warnings surfaced in the log that are unrelated to either patch, just flagging them:
|
|
Thanks! I'll ask someone to test it. |
android-actions/setup-android defaults `packages` to "tools platform-tools". The obsolete "tools" package is no longer in the SDK repository, so the step fails before Gradle runs: Warning: Failed to find package 'tools' Error: The process '.../sdkmanager' failed with exit code 1 Ask for platform-tools only. The workflow already installs the NDK and accepts licenses in its own steps, so nothing else relied on "tools".
|
The
I opened #786 for the one-line workflow fix on its own, but that PR can never show a green check itself: the The resulting tree ( The new run on this PR is currently sitting at |
The gap in the current workaround
l2c_fcr_chk_chan_modes()is, in AOSP, essentiallyreturn ertm_info.allowed_modes != 0— it strips the modes the peer does not advertise and reports whether anything is left:Forcing it to return 1 makes that one call site pass, but it does not put
L2CAP_FCR_CHAN_OPT_BASICback into the bitmask. When the mask has been reduced to 0,l2c_fcr_process_peer_cfg_req()re-tests it as soon as the peer sends its own config request, and asks for a disconnect.What that looks like on the wire
HCI snoop, AirPods Pro 3, PSM 0x1001:
The stack says why:
allowed:0is the whole bug. The connection was never refused by the AirPods — we drop it ourselves one round trip later, which surfaces in the app asi.e. indistinguishable from a peer refusal, which I suspect is why this has been hard to pin down.
The change
Hook
l2c_fcr_process_peer_cfg_req()alongside the existing two, call the original, and only rewrite the verdict when it asks for a disconnect:Two notes on the shape of this:
allowed_modesavoids hardcoding at_l2c_ccbfield offset, which would be fragile across OEM stacks.DISCONNECTis rewritten, so channels that negotiate normally (A2DP, AVRCP, …) are untouched.UNACCEPTABLEis passed through so the peer still gets its chance to renegotiate.Note for reviewers: the constants are
UNACCEPTABLE = 0,OK = 1,DISCONNECT = 2(stack/l2cap/l2c_int.h).OKbeing 1 rather than 0 is easy to get backwards — a version of this patch that returned 0 built fine and changed nothing.The symbol resolves through the existing
.gnu_debugdatapath with no changes to the finder:Verification
Xiaomi 14 Pro (
shennong), HyperOS 2.0.45 / Android 14, Snapdragon 8 Gen 3,/system_ext/lib64/libbluetooth_qti.so, Magisk + LSPosed, AirPods Pro 3 (A3065).Before:
read ret: -1on every attempt. After:Full AAP session then comes up — battery, model detection (A3065), listening-mode state, and the complete 22-entry control command list all read back from the device.
Possibly related
I have only this one device, so I can't confirm these share the root cause — but the signature to check is
allowed:0plusincompatible configurations disconnectinbt_l2cap, which is cheap to grep for:#672, #624, #416, #413, #393, #335, #211
A quick way for anyone to check their own device:
Unrelated note that cost me an hour
While tracking this down:
libl2c_fcr_hook.sois loaded via memfd, so it does not appear by name in/proc/<bluetooth pid>/maps— only the APK does. I briefly concluded the hook wasn't injecting at all.logcat | grep LibrePodsHookis the reliable check. Might be worth a line in the troubleshooting docs.