diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7ac2c16..8908485 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -16,12 +16,12 @@ jobs: name: macos-14 runs-on: macos-14 steps: + - name: Checkout + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: - go-version: '1.23.12' - - name: Checkout - uses: actions/checkout@v6 + go-version-file: 'go.mod' - name: Run unit tests run: go test - name: "Run macOS smoke tests" diff --git a/Makefile b/Makefile index c6fa21b..3342fde 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,10 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement @md5sum test.hex + $(TINYGO) build -o test.bin -size=short -target=xiao-esp32c3 ./examples/advertisement + @md5sum test.bin + $(TINYGO) build -o test.bin -size=short -target=xiao-esp32c3 ./examples/discover + @md5sum test.bin smoketest-linux: # Test on Linux. diff --git a/adapter_espradio.go b/adapter_espradio.go new file mode 100644 index 0000000..ef962c3 --- /dev/null +++ b/adapter_espradio.go @@ -0,0 +1,71 @@ +//go:build espradio + +package bluetooth + +import ( + "runtime" + + "tinygo.org/x/espradio" +) + +const maxConnections = 1 + +// Adapter represents the BLE adapter on the ESP32 via espradio VHCI transport. +type Adapter struct { + hciAdapter +} + +// DefaultAdapter is the default adapter on the current system. +// +// Make sure to call Enable() before using it to initialize the adapter. +var DefaultAdapter = &Adapter{ + hciAdapter: hciAdapter{ + isDefault: true, + connectHandler: func(device Device, connected bool) { + return + }, + connectedDevices: make([]Device, 0, maxConnections), + }, +} + +// Enable configures the BLE stack. It must be called before any +// Bluetooth-related calls (unless otherwise indicated). +// For WiFi+BLE co-existence, call espradio.Enable() first. +func (a *Adapter) Enable() error { + if err := espradio.BLEInit(); err != nil { + return err + } + + transport := &hciVHCI{} + + a.hci, a.att = newBLEStack(transport) + + a.enable() + + return nil +} + +// hciVHCI wraps espradio's BLE VHCI transport to implement the +// unexported hciTransport interface. +type hciVHCI struct { + t espradio.VHCITransport +} + +func (h *hciVHCI) startRead() { runtime.Gosched() } +func (h *hciVHCI) endRead() {} + +func (h *hciVHCI) Buffered() int { + return h.t.Buffered() +} + +func (h *hciVHCI) ReadByte() (byte, error) { + return h.t.ReadByte() +} + +func (h *hciVHCI) Read(buf []byte) (int, error) { + return h.t.Read(buf) +} + +func (h *hciVHCI) Write(buf []byte) (int, error) { + return h.t.Write(buf) +} diff --git a/adapter_hci.go b/adapter_hci.go index f36bc09..8dc2f5e 100644 --- a/adapter_hci.go +++ b/adapter_hci.go @@ -1,4 +1,4 @@ -//go:build hci || ninafw || cyw43439 +//go:build hci || ninafw || cyw43439 || espradio package bluetooth @@ -16,6 +16,7 @@ type hciAdapter struct { isDefault bool scanning bool + scanType ScanType connectHandler func(device Device, connected bool) @@ -62,6 +63,34 @@ func (a *hciAdapter) Address() (MACAddress, error) { return a.hci.address, nil } +// ScanType selects whether the scanner transmits while scanning. +type ScanType uint8 + +const ( + // ScanTypeActive sends a SCAN_REQ to advertisers that allow it, so scan + // response data (usually the complete local name) is reported as well. + // This is the default. + ScanTypeActive ScanType = iota + + // ScanTypePassive only listens. It uses less power and does not reveal the + // scanner's presence, but misses scan response data. + ScanTypePassive +) + +// hciValue returns the scan_type field for HCI LE Set Scan Parameters. +func (t ScanType) hciValue() uint8 { + if t == ScanTypePassive { + return 0x00 + } + return 0x01 +} + +// SetScanType sets the scan type used by Scan. Call it before Scan; changing it +// during a scan takes effect on the next call to Scan. +func (a *Adapter) SetScanType(t ScanType) { + a.scanType = t +} + func (a *Adapter) SetRandomAddress(mac MAC) error { if err := a.hci.sendCommandWithParams(ogfLECtrl< len(data) { + // A zero length marks the end of the significant part of the payload, + // and a length beyond the end of the buffer means it is malformed. + // Either way, stop here. + return 0, nil, nil, false + } + return data[1], data[2 : fieldLength+1], data[fieldLength+1:], true +} + +// findField returns the data of a specific field in the advertisement packet. func (buf *rawAdvertisementPayload) findField(fieldType byte) []byte { data := buf.Bytes() - for len(data) >= 2 { - fieldLength := data[0] - if int(fieldLength)+1 > len(data) { - // Invalid field length. + for { + typ, field, rest, ok := nextADField(data) + if !ok { return nil } - if fieldType == data[1] { - return data[2 : fieldLength+1] + if typ == fieldType { + return field } - data = data[fieldLength+1:] + data = rest } - return nil } // LocalName returns the local name (complete or shortened) in the advertisement @@ -376,19 +394,24 @@ func (buf *rawAdvertisementPayload) ServiceUUIDs() []UUID { // ManufacturerData returns the manufacturer data in the advertisement payload. func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement { var manufacturerData []ManufacturerDataElement - for index := 0; index < int(buf.len); index += int(buf.data[index]) + 1 { - fieldLength := int(buf.data[index+0]) - if fieldLength < 3 { - continue + data := buf.Bytes() + for { + fieldType, field, rest, ok := nextADField(data) + if !ok { + break } - fieldType := buf.data[index+1] + data = rest + if fieldType != 0xff { continue } - key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8 + if len(field) < 2 { // no room for the company ID + continue + } + key := uint16(field[0]) | uint16(field[1])<<8 manufacturerData = append(manufacturerData, ManufacturerDataElement{ CompanyID: key, - Data: buf.data[index+4 : index+fieldLength+1], + Data: field[2:], }) } return manufacturerData @@ -397,32 +420,41 @@ func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement // ServiceData returns the service data in the advertisment payload func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement { var serviceData []ServiceDataElement - for index := 0; index < int(buf.len); index += int(buf.data[index]) + 1 { - fieldLength := int(buf.data[index+0]) - if fieldLength < 3 { // field has only length and type and no data - continue + data := buf.Bytes() + for { + fieldType, field, rest, ok := nextADField(data) + if !ok { + break } - fieldType := buf.data[index+1] + data = rest + switch fieldType { case 0x16: // 16-bit uuid + if len(field) < 2 { + continue + } serviceData = append(serviceData, ServiceDataElement{ - UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)), - Data: buf.data[index+4 : index+fieldLength+1], + UUID: New16BitUUID(uint16(field[0]) + (uint16(field[1]) << 8)), + Data: field[2:], }) case 0x20: // 32-bit uuid + if len(field) < 4 { + continue + } serviceData = append(serviceData, ServiceDataElement{ - UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)), - Data: buf.data[index+6 : index+fieldLength+1], + UUID: New32BitUUID(uint32(field[0]) + (uint32(field[1]) << 8) + (uint32(field[2]) << 16) + (uint32(field[3]) << 24)), + Data: field[4:], }) case 0x21: // 128-bit uuid + if len(field) < 16 { + continue + } var uuidArray [16]byte - copy(uuidArray[:], buf.data[index+2:index+18]) + copy(uuidArray[:], field[:16]) serviceData = append(serviceData, ServiceDataElement{ UUID: NewUUID(uuidArray), - Data: buf.data[index+18 : index+fieldLength+1], + Data: field[16:], }) - default: - continue } } return serviceData diff --git a/gap_hci.go b/gap_hci.go index c674b90..7a5ef4a 100644 --- a/gap_hci.go +++ b/gap_hci.go @@ -1,4 +1,4 @@ -//go:build hci || ninafw || cyw43439 +//go:build hci || ninafw || cyw43439 || espradio package bluetooth @@ -42,8 +42,15 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { return err } - // passive scanning, every 40ms, for 30ms - if err := a.hci.leSetScanParameters(0x00, 0x0080, 0x0030, 0x00, 0x00); err != nil { + // Active scanning transmits, so the controller needs to know which of our + // own addresses to put in the SCAN_REQ. + localRandom := uint8(0) + if a.hci.address.isRandom { + localRandom = GAPAddressTypeRandomStatic + } + + // scan every 80ms, for 30ms + if err := a.hci.leSetScanParameters(a.scanType.hciValue(), 0x0080, 0x0030, localRandom, 0x00); err != nil { return err } diff --git a/gap_test.go b/gap_test.go index de6ffed..2ff6b95 100644 --- a/gap_test.go +++ b/gap_test.go @@ -193,3 +193,95 @@ func TestServiceUUIDs(t *testing.T) { } } } + +// Advertisement payloads arrive straight off the air and may be truncated or +// otherwise malformed. Parsing one must never panic, no matter what it claims. +func TestMalformedAdvertisementPayload(t *testing.T) { + tests := []struct { + name string + raw string + }{ + { + name: "manufacturer data length past end of payload", + raw: "\x02\x01\x06" + "\x0a\xff\x4c\x00\x01", + }, + { + name: "manufacturer data field claiming the maximum length", + raw: "\x02\x01\x06" + "\xff\xff\x4c\x00", + }, + { + name: "16-bit service data length past end of payload", + raw: "\x02\x01\x06" + "\x0a\x16\xd2\xfc\x40", + }, + { + name: "32-bit service data truncated inside the UUID", + raw: "\x02\x01\x06" + "\x04\x20\xd2\xfc", + }, + { + name: "128-bit service data truncated inside the UUID", + raw: "\x05\x21\xb8\x6c\x75\x05", + }, + { + name: "zero length field followed by a local name type byte", + raw: "\x00\x09foobar", + }, + { + name: "trailing zero padding after a valid field", + raw: "\x07\x09foobar" + "\x00\x00\x00", + }, + { + name: "truncated local name", + raw: "\x02\x01\x06" + "\x0c\x09foo", + }, + { + name: "single dangling length byte", + raw: "\x02\x01\x06" + "\x05", + }, + } + + check := func(t *testing.T, raw string) { + t.Helper() + var buf rawAdvertisementPayload + buf.len = uint8(copy(buf.data[:], raw)) + + // None of these may panic. The returned values are not checked: the + // input is garbage, so any output is acceptable as long as parsing + // stays inside the buffer. + buf.LocalName() + buf.ManufacturerData() + buf.ServiceData() + buf.ServiceUUIDs() + buf.HasServiceUUID(ServiceUUIDHeartRate) + buf.HasServiceUUID(ServiceUUIDAdafruitSound) + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + check(t, tc.raw) + }) + } + + // Exhaustive sweep: place a field of every possible declared length at + // every offset of a full-size payload, for each type this code parses. + t.Run("every field length at every offset", func(t *testing.T) { + for _, fieldType := range []byte{0x08, 0x09, 0x02, 0x03, 0x06, 0x07, 0x16, 0x20, 0x21, 0xff} { + for offset := 0; offset < 31; offset++ { + for fieldLength := 0; fieldLength <= 255; fieldLength++ { + for _, payloadLen := range []int{offset + 2, 31} { + if payloadLen > 31 { + continue + } + raw := make([]byte, payloadLen) + if offset < len(raw) { + raw[offset] = byte(fieldLength) + } + if offset+1 < len(raw) { + raw[offset+1] = fieldType + } + check(t, string(raw)) + } + } + } + } + }) +} diff --git a/gattc_hci.go b/gattc_hci.go index 28df2c8..987f707 100644 --- a/gattc_hci.go +++ b/gattc_hci.go @@ -1,4 +1,4 @@ -//go:build hci || ninafw || cyw43439 +//go:build hci || ninafw || cyw43439 || espradio package bluetooth diff --git a/gatts_hci.go b/gatts_hci.go index 9f9dbb3..20cc01e 100644 --- a/gatts_hci.go +++ b/gatts_hci.go @@ -1,4 +1,4 @@ -//go:build hci || ninafw || cyw43439 +//go:build hci || ninafw || cyw43439 || espradio package bluetooth diff --git a/go.mod b/go.mod index 9563607..eaf9516 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,17 @@ module tinygo.org/x/bluetooth -go 1.23.8 +go 1.25.0 require ( github.com/go-ole/go-ole v1.2.6 github.com/godbus/dbus/v5 v5.1.0 github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 - github.com/soypat/cyw43439 v0.1.0 + github.com/soypat/cyw43439 v0.1.2-0.20260731160358-f2a6af121857 github.com/tinygo-org/cbgo v0.0.4 golang.org/x/crypto v0.12.0 - tinygo.org/x/drivers v0.35.0 + golang.org/x/sys v0.11.0 + tinygo.org/x/drivers v0.35.1-0.20260604174950-1d695a231aef + tinygo.org/x/espradio v0.2.0 tinygo.org/x/tinyfont v0.6.0 tinygo.org/x/tinyterm v0.5.0 ) @@ -17,10 +19,9 @@ require ( require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/soypat/lneto v0.1.0 // indirect - github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect + github.com/soypat/lneto v0.3.2 // indirect + github.com/soypat/seqs v0.0.0-20260125140838-2c1c6b1bd69e // indirect github.com/tinygo-org/pio v0.3.0 // indirect - golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect golang.org/x/term v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 18da4c5..ad65387 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,14 @@ github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96/go.mod h1:CI github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soypat/cyw43439 v0.1.0 h1:3Nyqg2LSndhCYgCr2VXuL2nn73vyaJXAnD02veMoLvA= -github.com/soypat/cyw43439 v0.1.0/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc= -github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I= -github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U= -github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA= -github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= +github.com/soypat/cyw43439 v0.1.2-0.20260731160358-f2a6af121857 h1:FupkkbuNKByxNhVcFMOu7ZT3v4b+et0sE4ZzC66hIl0= +github.com/soypat/cyw43439 v0.1.2-0.20260731160358-f2a6af121857/go.mod h1:hStbAH1nOOWlo1ltrPd6V1GoIQYoW5/L6HcKZRlVp04= +github.com/soypat/lneto v0.3.1 h1:LaEsfTDpRlYjNbk6qV2jYMSNFs0gG79M6GrzpRY3EH8= +github.com/soypat/lneto v0.3.1/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= +github.com/soypat/lneto v0.3.2 h1:iUFeRSq2czT7Db6MMOsAnMCBlKCqvIr941zsNf9dcu0= +github.com/soypat/lneto v0.3.2/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= +github.com/soypat/seqs v0.0.0-20260125140838-2c1c6b1bd69e h1:xF3R+8683ngGNUeIy8PHJZiJZ/XIw+hlGgxg572P0Mw= +github.com/soypat/seqs v0.0.0-20260125140838-2c1c6b1bd69e/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -32,8 +34,8 @@ github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI= github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM= +golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -45,8 +47,14 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -tinygo.org/x/drivers v0.35.0 h1:cTK36tsI/S4Mg3hCPH0MBjV/ta7XKQ+wpvch4mVqgsE= -tinygo.org/x/drivers v0.35.0/go.mod h1:DQgKyHkB4G6IEOKVTAjApbKnWGwESN91EVJO+nMOE9Y= +tinygo.org/x/drivers v0.35.1-0.20260604174950-1d695a231aef h1:nG/qd6hSQonHse2l8DYUrCuJSiCfcFD9n8YMze8sVo0= +tinygo.org/x/drivers v0.35.1-0.20260604174950-1d695a231aef/go.mod h1:DQgKyHkB4G6IEOKVTAjApbKnWGwESN91EVJO+nMOE9Y= +tinygo.org/x/espradio v0.1.1-0.20260803204842-51f7cb095db5 h1:IibXiZ717j99GobRSUbqoc/FySzxZ0lu59Q9ohquyd8= +tinygo.org/x/espradio v0.1.1-0.20260803204842-51f7cb095db5/go.mod h1:1l1bd9M8w8iXQ5eFfOlyPpZT+0l+DutBLxSYIHPj2mI= +tinygo.org/x/espradio v0.1.1-0.20260803232258-42ec574d1fce h1:TdXH4wtWRbUMdFBc3VlOvVZbHCWKR3qUqQB1TcDwkCo= +tinygo.org/x/espradio v0.1.1-0.20260803232258-42ec574d1fce/go.mod h1:bib3tci08oBCaSE/V6BzpKiymkjMmhChCL8OR3sbDGM= +tinygo.org/x/espradio v0.2.0 h1:eD7CcEIjNWBz3BxRmQ9y8cFYhGdSvY0u8y/bHuooCSo= +tinygo.org/x/espradio v0.2.0/go.mod h1:bib3tci08oBCaSE/V6BzpKiymkjMmhChCL8OR3sbDGM= tinygo.org/x/tinyfont v0.6.0 h1:GibXDSFz6xrWnEDkDRo6vsbOyRw0MVj/eza3zNHMSHs= tinygo.org/x/tinyfont v0.6.0/go.mod h1:onflMSkpWl7r7j4MIqhPEVV39pn7yL4N3MOePl3G+G8= tinygo.org/x/tinyterm v0.5.0 h1:HusDSiTM290KzYM4+2X+ZfNM6Ll1yu13LpvIszlQE9M= diff --git a/hci.go b/hci.go index 08ad838..6fae349 100644 --- a/hci.go +++ b/hci.go @@ -1,4 +1,4 @@ -//go:build ninafw || hci || cyw43439 +//go:build ninafw || hci || cyw43439 || espradio package bluetooth @@ -151,10 +151,33 @@ type hci struct { pendingPkt uint16 } +// hciMaxPacketSize is the largest packet that can legitimately be received. An +// HCI event carries a single byte parameter length, so the largest one is the +// packet type byte, the event code, the length byte and 255 parameter bytes. +// This also covers the largest ACL packet that can arrive given maximumMTU. +const hciMaxPacketSize = hciEvtLenPos + 255 + 1 + +// hciTransportOverhead is the extra room a transport may need in the +// destination buffer on top of the bytes it hands back. The CYW43439 copies a +// whole entry out of its ring buffer before stripping the 3 byte SDIO header, +// and rounds the copy up to a 4 byte boundary. +const hciTransportOverhead = 3 + +// hciReadBufSize is the size of the packet read buffer. It must be large enough +// for the largest packet that can legitimately be received plus any transport +// overhead, otherwise such a packet can never be assembled. +const hciReadBufSize = (hciMaxPacketSize + hciTransportOverhead + 3) &^ 3 + +// alignUp4 rounds n up to a multiple of 4. Packet oriented transports read in +// 4 byte units, so the destination has to have room for the rounded up size. +func alignUp4(n int) int { + return (n + 3) &^ 3 +} + func newHCI(t hciTransport) *hci { return &hci{ transport: t, - buf: make([]byte, 256), + buf: make([]byte, hciReadBufSize), writebuf: make([]byte, 256), } } @@ -163,22 +186,24 @@ func (h *hci) start() error { h.transport.startRead() defer h.transport.endRead() - var data [32]byte for { - if i := h.transport.Buffered(); i > 0 { - if i > len(data) { - i = len(data) - } - if _, err := h.transport.Read(data[:i]); err != nil { - return err - } + available := h.transport.Buffered() + if available == 0 { + return nil + } - continue + // Discard whatever is left over. The read goes into the packet buffer + // rather than a small scratch buffer because a packet oriented + // transport hands back a whole packet at a time and fails the read + // outright when it does not fit. + aligned := alignUp4(available) + if aligned > len(h.buf) { + return ErrHCIInvalidPacket + } + if _, err := h.transport.Read(h.buf[:aligned]); err != nil { + return err } - return nil } - - return nil } func (h *hci) stop() error { @@ -193,26 +218,43 @@ func (h *hci) poll() error { h.transport.startRead() defer h.transport.endRead() - for h.transport.Buffered() > 0 || h.end > h.pos { + for { + // noRoom records that data is waiting but does not fit alongside what + // is already buffered. + noRoom := false + // perform read only if more data is available - available := h.transport.Buffered() - if available > 0 { - // limit to buffer size - if available > len(h.buf)-h.end { - available = len(h.buf) - h.end - } + if available := h.transport.Buffered(); available > 0 { + // Read in 4 byte aligned chunks. A packet oriented transport hands + // back a whole packet at a time and fails the read outright when it + // does not fit, so only read when there is room for all of it and + // leave the rest until the buffer has been drained. + aligned := alignUp4(available) + switch { + case h.end+aligned <= len(h.buf): + n, err := h.transport.Read(h.buf[h.end : h.end+aligned]) + if err != nil { + return err + } + h.end += n + case h.end == 0: + // There is nothing to drain, so this can never be read. + if debug { + println("hci poll packet too large:", available) + } - // read in 4 byte aligned chunks - aligned := available + (4-(available%4))%4 - n, err := h.transport.Read(h.buf[h.end : h.end+aligned]) - if err != nil { - return err + return ErrHCIInvalidPacket + default: + noRoom = true } - h.end += n + } + + if h.end == 0 { + return nil } // do processing - h.pos += 1 + h.pos = h.end done, err := h.processPacket() switch { case err == ErrHCIInvalidPacket || err == ErrHCIUnknown || err == ErrHCIUnknownEvent: @@ -242,7 +284,9 @@ func (h *hci) poll() error { h.pos = 0 return nil - case h.pos > h.end: + case noRoom: + // The buffer holds an incomplete packet and there is no room left + // to read the rest of it, so it can never be completed. if debug { println("hci poll buffer overflow", hex.EncodeToString(h.buf[:h.end])) } @@ -250,12 +294,12 @@ func (h *hci) poll() error { h.end = 0 time.Sleep(5 * time.Millisecond) - default: - time.Sleep(1 * time.Millisecond) + case h.transport.Buffered() == 0: + // Incomplete packet with nothing more to read for now. Keep it and + // pick up where we left off on the next poll. + return nil } } - - return nil } func (h *hci) processPacket() (bool, error) { @@ -263,16 +307,25 @@ func (h *hci) processPacket() (bool, error) { case hciACLDataPkt: if h.pos > hciACLLenPos { pktlen := int(binary.LittleEndian.Uint16(h.buf[3:5])) + + // Total size of the packet, including the leading packet type + // byte. The length comes off the wire, so it may be larger than + // the read buffer can ever hold. + pktTotal := hciACLLenPos + pktlen + 1 + if pktTotal > hciMaxPacketSize { + return true, ErrHCIInvalidPacket + } + switch { - case h.end < hciACLLenPos+pktlen: + case h.end < pktTotal: // need to read more data return false, nil - case h.pos >= hciACLLenPos+pktlen: + case h.pos >= pktTotal: if debug { - println("hci acl data recv:", h.pos, hex.EncodeToString(h.buf[:hciACLLenPos+pktlen+1])) + println("hci acl data recv:", h.pos, hex.EncodeToString(h.buf[:pktTotal])) } - h.pos = hciACLLenPos + pktlen + 1 + h.pos = pktTotal return true, h.handleACLData(h.buf[1:h.pos]) } } @@ -281,16 +334,21 @@ func (h *hci) processPacket() (bool, error) { if h.pos > hciEvtLenPos { pktlen := int(h.buf[hciEvtLenPos]) + pktTotal := hciEvtLenPos + pktlen + 1 + if pktTotal > hciMaxPacketSize { + return true, ErrHCIInvalidPacket + } + switch { - case h.end < hciEvtLenPos+pktlen: + case h.end < pktTotal: // need to read more data return false, nil - case h.pos >= hciEvtLenPos+pktlen: + case h.pos >= pktTotal: if debug { - println("hci event data recv:", h.pos, hex.EncodeToString(h.buf[:hciEvtLenPos+pktlen+1])) + println("hci event data recv:", h.pos, hex.EncodeToString(h.buf[:pktTotal])) } - h.pos = hciEvtLenPos + pktlen + 1 + h.pos = pktTotal return true, h.handleEventData(h.buf[1:h.pos]) } } @@ -324,6 +382,10 @@ func (h *hci) readBdAddr() error { return err } + if len(h.cmdResponse) < 7 { + return ErrHCIInvalidPacket + } + copy(h.address.MAC[:], h.cmdResponse[:7]) return nil @@ -567,6 +629,11 @@ type aclDataHeader struct { } func (h *hci) handleACLData(buf []byte) error { + // The ACL header is 4 bytes, followed by a 4 byte L2CAP header. + if len(buf) < 8 { + return ErrHCIInvalidPacket + } + aclHdr := aclDataHeader{ handle: binary.LittleEndian.Uint16(buf[0:]), dlen: binary.LittleEndian.Uint16(buf[2:]), @@ -575,10 +642,21 @@ func (h *hci) handleACLData(buf []byte) error { } aclFlags := (aclHdr.handle & 0xf000) >> 12 - if aclHdr.dlen-4 != aclHdr.len { + if aclHdr.dlen < 4 || aclHdr.dlen-4 != aclHdr.len { return errors.New("fragmented packet") } + // The L2CAP length comes off the wire, so check the payload is really + // present rather than trusting it. Computed as an int, since the uint16 + // arithmetic would wrap around. + end := 8 + int(aclHdr.len) + if end > len(buf) { + if debug { + println("invalid acl payload length", aclHdr.len, len(buf)) + } + return ErrHCIInvalidPacket + } + switch aclHdr.cid { case attCID: if aclFlags == 0x01 { @@ -586,16 +664,16 @@ func (h *hci) handleACLData(buf []byte) error { if debug { println("WARNING: att.handleACLData needs buffered packet") } - return h.att.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8]) + return h.att.handleData(aclHdr.handle&0x0fff, buf[8:end]) } else { - return h.att.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8]) + return h.att.handleData(aclHdr.handle&0x0fff, buf[8:end]) } case signalingCID: if debug { println("signaling cid", aclHdr.cid, hex.EncodeToString(buf)) } - return h.l2cap.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8]) + return h.l2cap.handleData(aclHdr.handle&0x0fff, buf[8:end]) default: if debug { @@ -607,6 +685,11 @@ func (h *hci) handleACLData(buf []byte) error { } func (h *hci) handleEventData(buf []byte) error { + // Every event has at least an event code and a parameter length byte. + if len(buf) < 2 { + return ErrHCIInvalidPacket + } + evt := buf[0] plen := buf[1] @@ -616,6 +699,10 @@ func (h *hci) handleEventData(buf []byte) error { println("evtDisconnComplete") } + if len(buf) < 5 { + return ErrHCIInvalidPacket + } + handle := binary.LittleEndian.Uint16(buf[3:]) h.att.removeConnection(handle) h.l2cap.removeConnection(handle) @@ -631,6 +718,10 @@ func (h *hci) handleEventData(buf []byte) error { } case evtCmdComplete: + if len(buf) < 6 || int(plen)+2 > len(buf) { + return ErrHCIInvalidPacket + } + h.cmdCompleteOpcode = binary.LittleEndian.Uint16(buf[3:]) h.cmdCompleteStatus = buf[5] if plen > 0 { @@ -646,6 +737,10 @@ func (h *hci) handleEventData(buf []byte) error { return nil case evtCmdStatus: + if len(buf) < 6 { + return ErrHCIInvalidPacket + } + h.cmdCompleteStatus = buf[2] h.cmdCompleteOpcode = binary.LittleEndian.Uint16(buf[4:]) if debug { @@ -660,12 +755,22 @@ func (h *hci) handleEventData(buf []byte) error { if debug { println("evtNumCompPkts", hex.EncodeToString(buf)) } + if len(buf) < 3 { + return ErrHCIInvalidPacket + } + // count of handles c := buf[2] pkts := uint16(0) + // The handle count comes off the wire, so make sure the event is + // actually long enough to hold that many entries. + if 5+(int(c)-1)*4+2 > len(buf) { + return ErrHCIInvalidPacket + } + for i := byte(0); i < c; i++ { - pkts += binary.LittleEndian.Uint16(buf[5+i*4:]) + pkts += binary.LittleEndian.Uint16(buf[5+int(i)*4:]) } if pkts > 0 && h.pendingPkt > pkts { @@ -685,6 +790,11 @@ func (h *hci) handleEventData(buf []byte) error { println("evtLEMetaEvent") } + // An LE meta event has at least a subevent code. + if len(buf) < 3 { + return ErrHCIInvalidPacket + } + switch buf[2] { case leMetaEventConnComplete, leMetaEventEnhancedConnectionComplete: if debug { @@ -695,12 +805,25 @@ func (h *hci) handleEventData(buf []byte) error { } } + // The enhanced variant carries two extra addresses before the + // connection interval, so it needs a longer event. + minLen := 20 + if buf[2] == leMetaEventEnhancedConnectionComplete { + minLen = 32 + } + if len(buf) < minLen { + if debug { + println("invalid connection complete length", len(buf)) + } + return ErrHCIInvalidPacket + } + h.connectData.connected = true h.connectData.status = buf[3] h.connectData.handle = binary.LittleEndian.Uint16(buf[4:]) h.connectData.role = buf[6] h.connectData.peerBdaddrType = buf[7] - copy(h.connectData.peerBdaddr[0:], buf[8:]) + copy(h.connectData.peerBdaddr[0:], buf[8:14]) switch buf[2] { case leMetaEventConnComplete: @@ -720,29 +843,46 @@ func (h *hci) handleEventData(buf []byte) error { return h.leSetAdvertiseEnable(false) case leMetaEventAdvertisingReport: + // Validate the whole report before touching h.advData, so a + // truncated one cannot leave a half filled report behind marked as + // reported. The fixed part is 13 bytes (up to and including the + // data length), followed by the advertisement data and one RSSI + // byte. + if len(buf) < 13 { + if debug { + println("invalid advertising report length", len(buf)) + } + return ErrHCIInvalidPacket + } + + eirLength := buf[12] + + // Note: the length must be computed as an int. Doing the + // arithmetic on the uint8 eirLength would wrap around. + if eirLength > 31 || 13+int(eirLength)+1 > len(buf) { + if debug { + println("invalid packet length", eirLength, len(buf)) + } + return ErrHCIInvalidPacket + } + h.advData.reported = true h.advData.numReports = buf[3] h.advData.typ = buf[4] h.advData.peerBdaddrType = buf[5] - copy(h.advData.peerBdaddr[0:], buf[6:]) - h.advData.eirLength = buf[12] + copy(h.advData.peerBdaddr[0:], buf[6:12]) + h.advData.eirLength = eirLength h.advData.rssi = 0 if debug { println("leMetaEventAdvertisingReport", plen, h.advData.numReports, h.advData.typ, h.advData.peerBdaddrType, h.advData.eirLength) } - if int(13+h.advData.eirLength+1) > len(buf) || h.advData.eirLength > 31 { - if debug { - println("invalid packet length", h.advData.eirLength, len(buf)) - } - return ErrHCIInvalidPacket - } - copy(h.advData.eirData[0:h.advData.eirLength], buf[13:13+h.advData.eirLength]) + copy(h.advData.eirData[0:eirLength], buf[13:13+eirLength]) // TODO: handle multiple reports if h.advData.numReports == 0x01 { - h.advData.rssi = int8(buf[int(13+h.advData.eirLength)]) + h.advData.rssi = int8(buf[13+int(eirLength)]) } return nil @@ -757,6 +897,10 @@ func (h *hci) handleEventData(buf []byte) error { println("leMetaEventRemoteConnParamReq") } + if len(buf) < 13 { + return ErrHCIInvalidPacket + } + connectionHandle := binary.LittleEndian.Uint16(buf[3:]) intervalMin := binary.LittleEndian.Uint16(buf[5:]) intervalMax := binary.LittleEndian.Uint16(buf[7:]) diff --git a/l2cap_hci.go b/l2cap_hci.go index c4267ff..cdd90cf 100644 --- a/l2cap_hci.go +++ b/l2cap_hci.go @@ -1,4 +1,4 @@ -//go:build ninafw || hci || cyw43439 +//go:build ninafw || hci || cyw43439 || espradio package bluetooth diff --git a/uuid_hci.go b/uuid_hci.go index 2028706..7d52968 100644 --- a/uuid_hci.go +++ b/uuid_hci.go @@ -1,4 +1,4 @@ -//go:build hci || ninafw || cyw43439 +//go:build hci || ninafw || cyw43439 || espradio package bluetooth