Skip to content

Return error if calling function will block host task#419

Open
h2zero wants to merge 1 commit intomasterfrom
err-on-block
Open

Return error if calling function will block host task#419
h2zero wants to merge 1 commit intomasterfrom
err-on-block

Conversation

@h2zero
Copy link
Copy Markdown
Owner

@h2zero h2zero commented Mar 30, 2026

This will prevent applications from hanging forever when calling a blocking function from the host task.

Summary by CodeRabbit

  • Bug Fixes
    • Added safety guards across Bluetooth operations to detect and prevent invalid API usage from problematic execution contexts, improving stability and preventing potential hangs or application crashes.

This will prevent applications from hanging forever when calling a blocking function from the host task.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This pull request adds runtime guards across multiple NimBLE methods to prevent blocking or discovery operations from executing within the NimBLE host task context. A new task-tracking mechanism records the host task identifier at startup, enabling callers to detect and reject unsafe operations with early returns and error logging.

Changes

Cohort / File(s) Summary
Task Tracking Infrastructure
src/NimBLEUtils.h, src/NimBLEUtils.cpp
Added static m_hostTaskHandle, getHostTaskHandle(), and inHostTask() APIs to track and detect execution within the NimBLE host task; updated debug assertion macro formatting.
Device Initialization
src/NimBLEDevice.cpp
Captures the current FreeRTOS task identifier into m_hostTaskHandle when host_task starts.
Client Operations
src/NimBLEClient.cpp
Added guards to reject synchronous connect() and secureConnection() calls from host task context, logging errors and returning failure codes early.
Remote Service & Characteristic Access
src/NimBLERemoteService.cpp, src/NimBLERemoteCharacteristic.cpp, src/NimBLERemoteValueAttribute.cpp
Added inHostTask() checks in getCharacteristic(), getCharacteristics() (refresh path), getDescriptor(), readValue(), and writeValue() to prevent cache access or discovery from host task context.
Scan Operations
src/NimBLEScan.cpp
Added guard in getResults() to prevent blocking scan invocation from host task, returning cached results immediately instead.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🚫 No blocking calls from within the host task zone,
Early guards catch those who wander alone,
Deadlocks prevented with a simple check,
Keeping the BLE stack safe on deck!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Aye, the title 'Return error if calling function will block host task' be perfectly aligned with the main objective—preventin' functions from blockin' the host task by returni' errors instead, just as the summaries describe across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch err-on-block

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/NimBLEClient.cpp`:
- Around line 683-685: The guard that rejects calls from the host task
(NimBLEUtils::inHostTask()) is currently placed at the start of getService(),
preventing access to cached services; move that inHostTask() check so it runs
immediately before the first call to retrieveServices() (i.e., only when a cache
miss would trigger a blocking retrieval), and leave the early-path that returns
a cached service intact so host-task callbacks can still return previously
discovered services from the cache via getService().

In `@src/NimBLEDevice.cpp`:
- Around line 879-881: The host task ID stored in NimBLEUtils::m_hostTaskHandle
is set before calling nimble_port_run() but never cleared when nimble_port_run()
returns, causing inHostTask() to give false positives after deinit; fix by
resetting NimBLEUtils::m_hostTaskHandle to an invalid value (e.g., 0/null) once
nimble_port_run() returns and before/after calling nimble_port_freertos_deinit()
so the stored task handle is not reused by unrelated code.

In `@src/NimBLERemoteCharacteristic.cpp`:
- Around line 158-160: The guard that blocks host-task context is placed too
early in getDescriptor(), preventing cached descriptors from being returned;
move the NimBLEUtils::inHostTask() check so it happens immediately before
calling retrieveDescriptors() (i.e., keep early cache-scan logic in
getDescriptor() intact, then if a discovery is required call the inHostTask()
guard before invoking retrieveDescriptors()); update references in
getDescriptor() and ensure setNotify() still treats a found cached CCCD
correctly (no change to setNotify() logic required).

In `@src/NimBLERemoteService.cpp`:
- Around line 78-80: The current early-return in getCharacteristic when
NimBLEUtils::inHostTask() is true prevents returning cached characteristics;
instead, only prevent calling the blocking retrieveCharacteristics(). Modify
getCharacteristic so it still scans the existing cache (the loop that checks
cached characteristics) even when inHostTask() is true, but conditionally skip
or gate the call to retrieveCharacteristics() (and log if desired) when
inHostTask() to avoid blocking the host task; reference functions/methods:
getCharacteristic(), retrieveCharacteristics(), and NimBLEUtils::inHostTask() to
locate and implement this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc557eb3-0366-4adb-8ddc-ea45aa79f46a

📥 Commits

Reviewing files that changed from the base of the PR and between dd32365 and 24a8919.

📒 Files selected for processing (8)
  • src/NimBLEClient.cpp
  • src/NimBLEDevice.cpp
  • src/NimBLERemoteCharacteristic.cpp
  • src/NimBLERemoteService.cpp
  • src/NimBLERemoteValueAttribute.cpp
  • src/NimBLEScan.cpp
  • src/NimBLEUtils.cpp
  • src/NimBLEUtils.h

Comment on lines +683 to +685
if (NimBLEUtils::inHostTask()) {
NIMBLE_LOGE(LOG_TAG, "getService cannot be called from host task");
return nullptr;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Arrr, cached services should still be reachable.

This now rejects getService() before the cache scan on Lines 688-693. That breaks host-task callbacks that only need a previously discovered service even though no blocking call would occur. Move the guard to just before the first retrieveServices() call so only cache misses are rejected.

⚓ Proposed fix
 NimBLERemoteService* NimBLEClient::getService(const NimBLEUUID& uuid) {
     NIMBLE_LOGD(LOG_TAG, ">> getService: uuid: %s", uuid.toString().c_str());
-    if (NimBLEUtils::inHostTask()) {
-        NIMBLE_LOGE(LOG_TAG, "getService cannot be called from host task");
-        return nullptr;
-    }
 
     for (auto& it : m_svcVec) {
         if (it->getUUID() == uuid) {
             NIMBLE_LOGD(LOG_TAG, "<< getService: found the service with uuid: %s", uuid.toString().c_str());
             return it;
         }
     }
+
+    if (NimBLEUtils::inHostTask()) {
+        NIMBLE_LOGE(LOG_TAG, "getService cannot discover from host task");
+        return nullptr;
+    }
 
     size_t prevSize = m_svcVec.size();
     if (retrieveServices(&uuid)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 683 - 685, The guard that rejects calls
from the host task (NimBLEUtils::inHostTask()) is currently placed at the start
of getService(), preventing access to cached services; move that inHostTask()
check so it runs immediately before the first call to retrieveServices() (i.e.,
only when a cache miss would trigger a blocking retrieval), and leave the
early-path that returns a cached service intact so host-task callbacks can still
return previously discovered services from the cache via getService().

Comment on lines +879 to 881
NimBLEUtils::m_hostTaskHandle = ble_npl_get_current_task_id();
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
nimble_port_freertos_deinit();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'm_hostTaskHandle|inHostTask\s*\(' src

Repository: h2zero/esp-nimble-cpp

Length of output: 5298


Arr, swab the deck—reset m_hostTaskHandle when the host task be walkin' the plank!

This bit o' code be stashin' the host-task ID at startup but never clairin' it when nimble_port_run() returns, ye scallywag! If th' NPL/RTOS be reusinn' that task handle later, unrelated callers might start trippin' inHostTask() checks and gettin' spurious early rejects after deinit.

⚓ Proposed fix
 void NimBLEDevice::host_task(void* param) {
     NIMBLE_LOGI(LOG_TAG, "NimBLE Started!");
     NimBLEUtils::m_hostTaskHandle = ble_npl_get_current_task_id();
     nimble_port_run(); // This function will return only when nimble_port_stop() is executed
+    NimBLEUtils::m_hostTaskHandle = nullptr;
     nimble_port_freertos_deinit();
 } // host_task
📝 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.

Suggested change
NimBLEUtils::m_hostTaskHandle = ble_npl_get_current_task_id();
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
nimble_port_freertos_deinit();
NimBLEUtils::m_hostTaskHandle = ble_npl_get_current_task_id();
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
NimBLEUtils::m_hostTaskHandle = nullptr;
nimble_port_freertos_deinit();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEDevice.cpp` around lines 879 - 881, The host task ID stored in
NimBLEUtils::m_hostTaskHandle is set before calling nimble_port_run() but never
cleared when nimble_port_run() returns, causing inHostTask() to give false
positives after deinit; fix by resetting NimBLEUtils::m_hostTaskHandle to an
invalid value (e.g., 0/null) once nimble_port_run() returns and before/after
calling nimble_port_freertos_deinit() so the stored task handle is not reused by
unrelated code.

