Most BLE fuzzing starts at the wrong layer. People point a radio at a device, throw malformed advertising packets at it, and call the result a security assessment. That approach tests the controller’s link-layer state machine, the RF front end, and the host’s connection management all at once, with no way to attribute a crash to a specific parser. If you want to find bugs in NimBLE’s ATT server, you need to feed it ATT PDUs over a deterministic HCI transport, not over the air.

This article describes a record-replay HCI harness for NimBLE’s ATT server: capture real HCI ACL and command traffic from a working connection, replay it into a host-only NimBLE build, and mutate the ATT payloads while keeping the HCI framing valid. The goal is to exercise nimble/host ATT parsing and state transitions without a physical controller, and to pin every finding to a specific NimBLE revision and build configuration.

Why the ATT server is the right target

NimBLE is an open-source Bluetooth 5.4 stack that includes both host and controller. The repository layout puts ATT and GATT in nimble/host, alongside L2CAP, SM, and GAP. The controller lives in nimble/controller, and the transport layer in nimble/transport supports UART, emSPI, and RAM (used when host and controller share a CPU). The ATT server is reachable from any peer that completes a connection and sends an ATT PDU; it does not require pairing, bonding, or a specific GATT profile. That makes it a high-value target for a host-side fuzzer.

The Bluetooth Core Specification defines ATT as a client-server protocol carried over L2CAP. The ATT Test Suite and ATT ICS are published alongside the Core Specification 5.4 documents. The spec is the authority on PDU formats, but it does not tell you how NimBLE implements them. For that, you read the source.

What the harness actually replays

A record-replay HCI harness has three jobs: capture, normalize, and replay.

Capture. Use a btsnoop log from a real connection between a central and a NimBLE peripheral. The log contains HCI ACL packets in both directions, plus HCI commands and events. You care about the ACL packets that carry L2CAP frames with ATT PDUs. The HCI ACL header is 4 bytes: handle (12 bits), PB flag (2 bits), BC flag (2 bits), and total length (16 bits). The L2CAP header is 4 bytes: length (16 bits) and CID (16 bits). ATT PDUs start after that. If you cannot parse those fields by hand, you are not ready to fuzz this layer.

Normalize. Strip connection-specific state that will not match your replay target: connection handles, sequence numbers, and any controller-specific metadata. Keep the ATT opcode, handle, and value fields intact. The point is to make the replay deterministic, not to preserve the original timing.

Replay. Feed the normalized ACL packets into a host-only NimBLE build through the HCI transport. NimBLE’s transport layer supports UART, emSPI, and RAM. For a host-only build, the RAM transport is the simplest: you write HCI packets into a buffer and the host processes them as if they came from a controller. You do not need a radio, and you do not need a real controller.

The critical detail is that the host expects a controller to have already established the connection. In a host-only build, you must either emulate the controller’s role in connection setup or start from a state where the connection already exists. The cleanest approach is to replay the full HCI command/event sequence that led to the connection, then replay the ACL traffic. That gives you a reproducible starting state.

Mutation strategy

Once you can replay a valid ATT exchange, you mutate the ATT PDU fields. The ATT opcode is 1 byte. The handle is 2 bytes, little-endian. The value field is variable-length. A useful mutation set includes:

  • Opcode values that are reserved or undefined in the spec.
  • Handle values outside the range of any registered attribute.
  • Value lengths that disagree with the opcode’s expected format.
  • Truncated PDUs where the L2CAP length claims more bytes than the ACL packet contains.
  • PDUs that violate the ATT MTU negotiated during the exchange.

Do not mutate the HCI framing. If you corrupt the ACL header, you are testing the HCI parser, not the ATT server. Keep the HCI handle, PB flag, BC flag, and length consistent with the L2CAP frame you are delivering. If you want to fuzz the HCI parser, do that in a separate harness with a separate corpus.

Instrumentation and coverage

NimBLE is a C codebase. You can build it with AddressSanitizer and UndefinedBehaviorSanitizer for host-only targets. That catches memory errors and undefined behavior in the ATT parser without needing hardware. For coverage, compile with -fprofile-arcs -ftest-coverage or use LLVM’s source-based coverage. The goal is to see which lines in nimble/host ATT handling are reached by your corpus.

Assertion hooks matter more than coverage in some cases. NimBLE uses assertions for invariant violations. If your fuzzer trips an assertion, that is a finding, even if it is not a memory-safety bug. Record the exact input that triggered it.

What this harness does not do

It does not test the controller. It does not test RF behavior. It does not test pairing or encryption. It does not test the interaction between ATT and GATT service discovery in a way that requires a real peer. If you need those, you need a different harness or a real device.

It also does not replace hardware testing. A host-only build may have different memory layout, different compiler optimizations, and different timing than a firmware build for nRF52 or ESP32. A bug found in the host-only build is a candidate, not a confirmed vulnerability. You confirm it on the target hardware with the same NimBLE revision and the same build configuration.

Reproducibility requirements

Every finding must be pinned to:

  • The exact NimBLE commit hash or release tag.
  • The build configuration: host-only vs. combined, transport type, sanitizer flags, optimization level.
  • The HCI capture file used as the seed corpus, with a hash.
  • The mutation that triggered the finding, as a byte-level diff.

Without those, you have a crash, not a reproducible bug. The NimBLE repository includes a SECURITY.md and a THREAT_MODEL.md; read them before you report anything. They tell you what the project considers in scope.

FAQ

Can I use Zephyr’s HCI transport instead of NimBLE’s? Zephyr has its own Bluetooth host stack, including subsys/bluetooth/host/att.c. If you are fuzzing NimBLE, use NimBLE’s transport layer. If you are fuzzing Zephyr’s ATT implementation, use Zephyr’s. Do not mix them.

Do I need a real controller to capture the seed corpus? Yes, for the initial capture. You need a real connection to get realistic ATT traffic. After that, you replay without a controller.

What about the Bluetooth Core Specification’s ATT Test Suite? The ATT Test Suite is for qualification testing, not fuzzing. It defines valid and invalid behaviors that a compliant implementation must exhibit. It is a useful reference for what the spec requires, but it is not a fuzzing corpus.

Where do I start in the NimBLE source? Start in nimble/host. The ATT implementation is there. The transport layer is in nimble/transport. The HCI command and event handling is in nimble/host as well. Read the code before you fuzz it.

Sources