From dff0d4d2bd6c0fd401876cad6257324c49416d9c 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 Fail oversized user-specified advertising names before CoreBluetooth can fall back to ATT Prepare Write, which is not handled by Nordic's buttonless DFU set-name command. --- .../Implementation/DFUServiceDelegate.swift | 2 ++ .../Implementation/DFUServiceInitiator.swift | 10 ++++-- .../Characteristics/ButtonlessDFU.swift | 34 ++++++++++++++++++- .../Peripheral/SecureDFUPeripheral.swift | 2 +- 4 files changed, 44 insertions(+), 4 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..e87bc86d 100644 --- a/Library/Classes/Implementation/DFUServiceInitiator.swift +++ b/Library/Classes/Implementation/DFUServiceInitiator.swift @@ -281,8 +281,14 @@ 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 depends on the + current ATT MTU, and can be up to 20 bytes. To work with the default + ATT MTU, keep the name to 18 UTF-8 bytes or fewer. Longer names may + work after a larger ATT MTU has been negotiated. + + The library validates the name before writing it to the device and fails + with ``DFUError/invalidAdvertisementName`` if it is too long for the + current connection. */ @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..a1defc32 100644 --- a/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift +++ b/Library/Classes/Implementation/SecureDFU/Characteristics/ButtonlessDFU.swift @@ -99,7 +99,10 @@ extension ButtonlessDFUResultCode : CustomStringConvertible { internal enum ButtonlessDFURequest { case enterBootloader case set(name: String) - + + private static let setNameRequestOverhead = 2 + private static let maximumAdvertisingNameLength = 20 + var data: Data { switch self { case .enterBootloader: @@ -111,6 +114,28 @@ internal enum ButtonlessDFURequest { return data } } + + func maximumPayloadLength(for peripheral: CBPeripheral) -> Int { + let writePayloadLength = peripheral.maximumWriteValueLength(for: .withResponse) + switch self { + case .enterBootloader: + return writePayloadLength + case .set: + return min( + Self.maximumAdvertisingNameLength, + max(0, writePayloadLength - Self.setNameRequestOverhead) + ) + } + } + + var payloadLength: Int { + switch self { + case .enterBootloader: + return data.count + case .set(let name): + return name.lengthOfBytes(using: .utf8) + } + } } extension ButtonlessDFURequest : CustomStringConvertible { @@ -270,6 +295,13 @@ internal class ButtonlessDFU : NSObject, CBPeripheralDelegate, DFUCharacteristic peripheral.delegate = self let buttonlessUUID = characteristic.uuid.uuidString + let maximumPayloadLength = request.maximumPayloadLength(for: peripheral) + guard request.payloadLength <= maximumPayloadLength else { + logger.e("\(request) exceeds maximum payload length \(maximumPayloadLength)") + report?(.invalidAdvertisementName, + "Alternative advertising name is too long. Maximum length is \(maximumPayloadLength) bytes.") + return + } logger.v("Writing to characteristic \(buttonlessUUID)...") logger.d("peripheral.writeValue(0x\(request.data.hexString), for: \(buttonlessUUID), 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