Skip to content

setConnectionParameters fails silently when debugfs is unavailable, so connection parameters are dropped without any indication #27

Description

@Apollon77

Summary

BluetoothHciSocket::setConnectionParameters writes the kernel's LE connection-parameter defaults by shelling out to echo four times and ignoring every return value. Where /sys/kernel/debug is not mounted — which is the normal case in a container — all four writes fail silently and the caller's connection parameters are never applied.

Details

src/BluetoothHciSocket.cpp:240-257:

void BluetoothHciSocket::setConnectionParameters(
    unsigned short connMinInterval,
    unsigned short connMaxInterval,
    unsigned short connLatency,
    unsigned short supervisionTimeout
){
  char command[128];

  // override the HCI devices connection parameters using debugfs
  sprintf(command, "echo %u > /sys/kernel/debug/bluetooth/hci%d/conn_min_interval", connMinInterval, this->_devId);
  system(command);
  sprintf(command, "echo %u > /sys/kernel/debug/bluetooth/hci%d/conn_max_interval", connMaxInterval, this->_devId);
  system(command);
  sprintf(command, "echo %u > /sys/kernel/debug/bluetooth/hci%d/conn_latency", connLatency, this->_devId);
  system(command);
  sprintf(command, "echo %u > /sys/kernel/debug/bluetooth/hci%d/supervision_timeout", supervisionTimeout, this->_devId);
  system(command);
}

It is called from kernelConnectWorkArounds, which runs on HCI_CHANNEL_RAW only, before the kernel-side L2CAP connect that stands in for the suppressed LE Create Connection. So in raw mode these four values are how a caller's requested connection parameters reach the controller at all.

Three problems, in order of consequence:

  1. system() return values are unchecked, so a failure — non-existent path, read-only mount, insufficient privilege, no /bin/sh — is indistinguishable from success. The caller believes its parameters were applied.
  2. /sys/kernel/debug is usually absent in containers. Docker does not mount debugfs by default even with --privileged, so every write fails and the kernel keeps its own defaults.
  3. Four fork/exec pairs per connection attempt on a latency-sensitive path, where four open/write/close calls would do the same work without a shell.

Observed consequence

This is not hypothetical. In a Docker deployment (matter-js/matterjs-server#929) the silent failure is what produced the working configuration: the writes failed, the kernel kept its own defaults of 30–50 ms, and the peripheral being commissioned explicitly requested Min interval: 24 / Max interval: 40 (30–50 ms) in its L2CAP Connection Parameter Update Request. The same device connected at 22.5 ms — the value noble asks for — when driven through the HCI user channel, which bypasses this code path entirely.

So the current behaviour is: mount debugfs and the caller's parameters are honoured; do not mount it and the kernel's are used, with no way to tell which happened. The second case worked better for that device, which is a fair sign the values being written deserve a second look too — but the diagnosability is the bug.

Proposal

  1. Replace system() with direct open/write/close on each attribute. Removes the shell dependency, the sprintf into a fixed buffer, and three quarters of the process spawns.
  2. Check the result. On failure, surface it — a debug log at minimum, ideally something the JS layer can observe, so "your connection parameters were ignored" is discoverable rather than invisible.
  3. Document that raw mode routes connection parameters through debugfs and that they are silently dropped when it is unavailable. This is genuinely surprising: a caller passing minInterval/maxInterval to noble's connect() has no way to know whether they took effect.

Point 2 is the one that matters. Points 1 and 3 are cleanup.

Test plan

Hard to unit test given it touches a global filesystem path, but:

  • with a writable temp path substituted for the debugfs prefix, the four attributes receive the expected values
  • with a non-existent path, the failure is reported rather than swallowed
  • no shell is spawned

Provenance

The code and the container behaviour are directly verifiable. The connection-interval consequence comes from comparing two btmon captures from the same reporter on the same adapter, one in raw mode and one on the user channel — so the mechanism is evidenced, though I have not instrumented the system() calls themselves to confirm they return non-zero in that specific container.

Filed from work on stoprocent/noble#109 and stoprocent/noble#112.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions