Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/interpose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ The injected simulator dylib that routes a guest app's CoreBluetooth calls to th
It hooks the CoreBluetooth central surface with Objective-C runtime swizzling, holds a shadow
registry for the framework's opaque objects, and carries each operation to the helper over the
loopback transport, delivering host events back as the guest's delegate callbacks. Scan, connect,
read, write, notify, and RSSI are routed; service and characteristic discovery are not routed in the
central interposer, and the peripheral role is not implemented. The dylib is a simulator slice,
loaded only by a debug scheme and never by a shipped app (see `SECURITY.md` for the fence).
service and characteristic discovery, read, write, notify, and RSSI are routed; the peripheral role
is not implemented. The dylib is a simulator slice, loaded only by a debug scheme and never by a
shipped app (see `SECURITY.md` for the fence).
12 changes: 7 additions & 5 deletions packages/interpose/include/simble_interpose.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ void simble_uninstall_hooks(void);

/** How many times each routed central operation has fired since install. */
typedef struct {
int scan_start; ///< scanForPeripheralsWithServices:options: calls routed to the helper.
int connect; ///< connectPeripheral:options: calls routed to the helper.
int read; ///< readValueForCharacteristic: calls routed to the helper.
int write; ///< writeValue:forCharacteristic:type: calls routed to the helper.
int set_notify; ///< setNotifyValue:forCharacteristic: calls routed to the helper.
int scan_start; ///< scanForPeripheralsWithServices:options: calls routed.
int connect; ///< connectPeripheral:options: calls routed to the helper.
int discover_services; ///< discoverServices: calls routed to the helper.
int discover_characteristics; ///< discoverCharacteristics:forService: calls routed.
int read; ///< readValueForCharacteristic: calls routed to the helper.
int write; ///< writeValue:forCharacteristic:type: calls routed.
int set_notify; ///< setNotifyValue:forCharacteristic: calls routed.
} simble_hook_stats;

