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:
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.
/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.
- 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
- 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.
- 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.
- 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.
Summary
BluetoothHciSocket::setConnectionParameterswrites the kernel's LE connection-parameter defaults by shelling out toechofour times and ignoring every return value. Where/sys/kernel/debugis 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:It is called from
kernelConnectWorkArounds, which runs onHCI_CHANNEL_RAWonly, before the kernel-side L2CAP connect that stands in for the suppressedLE 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:
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./sys/kernel/debugis 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.fork/execpairs per connection attempt on a latency-sensitive path, where fouropen/write/closecalls 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
system()with directopen/write/closeon each attribute. Removes the shell dependency, thesprintfinto a fixed buffer, and three quarters of the process spawns.minInterval/maxIntervalto noble'sconnect()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:
Provenance
The code and the container behaviour are directly verifiable. The connection-interval consequence comes from comparing two
btmoncaptures 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 thesystem()calls themselves to confirm they return non-zero in that specific container.Filed from work on stoprocent/noble#109 and stoprocent/noble#112.