Wild pointer

The pointer is called wild when the object it points to is released or reclaimed, but no changes are made to the pointer so that it still points to the reclaimed memory address

Compared with NSException, wild pointer exception can be called half of crash field. Compared with NSException, wild pointer has two characteristics:

  • While large companies already have a variety of unit, behavioral, automated, and artificial tests that try to simulate user scenarios, wild pointer exceptions can always evade testing and thrive online. The reason is not just that tests cannot cover all usage scenarios





    The resulting wild pointer is diverse: first, the memory freed does not mean that the memory will be overwritten or the data will be corrupted immediately, and accessing the memory at this time does not necessarily cause errors. Second, multithreading creates a complex application environment in which unprotected data can be fatal. In addition, the design of the code is not strict is also one of the important reasons for the exception of wild pointer

  • It is difficult to identify NSException as encapsulation at a high level of abstraction, which means it can provide more error information to refer to. The wild pointer is almost from the C language level, often we can get only the system stack information, it is difficult to locate the error code location, let alone to reproduce the repair





positioning

The biggest difficulty to solve the wild pointer lies in location. Usually when a crash occurs online and needs to be fixed, the most important step for developers is to recreate the crash. The two characteristics of the wild pointer mentioned above will prevent us from locating the problem. For these two characteristics, some corresponding processing can be done to reduce their interference:

  • Collecting auxiliary information Auxiliary information includes device information and user behavior information, which can be used to reproduce problems. For example, user behaviors can form user usage paths to reproduce user usage scenarios. When a crash occurs, the current page information can be collected and the user can use the path to quickly locate the approximate location of the problem. After verification, the auxiliary information can effectively reduce the interference of the system stack to the problem recurrence

  • Due to the feature that wild Pointers do not necessarily crash, even if we determine the rough range through stack information and auxiliary information, it does not mean that we can successfully reproduce crash. A good wild-pointer crash can result in one day of development and three days of debug. If wild-pointer crashes are not random, then the problem is much simpler





    Xcode provides Malloc Scribble to populate the freed memory so that wild pointer access is bound to crash. In addition, By referring to this principle, Bugly modified the free function to fill the freed object with illegal data, which also effectively improved the crash rate of wild Pointers

  • Zombie Objects is a completely different kind of wild pointer debugging mechanism, which marks the released object as a Zombie object, and sends a message to the Zombie object again, crashing and printing the relevant call information. This mechanism simultaneously locates the crash class object and has a relatively clear call stack

The solution

Sorting through the above, you can see that there are two main ways to cope: auxiliary information + object memory padding and Zombie Objects. In the case of the former, populating the memory of freed objects is risky, as Xcode9’s Malloc Scribble starts without populating the memory address of the object. Second, filling memory requires hooking lower-level apis, which means higher code capabilities. Therefore, it is a feasible solution to locate the wild pointer anomaly using the implementation idea of Zombie Objects

forwarding

Forwarding is an interesting mechanism that works by inserting an intermediate layer between the two sides of the communication. Instead of coupling the receiver, the sender simply sends the data to the middle tier, which then sends it to the specific receiver. There are a lot of interesting things you can do based on the idea of forwarding:

  • Message forwarding iOS’s messaging mechanism allows us to send an unregistered message to an object, which usually raises an unrecognized selector exception. But before an exception is thrown, there is a message forwarding mechanism that allows us to respecify the recipient of the message to handle the message. It is this mechanism that makes it feasible to prevent unrecognized selector crashes





  • Breaking the reference loop Circular references are one of the most common memory problems in an ARC environment. When multiple objects form a reference loop, it is very possible that none of the objects in the loop can be freed. Using Proxy for reference, you can destroy the reference ring. XXShield protects against crash by inserting WeakProxy layer





  • Componentization of routing and forwarding is an architectural scheme that must be considered when the project volume reaches a certain level. The project is divided into basic components and business components and added into the middle layer to achieve the effect of decoupling between components. Since service components are not dependent on each other, appropriate solutions are needed to realize component communication, and routing design is a common communication method. Each module implements canOpenURL: interface to judge whether to process the corresponding jump logic, and the module splice parameter information in THE URL to pass:





Message is sent

Message sending is objective-C’s core mechanism, and any object method call is converted to objc_msgSend. There is an important variable involved in this process: the ISA pointer. Most developers place the ISA pointer at the address where it points to the class structure itself, which is used to indicate the type of the object. But the ISA pointer is actually more complex than we think. For example, objc_msgSend relies on ISA to find messages. Read objc_msgSend in assembly to learn more about the matching process:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; 
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};
Copy the code

Because method calls are related to isa Pointers, if we modify the ISA pointer of a class to point to a target class, we can implement interception of object method calls, also known as object method forwarding. We can’t modify the ISA pointer directly, but the Runtime provides an object_SetClass interface that allows us to dynamically relocate a class

The relocation of ClassA to ClassB requires that the memory structures of the two classes are aligned, otherwise unexpected problems may occur

In general, we should not violate the memory structure alignment principle of relocation classes. However, in the wild pointer problem, the memory owned by the object is in an indeterminate state after being freed, so it is not necessarily a bad thing to do some damage. Just remember that when the object memory is finally freed, it should be relocated again to prevent the risk of memory leakage

Code implementation

We can implement a Zombie proxy-like mechanism borrowed from Zombie Objects. By relocating the type, the isa pointer to the object Dealloc is pointed to a target class before the object dealloc is forwarded for subsequent calls. All method calls in the target class use a mechanism called NSException to throw exceptions and output the actual type of the calling object and the calling method to help locate:





Because of its actual use for forwarding, the relocated class is more consistent with Proxy properties, so I made it a subclass of NSProxy. Most people may not know that iOS has two root classes, NSProxy and NSObject. Also, in order to override memory management-related methods such as Retain, the target class should be set to not support ARC:

@interface LXDZombieProxy : NSProxy

@property (nonatomic, assign) Class originClass;

@end

@implementation LXDZombieProxy

- (void)_throwMessageSentExceptionWithSelector: (SEL)selector
{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException 
                                   reason:[NSString stringWithFormat:@"(-[%@ %@]) was sent to a zombie object at address: %p", NSStringFromClass(self.originClass), NSStringFromSelector(selector), self] 
                                 userInfo:nil];
}

#define LXDZombieThrowMesssageSentException() [self _throwMessageSentExceptionWithSelector: _cmd]

- (id)retain
{
    LXDZombieThrowMesssageSentException();
    return nil;
}

- (oneway void)release
{
    LXDZombieThrowMesssageSentException();
}

- (id)autorelease
{
    LXDZombieThrowMesssageSentException();
    return nil;
}

- (void)dealloc
{
    LXDZombieThrowMesssageSentException();
    [super dealloc];
}

- (NSUInteger)retainCount
{
    LXDZombieThrowMesssageSentException();
    return 0;
}

@end
Copy the code

Because the iOS methods are actually implemented as a chain mechanism that calls up, you only need to hook the dealloc methods of the two root classes to ensure that the object type is repositioned. There are a few things to note after you hook dealloc:

  • Since we need to implement a forwarding mechanism, this means that objects that should be freed cannot be freed after type relocation. Over time, the number of relocation class objects will increase. As a rule of thumb, the average wild pointer has a high probability of being accessed again within 30 seconds, so we can delay the release of the object 30 seconds after the type relocation is complete. Or you can build a Zombie Pool, and when the memory footprint reaches a certain size, use the appropriate algorithm to eliminate it

  • The whitelist mechanism does not monitor all class objects, such as system private classes, monitoring related utility classes, classes that explicitly do not have wild Pointers, and so on. We need a global whitelist system to ensure that dealloc for these classes is properly executed and does not need to be forwarded

  • The potential crash is implemented through the code replacing dealloc with method_setImplementation, which, due to the way I implemented it by blocking to IMP, references any captured external objects. However, after the object is relocated, any call will cause crash, so we need to deal with this situation