Comment on lines +158 to +160
if (NimBLEUtils::inHostTask()) {
NIMBLE_LOGE(LOG_TAG, "getDescriptor cannot be called from host task");
return nullptr;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Arrr, let cached descriptors pass; only block discovery.

This return fires before the cache scan on Lines 167-172, so even an already discovered descriptor now looks missing from host-task callbacks. Downstream, setNotify() on Lines 249-257 treats that nullptr as “CCCD not found” and returns true, so a cached CCCD becomes a silent no-op. Move the guard to just before retrieveDescriptors().

⚓ Proposed fix
 NimBLERemoteDescriptor* NimBLERemoteCharacteristic::getDescriptor(const NimBLEUUID& uuid) const {
     NIMBLE_LOGD(LOG_TAG, ">> getDescriptor: uuid: %s", uuid.toString().c_str());
-    if (NimBLEUtils::inHostTask()) {
-        NIMBLE_LOGE(LOG_TAG, "getDescriptor cannot be called from host task");
-        return nullptr;
-    }
 
     NimBLEUUID             uuidTmp{uuid};
     NimBLETaskData         taskData(const_cast<NimBLERemoteCharacteristic*>(this));
     NimBLEDescriptorFilter filter{nullptr, &uuidTmp, &taskData};
 
     for (const auto& dsc : m_vDescriptors) {
         if (dsc->getUUID() == uuid) {
             filter.dsc = dsc;
             goto Done;
         }
     }
+
+    if (NimBLEUtils::inHostTask()) {
+        NIMBLE_LOGE(LOG_TAG, "getDescriptor cannot discover from host task");
+        goto Done;
+    }
 
     if (!retrieveDescriptors(&filter) || filter.dsc) {
         goto Done;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLERemoteCharacteristic.cpp` around lines 158 - 160, The guard that
blocks host-task context is placed too early in getDescriptor(), preventing
cached descriptors from being returned; move the NimBLEUtils::inHostTask() check
so it happens immediately before calling retrieveDescriptors() (i.e., keep early
cache-scan logic in getDescriptor() intact, then if a discovery is required call
the inHostTask() guard before invoking retrieveDescriptors()); update references
in getDescriptor() and ensure setNotify() still treats a found cached CCCD
correctly (no change to setNotify() logic required).

Comment on lines +78 to +80
if (NimBLEUtils::inHostTask()) {
NIMBLE_LOGE(LOG_TAG, "getCharacteristic cannot be called from host task");
return nullptr;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Arrr, don’t sink cached characteristic lookups.

This guard runs before the cache scan on Lines 85-91, so host-task callbacks can no longer fetch an already discovered characteristic even though no blocking call would occur. Gate only retrieveCharacteristics() here.

⚓ Proposed fix
 NimBLERemoteCharacteristic* NimBLERemoteService::getCharacteristic(const NimBLEUUID& uuid) const {
     NIMBLE_LOGD(LOG_TAG, ">> getCharacteristic: uuid: %s", uuid.toString().c_str());
-    if (NimBLEUtils::inHostTask()) {
-        NIMBLE_LOGE(LOG_TAG, "getCharacteristic cannot be called from host task");
-        return nullptr;
-    }
 
     NimBLERemoteCharacteristic* pChar     = nullptr;
 
     for (const auto& it : m_vChars) {
         if (it->getUUID() == uuid) {
             pChar = it;
             NIMBLE_LOGD(LOG_TAG, "<< getCharacteristic: found in cache");
             return pChar;
         }
     }
+
+    if (NimBLEUtils::inHostTask()) {
+        NIMBLE_LOGE(LOG_TAG, "getCharacteristic cannot discover from host task");
+        return nullptr;
+    }
 
     if (retrieveCharacteristics(&uuid, &pChar) && pChar == nullptr) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLERemoteService.cpp` around lines 78 - 80, The current early-return
in getCharacteristic when NimBLEUtils::inHostTask() is true prevents returning
cached characteristics; instead, only prevent calling the blocking
retrieveCharacteristics(). Modify getCharacteristic so it still scans the
existing cache (the loop that checks cached characteristics) even when
inHostTask() is true, but conditionally skip or gate the call to
retrieveCharacteristics() (and log if desired) when inHostTask() to avoid
blocking the host task; reference functions/methods: getCharacteristic(),
retrieveCharacteristics(), and NimBLEUtils::inHostTask() to locate and implement
this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant