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
9 changes: 5 additions & 4 deletions packages/interpose/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ endif()

set(SIMBLE_FRAMEWORKS "-framework CoreBluetooth" "-framework Foundation")

# The interposer core: the central swizzles, the shadow registry, and the loopback transport.
# No load constructor here; the dylib and the host harness add their own entry point, so both
# reuse this. The .m files use ARC.
# The interposer core: the central and peripheral swizzles, the shadow registry, and the loopback
# transport. No load constructor here; the dylib and the host harness add their own entry point, so
# both reuse this. The .m files use ARC.
add_library(interpose_core STATIC
src/hooks/central_hooks.m
src/hooks/peripheral_hooks.m
src/registry/shadow.m
src/transport/client.c
)
target_include_directories(interpose_core PUBLIC include src/transport src/registry)
target_include_directories(interpose_core PUBLIC include src/transport src/registry src/hooks)
target_compile_options(interpose_core PRIVATE $<$<COMPILE_LANGUAGE:OBJC>:-fobjc-arc>)
target_link_libraries(interpose_core PUBLIC simble_protocol_c ${SIMBLE_FRAMEWORKS})

Expand Down
13 changes: 7 additions & 6 deletions packages/interpose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ SPDX-FileCopyrightText: 2026 Nirapod Labs

