feat: Auto-detect L2CAP function offsets via dlsym for Android#449
feat: Auto-detect L2CAP function offsets via dlsym for Android#449ayaanngandhi wants to merge 2 commits intokavishdevar:mainfrom
Conversation
…Version - Fixed compilation error: static declaration follows non-static declaration - Added explicit ndkVersion = 27.1.12297006 to build.gradle.kts - Functions getModuleBase, findLibraryPath, findSymbolOffset now match header declarations
📝 WalkthroughWalkthroughPull request adds ndkVersion property configuration to Android build script and modifies C++ hook implementation to use dynamic symbol resolution for hook offsets instead of hardcoded values, with system property override support and fallback mechanisms. Changes
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
android/app/src/main/cpp/l2c_fcr_hook.cpp (1)
204-217:⚠️ Potential issue | 🔴 CriticalNull pointer dereference on Line 210 — will crash if
p_device_infois null.Line 206 guards the writes with
if (p_device_info), but Line 210 dereferencesp_device_info->vendorunconditionally. Ifp_device_infois null, this crashes the Bluetooth process.🐛 Proposed fix — move the log inside the null guard
tBTA_STATUS fake_BTA_DmSetLocalDiRecord(tSDP_DI_RECORD* p_device_info, uint32_t* p_handle) { LOGI("BTA_DmSetLocalDiRecord hooked - forcing vendor fields"); if (p_device_info) { p_device_info->vendor = 0x004C; p_device_info->vendor_id_source = 0x0001; + LOGI("Set vendor=0x%04x, vendor_id_source=0x%04x", p_device_info->vendor, p_device_info->vendor_id_source); + } else { + LOGE("p_device_info is null, skipping vendor override"); } - LOGI("Set vendor=0x%04x, vendor_id_source=0x%04x", p_device_info->vendor, p_device_info->vendor_id_source); if (original_BTA_DmSetLocalDiRecord) { return original_BTA_DmSetLocalDiRecord(p_device_info, p_handle); }
🤖 Fix all issues with AI agents
In `@android/app/build.gradle.kts`:
- Line 12: Update the pinned NDK version string assigned to ndkVersion from
"27.1.12297006" to a more current release; change the value to either the
conservative LTS bump "27.3.13750724" or the latest stable "29.0.14206865"
(replace the existing ndkVersion = "27.1.12297006" line accordingly).
In `@android/app/src/main/cpp/l2c_fcr_hook.cpp`:
- Around line 474-478: The fallback dlopen call using RTLD_NOW can force-load
and reinitialize the library; instead, if findLibraryPath located the library in
/proc/self/maps but dlopen(lib_path, RTLD_NOW | RTLD_NOLOAD) returns null, do
not call dlopen(lib_path, RTLD_NOW). Modify the logic around lib_handle so that
when findLibraryPath indicates the library is mapped but RTLD_NOLOAD failed, you
log a warning (use the module's logging facility) and return 0/skip hooking
rather than forcing a load; keep the fallback dlopen only for cases where
findLibraryPath did not report an existing mapping. Ensure you update the block
handling dlopen(lib_path, RTLD_NOW | RTLD_NOLOAD), dlopen(lib_path, RTLD_NOW),
lib_handle and the early return flow accordingly.
- Around line 441-468: The dlopen(NULL)/dlsym path in findSymbolOffset can
return an address from a different library, producing an incorrect offset when
you subtract getModuleBase(module_name); update findSymbolOffset to verify that
the resolved sym_addr actually lies within the target module before returning an
offset — either (A) obtain the module base via getModuleBase(module_name) and
module size (or parse /proc/self/maps) and check sym_addr is between base and
base+size, or (B) prefer a targeted lookup by dlopen(module_path) / dlsym on the
specific module (falling back to dlopen(NULL) only if the module-specific lookup
fails); ensure error logs mention when the address is out-of-range and do not
return an offset unless the address validation passes so findAndHookFunction
cannot compute a bogus hook target.
🧹 Nitpick comments (1)
android/app/src/main/cpp/l2c_fcr_hook.cpp (1)
259-377: Extract the repeated property-parsing boilerplate into a shared helper.The hex-string-from-system-property parsing is duplicated verbatim across all five offset loaders (~20 identical lines each). This makes it easy for inconsistencies to creep in — for example,
loadSdpOffset(Line 192) uses%xforuintptr_twhile the changed loaders correctly use%lxwith a cast.A small helper would collapse all five to one-liners:
♻️ Sketch of a shared helper
// Returns parsed offset from the named system property, or 0 if absent / malformed. static uintptr_t loadOffsetFromProperty(const char* property_name, const char* label) { char value[PROP_VALUE_MAX] = {0}; int len = __system_property_get(property_name, value); if (len <= 0) return 0; LOGI("Read %s offset from property: %s", label, value); const char* parse_start = value; if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) parse_start = value + 2; char* endptr = nullptr; errno = 0; uintptr_t offset = strtoul(parse_start, &endptr, 16); if (errno == 0 && endptr != parse_start && *endptr == '\0' && offset > 0) { LOGI("Parsed %s offset from property: 0x%lx", label, (unsigned long)offset); return offset; } LOGE("Failed to parse %s offset from property value: %s", label, value); return 0; }Each loader then simplifies to:
uintptr_t loadL2cuProcessCfgReqOffset() { uintptr_t off = loadOffsetFromProperty("persist.librepods.cfg_req_offset", "l2cu_process_our_cfg_req"); if (off > 0) return off; // dynamic lookup … }
| android { | ||
| namespace = "me.kavishdevar.librepods" | ||
| compileSdk = 36 | ||
| ndkVersion = "27.1.12297006" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Android NDK 27.1.12297006 release date current version 2025 2026
💡 Result:
- Android NDK 27.1.12297006 corresponds to NDK r27b, which was announced/released on September 5, 2024 (stable channel in SDK Manager). [1]
- Current (latest stable) NDK as of Feb 6, 2026: NDK r29,
ndkVersion "29.0.14206865"(r29 series is dated October 2025 in the revision history). [2][3] - Current LTS line (r27 LTS) latest point release shown on the downloads page: NDK r27d,
ndkVersion "27.3.13750724". [2]
Sources:
[1] https://groups.google.com/g/android-ndk-announce/c/rpC6OBfwq7g
[2] https://developer.android.com/ndk/downloads
[3] https://developer.android.com/ndk/downloads/revision_history
Pinning ndkVersion is good practice, but update to a more current release.
NDK 27.1.12297006 (r27b) is from September 2024 and is ~17 months behind the current stable NDK r29 (29.0.14206865, October 2025). Either update to the latest r27 LTS point release (27.3.13750724) for a conservative bump, or upgrade to the latest stable NDK r29 (29.0.14206865) to benefit from recent security and ABI improvements.
🤖 Prompt for AI Agents
In `@android/app/build.gradle.kts` at line 12, Update the pinned NDK version
string assigned to ndkVersion from "27.1.12297006" to a more current release;
change the value to either the conservative LTS bump "27.3.13750724" or the
latest stable "29.0.14206865" (replace the existing ndkVersion = "27.1.12297006"
line accordingly).
| uintptr_t findSymbolOffset(const char* symbol_name, const char* module_name) { | ||
| LOGI("findSymbolOffset: Looking up symbol '%s' in module '%s'", symbol_name, module_name); | ||
|
|
||
| // First, try dlopen(NULL) to search all loaded libraries | ||
| void* handle = dlopen(NULL, RTLD_NOW); | ||
| if (handle) { | ||
| dlerror(); // Clear any existing error | ||
| void* sym_addr = dlsym(handle, symbol_name); | ||
| const char* error = dlerror(); | ||
|
|
||
| if (sym_addr && !error) { | ||
| uintptr_t base_addr = getModuleBase(module_name); | ||
| if (base_addr > 0) { | ||
| uintptr_t offset = reinterpret_cast<uintptr_t>(sym_addr) - base_addr; | ||
| LOGI("findSymbolOffset: Found '%s' via dlopen(NULL) at %p, base=%p, offset=0x%lx", | ||
| symbol_name, sym_addr, (void*)base_addr, (unsigned long)offset); | ||
| dlclose(handle); | ||
| return offset; | ||
| } else { | ||
| LOGE("findSymbolOffset: Found symbol but couldn't get module base for %s", module_name); | ||
| } | ||
| } else { | ||
| LOGI("findSymbolOffset: dlsym(NULL, '%s') failed: %s", symbol_name, error ? error : "symbol not found"); | ||
| } | ||
| dlclose(handle); | ||
| } else { | ||
| LOGE("findSymbolOffset: dlopen(NULL) failed: %s", dlerror()); | ||
| } |
There was a problem hiding this comment.
dlopen(NULL) + dlsym may resolve the symbol from a different library, producing an incorrect offset.
dlsym(handle, symbol_name) with a NULL handle searches all loaded shared objects globally. If the symbol happens to exist in a library other than module_name, sym_addr will point into that other library's address space while base_addr is the base of module_name — the subtraction on Line 454 would yield a bogus offset. When later added back to base_addr in findAndHookFunction, this hooks arbitrary memory.
Consider validating that the resolved symbol actually falls within the module's mapped range, or prefer the more targeted second approach (dlopen by path) as the primary lookup.
🛡️ Proposed fix — add a sanity check on the resolved address
if (sym_addr && !error) {
uintptr_t base_addr = getModuleBase(module_name);
if (base_addr > 0) {
uintptr_t offset = reinterpret_cast<uintptr_t>(sym_addr) - base_addr;
+ // Sanity check: offset should be reasonable (< 256MB) and sym should be above base
+ if (reinterpret_cast<uintptr_t>(sym_addr) >= base_addr && offset < 0x10000000) {
LOGI("findSymbolOffset: Found '%s' via dlopen(NULL) at %p, base=%p, offset=0x%lx",
symbol_name, sym_addr, (void*)base_addr, (unsigned long)offset);
dlclose(handle);
return offset;
+ } else {
+ LOGI("findSymbolOffset: Symbol '%s' at %p is not within module %s (base=%p), skipping",
+ symbol_name, sym_addr, module_name, (void*)base_addr);
+ }
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uintptr_t findSymbolOffset(const char* symbol_name, const char* module_name) { | |
| LOGI("findSymbolOffset: Looking up symbol '%s' in module '%s'", symbol_name, module_name); | |
| // First, try dlopen(NULL) to search all loaded libraries | |
| void* handle = dlopen(NULL, RTLD_NOW); | |
| if (handle) { | |
| dlerror(); // Clear any existing error | |
| void* sym_addr = dlsym(handle, symbol_name); | |
| const char* error = dlerror(); | |
| if (sym_addr && !error) { | |
| uintptr_t base_addr = getModuleBase(module_name); | |
| if (base_addr > 0) { | |
| uintptr_t offset = reinterpret_cast<uintptr_t>(sym_addr) - base_addr; | |
| LOGI("findSymbolOffset: Found '%s' via dlopen(NULL) at %p, base=%p, offset=0x%lx", | |
| symbol_name, sym_addr, (void*)base_addr, (unsigned long)offset); | |
| dlclose(handle); | |
| return offset; | |
| } else { | |
| LOGE("findSymbolOffset: Found symbol but couldn't get module base for %s", module_name); | |
| } | |
| } else { | |
| LOGI("findSymbolOffset: dlsym(NULL, '%s') failed: %s", symbol_name, error ? error : "symbol not found"); | |
| } | |
| dlclose(handle); | |
| } else { | |
| LOGE("findSymbolOffset: dlopen(NULL) failed: %s", dlerror()); | |
| } | |
| uintptr_t findSymbolOffset(const char* symbol_name, const char* module_name) { | |
| LOGI("findSymbolOffset: Looking up symbol '%s' in module '%s'", symbol_name, module_name); | |
| // First, try dlopen(NULL) to search all loaded libraries | |
| void* handle = dlopen(NULL, RTLD_NOW); | |
| if (handle) { | |
| dlerror(); // Clear any existing error | |
| void* sym_addr = dlsym(handle, symbol_name); | |
| const char* error = dlerror(); | |
| if (sym_addr && !error) { | |
| uintptr_t base_addr = getModuleBase(module_name); | |
| if (base_addr > 0) { | |
| uintptr_t offset = reinterpret_cast<uintptr_t>(sym_addr) - base_addr; | |
| // Sanity check: offset should be reasonable (< 256MB) and sym should be above base | |
| if (reinterpret_cast<uintptr_t>(sym_addr) >= base_addr && offset < 0x10000000) { | |
| LOGI("findSymbolOffset: Found '%s' via dlopen(NULL) at %p, base=%p, offset=0x%lx", | |
| symbol_name, sym_addr, (void*)base_addr, (unsigned long)offset); | |
| dlclose(handle); | |
| return offset; | |
| } else { | |
| LOGI("findSymbolOffset: Symbol '%s' at %p is not within module %s (base=%p), skipping", | |
| symbol_name, sym_addr, module_name, (void*)base_addr); | |
| } | |
| } else { | |
| LOGE("findSymbolOffset: Found symbol but couldn't get module base for %s", module_name); | |
| } | |
| } else { | |
| LOGI("findSymbolOffset: dlsym(NULL, '%s') failed: %s", symbol_name, error ? error : "symbol not found"); | |
| } | |
| dlclose(handle); | |
| } else { | |
| LOGE("findSymbolOffset: dlopen(NULL) failed: %s", dlerror()); | |
| } |
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/l2c_fcr_hook.cpp` around lines 441 - 468, The
dlopen(NULL)/dlsym path in findSymbolOffset can return an address from a
different library, producing an incorrect offset when you subtract
getModuleBase(module_name); update findSymbolOffset to verify that the resolved
sym_addr actually lies within the target module before returning an offset —
either (A) obtain the module base via getModuleBase(module_name) and module size
(or parse /proc/self/maps) and check sym_addr is between base and base+size, or
(B) prefer a targeted lookup by dlopen(module_path) / dlsym on the specific
module (falling back to dlopen(NULL) only if the module-specific lookup fails);
ensure error logs mention when the address is out-of-range and do not return an
offset unless the address validation passes so findAndHookFunction cannot
compute a bogus hook target.
| void* lib_handle = dlopen(lib_path, RTLD_NOW | RTLD_NOLOAD); | ||
| if (!lib_handle) { | ||
| // Library might not be loaded yet, try loading it | ||
| lib_handle = dlopen(lib_path, RTLD_NOW); | ||
| } |
There was a problem hiding this comment.
RTLD_NOW fallback may force-load a library with side effects.
If findLibraryPath found the library in /proc/self/maps, it's already mapped. RTLD_NOLOAD failing here likely indicates the mapping isn't from the dynamic linker (e.g., mapped by the runtime in a non-standard way). Falling through to dlopen(lib_path, RTLD_NOW) in that situation could trigger constructors or re-initialize state in the Bluetooth stack.
Consider logging a warning and skipping to return 0 instead of the fallback load.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/l2c_fcr_hook.cpp` around lines 474 - 478, The
fallback dlopen call using RTLD_NOW can force-load and reinitialize the library;
instead, if findLibraryPath located the library in /proc/self/maps but
dlopen(lib_path, RTLD_NOW | RTLD_NOLOAD) returns null, do not call
dlopen(lib_path, RTLD_NOW). Modify the logic around lib_handle so that when
findLibraryPath indicates the library is mapped but RTLD_NOLOAD failed, you log
a warning (use the module's logging facility) and return 0/skip hooking rather
than forcing a load; keep the fallback dlopen only for cases where
findLibraryPath did not report an existing mapping. Ensure you update the block
handling dlopen(lib_path, RTLD_NOW | RTLD_NOLOAD), dlopen(lib_path, RTLD_NOW),
lib_handle and the early return flow accordingly.
Summary
Adds automatic offset detection for L2CAP hook functions, eliminating the need for users to manually find and set offsets on most custom ROMs.
Problem
The hardcoded fallback offset
0x00a55e30doesn't work on most custom ROMs, causing L2CAP connection failures. Users had to manually extract offsets from theirlibbluetooth_jni.sousing tools likenmorreadelf.Solution
Added runtime symbol lookup using
dlsym()to automatically find function offsets when the ROM exports them in the dynamic symbol table.Changes
findSymbolOffset()— usesdlsymto look up symbols at runtimefindLibraryPath()— locates the Bluetooth library via/proc/self/mapsdlsymlookup (auto-detection)Symbols auto-detected
l2c_fcr_chk_chan_modesl2cu_process_our_cfg_reql2c_csm_executel2cu_send_peer_info_reqTesting
Tested on Project Elixir v4.2 (Android 14, Realme 6) with AirPods Pro, 1st Generation, where symbols are exported. Auto-detection correctly found offset
0x7f2ac0without manual configuration.Backward Compatibility
persist.librepods.hook_offset, etc.)Summary by CodeRabbit
Release Notes