Skip to content

Instantly share code, notes, and snippets.

@EthanArbuckle
Last active March 3, 2025 18:46
Show Gist options
  • Save EthanArbuckle/b04e5a2825d99f2fd5efe071be5cb27f to your computer and use it in GitHub Desktop.
Save EthanArbuckle/b04e5a2825d99f2fd5efe071be5cb27f to your computer and use it in GitHub Desktop.
Watch for changes to an ivar
//
// main.m
//
// Created by ethanarbuckle
//
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <mach/mach.h>
#import <execinfo.h>
#import <pthread.h>
#import <dlfcn.h>
#import "mach_excServer.h"
static void *watched_address;
static mach_port_t exception_port;
static void print_thread_backtrace(thread_t thread) {
arm_thread_state64_t thread_state;
mach_msg_type_number_t thread_state_count = ARM_THREAD_STATE64_COUNT;
kern_return_t kr = thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&thread_state, &thread_state_count);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to get thread state: %d", kr);
return;
}
void *frame_ptr = (void *)thread_state.__fp;
void *link_register = (void *)thread_state.__lr;
void *frames[32];
int frame_count = 0;
frames[frame_count++] = (void *)thread_state.__pc;
if (link_register) {
frames[frame_count++] = link_register;
}
while (frame_ptr && frame_count < 32) {
void **fp = (void **)frame_ptr;
void *saved_lr = fp[1];
if (!saved_lr) {
break;
}
frames[frame_count++] = saved_lr;
frame_ptr = *fp;
}
char **symbols = backtrace_symbols(frames, frame_count);
if (symbols) {
for (int i = 0; i < frame_count; i++) {
printf("%s\n", symbols[i]);
}
free(symbols);
}
}
kern_return_t catch_mach_exception_raise(mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt) {
return KERN_FAILURE;
}
kern_return_t catch_mach_exception_raise_state_identity(mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) {
NSLog(@"Caught ivar modification from:");
uintptr_t fault_address = code[1];
if (fault_address == (uintptr_t)watched_address) {
print_thread_backtrace(thread);
memcpy(new_state, old_state, old_stateCnt * sizeof(natural_t));
*new_stateCnt = old_stateCnt;
arm_thread_state64_t *state = (arm_thread_state64_t *)new_state;
state->__pc += 4;
return KERN_SUCCESS;
}
return KERN_FAILURE;
}
kern_return_t catch_mach_exception_raise_state(mach_port_t exception_port, exception_type_t exception, const mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, const thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) {
return KERN_FAILURE;
}
void *exception_handler(void *unused) {
mach_msg_server(mach_exc_server, sizeof(union __RequestUnion__catch_mach_exc_subsystem), exception_port, MACH_MSG_OPTION_NONE);
abort();
}
static kern_return_t set_thread_debug_state(thread_act_t thread, void *addr) {
arm_debug_state64_t debug_state = {};
mach_msg_type_number_t count = ARM_DEBUG_STATE64_COUNT;
kern_return_t kr = thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&debug_state, &count);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to get debug state: %d", kr);
return kr;
}
for (int i = 0; i < 16; i++) {
debug_state.__wcr[i] = 0;
debug_state.__wvr[i] = 0;
}
debug_state.__wvr[0] = (uint64_t)addr;
debug_state.__bcr[0] = 0xe5;
debug_state.__bvr[0] = (uint64_t)addr;
debug_state.__wcr[0] = (0xf << 5) | (2 << 3) | (3 << 1) | 1;
kr = thread_set_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&debug_state, ARM_DEBUG_STATE64_COUNT);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to set debug state: %d", kr);
return kr;
}
arm_debug_state64_t verify = {};
count = ARM_DEBUG_STATE64_COUNT;
kr = thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&verify, &count);
if (kr == KERN_SUCCESS) {
if (verify.__bvr[0] != debug_state.__bvr[0] || verify.__bcr[0] != debug_state.__bcr[0]) {
printf("state mismatch:\n");
printf("BVR0: got 0x%llx, wanted 0x%llx\n", verify.__bvr[0], debug_state.__bvr[0]);
printf("BCR0: got 0x%llx, wanted 0x%llx\n", verify.__bcr[0], debug_state.__bcr[0]);
}
}
return kr;
}
void watch_ivar(id instance, const char *ivarName) {
Ivar ivar = class_getInstanceVariable([instance class], ivarName);
if (ivar == NULL) {
NSLog(@"Ivar %s not found", ivarName);
return;
}
ptrdiff_t offset = ivar_getOffset(ivar);
void *addr = (__bridge void *)instance + offset;
watched_address = addr;
kern_return_t kr = mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &exception_port);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to allocate exception port: %d", kr);
return;
}
kr = mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to insert port right: %d", kr);
return;
}
kr = task_set_exception_ports(mach_task_self_, EXC_MASK_BREAKPOINT, exception_port, EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES, ARM_THREAD_STATE64);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to set exception ports: %d", kr);
return;
}
pthread_t exc_thread;
pthread_create(&exc_thread, NULL, exception_handler, NULL);
thread_act_array_t threads;
mach_msg_type_number_t thread_count;
kr = task_threads(mach_task_self(), &threads, &thread_count);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to get task threads: %d", kr);
return;
}
for (int i = 0; i < thread_count; i++) {
kr = set_thread_debug_state(threads[i], addr);
if (kr != KERN_SUCCESS) {
NSLog(@"Failed to set debug state for thread %d: %d", i, kr);
}
mach_port_deallocate(mach_task_self(), threads[i]);
}
vm_deallocate(mach_task_self(), (vm_address_t)threads, thread_count * sizeof(*threads));
}
@interface TestClass : NSObject {
@public
NSString *_testIvar;
}
@end
@implementation TestClass
- (instancetype)init {
if (self = [super init]) {
_testIvar = @"1st value";
int delay = 5;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self->_testIvar = @"4th value";
});
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestClass *obj = [[TestClass alloc] init];
watch_ivar(obj, "_testIvar");
obj->_testIvar = @"2nd value";
sleep(1);
[obj setValue:@"3rd value" forKey:@"_testIvar"];
CFRunLoopRun();
}
return 0;
}
```
Caught ivar modification from:
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
2 ivarmon 0x0000000100488da4 main + 100
3 dyld 0x000000019d7cc274 start + 2840
Caught ivar modification from:
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
2 Foundation 0x000000019edc846c -[NSObject(NSKeyValueCoding) setValue:forKey:] + 324
3 ivarmon 0x0000000100488dc8 main + 136
4 dyld 0x000000019d7cc274 start + 2840
Caught ivar modification from:
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48
2 ivarmon 0x0000000100488ca0 __17-[TestClass init]_block_invoke + 48
3 libdispatch.dylib 0x00000001006ea8a4 _dispatch_client_callout + 20
4 libdispatch.dylib 0x00000001006ee108 _dispatch_continuation_pop + 700
5 libdispatch.dylib 0x00000001007099c4 _dispatch_source_latch_and_call + 488
6 libdispatch.dylib 0x0000000100708068 _dispatch_source_invoke + 872
7 libdispatch.dylib 0x00000001006fdbf4 _dispatch_main_queue_drain + 768
8 libdispatch.dylib 0x00000001006fd8e4 _dispatch_main_queue_callback_4CF + 44
9 CoreFoundation 0x000000019dc75680 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
10 CoreFoundation 0x000000019dc3517c __CFRunLoopRun + 1996
11 CoreFoundation 0x000000019dc34334 CFRunLoopRunSpecific + 572
12 CoreFoundation 0x000000019dcafa10 CFRunLoopRun + 64
13 ivarmon 0x0000000100488dcc main + 140
14 dyld 0x000000019d7cc274 start + 2840
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment