Skip to content

Raw HCI connect can launch a second controller attempt after L2CAP failure #21

Description

@stoprocent

Summary

In raw HCI mode, one noble LE Create Connection write can result in two controller connection attempts when the kernel L2CAP workaround reaches the controller but ultimately fails.

BluetoothHciSocket::Write() calls kernelConnectWorkArounds() and suppresses the original HCI command only when that function returns true:

if (this->_mode == HCI_CHANNEL_RAW && this->kernelConnectWorkArounds(buffer.Data(), buffer.Length())) {
  return;
}

write(this->_socket, buffer.Data(), buffer.Length());

For a recognized LE connection command, kernelConnectWorkArounds() opens and synchronously connects a kernel L2CAP socket. If that connect later fails, it erases the socket and returns false:

l2socket_ptr->connect();

if (!l2socket_ptr->isConnected()) {
  this->_l2sockets_connecting.erase(bdaddr_dst);
  return false;
}

Returning false makes Write() send the original raw LE Create Connection command even when the L2CAP connect already caused the kernel/controller to perform a complete first attempt.

Capture evidence

The btmon capture attached to matter-js/matterjs-server#929 contains this exact shape:

  1. noble requests its pre-connect HCI reset
  2. an unlabeled/kernel LE Create Connection uses the kernel-side 30–50 ms connection parameters
  3. that connection completes on handle 72 and then fails with 0x3e
  4. a process-attributed MainThread LE Create Connection is sent with noble's 7.5–22.5 ms defaults
  5. the second connection completes on handle 71
  6. an application retry has already requested another reset, which is transmitted immediately after handle 71 becomes live

The first attempt also produces the expected kernel bookkeeping commands (LE Add Device To Accept List and LE Read Remote Used Features). Those commands are therefore not sufficient evidence that an unrelated BlueZ auto-connect stole the connection; they can be downstream of noble's L2CAP workaround itself.

Impact

  • noble's JavaScript HCI layer observes events from both attempts for one logical connect
  • a success-then-0x3e sequence can consume/alter the JavaScript connection queue
  • the application can retry while the raw fallback attempt is still pending
  • noble's reset-before-first-connect behavior can then queue a reset that lands after the fallback connection succeeds

This likely explains the core capture behind stoprocent/noble#97, noble#98, and noble#102 more directly than an independent BlueZ connection race.

Proposed direction

The workaround needs to distinguish at least three outcomes:

  1. command not handled — send the original raw HCI command
  2. L2CAP setup failed before any controller attempt — raw fallback may be appropriate
  3. kernel/controller attempt started and later failed — do not start a second raw attempt

Useful first steps would be:

  • expose or log the L2CAP failure stage and errno
  • avoid falling through once connect() has initiated a controller attempt
  • add a regression harness that asserts one logical connect produces only one LE Create Connection
  • validate on Raspberry Pi and a desktop Linux adapter because the workaround is historically load-bearing there

References

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