Skip to content

Bluetooth: SDP: Add bt_sdp_unregister_service() API - #109101

Open
chengkai15 wants to merge 3 commits into
zephyrproject-rtos:mainfrom
chengkai15:sdp_unregister_service
Open

Bluetooth: SDP: Add bt_sdp_unregister_service() API#109101
chengkai15 wants to merge 3 commits into
zephyrproject-rtos:mainfrom
chengkai15:sdp_unregister_service

Conversation

@chengkai15

@chengkai15 chengkai15 commented May 14, 2026

Copy link
Copy Markdown
Member

Add bt_sdp_unregister_service() to complement the existing bt_sdp_register_service(). Currently there is no way to remove a Service Record from the SDP database once registered, which forces callers into workarounds (-EEXIST tolerance, guard flags that can never be reset, no-op unregister stubs).

The implementation follows the same pattern as bt_l2cap_br_server_unregister(): NULL check → sys_slist_find_and_remove() → LOG_DBG → return.

@zephyrbot zephyrbot added area: Bluetooth area: Bluetooth Classic Bluetooth Classic (BR/EDR) area: Tests Issues related to a particular existing or missing test labels May 14, 2026
@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch from cfb3cac to 7fd34cf Compare May 14, 2026 05:40
jhedberg
jhedberg previously approved these changes May 15, 2026

@lylezhu2012 lylezhu2012 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is the file tests/bluetooth/classic/sdp_s/src/sdp_server.c changed in this PR?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the SDP service cannot be removed anytime.
At least, No SDP discovery is ongoing. Even, it needs to ensure that there are no dynamic L2CAP connections. Even more securely, There is no ACL connections.

And I think we need to add more comments to explain the potential risks associated with this change.

@chengkai15 chengkai15 May 18, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the review @lylezhu2012, this is a valid concern.

I agree that blindly removing a service record from sdp_db is unsafe if there are active SDP transactions (especially multi-PDU responses with continuation state). Here are a few options maybe for adding safety:

  1. Documentation only — Add detailed comments/doxygen explaining the preconditions (no active SDP transactions, ideally no ACL connections), leave enforcement to the caller. This matches how
    bt_l2cap_br_server_unregister() works today.

  2. Runtime check on SDP L2CAP channel — Return -EBUSY if any SDP PSM (0x0001) L2CAP channel is currently connected. This catches the most obvious dangerous case.

  3. Runtime check on BR/EDR ACL connections — Return -EBUSY if any BT_CONN_TYPE_BR connection exists. Safest, but may be too restrictive for use cases that need dynamic service registration/unregistration
    while connected.

  4. Hybrid (option 2 + documentation) — Check for active SDP L2CAP channels at runtime, and document the residual race window (a remote device could open an SDP channel between the check and the removal) as
    the caller's responsibility.

it seem option 4 is a good chocie . I'd like to hear what approach other's idea before making changes.

@lylezhu2012 @jhedberg What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've done with option 4 . any feadback?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a missing Classic Bluetooth SDP server API to unregister (remove) a previously registered SDP Service Record, complementing the existing bt_sdp_register_service() and reducing the need for caller-side workarounds.

Changes:

  • Add public API + Doxygen documentation for bt_sdp_unregister_service().
  • Implement bt_sdp_unregister_service() in the SDP server, including an “active SDP channel” -EBUSY guard.
  • Extend the classic SDP server test shell to support unregistering a record by index.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
subsys/bluetooth/host/classic/sdp.c Implements bt_sdp_unregister_service() by removing a record from the internal SDP DB, with a busy check for active SDP server channels.
include/zephyr/bluetooth/classic/sdp.h Declares the new public API and documents expected return values and concurrency caveats.
tests/bluetooth/classic/sdp_s/src/sdp_server.c Adds a shell command to unregister a test SDP record, updating the test’s local “registered” tracking.

Comment on lines +1757 to +1763
if (!sys_slist_find_and_remove(&sdp_db, &service->node)) {
return -ENOENT;
}

LOG_DBG("Service unregistered at %u", service->handle);

return 0;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

remove redundant num_services