The injected simulator dylib that routes a guest app's CoreBluetooth calls to the host.

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,
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).
It hooks the CoreBluetooth central and peripheral surfaces 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. On the
central side: scan, connect, service and characteristic discovery, read, write, notify, and RSSI. On
the peripheral side: publish a service, advertise, respond to read and write requests, and notify
subscribers. The dylib is a simulator slice, loaded only by a debug scheme and never by a shipped
app (see `SECURITY.md` for the fence).
14 changes: 10 additions & 4 deletions packages/interpose/include/simble_interpose.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ extern "C" {
int simble_interpose_version(void);

/**
* @brief Install the CoreBluetooth central swizzles.
* @brief Install the CoreBluetooth central and peripheral swizzles.
*
* Exchanges the CBCentralManager and CBPeripheral method implementations for the routed
* ones. Idempotent: a second call is a no-op while the swizzles are installed.
* Exchanges the CBCentralManager, CBPeripheral, and CBPeripheralManager method implementations for
* the routed ones. Idempotent: a second call is a no-op while the swizzles are installed.
*
* @return The number of selectors that could not be swizzled; 0 means every one is in place.
*/
Expand All @@ -50,7 +50,7 @@ int simble_install_hooks(void);
*/
void simble_uninstall_hooks(void);

/** How many times each routed central operation has fired since install. */
/** How many times each routed operation has fired since install. */
typedef struct {
int scan_start; ///< scanForPeripheralsWithServices:options: calls routed.
int connect; ///< connectPeripheral:options: calls routed to the helper.
Expand All @@ -59,6 +59,12 @@ typedef struct {
int read; ///< readValueForCharacteristic: calls routed to the helper.
int write; ///< writeValue:forCharacteristic:type: calls routed.
int set_notify; ///< setNotifyValue:forCharacteristic: calls routed.
int add_service; ///< addService: calls routed to the helper.
int remove_service; ///< removeService: and removeAllServices: calls routed.
int start_advertising; ///< startAdvertising: calls routed to the helper.
int respond; ///< respondToRequest:withResult: calls routed.
int update_value; ///< updateValue:forCharacteristic:onSubscribedCentrals: calls routed.
int peripheral_event; ///< peripheral events delivered to the guest delegate.
} simble_hook_stats;

/**
Expand Down
57 changes: 31 additions & 26 deletions packages/interpose/src/hooks/central_hooks.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@

#import "../registry/shadow.h"
#import "../transport/client.h"
#import "hooks_internal.h"
#import "simble_interpose.h"

#import <CoreBluetooth/CoreBluetooth.h>
#import <objc/runtime.h>
#import <pthread.h>

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

simble_hook_stats *simble_internal_stats(void) { return &g_stats; }

// The guest's bundle id and display name, for the HELLO the helper shows. Guest-reported,
// names the app, gates nothing.
static size_t copyAppId(char *buf, size_t cap) {
Expand Down Expand Up @@ -154,8 +157,8 @@ static void deliverEvent(CBCentralManager *manager, const simble_event *event) {
break;
}
default:
// A peripheral-role event is not delivered by the central interposer; the peripheral role
// is not implemented here.
// A peripheral-role event routes to the peripheral delivery path.
simble_deliver_peripheral_event(event);
break;
}
}
Expand Down Expand Up @@ -198,10 +201,10 @@ static void startEventReaderOnce(CBCentralManager *manager) {
SEL replacement;
} SwizzlePair;

static SwizzlePair g_pairs[16];
static SwizzlePair g_pairs[24];
static size_t g_pair_count = 0;

static int swizzle(Class cls, SEL original, SEL replacement) {
int simble_swizzle(Class cls, SEL original, SEL replacement) {
Method origMethod = class_getInstanceMethod(cls, original);
Method replMethod = class_getInstanceMethod(cls, replacement);
if (!origMethod || !replMethod)
Expand Down Expand Up @@ -621,29 +624,31 @@ int simble_install_hooks(void) {
}
int failures = 0;
Class managerClass = [CBCentralManager class];
failures += swizzle(managerClass, @selector(initWithDelegate:queue:),
@selector(simble_initWithDelegate:queue:));
failures += swizzle(managerClass, @selector(state), @selector(simble_state));
failures += swizzle(managerClass, @selector(scanForPeripheralsWithServices:options:),
@selector(simble_scanForPeripheralsWithServices:options:));
failures += swizzle(managerClass, @selector(stopScan), @selector(simble_stopScan));
failures += swizzle(managerClass, @selector(connectPeripheral:options:),
@selector(simble_connectPeripheral:options:));
failures += swizzle(managerClass, @selector(cancelPeripheralConnection:),
@selector(simble_cancelPeripheralConnection:));
failures += simble_swizzle(managerClass, @selector(initWithDelegate:queue:),
@selector(simble_initWithDelegate:queue:));
failures += simble_swizzle(managerClass, @selector(state), @selector(simble_state));
failures += simble_swizzle(managerClass, @selector(scanForPeripheralsWithServices:options:),
@selector(simble_scanForPeripheralsWithServices:options:));
failures += simble_swizzle(managerClass, @selector(stopScan), @selector(simble_stopScan));
failures += simble_swizzle(managerClass, @selector(connectPeripheral:options:),
@selector(simble_connectPeripheral:options:));
failures += simble_swizzle(managerClass, @selector(cancelPeripheralConnection:),
@selector(simble_cancelPeripheralConnection:));

Class peripheralClass = [CBPeripheral class];
failures +=
swizzle(peripheralClass, @selector(discoverServices:), @selector(simble_discoverServices:));
failures += swizzle(peripheralClass, @selector(discoverCharacteristics:forService:),
@selector(simble_discoverCharacteristics:forService:));
failures += swizzle(peripheralClass, @selector(readValueForCharacteristic:),
@selector(simble_readValueForCharacteristic:));
failures += swizzle(peripheralClass, @selector(writeValue:forCharacteristic:type:),
@selector(simble_writeValue:forCharacteristic:type:));
failures += swizzle(peripheralClass, @selector(setNotifyValue:forCharacteristic:),
@selector(simble_setNotifyValue:forCharacteristic:));
failures += swizzle(peripheralClass, @selector(readRSSI), @selector(simble_readRSSI));
failures += simble_swizzle(peripheralClass, @selector(discoverServices:),
@selector(simble_discoverServices:));
failures += simble_swizzle(peripheralClass, @selector(discoverCharacteristics:forService:),
@selector(simble_discoverCharacteristics:forService:));
failures += simble_swizzle(peripheralClass, @selector(readValueForCharacteristic:),
@selector(simble_readValueForCharacteristic:));
failures += simble_swizzle(peripheralClass, @selector(writeValue:forCharacteristic:type:),
@selector(simble_writeValue:forCharacteristic:type:));
failures += simble_swizzle(peripheralClass, @selector(setNotifyValue:forCharacteristic:),
@selector(simble_setNotifyValue:forCharacteristic:));
failures += simble_swizzle(peripheralClass, @selector(readRSSI), @selector(simble_readRSSI));

failures += simble_install_peripheral_hooks();

g_installed = (failures == 0);
pthread_mutex_unlock(&g_install_lock);
Expand Down
53 changes: 53 additions & 0 deletions packages/interpose/src/hooks/hooks_internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: Apache-2.0
* SPDX-FileCopyrightText: 2026 Nirapod Labs
*/

/**
* @file hooks_internal.h
* @brief Shared swizzle plumbing across the central and peripheral hook units.
*
* @details
* The central unit owns the route counters, the swizzle exchange, and the event reader; the
* peripheral unit reuses them through these declarations. Not a public header.
*
* @author Nirapod Labs
* @date 2026
*/

#ifndef SIMBLE_HOOKS_INTERNAL_H
#define SIMBLE_HOOKS_INTERNAL_H

#import "simble_interpose.h"
#import "simble_protocol.h"

#import <objc/runtime.h>

/**
* @brief Exchange original and routed IMPs for a selector, recording the pair for uninstall.
*
* @param[in] cls The class carrying the selector.
* @param[in] original The selector to route.
* @param[in] replacement The routed selector.
* @return 0 on success, -1 when either method is absent.
*/
int simble_swizzle(Class cls, SEL original, SEL replacement);

/** @brief The shared route counters the public simble_get_hook_stats reads. */
simble_hook_stats *simble_internal_stats(void);

/**
* @brief Install the CBPeripheralManager swizzles.
*
* @return The number of selectors that failed to swizzle.
*/
int simble_install_peripheral_hooks(void);

/**
* @brief Translate one peripheral-role host event into the guest's delegate callbacks.
*
* @param[in] event The decoded peripheral event.
*/
void simble_deliver_peripheral_event(const simble_event *event);

#endif
Loading
Loading