8.2. Bluetooth A2DP sink-latency: a two-bug chain in the HAL path (one confirmed, one inferred)¶
A logcat symptom reported on-device — the audio HAL logging an obviously
wrong, perfectly constant Bluetooth sink-latency value thousands of times
during a single playback session — was traced statically to two independent
bugs in the vendor Bluetooth-audio bridge. Bug 1 is confirmed directly
against raw disassembly of the libraries actually present on this device; Bug
2’s HIDL-boundary unit conversion is inferred from arithmetic that exactly
reproduces the observed value (see Honest limits below) rather than read
directly off an instruction. No source was available; everything below is
read from the shipped binaries in
artifacts/vendor_a and the Bluetooth Mainline module in
artifacts/system_a.
8.2.1. The symptom¶
09-15 18:38:50.647 1030 16195 I btaudio_offload_qti: audio_get_a2dp_sink_latency_qti: Sink Latency = 57792
PID 1030 is android.hardware.audio.service (the audio HAL process). The
line repeats at the log rate of the caller (hundreds of times during one BT
session) with the same value every time, and the value itself (57792, read
as milliseconds by the caller) is physically absurd — no real A2DP path has
57.8 seconds of latency. Both the constancy and the magnitude are explained
below by two separate, stackable defects.
8.2.2. The two processes involved¶
This device runs the classic Boox version split (The Onyx/Boox platform layer: an
Android-15-vintage system over an Android-11 vendor), and that split
runs straight through the Bluetooth-audio path:
Client / caller — the audio HAL process,
vendor-side, still on the legacy Qualcomm HIDLvendor.qti.hardware.bluetooth_audio@2.{0,1}interface:artifacts/vendor_a/lib64/btaudio_offload_if.soexportsaudio_get_a2dp_sink_latency/audio_sink_get_a2dp_latency, which tail-call the mangled C++ symbolvendor::qti::btoffload::audio_get_a2dp_sink_latency_qti()— imported (Uinnm -D) from:artifacts/vendor_a/lib64/libbluetooth_audio_session_qti.so(the HIDL-@2.0 build) and its siblinglibbluetooth_audio_session_qti_2_1.so(theV2_1namespace variant) — these hold the actual logic.
Server — the Bluetooth process, now a Mainline APEX (
com.android.btservices), extracted fromartifacts/system_a/system/apex/com.android.btservices.apex→apex_payload.img→app/Bluetooth*/:lib64/libbluetooth_jni.so(13 MB, stripped) statically contains the whole Fluoride/GD Bluetooth stack, including the AVDTP wire parser and theIBluetoothAudioPortserver side that the HAL calls into over HIDL/hwbinder. This process’s ownNEEDEDlist only mentions the new AIDLvendor.qti.hardware.bluetooth.audio-V1-ndk.so— the legacy-HIDLIBluetoothAudioPortserver code is statically linked in, not a separate dependency, which is why it doesn’t show up as a named library.
8.2.3. Bug 1 — the “sink latency” is queried once per connection, then frozen forever¶
vendor::qti::btoffload::audio_get_a2dp_sink_latency_qti() at file offset
0x2f6e8 in libbluetooth_audio_session_qti.so is a 2-instruction stub
(b into a shared tail-code block at 0x2f250 — this address has no
public symbol of its own because the library is stripped and the nearest
preceding exported symbol, an unrelated std::function::swap, is what
nm/objdump misattribute it to; the same “nearest-symbol mis-snap”
gotcha as the kernel funcmap, see Open questions and limits of analysis). Reading the real
body at 0x2f250:
0x2f27c bl pthread_mutex_lock
0x2f280 ldrb w8, [x19, #0xa8] ; BluetoothA2dpControl* cached flag
0x2f284 strh wzr, [x19, #0xaa] ; zero the 16-bit cache slot
0x2f288 cbz w8, 0x2f2ec ; flag==0 -> skip query, keep it zero
0x2f29c add x1, x19, #0xae
...
0x2f2ac bl BluetoothA2dpControl::getSinkLatency(SessionType const&, u64*, u64*, timespec*)
0x2f2b0 ldr x8, [sp, #0x8] ; raw u64 result
... (reciprocal-multiply divide by 10, see Bug 2)
0x2f2e4 strh w9, [x19, #0xaa] ; cache the /10 result
0x2f2e8 bl __android_log_print ; "%s: Sink Latency = %d"
0x2f2ec ... pthread_mutex_unlock
0x2f2f4 ldrh w0, [x19, #0xaa] ; return the (possibly stale) cached value
but the real caching bug is one level down, in
BluetoothA2dpControl::getSinkLatency itself (file offset 0x1f2fc,
same library):
0x1f344 ldrb w8, [x22, #0x20] ; object's "already cached" bool
0x1f348 cbz w8, 0x1f35c ; 0 -> go compute a fresh value
0x1f34c ldr x8, [x22] ; !=0 -> load the FROZEN raw value ...
0x1f350 mov w21, #1
0x1f354 str x8, [x20] ; ... and hand it back, unchanged
0x1f358 b 0x1f3fc ; skip every line below — no re-query
; --- the "compute a fresh value" path, taken exactly once per object ---
0x1f364 bl BluetoothAudioSessionInstance::GetSessionInstance
0x1f37c bl BluetoothAudioSession::GetPresentationPosition(u64*,u64*,timespec*)
0x1f3d8 ldr x8, [x22] ; the HIDL reply, written by the callee
; directly into this->field_0
0x1f3ec str x8, [x20] ; returned to the caller, verbatim
0x1f3f0 strb w9, [x22, #0x20] ; w9==1: sets the "cached" bool — for good
BluetoothA2dpControl is (per The Onyx/Boox platform layer conventions for these
Qualcomm session objects) one instance per active A2DP connection. Its first
getSinkLatency() call does a real HIDL round-trip and latches the result;
every subsequent call for the lifetime of that connection returns the exact
same frozen 64-bit value from offset 0 of the object, no matter how the real
presentation delay changes as the codec/link state evolves. This alone
accounts for “the exact same number, printed hundreds of times” — independent
of whatever that number’s magnitude means.
8.2.4. Bug 2 — a factor-of-10⁵ unit mismatch, silently wrapped through a 16-bit field¶
The value BluetoothAudioSession::GetPresentationPosition writes into
BluetoothA2dpControl’s cache (and that audio_get_a2dp_sink_latency_qti
later divides by 10, per the reciprocal-multiply sequence at 0x2f2b4–
0x2f2cc: mov/movk build the magic constant
0xCCCCCCCCCCCCCCCD, then umulh + lsr #3 — the standard
compiler-generated unsigned-divide-by-10) is a HIDL nanosecond value, not
“tenths of a millisecond”. Tracing the actual origin of that number in the
Bluetooth process settles the unit:
artifacts/btapex_payload/lib64/libbluetooth_jni.socontains, verbatim, the real AOSP AVDTP delay-report parser. Its own debug string says exactly what unit the wire value is in:"Delay Reporting: %u (in 1/10 milliseconds) "
and the parser (
avdt_msg_prs_delay_rpt, xref’d from its__func__string at file offset0xa68c7; body starts~0x9409d8) confirms it in code — it reads two raw bytes from the AVDTP packet, byte-swaps them (rev/lsr #16, i.e. big-endian → hostu16), and stores that unscaled 16-bit integer straight into the parsed-message struct (strh w8, [x19, #0x6]) — no divide, no multiply. The wire value is a plain count of 1/10 ms units, per the AVDTP “Delay Reporting” extension.That raw
u16(still 1/10 ms units) is confirmed to survive unscaled through the next hop too:btif_a2dp_control_set_audio_delay(u16 delay_report)(xref’d via its own__func__string, body at0x7912c8) doesand w8, w0, #0xffffthen, purely to print a human-readable debug line,ucvtf+fdiv s0, s0, #10.0(float divide-by-10 → milliseconds, for the log only) — and separately stores the untouched rawu16(strh w19, [x8, #0x190],w19never reassigned from the originalandresult) into Bluetooth-process global state. Every layer reachable inside the Bluetooth process — wire parser, AVDTP SCB handler,btiflayer — treats and forwards the value consistently as raw 1/10 ms units. No ×10⁵ (1/10 ms → ns) conversion was found in any of them.
That conversion must therefore happen exactly at the HIDL boundary — inside
the (statically linked, unexported, and so not directly symbol-searchable)
IBluetoothAudioPort::getPresentationPosition() server implementation in
libbluetooth_jni.so that marshals the reply — which could not be pinned
to a specific instruction (see Honest limits below).
But the arithmetic reproducing the observed value exactly is strong evidence
on its own, even without it. Assume a perfectly ordinary 150.0 ms real sink
delay:
wire value (1/10 ms units): 1500
correctly relayed as ns: 1500 * 100_000 = 150_000_000 ns
audio_get_a2dp_sink_latency_qti
divides by 10 (as if the input were still 1/10 ms): 15_000_000
... then truncates into a 16-bit field (strh): 15_000_000 mod 65536
= 57_792
57792 — an exact match for the logged value, reproduced from a normal 150 ms real latency run through the wrong divisor and truncated into a field four bytes too small. The value the HAL logs and hands to AudioFlinger as “sink latency in milliseconds” is 64-bit-nanoseconds divided by 10 instead of 1,000,000, then reduced modulo 65536 — a number with no remaining physical meaning that coincidentally falls in the plausible millisecond range.
8.2.5. Combined effect¶
Per BT connection: the very first getSinkLatency() call captures whatever
nanosecond delay HIDL reports at that instant, mis-scales it by 10⁵ and
truncates it into 16 bits (Bug 2), and every later call — regardless of
codec, renegotiation, or how long the session runs — replays that exact same
mangled number forever (Bug 1). That combination accounts for the logged
symptom: a downstream AudioFlinger consumer would be treating the reported
latency as both wrong and constant, and would be sizing anything derived
from it (buffer/frame counts, jitter windows) off a number that bears no
relationship to the link’s actual behaviour — though what AudioFlinger
itself actually does with the value was not traced.
8.2.6. Honest limits¶
The exact instruction that converts the AVDTP 1/10 ms wire unit into the nanosecond value
BluetoothAudioSession::GetPresentationPositionhands back over HIDL was not located.libbluetooth_jni.sois a 13 MB stripped binary with the entire GD/Fluoride stack statically linked and no exported C++ symbols for its internals; the HIDL server-side marshalling code forIBluetoothAudioPortcould not be isolated by string/symbol cross-reference. The ×100,000 relayed-as-ns step above is inferred from (a) the wire unit being definitively 1/10 ms (verified in-binary), (b) the HIDL field being definitively read and divided-by-10 on the HAL side as if it were still 1/10 ms (verified in-binary), and (c) the arithmetic reproducing the observed 57792 exactly under the standard AOSP HIDL convention (remoteDeviceAudioDelayNanos, a real nanoseconds field) for that callback — not from having read the conversion instruction itself.Whether
BluetoothA2dpControl’s cached-flag (object offset0x20) is ever cleared on disconnect/reconnect (which would bound Bug 1 to “frozen per connection” rather than “frozen for the process lifetime”) was not traced; the constructor/destructor and any connection-teardown callback were not located.No live device trace was taken; this is a purely static reconstruction from the binaries shipped on this device, cross-checked against the single logcat line quoted above.
8.2.7. Provenance¶
- Source:
artifacts/vendor_a/lib64/btaudio_offload_if.so,artifacts/vendor_a/lib64/libbluetooth_audio_session_qti.so(+_qti_2_1.so), extracted read-only with7zfromartifacts/super/vendor_a.img; andartifacts/system_a/system/apex/com.android.btservices.apex→apex_payload.img→lib64/libbluetooth_jni.so, extracted the same way fromartifacts/super/system_a.img(the APEX’s innerapex_payload.imgis itself an ext4 image, unpacked with a second7zpass).- Method:
nm -Dfor dynamic symbols; segment-mapped AArch64 disassembly with the pixillvm-objdump(systemobjdumpcannot demangle/handle this cleanly) and a small capstone-based ADRP/ADD cross-referencer to resolve which functions reference which.rodatastrings in a stripped 13 MB binary with no local symbol table. Every address and instruction sequence quoted above was read directly off these binaries; the AVDTP-unit and HIDL-nanosecond conventions cited are the device’s own debug strings and the standard public HIDLIBluetoothAudioPortcontract, not source code available in this dump.- Cross-refs:
Wireless drivers and HAL (the
bluetooth@1.0-service-qti/bluetooth_audioHAL entry this bug lives under), The Onyx/Boox platform layer (the Android-15-system/Android-11-vendor split that puts the HIDL client and the AIDL-capable Mainline BT stack on either side of this boundary).