Skip to content

services/ans: Fix category 0 skipped by immediate-notify-all command#2267

Open
94xhn wants to merge 1 commit into
apache:masterfrom
94xhn:fix/ans-ctrl-pt-immediate-notify-skips-cat0
Open

services/ans: Fix category 0 skipped by immediate-notify-all command#2267
94xhn wants to merge 1 commit into
apache:masterfrom
94xhn:fix/ans-ctrl-pt-immediate-notify-skips-cat0

Conversation

@94xhn

@94xhn 94xhn commented Jul 12, 2026

Copy link
Copy Markdown

Summary

ble_svc_ans_access() in nimble/host/services/ans/src/ble_svc_ans.c has an
off-by-one loop boundary that causes category 0 (BLE_SVC_ANS_CAT_ID_SIMPLE_ALERT,
the most basic ANS category) to be silently skipped whenever a client sends the
Notify New Alert Immediately or Notify Unread Alert Immediately control-point
command.

for (i = BLE_SVC_ANS_CAT_NUM - 1; i > 0; --i) {
    if ((ble_svc_ans_new_alert_cat >> i) & 0x01) {
        ble_svc_ans_new_alert_notify(i, NULL);
    }
}

i is signed (int i;, declared at line 160), BLE_SVC_ANS_CAT_NUM is 8
(nimble/host/services/ans/include/services/ans/ble_svc_ans.h line 63), and
BLE_SVC_ANS_CAT_ID_SIMPLE_ALERT is 0 (same header, line 51). The loop
condition i > 0 means the body never executes with i == 0, so even when a
client has enabled category 0 via
BLE_SVC_ANS_CMD_EN_NEW_ALERT_CAT (cmd id 0, cat_bit_mask = 1 << 0), an
immediate-notify-all command will never emit a notification for it.

This is a plain logic bug, not a hypothetical: category 0 is a normal,
reachable category per the ANS spec, there is no special-casing that excludes
it elsewhere in the file, and the array it indexes into
(ble_svc_ans_new_alert_cnt[BLE_SVC_ANS_CAT_NUM], size 8) has a valid slot 0.

The identical pattern is duplicated for the Unread Alert Status characteristic:

for (i = BLE_SVC_ANS_CAT_NUM - 1; i > 0; --i) {
    if ((ble_svc_ans_unr_alert_cat >> i) & 0x01) {
        ble_svc_ans_unr_alert_notify(i, NULL);
    }
}

Fix

Change both loop bounds from i > 0 to i >= 0 so the iteration includes
category 0. This is a minimal, single-character (>>=) change in two
places; no other logic is touched.

History check

The repo is a shallow clone (--depth 1), so I used gh api repos/apache/mynewt-nimble/commits?path=nimble/host/services/ans/src/ble_svc_ans.c
to pull the full history of this file (16 commits). The file has contained
this loop shape since it was moved from profiles to services in 2016.
One later commit, 2b5468af4 ("nimble/ans: Fix out of bounds access",
2017), touched the same function but fixed a different problem — handling of
an invalid cat_id (neither < BLE_SVC_ANS_CAT_NUM nor 0xff) — and did
not touch this loop boundary. So this off-by-one has never been fixed.

Verification

Because ble_svc_ans_new_alert_notify()/ble_svc_ans_unr_alert_notify()
ultimately call ble_gatts_notify(), which needs a full BLE host runtime to
run, I wrote a standalone reproduction that extracts the real switch/loop
logic from ble_svc_ans_access() verbatim (same variable types, same masks,
same loop bounds) and replaces only the notify call with a recording stub
function, compiled with/without a -DBUGGY macro to toggle the old vs. new
loop bound:

  • Before fix (-DBUGGY, i > 0):
    • Case 1 (only category 0 enabled): 0 notifications sent — category 0
      is silently dropped.
    • Case 2 (all 8 categories enabled): 7 notifications sent (expected
      8) — category 0 is the one missing.
  • After fix (i >= 0):
    • Case 1: 1 notification sent (category 0), as expected.
    • Case 2: 8 notifications sent (categories 7..0), as expected.

Both binaries were actually compiled (gcc, MinGW) and run on this machine;
output matches the analysis above exactly.

Disclosure

This investigation, the fix, and this PR description were prepared with the
assistance of Claude (Anthropic AI). I (the human submitter) reviewed the
diff, the reasoning above, and the reproduction results before submitting.
Happy to answer any follow-up questions about the change.

The Alert Notification Service control point handler for
BLE_SVC_ANS_CMD_NOT_NEW_ALERT_IMMEDIATE and
BLE_SVC_ANS_CMD_NOT_UNR_ALERT_IMMEDIATE loops over all supported
categories when the peer writes cat_id == 0xff ("all categories"):

    for (i = BLE_SVC_ANS_CAT_NUM - 1; i > 0; --i) {
        if ((ble_svc_ans_new_alert_cat >> i) & 0x01) {
            ble_svc_ans_new_alert_notify(i, NULL);
        }
    }

The loop condition `i > 0` stops before `i` reaches 0, so category ID
0 (BLE_SVC_ANS_CAT_ID_SIMPLE_ALERT, "Simple Alert") is never notified
by this path, even when it is enabled and has a pending alert. If
Simple Alert is the only category the peer has enabled, a 0xff
immediate-notify request results in zero notifications being sent.

Change the bound to `i >= 0` so category 0 is included, matching the
same fix in both the new-alert and unread-alert-status branches.

Signed-off-by: 94xhn <87560781+94xhn@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

host size/XS Extra small PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants