From 56f46ecab3e0559a75573451969b97ba990e3fa5 Mon Sep 17 00:00:00 2001 From: Nick Brook Date: Tue, 5 May 2026 15:02:11 +0100 Subject: [PATCH] Validate buttonless DFU advertising name length Reject user-specified advertising names longer than 18 UTF-8 bytes before CoreBluetooth can fall back to ATT Prepare Write, which Nordic's buttonless DFU set-name command does not handle. The limit is hard-coded rather than derived from `maximumWriteValueLength(for: .withResponse)` because CoreBluetooth always reports 512 once Long Write is supported, so the dynamic value is not a useful guard against the prepare-write fallback. 18 bytes leaves room for the 1-byte op code and 1-byte length field within the 20-byte payload of the default ATT MTU. --- .../Implementation/DFUServiceDelegate.swift | 2 ++ .../Implementation/DFUServiceInitiator.swift | 14 ++++++++--- .../Characteristics/ButtonlessDFU.swift | 24 +++++++++++++++++-- .../Peripheral/SecureDFUPeripheral.swift | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Library/Classes/Implementation/DFUServiceDelegate.swift b/Library/Classes/Implementation/DFUServiceDelegate.swift index 3132987e..a164c527 100644 --- a/Library/Classes/Implementation/DFUServiceDelegate.swift +++ b/Library/Classes/Implementation/DFUServiceDelegate.swift @@ -220,6 +220,8 @@ internal enum DFURemoteError : Int { /// Error raised when the CRC reported by the remote device does not match. /// Service has done 3 attempts to send the data. case crcError = 309 + /// The requested advertising name is too long for the current ATT MTU. + case invalidAdvertisementName = 310 /// The service went into an invalid state. The service will try to close /// without crashing. Recovery to a know state is not possible. case invalidInternalState = 500 diff --git a/Library/Classes/Implementation/DFUServiceInitiator.swift b/Library/Classes/Implementation/DFUServiceInitiator.swift index 28bd35fa..5ebebf8e 100644 --- a/Library/Classes/Implementation/DFUServiceInitiator.swift +++ b/Library/Classes/Implementation/DFUServiceInitiator.swift @@ -280,9 +280,17 @@ import CoreBluetooth If ``alternativeAdvertisingNameEnabled`` is `true` then this specifies the alternative name to use. If `nil` (default) then a random name is generated. - - The maximum length of the alternative advertising name is 20 bytes. - Longer name will be truncated. UTF-8 characters can be cut in the middle. + + The maximum length of the alternative advertising name is 18 UTF-8 bytes. + This is required to keep the Set Name request (1-byte op code, 1-byte + length and the name itself) within the 20-byte payload of the default + ATT MTU. CoreBluetooth always reports the maximum write-with-response + length as 512 once Long Write is supported, so the limit cannot be + derived dynamically and a longer name would fall back to ATT Prepare + Write, which Nordic's buttonless DFU service does not handle. + + The library validates the name before writing it to the device and fails + with ``DFUError/invalidAdvertisementName`` if it is too long. */ @objc public var alternativeAdvertisingName: String? = nil diff --git a/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift b/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift index f70cca20..7cf45f43 100644 --- a/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift +++ b/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift @@ -99,7 +99,19 @@ extension ButtonlessDFUResultCode : CustomStringConvertible { internal enum ButtonlessDFURequest { case enterBootloader case set(name: String) - + + /// Maximum length in bytes of the UTF-8 encoded advertising name accepted + /// by the Set Name request. + /// + /// The Set Name request is composed of a 1-byte op code, a 1-byte length + /// field and the UTF-8 encoded name. With the default ATT MTU of 23 bytes, + /// the maximum write payload is 20 bytes, leaving 18 bytes for the name + /// itself. CoreBluetooth always reports `maximumWriteValueLength(for: .withResponse)` + /// as 512 when Long Write is supported, so the limit cannot be derived + /// dynamically and is hard-coded here to avoid a fallback to ATT Prepare + /// Write, which Nordic's buttonless DFU service does not handle. + static let maximumAdvertisingNameLength = 18 + var data: Data { switch self { case .enterBootloader: @@ -270,7 +282,15 @@ internal class ButtonlessDFU : NSObject, CBPeripheralDelegate, DFUCharacteristic peripheral.delegate = self let buttonlessUUID = characteristic.uuid.uuidString - + if case .set(let name) = request, + name.lengthOfBytes(using: .utf8) > ButtonlessDFURequest.maximumAdvertisingNameLength { + let maximum = ButtonlessDFURequest.maximumAdvertisingNameLength + logger.e("\(request) exceeds maximum advertising name length \(maximum)") + report?(.invalidAdvertisementName, + "Alternative advertising name is too long. Maximum length is \(maximum) bytes.") + return + } + logger.v("Writing to characteristic \(buttonlessUUID)...") logger.d("peripheral.writeValue(0x\(request.data.hexString), for: \(buttonlessUUID), type: .withResponse)") peripheral.writeValue(request.data, for: characteristic, type: .withResponse) diff --git a/Library/Classes/Implementation/SecureDFU/Peripheral/SecureDFUPeripheral.swift b/Library/Classes/Implementation/SecureDFU/Peripheral/SecureDFUPeripheral.swift index 1db33e2e..c143f3fe 100644 --- a/Library/Classes/Implementation/SecureDFU/Peripheral/SecureDFUPeripheral.swift +++ b/Library/Classes/Implementation/SecureDFU/Peripheral/SecureDFUPeripheral.swift @@ -65,7 +65,7 @@ internal class SecureDFUPeripheral : BaseCommonDFUPeripheral