ARC `BAD_ACCESS` when using the NSString category method

I call my utility method as follows:

NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; [dateFormat setDateFormat:@"dd.MM.yy HH:mm"]; NSString *dateString = [dateFormat stringFromDate:[NSDate date]]; return [[Environment sharedInstance].versionLabelFormat replaceTokensWithStrings: @"VERSION", APP_VERSION, @"BUILD", APP_BULD_NUMBER, @"DATETIME" , dateString, nil ]; 

This is the NSString category method.

 -(NSString *)replaceTokensWithStrings:(NSString *)firstKey, ... NS_REQUIRES_NIL_TERMINATION{ NSString *result = self; va_list _arguments; va_start(_arguments, firstKey); for (NSString *key = firstKey; key != nil; key = va_arg(_arguments, NSString*)) { // The value has to be copied to prevent crashes NSString *value = [(NSString *)(va_arg(_arguments, NSString*))copy]; if(!value){ // Every key has to have a value pair otherwise the replacement is invalid and nil is returned NSLog(@"Premature occurence of nil. Each token must be accompanied by a value: %@", result); return nil; } result = [result replaceToken:key withString:value]; } va_end(_arguments); // Check if there are any tokens which were not yet replaced (for example if one value was nil) if([result rangeOfString:@"{"].location == NSNotFound){ return result; } else { NSLog(@"Failed to replace tokens failed string still contains tokens: %@", result); return nil; } } 

No, on the next line I had to add a copy statement, otherwise there would be a Zombie with dateString :

 NSString *value = [(NSString *)(va_arg(_arguments, NSString*))copy]; 

To be more specific, a Zombie report told me the following:

  1 Malloc NSDateFormatter stringForObjectValue: Autorelease NSDateFormatter stringForObjectValue: 2 CFRetain MyClass versionString: 3 CFRetain replaceToken:withString: 2 CFRelease replaceToken:withString: 1 CFRelease replaceTokensWithStrings: ( One release too much!) 0 CFRelease MyClass versionString: -1 Zombie GSEventRunModal 

Although the copy statement seems to fix the problem, I would like to understand what does not match the ARC with the code, so BAD_ACCESS will happen without copy for the string of values.

+7
source share
2 answers

As others have argued, the problem is how you retrieve objects from a list of variable arguments that may not be compatible with ARC.

va_arg has a fun way of returning a value of a specific type, which ARC probably doesn't know about. I'm not sure if this is a bug in clang or if this is the intended behavior for ARC. I will clarify this problem and update the message accordingly.

As a workaround, just avoid the problem by using void pointers in processing arguments and correctly converting them into objects in a safe way to ARC:

 for (NSString *key = firstKey; key != nil; key = (__bridge NSString *)va_arg(_arguments, void *)) { NSString *value = (__bridge NSString *)va_arg(_arguments, void *); NSAssert(value != NULL, @"Premature occurence of nil."); result = [result stringByReplacingToken:key withString:value]; } 

Edit: Listing __bridge tells ARC not to do something about property rights. He simply expects the property to be alive and not transfer or refuse the property. However, the key and value variables support strong object references during use.

Second Edit: It seems that clang / ARC should know the type in va_arg and either warn or do the right thing ( see this, for example ).

I tried to reproduce your problem without success. Everything works for me:

 $ clang --version > Apple clang version 4.0 (tags/Apple/clang-421.10.48) (based on LLVM 3.1svn) 

What version of Xcode are you using?

+5
source

This is a bug in clang.

He should work

ARC is compatible with variable argument lists. If this is not the case, you will get an error from the compiler.

value is a strong reference, while the result of va_arg(_arguments, NSString *) is an unsafe unreachable reference: you can write va_arg(_arguments, __unsafed_unretained NSString *) and get the same compiled assembly, but if you try with a different owner qualifier, you will get compiler error because it is not supported.

So, while storing the value in value and assuming that the variable is actually used, the compiler should issue an objc_retain call and balance it with the objc_release call when this variable is destroyed. According to the report, the second call is emitted, while the first is missing.

This crashes with the string returned by stringWithDate: (and only that), because it is the only string that is not a constant. All other parameters are constant lines generated by the compiler, which simply ignore any memory management method, since they are stored in memory until the executable is loaded.

So, we need to understand why the compiler releases a release without saving it accordingly. Since you are not doing manual memory management or cheating ownership rules by casting with __bridge_transfer or __bridge_retained , we can assume that the problem comes from the compiler.

Undefined behavior is not the cause

There are two reasons why the compiler can fix an invalid build. Either the code containing the undefined behavior, or there is an error in the compiler.

Undefined behavior occurs when your code tries to execute something that is not defined by the C standard: when the compiler encounters undefined, it has the right to do whatever it wants. undefined behavior leads to programs that may or may not be broken. In most cases, the problem occurs in the same place as the undefined behavior, but sometimes it may seem unrelated, because compilers expect that the undefined behavior will not happen and relies on this expectation to perform some optimizations.

So, let's see if your code contains undefined behavior. Yes, since the replaceTokensWithStrings: method can call va_start and return without va_end , which is called ( return nil inside the for loop). The C standard explicitly states (in section 7.15.1.3) that this behavior is undefined.

If we replace return nil with break , your code will now be valid. However, this does not solve the problem.

Break compiler

Now that we have eliminated all the other possible causes, we need to face reality. There is an error in clang. We can see this by doing a lot of subtle changes that create a valid compiled assembly:

  • If we compile with -O0 instead of -Os , it works.
  • If we compile with clang 4.1, it works.
  • If we send a message to an object before the if condition, it works.
  • If we replace va_arg(_arguments, NSString *) with va_arg(_arguments, id) , it will work. I suggest you use this workaround.
+2
source

All Articles