In order to ensure that the object can achieve the release condition to complete the memory collection, the original implementation of dealloc needs to store the root class name as the key stored in the global dictionary. The __lxd_dealloc interface is provided to do the work of releasing objects:

static inline void __lxd_dealloc(__unsafe_unretained id obj) { Class currentCls = [obj class]; Class rootCls = currentCls; while (rootCls ! = [NSObject class] && rootCls ! = [NSProxy class]) { rootCls = class_getSuperclass(rootCls); } NSString *clsName = NSStringFromClass(rootCls); LXDDeallocPointer deallocImp = NULL; [[_rootClassDeallocImps objectForKey: clsName] getValue: &deallocImp]; if (deallocImp ! = NULL) { deallocImp(obj); } } NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary]; for (Class rootClass in _rootClasses) { IMP originalDeallocImp = __lxd_swizzleMethodWithBlock(class_getInstanceMethod(rootClass, @selector(dealloc)), swizzledDeallocBlock); [deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)]; }Copy the code

After the object’s dealloc has been invoked, checks whether the object type is whitelisted. If so, proceed with the object release. Otherwise, delay the release for 30 seconds. Across references, NSValue is used to store object information and __unsafe_unretained to prevent temporary variable references:

swizzledDeallocBlock = [^void(id obj) { Class currentClass = [obj class];  NSString *clsName = NSStringFromClass(currentClass); If ([__lxd_sniff_white_list() containsObject: clsName]) { __lxd_dealloc(obj); } else { NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))]; object_setClass(obj, [LXDZombieProxy class]); ((LXDZombieProxy *)obj).originClass = currentClass; // Delay releasing the object for 30 seconds, Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __unsafe_unretained id deallocObj = nil; [objVal getValue: &deallocObj]; object_setClass(deallocObj, currentClass); __lxd_dealloc(deallocObj); }); } } copy];Copy the code

Specific implementation code can be downloaded LXDZombieSniffer

problems

A wild pointer problem is a crash caused by access to illegal memory. That is, two conditions must be met: the memory is illegal and the pointer address is not NULL. There are three Pointers for different modifications in iOS:

  • __strong Default modifier. After an assignment, the decorated pointer performs a retain operation on the object to which it points. The pointer does not change with the life cycle of the object

  • __unsafed_unretained pointer modifier of a non-secure object. A decorated pointer does not hold a pointer to an object, nor does it change with the object’s lifetime. It is the same as assign

  • __weak Weak object pointer modifier. The modified pointer does not hold the pointer to the object, and the contents of the modified pointer are reset to nil at the end of the object’s life cycle and memory is reclaimed

__strong and __unsafed_unretained can cause wild pointer access exceptions. However, after category relocation is used, the object that should be released will be delayed or not released, that is, the weak pointer that should be reset will not be reset. In this case, accessing the object using the weak pointer will be forwarded to ZombieProxy and crash will occur:

__weak id weakObj = nil;
@autoreleasepool {
    NSObject *obj = [NSObject new];
    weakObj = obj;
}
/// The operate should be crashed
NSLog(@"%@", weakObj);
Copy the code

However, in the above test, it was found that weakObj was successfully set to nil even after the object was repositioned as Zombie and was prevented from being released. After the objc_Runtime source code run and adding breakpoint tests, there is no call to reset the weak pointer. Even the watch set var weakObj of LLVM was used to monitor the weak pointer, but it still could not find the call. But weakObj is reset to nil after dealloc is called, regardless of whether the object is released or not. This is also up until the article out of the perplexing disease

reference

How to locate obj-C wild pointer random Crash(a) How to locate obJ-C wild pointer random Crash(B) how to locate obJ-C wild pointer random Crash(C)