3

I have run into some weird behavior on iPhone 6, iOS 8.3.

appVersion is an NSString* parameter being passed in.

  NSLog(@"A:%@:%d",appVersion,(int)appVersion.length);
  if (!appVersion)
    NSLog(@"a");
  if (appVersion == 0)
    NSLog(@"b");
  if (appVersion == nil)
    NSLog(@"c");
  if (appVersion == NULL)
    NSLog(@"d");
  if (appVersion == Nil)
    NSLog(@"e");
  if ([appVersion isEqual:[NSNull null]])
    NSLog(@"f");

  NSString* av = [NSString stringWithFormat:@"%@",appVersion];
  if ([av isEqualToString:@"(null)"])
    NSLog(@"g");
  if (((int)appVersion) == 0)
    NSLog(@"h");

  if (appVersion) {
    NSLog(@"B:%@:%d",appVersion,(int)appVersion);
    params[@"appversion"] = appVersion;
  }

The release build of the app returns:

A:(null):0
g
h
B:(null):0

and then crashes ('object cannot be nil (key: appversion)').

The debug build returns:

a
b
c
d
e
g
h

What is nil, but not nil?

aepryus
  • 4,715
  • 5
  • 28
  • 41
  • How you assign the value for appVersion?? – DilumN Apr 18 '15 at 18:10
  • @BC_Dilum, it is being pulled out of an NSDictionary returned from a couchbase document. – aepryus Apr 18 '15 at 18:13
  • 1
    Can you add that code as well? – DilumN Apr 18 '15 at 18:16
  • @Cristik it's an iOS app so to some degree, but appVersion isn't getting reset; it is already some version of 0 to begin with and a little hard to imagine another thread hitting right between the if and the set, 100% of the time. – aepryus Apr 18 '15 at 18:25
  • Log `appVersion class` – rmaddy Apr 18 '15 at 18:27
  • @Cristik yes, it's multithreaded. – aepryus Apr 18 '15 at 18:29
  • @Cristik No, it's set once. – aepryus Apr 18 '15 at 18:33
  • @rmaddy NSLog(@"%@",appVersion.class) prints: (null) – aepryus Apr 18 '15 at 18:35
  • @BC_Dilum the value is being pulled out of a different NSMutableDictionary, meaning it is being pulled out of a NSMutableDictionary that doesn't contain the relevant key. – aepryus Apr 18 '15 at 18:43
  • Seems to be some weird optimization issue. The code works as expected in debug mode but in release mode it seems to be both nil and not nil at the same time. Though it only seems to be non-nil for the `if` statements in release mode. Can you create an app with just this code that demonstrates the problem? – rmaddy Apr 18 '15 at 19:00
  • I second the request to update your question with the code that assigns appVersion. – rmaddy Apr 18 '15 at 19:03
  • @rmaddy In working on trying to find some useful code that in regard, I may have found the issue. I'll post as soon as I track it down a bit more. – aepryus Apr 18 '15 at 19:06

3 Answers3

3

I'm working in some legacy code and hadn't noticed that there is a difference in the method signature between the .h and .m file.

The .h file has:

- (void) verifyWinner:(NSString*)baseAcctId
           appVersion:(NSString*)appVersion
           onComplete:(OnCompleteWinnerVerifier)onComplete __attribute__((nonnull));

I'm guessing that the original developer wanted to prevent onComplete being set to nil. However, for some reason __attribute__((nonnull)) is being associated with each of the parameters.

Because of the __attribute__ tag, XCode is optimizing away all the != nil checks for the release build, there by causing the crash.

This problem has only now cropped up with XCode 6.3. So, perhaps Apple recently added the optimization or else the introduced a bug in 6.3 that associates the __attribute__ with each of the parameters instead of just the parameters it is next to (for optimization purposes anyway).

aepryus
  • 4,715
  • 5
  • 28
  • 41
  • Nice. So if a parameter is declared as nonnull, you can't even check it at runtime because the check is removed by the optimising compiler. In other words, passing nil to a nonnull parameter is undefined behaviour. – gnasher729 Apr 18 '15 at 19:40
  • @gnasher729 Yes, and if its getting associated with each of the parameters, this could cause a lot of problems. I should probably file a bug report. – aepryus Apr 18 '15 at 19:43
  • Nice find. Definitely send a bug report with a simple reproducible bit of code demonstrating the issue. – rmaddy Apr 18 '15 at 19:46
  • 1
    I think this is all intentional behavior. Check out this related post from [clang developer Doug Gregor](http://lists.cs.uiuc.edu/pipermail/cfe-dev/2015-March/041798.html). In particular, he calls out that the syntax you show means "all pointer arguments cannot be null" and how confusing it is. He also mentions that clang has recently "started considering violations of the __attribute__((nonnull)) contract to be undefined behavior" which explains why you're just seeing this now. – Jesse Rusak Apr 18 '15 at 19:50
  • @JesseRusak Yes, I think you are right. I'm just discovering that the syntax includes the parameter numbers. So, in this case it should have been defined as __attribute__((nonnull (3) )). – aepryus Apr 18 '15 at 19:53
0

Check for [NSNull null]

The NSNull class defines a singleton object you use to represent null values in situations where nil is prohibited as a value (typically in a collection object such as an array or a dictionary).

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/NumbersandValues/Articles/Null.html

JDM
  • 883
  • 1
  • 8
  • 20
  • NSNull is my 'f' check above. In both versions it returns NO. – aepryus Apr 18 '15 at 18:11
  • Sorry missed that... Try using == , according to apple it makes a difference: "The NSNull instance is semantically equivalent to nil, however it is also important to appreciate that it is not equal to nil. To test for a null object value, you must therefore make a direct object comparison." – JDM Apr 18 '15 at 18:15
  • Gives a compile time warning: Comparison of distinct pointer types ('NSString *' and 'NSNull *') – aepryus Apr 18 '15 at 18:17
  • Ok, I cast the string to id, but it still returned NO. – aepryus Apr 18 '15 at 18:19
  • What kind of info do you get if you put a breakpoint at the log statement, if you print the variable? I actually just realized it can't be ]NSNull null] because that would not give you issues when inserting into a dictionary. – JDM Apr 18 '15 at 18:25
  • I can't reproduce the problem for the debug build. :-( – aepryus Apr 18 '15 at 18:27
0

The result looks bizarre. There's a nice article; google for "What every programmer should know about undefined behaviour" by Chris Lattner (chief developer of Swift, so he should know what he is talking about).

It looks that after the very first NSLog statement the optimising compiler decided that appVersion cannot possibly be nil, because passing nil to NSLog would be undefined behaviour. That explains why a to e are not printed.

"h" is printed, because appVersion is a 64 bit pointer, int is only 32 bit, so converting a non-nil appVersion to int just might have a result of zero. Optimiser can't remove that check even if it is sure that appVersion is not nil.

And because the compiler is sure that appVersion is not nil, the last test isn't done, appVersion is stored into param, and because it is nil, you crash.

gnasher729
  • 51,477
  • 5
  • 75
  • 98