Comment thread subsys/bluetooth/host/classic/sdp.c Outdated
Comment on lines +1749 to +1755
for (int i = 0; i < ARRAY_SIZE(bt_sdp_pool); i++) {
if (bt_sdp_pool[i].chan.chan.conn) {
LOG_WRN("Active SDP channel exists on conn %p",
bt_sdp_pool[i].chan.chan.conn);
return -EBUSY;
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated

Comment on lines +276 to +280
index = strtoul(argv[1], NULL, 16);
if (index >= MAX_SDP_RECORD_COUNT) {
shell_error(sh, "Invalid index %d", index);
return -EINVAL;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated


if (!sdp_rec_reg[index]) {
shell_error(sh, "The SDP record %d is not registered", index);
return -ENOEXEC;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

-ENOEXEC was Zephyr shell command stand pattern.which was not a need

Comment thread subsys/bluetooth/host/classic/sdp.c Outdated
return -EINVAL;
}

for (int i = 0; i < ARRAY_SIZE(bt_sdp_pool); i++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for (int i = 0; i < ARRAY_SIZE(bt_sdp_pool); i++) {
ARRAY_FOR_EACH(bt_sdp_pool, i) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

udpated

return -EBUSY;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the SDP session connections of SDP Client should also be checked.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

thanks for your suggestion

The bt_sdp_unregister_service() only modifies the local SDP server database (sdp_db). The SDP client pool (bt_sdp_client_pool) holds channels used to discover remote devices' SDP databases — those channels
never access or iterate over our local sdp_db.

Therefore, an active outgoing SDP client session has no interaction with the local service record being removed, and checking it would be unnecessary

Comment thread subsys/bluetooth/host/classic/sdp.c Outdated
}

for (int i = 0; i < ARRAY_SIZE(bt_sdp_pool); i++) {
if (bt_sdp_pool[i].chan.chan.conn) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (bt_sdp_pool[i].chan.chan.conn) {
if (bt_sdp_pool[i].chan.chan.conn != NULL) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do you want to add the shell commander to this file? This is a test suite for SDP server role. If you want to add a shell commander for the changes, I think the test case is needed for this shell commander.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@lylezhu2012 thanks for your suggest

The unregister_sdp shell command is placed in sdp_server.c because it operates on the local SDP server database (sdp_db) — the same database that register_sdp and register_sdp_all manage in this file. It is the inverse of register_sdp, so co-locating them is natural.

I've also added a pytest test case (test_sdp_unregister) that exercises the full lifecycle: register → verify discoverable → disconnect SDP → unregister → reconnect → verify gone → re-register → verify discoverable again. This validates both the shell command and the -EBUSY guard requiring SDP disconnection before removal.

@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch from 0408078 to faea9ec Compare May 19, 2026 13:41
@chengkai15
chengkai15 requested a review from lylezhu2012 May 19, 2026 13:43
@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch 2 times, most recently from 8082d46 to f350d55 Compare May 26, 2026 12:43
@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch from 29cd43d to f350d55 Compare May 27, 2026 05:13
@sonarqubecloud

Copy link
Copy Markdown

@chengkai15

Copy link
Copy Markdown
Member Author

@lylezhu2012 @jhedberg would appreciate another look when you have a moment

jhedberg
jhedberg previously approved these changes Jun 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually, the condition is not always true. The value of state.current_svc is came from the sdp require of peer device. This is my concern about this change.

I hope to have CI/CV to ensure that changes are error-free. So this PR is also the motivation for my current work on classic simulation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The num_services counter was only used in a single condition (state.current_svc < num_services) within the SSA response continuation state logic. Since record indices are assigned sequentially from 0 and state.current_svc is always set from record->index (which is always < num_services for any valid record), this condition is always true — making it dead code.

Remove the variable, its increment in bt_sdp_register_service(), and the redundant condition wrapper, unconditionally entering dry_run mode when the packet is full. dc5f28f

Comment on lines 512 to 517

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you run the test suites locally? I cannot pass the test suite.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I test pass a month ago, I am trying it agin

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for flagging this — you were right, the suite did not pass locally. I tracked it down and fixed it; 4/4 cases pass now.

Root cause was in the test shell command, not the SDP code:

cmd_unregister_sdp() declared int err; (uninitialized) and then called shell_strtoul(argv[1], 16, &err). Unlike strtoul(), shell_strtoul() only writes *err on the error paths — on the success path it returns directly (subsys/shell/shell_utils.c:661) without touching *err. So err kept a stale stack value, the if (err || ...) guard tripped, and the command printed Invalid index 0 and returned without ever calling bt_sdp_unregister_service().

That made test_sdp_unregister fail at the "record is gone" assertion, and because the DUT is session-scoped the leftover record then polluted test_sdp_discover, which asserts an empty database at the start.

Fix: initialize int err = 0; before the shell_strtoul() call (amended into the test commit).

Two more things were needed to actually run the suite on native_sim (these are infrastructure gaps, not SDP bugs):

  • CONFIG_ZTEST_SHELL=y in prj.conf — otherwise the native_sim image runs an empty ztest and exits before the pytest harness can attach to the shell.
  • CONFIG_UART_NATIVE_PTY_0_ON_STDINOUT=y via testcase.yaml extra_args — so the shell UART is on stdin/stdout (the harness uses process pipes, not a PTY).

With those + the err fix, the full lifecycle passes: register → discoverable → disconnect → unregister → gone → re-register → discoverable again.

Re state.current_svc (sdp.c:1521): test_sdp_discover_with_range registers the large record and does a full-attribute scan, which exercises multi-PDU SSA continuation across many rounds, and it passes — so removing the if (state.current_svc < num_services) guard does not break the continuation path for the cases covered here. I agree the peer-controlled continuation value is worth broader CV coverage, which I understand is what your classic simulation work targets.

@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch 2 times, most recently from 219f299 to e1b6c29 Compare June 30, 2026 13:20
}

err = bt_sdp_unregister_service(&spp_rec[index]);
if (err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (err) {
if (err != 0) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated

@chengkai15
chengkai15 force-pushed the sdp_unregister_service branch 2 times, most recently from 73e058d to 282a610 Compare July 9, 2026 12:21
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@chengkai15
chengkai15 requested a review from lylezhu2012 July 14, 2026 07:28
@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale because it has been open (more than) 30 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 7 days. Note that if it gets closed, you can ask someone to reopen it for you if you do not have the permissions to do so.

@github-actions github-actions Bot added the Stale label Aug 14, 2026
@github-actions github-actions Bot closed this Aug 22, 2026
@chengkai15 chengkai15 reopened this Aug 26, 2026
The num_services counter was only used in a single condition
(state.current_svc < num_services) within the SSA response
continuation state logic. Since record indices are assigned
sequentially from 0 and state.current_svc is always set from
record->index (which is always < num_services for any valid
record), this condition is always true — making it dead code.

Remove the variable, its increment in bt_sdp_register_service(),
and the redundant condition wrapper, unconditionally entering
dry_run mode when the packet is full.

Signed-off-by: Kai Cheng <chengkai@xiaomi.com>
Add a new API to remove a previously registered Service Record from
the SDP database. This complements bt_sdp_register_service() and
follows the same pattern as bt_l2cap_br_server_unregister().

The function checks for active SDP server L2CAP channels and returns
-EBUSY if any exist, preventing unsafe removal during active
transactions. A residual race window remains between the check and
the actual removal; the caller is responsible for ensuring no new
SDP connections are established during this call.

Signed-off-by: Kai Cheng <chengkai@xiaomi.com>
…test

Add a shell command to unregister SDP service records by index, and
a pytest test case that verifies the full lifecycle:
1. Register a record and verify it is discoverable
2. Disconnect SDP, unregister, reconnect and verify it is gone
3. Re-register and verify it is discoverable again

Initialize the error status variable before calling shell_strtoul():
unlike strtoul(), shell_strtoul() does not clear the output error code
on the success path, so an uninitialized 'err' left a stale stack value
that made the 'Invalid index' guard trip and silently skipped the
actual unregister call.

Also enable CONFIG_ZTEST_SHELL (so the native_sim image stays in the
shell instead of exiting after an empty ztest run) and route the shell
UART to stdin/stdout via CONFIG_UART_NATIVE_PTY_0_ON_STDINOUT, so the
pytest twister harness can drive the shell over the process pipes.

Signed-off-by: Kai Cheng <chengkai@xiaomi.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

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

Labels

area: Bluetooth Classic Bluetooth Classic (BR/EDR) area: Bluetooth area: Tests Issues related to a particular existing or missing test Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants