blob: 927d77072cad50cdb97e52b4098d27b79a2429b9 [file]
/**
* Copyright (c) 2024 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "MTRMockCB.h"
#import "MTRTestCase.h"
#import "MTRTestKeys.h"
#import "MTRTestStorage.h"
#import <Matter/Matter.h>
#import <XCTest/XCTest.h>
#import <stdatomic.h>
NS_ASSUME_NONNULL_BEGIN
@interface MTRBleTests : MTRTestCase
@end
@interface TestBrowserDelegate : NSObject <MTRCommissionableBrowserDelegate>
@property (strong) void (^onDidFindCommissionableDevice)(MTRDeviceController *, MTRCommissionableBrowserResult *);
@property (strong) void (^onDidRemoveCommissionableDevice)(MTRDeviceController *, MTRCommissionableBrowserResult *);
@end
@implementation TestBrowserDelegate
- (void)controller:(nonnull MTRDeviceController *)controller didFindCommissionableDevice:(MTRCommissionableBrowserResult *)device
{
__auto_type block = self.onDidFindCommissionableDevice;
if (block) {
block(controller, device);
}
}
- (void)controller:(nonnull MTRDeviceController *)controller didRemoveCommissionableDevice:(MTRCommissionableBrowserResult *)device
{
__auto_type block = self.onDidRemoveCommissionableDevice;
if (block) {
block(controller, device);
}
}
@end
@implementation MTRBleTests {
MTRDeviceController * _controller;
}
- (void)setUp
{
[super setUp];
[self.class.mockCoreBluetooth reset];
_controller = [self createControllerOnTestFabric];
XCTAssertNotNil(_controller);
}
- (void)tearDown
{
_controller = nil; // MTRTestCase handles shutdown
[super tearDown];
}
- (void)testBleCommissionableBrowserResultAdditionAndRemoval
{
__block MTRCommissionableBrowserResult * device;
XCTestExpectation * didFindDevice = [self expectationWithDescription:@"did find device"];
TestBrowserDelegate * delegate = [[TestBrowserDelegate alloc] init];
delegate.onDidFindCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
if ([result.instanceName isEqualToString:@"BLE"]) { // TODO: This is a lame API
XCTAssertNil(device);
XCTAssertEqualObjects(result.vendorID, @0xfff1);
XCTAssertEqualObjects(result.productID, @0x1234);
XCTAssertEqualObjects(result.discriminator, @0x444);
device = result;
[didFindDevice fulfill];
}
};
XCTestExpectation * didRemoveDevice = [self expectationWithDescription:@"did remove device"];
delegate.onDidRemoveCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
if ([result.instanceName isEqualToString:@"BLE"]) {
XCTAssertNotNil(device);
XCTAssertEqual(result, device);
[didRemoveDevice fulfill];
}
};
XCTAssertTrue([_controller startBrowseForCommissionables:delegate queue:dispatch_get_main_queue()]);
NSUUID * peripheralID = [NSUUID UUID];
[self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:peripheralID vendorID:@0xfff1 productID:@0x1234 discriminator:@0x444];
[self.class.mockCoreBluetooth removeMockPeripheralWithIdentifier:peripheralID];
// BleConnectionDelegateImpl kCachePeripheralTimeoutInSeconds is approximately 10 seconds
[self waitForExpectations:@[ didFindDevice, didRemoveDevice ] timeout:14 enforceOrder:YES];
XCTAssertTrue([_controller stopBrowseForCommissionables]);
}
- (void)testBleCommissionAfterStopBrowseUAF
{
__block MTRCommissionableBrowserResult * device;
XCTestExpectation * didFindDevice = [self expectationWithDescription:@"did find device"];
TestBrowserDelegate * delegate = [[TestBrowserDelegate alloc] init];
delegate.onDidFindCommissionableDevice = ^(MTRDeviceController * controller, MTRCommissionableBrowserResult * result) {
if ([result.instanceName isEqualToString:@"BLE"]) {
XCTAssertNil(device);
XCTAssertEqualObjects(result.vendorID, @0xfff1);
XCTAssertEqualObjects(result.productID, @0x1234);
XCTAssertEqualObjects(result.discriminator, @0x444);
device = result;
[didFindDevice fulfill];
}
};
XCTAssertTrue([_controller startBrowseForCommissionables:delegate queue:dispatch_get_main_queue()]);
NSUUID * peripheralID = [NSUUID UUID];
[self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:peripheralID vendorID:@0xfff1 productID:@0x1234 discriminator:@0x444];
[self waitForExpectations:@[ didFindDevice ] timeout:2 enforceOrder:YES];
XCTAssertTrue([_controller stopBrowseForCommissionables]);
// Attempt to use the MTRCommissionableBrowserResult after we stopped browsing
// This used to result in a UAF because BLE_CONNECTION_OBJECT is a void*
// carrying a CBPeripheral without retaining it. When browsing is stopped,
// BleConnectionDelegateImpl releases all cached CBPeripherals.
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@54321 discriminator:@0x444];
[_controller setupCommissioningSessionWithDiscoveredDevice:device
payload:payload
newNodeID:@999
error:NULL];
[_controller cancelCommissioningForNodeID:@999 error:NULL];
}
- (void)testShutdownBlePowerOffRaceUAF
{
// Attempt a PASE connection over BLE, this will call BleConnectionDelegateImpl::NewConnection()
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@54321 discriminator:@0xb1e];
payload.discoveryCapabilities = MTRDiscoveryCapabilitiesBLE;
NSError * error;
XCTAssertTrue([_controller setupCommissioningSessionWithPayload:payload newNodeID:@999 error:&error],
"setupCommissioningSessionWithPayload failed: %@", error);
// Create a race between shutdown and a CBManager callback that used to provoke a UAF.
// Note that on the order of 100 iterations can be necessary to reproduce the crash.
__block atomic_int tasks = 2;
dispatch_semaphore_t done = dispatch_semaphore_create(0);
dispatch_block_t shutdown = ^{
// Shut down the controller. This causes the SetupCodePairer to call
// BleConnectionDelegateImpl::CancelConnection(), then the SetupCodePairer
// is deallocated along with the DeviceCommissioner
[self->_controller shutdown];
self->_controller = nil;
if (atomic_fetch_sub(&tasks, 1) == 1) {
dispatch_semaphore_signal(done);
}
};
dispatch_block_t powerOff = ^{
// Cause CBPeripheralManager to signal a state change that
// triggers a callback to the SetupCodePairer
self.class.mockCoreBluetooth.state = CBManagerStatePoweredOff;
if (atomic_fetch_sub(&tasks, 1) == 1) {
dispatch_semaphore_signal(done);
}
};
dispatch_queue_t pool = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_async(pool, shutdown);
dispatch_async(pool, powerOff);
dispatch_wait(done, DISPATCH_TIME_FOREVER);
}
- (void)testShutdownAndBlePowerOffAfterSuccessfulConnect
{
// Attempt a PASE connection over BLE, this will call BleConnectionDelegateImpl::NewConnection()
MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@54321 discriminator:@0xb1e];
payload.discoveryCapabilities = MTRDiscoveryCapabilitiesBLE;
NSError * error;
XCTAssertTrue([_controller setupCommissioningSessionWithPayload:payload newNodeID:@999 error:&error],
"setupCommissioningSessionWithPayload failed: %@", error);
// Now make a matching device appear. The commissioner will connect over BLE
// and attempt to establish a PASE connection by doing a BLE write.
XCTestExpectation * didConnect = [self expectationWithDescription:@"did connect to peripheral"];
self.class.mockCoreBluetooth.onConnectPeripheralWithOptions = ^BOOL(CBPeripheral * peripheral, NSDictionary<NSString *, id> * _Nullable options) {
[didConnect fulfill];
return YES; // let connection establishment succeed
};
XCTestExpectation * didWrite = [self expectationWithDescription:@"did perform write"];
self.class.mockCoreBluetooth.onWriteValueForCharacteristic = ^(CBPeripheral * peripheral, NSData * value, CBMutableCharacteristic * characteristic, CBCharacteristicWriteType type) {
[didWrite fulfill];
};
[self.class.mockCoreBluetooth addMockCommissionableMatterDeviceWithIdentifier:[NSUUID UUID] vendorID:@0xfff1 productID:@0x1234 discriminator:@0xb1e];
[self waitForExpectations:@[ didConnect, didWrite ] timeout:5];
// The BLE connection in the Matter stack is now in an established state,
// waiting for the commissionee to respond to the BLE write. Shutting down
// the controller will free the DeviceCommissioner and its SetupCodePairer.
[_controller shutdown];
_controller = nil;
// Now trigger a CBCentralManager state update, attempting to provoke a UAF.
self.class.mockCoreBluetooth.state = CBManagerStatePoweredOff;
}
@end
NS_ASSUME_NONNULL_END