/**
Expand Down
101 changes: 97 additions & 4 deletions packages/interpose/src/hooks/central_hooks.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
#import <objc/runtime.h>
#import <pthread.h>

static simble_hook_stats g_stats = {0, 0, 0, 0, 0};
static simble_hook_stats g_stats = {0, 0, 0, 0, 0, 0, 0};
static pthread_mutex_t g_install_lock = PTHREAD_MUTEX_INITIALIZER;
static int g_installed = 0;

Expand Down Expand Up @@ -354,13 +354,69 @@ static BOOL peripheralRouteId(CBPeripheral *peripheral, uint8_t *out, size_t cap
return simble_shadow_peripheral_id(peripheral, out, cap, outLen);
}

// Convert a CBUUID array filter to parallel UTF-8 string and length arrays held by an NSArray, so
// the encoder reads stable pointers. Returns the count; sets *uuidsOut and *lensOut to malloc'd
// arrays the caller frees, or NULL when the filter is empty.
static size_t buildUUIDFilter(NSArray<CBUUID *> *uuids, NSMutableArray<NSString *> *held,
const char ***uuidsOut, size_t **lensOut) {
size_t count = uuids.count;
if (count == 0) {
*uuidsOut = NULL;
*lensOut = NULL;
return 0;
}
const char **strs = calloc(count, sizeof(char *));
size_t *lens = calloc(count, sizeof(size_t));
for (size_t i = 0; i < count; i++) {
NSString *s = uuids[i].UUIDString;
[held addObject:s];
strs[i] = s.UTF8String;
lens[i] = strlen(strs[i]);
}
*uuidsOut = strs;
*lensOut = lens;
return count;
}

- (void)simble_discoverServices:(NSArray<CBUUID *> *)serviceUUIDs {
if (!simble_shadow_is_managed_peripheral(self)) {
[self simble_discoverServices:serviceUUIDs];
return;
}
// Service discovery is not routed in the central interposer; a routed discoverServices is a
// no-op. The routed read, write, and notify paths address a characteristic by its UUID.
// Route DISCOVER_SERVICES, mint a shadow service for each discovered UUID on this peripheral, and
// deliver peripheral:didDiscoverServices:.
uint8_t pid[64];
size_t idLen = 0;
if (!simble_shadow_peripheral_id(self, pid, sizeof(pid), &idLen))
return;
NSMutableArray<NSString *> *held = [NSMutableArray array];
const char **uuids = NULL;
size_t *lens = NULL;
size_t count = buildUUIDFilter(serviceUUIDs, held, &uuids, &lens);
simble_response resp;
simble_status st = simble_client_discover_services(pid, idLen, uuids, lens, count, &resp);
free(uuids);
free(lens);
g_stats.discover_services++;
CBPeripheral *peripheral = self;
if (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_SERVICES_DISCOVERED) {
for (size_t i = 0; i < resp.uuid_count; i++) {
CBUUID *uuid = [CBUUID UUIDWithString:[NSString stringWithUTF8String:resp.uuids[i]]];
simble_shadow_service(peripheral, uuid);
}
}
SimbleManagerEntry *entry = simble_shadow_manager_entry(simble_shadow_owner(self));
dispatchOnManagerQueue(entry, ^{
id<CBPeripheralDelegate> delegate = peripheral.delegate;
NSError *error = (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_SERVICES_DISCOVERED)
? nil
: [NSError errorWithDomain:CBErrorDomain
code:resp.error_code
userInfo:nil];
if ([delegate respondsToSelector:@selector(peripheral:didDiscoverServices:)]) {
[delegate peripheral:peripheral didDiscoverServices:error];
}
});
}

- (void)simble_discoverCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs
Expand All @@ -369,7 +425,44 @@ - (void)simble_discoverCharacteristics:(NSArray<CBUUID *> *)characteristicUUIDs
[self simble_discoverCharacteristics:characteristicUUIDs forService:service];
return;
}
// Characteristic discovery is likewise not routed; a routed discoverCharacteristics is a no-op.
// Route DISCOVER_CHARACTERISTICS, mint a shadow characteristic for each discovered UUID on the
// service, and deliver peripheral:didDiscoverCharacteristics:forService:error:.
uint8_t pid[64];
size_t idLen = 0;
CBUUID *serviceUUID = nil;
if (!simble_shadow_resolve_service(service, pid, sizeof(pid), &idLen, &serviceUUID))
return;
const char *svc = serviceUUID.UUIDString.UTF8String;
NSMutableArray<NSString *> *held = [NSMutableArray array];
const char **uuids = NULL;
size_t *lens = NULL;
size_t count = buildUUIDFilter(characteristicUUIDs, held, &uuids, &lens);
simble_response resp;
simble_status st = simble_client_discover_characteristics(pid, idLen, svc, strlen(svc), uuids,
lens, count, &resp);
free(uuids);
free(lens);
g_stats.discover_characteristics++;
CBPeripheral *peripheral = self;
if (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_CHARS_DISCOVERED) {
for (size_t i = 0; i < resp.uuid_count; i++) {
CBUUID *uuid = [CBUUID UUIDWithString:[NSString stringWithUTF8String:resp.uuids[i]]];
simble_shadow_characteristic(service, uuid);
}
}
SimbleManagerEntry *entry = simble_shadow_manager_entry(simble_shadow_owner(self));
dispatchOnManagerQueue(entry, ^{
id<CBPeripheralDelegate> delegate = peripheral.delegate;
NSError *error = (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_CHARS_DISCOVERED)
? nil
: [NSError errorWithDomain:CBErrorDomain
code:resp.error_code
userInfo:nil];
if ([delegate
respondsToSelector:@selector(peripheral:didDiscoverCharacteristicsForService:error:)]) {
[delegate peripheral:peripheral didDiscoverCharacteristicsForService:service error:error];
}
});
}

- (void)simble_readValueForCharacteristic:(CBCharacteristic *)characteristic {
Expand Down
14 changes: 14 additions & 0 deletions packages/interpose/src/registry/shadow.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ CBService *_Nullable simble_shadow_service(CBPeripheral *peripheral, CBUUID *ser
CBCharacteristic *_Nullable simble_shadow_characteristic(CBService *service,
CBUUID *characteristicUUID);

/**
* @brief Resolve a minted service to its peripheral id and service UUID, so a discovery on it can
* be routed by host identity.
*
* @param[in] service The minted service.
* @param[out] peripheralOut Buffer the peripheral id is copied into.
* @param[in] peripheralCap Capacity of @p peripheralOut.
* @param[out] peripheralLen Bytes written to @p peripheralOut.
* @param[out] serviceUUID Set to the service CBUUID.
* @return YES on a hit, NO if @p service was not minted here.
*/
BOOL simble_shadow_resolve_service(CBService *service, uint8_t *peripheralOut, size_t peripheralCap,
size_t *peripheralLen, CBUUID *_Nullable *_Nonnull serviceUUID);

/**
* @brief Resolve a minted characteristic to its peripheral id, service UUID, and characteristic
* UUID, so an operation on it can be routed by host identity.
Expand Down
49 changes: 49 additions & 0 deletions packages/interpose/src/registry/shadow.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ @interface SimbleShadowMeta : NSObject
@property(nonatomic, strong, nullable) CBUUID *characteristicUUID; // characteristic only
@property(nonatomic, weak, nullable) CBCentralManager *owner; // owning manager (peripheral)
@property(nonatomic, weak, nullable) CBPeripheral *peripheral; // owning peripheral (service)
@property(nonatomic, strong, nullable)
NSMutableArray *children; // attached services or characteristics
@end

@implementation SimbleShadowMeta
Expand Down Expand Up @@ -70,6 +72,11 @@ static void shadowDealloc(id self, SEL _cmd) {
(void)_cmd;
}

// Return the attached child list (services on a peripheral, characteristics on a service) the
// registry minted, snapshotted under the lock. CoreBluetooth's own getter reads private ivars a
// stand-in never had, so the stand-in answers from the registry instead.
static id shadowChildren(id self, SEL _cmd);

// Build a stand-in subclass of a CoreBluetooth class with the no-op dealloc, once per class.
static Class makeShadowSubclass(Class base, const char *name) {
Class cls = objc_allocateClassPair(base, name, 0);
Expand All @@ -93,6 +100,12 @@ static Class makeShadowSubclass(Class base, const char *name) {
g_serviceShadowClass = makeShadowSubclass([CBService class], "SimbleShadowService");
g_characteristicShadowClass =
makeShadowSubclass([CBCharacteristic class], "SimbleShadowCharacteristic");
// The stand-in peripheral's services and the stand-in service's characteristics answer from the
// registry's attached child list.
class_addMethod(g_peripheralShadowClass, sel_registerName("services"), (IMP)shadowChildren,
"@@:");
class_addMethod(g_serviceShadowClass, sel_registerName("characteristics"), (IMP)shadowChildren,
"@@:");
}

static NSValue *ptr(id object) { return [NSValue valueWithPointer:(__bridge const void *)object]; }
Expand All @@ -112,6 +125,15 @@ static Class makeShadowSubclass(Class base, const char *name) {
return object ? objc_getAssociatedObject(object, kShadowMetaKey) : nil;
}

static id shadowChildren(id self, SEL _cmd) {
(void)_cmd;
SimbleShadowMeta *meta = metaOf(self);
[g_lock lock];
NSArray *snapshot = meta.children ? [meta.children copy] : @[];
[g_lock unlock];
return snapshot;
}

void simble_shadow_register_manager(CBCentralManager *manager,
id<CBCentralManagerDelegate> delegate, dispatch_queue_t queue) {
if (!manager)
Expand Down Expand Up @@ -215,6 +237,10 @@ BOOL simble_shadow_peripheral_id(CBPeripheral *peripheral, uint8_t *out, size_t
objc_setAssociatedObject(minted, kShadowMetaKey, meta, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
g_services[key] = minted;
[g_minted addObject:ptr(minted)];
// Attach the minted service to the peripheral so its services getter returns it.
if (!pmeta.children)
pmeta.children = [NSMutableArray new];
[pmeta.children addObject:minted];
[g_lock unlock];
return minted;
}
Expand Down Expand Up @@ -244,10 +270,33 @@ BOOL simble_shadow_peripheral_id(CBPeripheral *peripheral, uint8_t *out, size_t
objc_setAssociatedObject(minted, kShadowMetaKey, meta, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
g_characteristics[key] = minted;
[g_minted addObject:ptr(minted)];
// Attach the minted characteristic to the service so its characteristics getter returns it.
if (!smeta.children)
smeta.children = [NSMutableArray new];
[smeta.children addObject:minted];
[g_lock unlock];
return minted;
}

BOOL simble_shadow_resolve_service(CBService *service, uint8_t *peripheralOut, size_t peripheralCap,
size_t *peripheralLen, CBUUID *_Nullable *_Nonnull serviceUUID) {
if (!service)
return NO;
[g_lock lock];
BOOL minted = [g_minted containsObject:ptr(service)];
[g_lock unlock];
if (!minted)
return NO;
SimbleShadowMeta *meta = metaOf(service);
NSData *pidData = meta.peripheralId;
if (!pidData || (size_t)pidData.length > peripheralCap)
return NO;
memcpy(peripheralOut, pidData.bytes, pidData.length);
*peripheralLen = pidData.length;
*serviceUUID = meta.serviceUUID;
return YES;
}

BOOL simble_shadow_resolve_characteristic(CBCharacteristic *characteristic, uint8_t *peripheralOut,
size_t peripheralCap, size_t *peripheralLen,
CBUUID *_Nullable *_Nonnull serviceUUID,
Expand Down
27 changes: 27 additions & 0 deletions packages/interpose/src/transport/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,33 @@ simble_status simble_client_disconnect(const uint8_t *peripheral_id, size_t peri
return do_request(payload, n, out);
}

simble_status simble_client_discover_services(const uint8_t *peripheral_id, size_t peripheral_len,
const char *const *uuids, const size_t *uuid_lens,
size_t uuid_count, simble_response *out) {
uint8_t token[32];
if (read_token(token) != 0)
return SIMBLE_ERR_TRUNCATED;
uint8_t payload[1024];
int n = simble_encode_discover_services(token, sizeof(token), peripheral_id, peripheral_len,
uuids, uuid_lens, uuid_count, payload, sizeof(payload));
return do_request(payload, n, out);
}

simble_status simble_client_discover_characteristics(const uint8_t *peripheral_id,
size_t peripheral_len, const char *service,
size_t service_len, const char *const *uuids,
const size_t *uuid_lens, size_t uuid_count,
simble_response *out) {
uint8_t token[32];
if (read_token(token) != 0)
return SIMBLE_ERR_TRUNCATED;
uint8_t payload[1024];
int n = simble_encode_discover_characteristics(token, sizeof(token), peripheral_id,
peripheral_len, service, service_len, uuids,
uuid_lens, uuid_count, payload, sizeof(payload));
return do_request(payload, n, out);
}

simble_status simble_client_read_characteristic(const uint8_t *peripheral_id, size_t peripheral_len,
const char *service, size_t service_len,
const char *characteristic, size_t char_len,
Expand Down
34 changes: 34 additions & 0 deletions packages/interpose/src/transport/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,40 @@ simble_status simble_client_connect(const uint8_t *peripheral_id, size_t periphe
simble_status simble_client_disconnect(const uint8_t *peripheral_id, size_t peripheral_len,
simble_response *out);

/**
* @brief Discover services on a connected peripheral, optionally filtered.
*
* @param[in] peripheral_id Host peripheral identifier bytes.
* @param[in] peripheral_len Length of @p peripheral_id.
* @param[in] uuids Array of UTF-8 service UUID strings to filter on, or NULL.
* @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids.
* @param[in] uuid_count Number of UUIDs; 0 omits the filter.
* @param[out] out The decoded response; ::SIMBLE_RESP_SERVICES_DISCOVERED on success.
* @return ::SIMBLE_OK when a response decoded cleanly, a ::simble_status error otherwise.
*/
simble_status simble_client_discover_services(const uint8_t *peripheral_id, size_t peripheral_len,
const char *const *uuids, const size_t *uuid_lens,
size_t uuid_count, simble_response *out);

/**
* @brief Discover characteristics of a service on a connected peripheral, optionally filtered.
*
* @param[in] peripheral_id Host peripheral identifier bytes.
* @param[in] peripheral_len Length of @p peripheral_id.
* @param[in] service Service UUID, UTF-8.
* @param[in] service_len Length of @p service.
* @param[in] uuids Array of UTF-8 characteristic UUID strings to filter on, or NULL.
* @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids.
* @param[in] uuid_count Number of UUIDs; 0 omits the filter.
* @param[out] out The decoded response; ::SIMBLE_RESP_CHARS_DISCOVERED on success.
* @return ::SIMBLE_OK when a response decoded cleanly, a ::simble_status error otherwise.
*/
simble_status simble_client_discover_characteristics(const uint8_t *peripheral_id,
size_t peripheral_len, const char *service,
size_t service_len, const char *const *uuids,
const size_t *uuid_lens, size_t uuid_count,
simble_response *out);

/**
* @brief Read a characteristic value from a connected peripheral.
*
Expand Down
Loading
Loading