Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCNConfigRealtime crash "CFString cannot be created from a negative number of bytes" #14094

Open
pyrtsa opened this issue Nov 12, 2024 · 11 comments

Comments

@pyrtsa
Copy link

pyrtsa commented Nov 12, 2024

Description

Expected to happen

Calling RemoteConfig.addOnConfigUpdateListener(remoteConfigUpdateCompletion:) should set up a listener for realtime config updates without crashing the app, regardless of network conditions.

Actual outcome

💥 We see intermittent crashes in Crashlytics where -[RCNConfigRealtime URLSession:dataTask:didReceiveData:] attempted to construct a substring from an opening brace { through to a closing one }, expecting that they unconditionally occur in that order in the piece of NSData returned by NSURLSession, but they didn't.

The precise crash is an uncaught Obj-C exception CFString cannot be created from a negative number of bytes thrown from the call to -[NSString substringWithRange:] at RCNConfigRealtime.m:583. The crash would be avoided by adding the necessary requirement that beginRange.location < endRange.location before continuing the logic.

But please note that -[NSURLSessionDataDelegate URLSession:dataTask:didReceiveData:] provides no guarantees as to where the NSData input is being cut: it could be an arbitrary slice of the full response body, and the SDK should probably include some buffering to pick up JSON objects divided in two or more such slices.

Also the previous check for if ([strData containsString:kServerForbiddenStatusCode]) { ... } is probably not working in all cases when the matched pattern kServerForbiddenStatusCode of the bytes "code": 403 could be similarly split.

Reproducing the issue

Precise steps to reproduce unknown, as the reproduction of this crash requires a particular kind of response from the Remote Config server and a particular buffering of the response body by NSURLSession.

Firebase SDK Version

11.4.0

Xcode Version

16.0

Installation Method

Swift Package Manager

Firebase Product(s)

Analytics, Crashlytics, Remote Config

Targeted Platforms

iOS

Relevant Log Output

EXC_BREAKPOINT 0x00000001911cc560

CFString cannot be created from a negative number of bytes

Crashed: com.apple.main-thread
0  CoreFoundation                 0x1a4560 __CFStringCreateImmutableFunnel3.cold.1 + 16
1  CoreFoundation                 0x7fc4 __CFStringCreateImmutableFunnel3 + 88
2  CoreFoundation                 0x4243c -[__NSCFString substringWithRange:] + 160
3  (redacted)                     0xd046b4 -[RCNConfigRealtime URLSession:dataTask:didReceiveData:] + 583 (RCNConfigRealtime.m:583)
4  CFNetwork                      0xa2c18 CFHostCreateCopy + 2272
5  libdispatch.dylib              0x61c9c _dispatch_call_block_and_release + 24
6  libdispatch.dylib              0x62cc0 _dispatch_client_callout + 16
7  libdispatch.dylib              0x10548 _dispatch_main_queue_drain + 988
8  libdispatch.dylib              0x1015c _dispatch_main_queue_callback_4CF$VARIANT$mp + 36
9  CoreFoundation                 0x52ff8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
10 CoreFoundation                 0x50440 __CFRunLoopRun + 2084
11 CoreFoundation                 0x4f7c8 CFRunLoopRunSpecific + 572
12 GraphicsServices               0x1814 GSEventRunModal + 160
13 UIKitCore                      0x3d3268 -[UIApplication _run] + 868
14 UIKitCore                      0x47d90c UIApplicationMain + 312
15 SwiftUI                        0x37d56c OUTLINED_FUNCTION_283 + 352484
16 SwiftUI                        0x33652c OUTLINED_FUNCTION_283 + 61604
17 SwiftUI                        0x3407e4 OUTLINED_FUNCTION_283 + 103260
18 (redacted)                     0x8678 main + 4332357240 (redacted)
19 ???                            0x1b25bb228 (Missing)

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
    {
      "identity" : "firebase-ios-sdk",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/firebase-ios-sdk",
      "state" : {
        "revision" : "8328630971a8fdd8072b36bb22bef732eb15e1f0",
        "version" : "11.4.0"
      }
    }

If using CocoaPods, the project's Podfile.lock

No response

@google-oss-bot
Copy link

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@MichaelVerdon
Copy link

Hey there, can you please give an example or snippet of what you mean?

@google-oss-bot
Copy link

Hey @pyrtsa. We need more information to resolve this issue but there hasn't been an update in 5 weekdays. I'm marking the issue as stale and if there are no new updates in the next 5 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

@pyrtsa
Copy link
Author

pyrtsa commented Dec 7, 2024

Hey there, can you please give an example or snippet of what you mean?

I'm sorry @MichaelVerdon, I thought I shared all the pertinent information in the original message already.

But allow me to better illustrate the problem with a code snippet. When RCNConfigRealtime sets itself up to listen to config updates, it appears to make a long-polling HTTP request to the backend using plain old NSURLSessionDataDelegate methods to handle the incoming data. Whenever beginRealtimeStream performs the following calls:

strongSelf->_dataTask = [strongSelf->_session dataTaskWithRequest:strongSelf->_request];
[strongSelf->_dataTask resume];

as the NSURLSession instance _session starts receiving data it calls back the delegate method -[NSURLSessionDataDelegate URLSession:dataTask:didReceiveData:] with one or more successive byte sequences of the HTTP body data which all concatenated form the complete HTTP response body.

The way RCNConfigRealtime currently implements the delegate callback method, however, is not tolerant to the arbitrary partition of HTTP body parts: this is what this whole issue is about. The issue still exists in the latest Firebase release 11.6.0; please refer to the full source code of the method implementation at this link.

Issue 1. The first problematic part is the construction of a UTF-8 decoded NSString value of the body part in:

NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

In case the HTTP body contains any multibyte UTF-8 code unit sequences, it is possible that the incoming NSData data could end (or start1) inside such a code unit sequence and what remains within data would be a broken UTF-8 string. This is all due to data not, in general, being the complete HTTP response body but just an arbitrary slice of it.

Issue 2. Next, the delegate method makes an apparent attempt at error handling by trying to find the character sequence "code": 403 i.e. kServerForbiddenStatusCode in it:

if ([strData containsString:kServerForbiddenStatusCode]) {
  // ...
  [self propagateErrors:error];
  return;
}

This logic is problematic for the same reasons as before: the NSURLSession API in no way guarantees that the characters (UTF-8 bytes) of "code": 403 come all in the same slice of data.

Issue 3. This is what really caught us in form of a crash report, however infrequent. After the above error handling, the delegate method continues to search for what seems like the {/} delimiters of a JSON object:

  NSRange endRange = [strData rangeOfString:@"}"];
  NSRange beginRange = [strData rangeOfString:@"{"];
  if (beginRange.location != NSNotFound && endRange.location != NSNotFound) {
    // ...
    NSRange msgRange =
        NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1);
    strData = [strData substringWithRange:msgRange];  // 💥 crash due to invalid `msgRange`
    data = [strData dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data
                                                             options:NSJSONReadingMutableContainers
                                                               error:&dataError];

    [self evaluateStreamResponse:response error:dataError];
  }

The problem in this logic is that, again, it is not guaranteed by NSURLSession that the slice of data contains each JSON object returned by the backend fully. In rare cases where the crash happens, there has been a slice of data missing its closing }, after which the subsequent method call received data containing something like },{"whatever":"..."}.

There are two problematic parts in issue 3:

  • Firstly, it's because the logic requires both { and } to be found in the same slice of HTTP response body data, is is possible for some response JSON objects to go missing when only either is found in the slice. That could imply some client apps missing out remote config updates.
  • Secondly, when the logic meets a partition of data such as },{"whatev, then both delimiters are found but endRange.location is less than beginRange.location and thus msgRange is a negative range which crashes the call to -[NSData substringWithRange:]. 💥

Now, the simplest way to fix these issues is to add buffering to the delegate method implementation: instead of looking for patters within data, also keep as an instance variable of RCNConfigRealtime those data bytes of the previous -[NSURLSessionDataDelegate URLSession:dataTask:didReceiveData:] call which weren't fully consumed yet.

It might be clearest to do this by not converting the bytes to NSString at all but instead performing all logic with NSData values.

Footnotes

  1. In the successive data received in a subsequent call to -[NSURLSessionDataDelegate URLSession:dataTask:didReceiveData:].

@parvez-keeptruckin
Copy link

+1
We are also facing the same issue. is there any update on the fix availability?

@oko17
Copy link

oko17 commented Jan 2, 2025

+1 same issue

@MichaelVerdon
Copy link

Thanks alot for the additional information @pyrtsa. I have been unable to reproduce the issue but obtained different issues but I will keep trying, can I see a snippet of how you implemented the ConfigUpdateListener or maybe even a MCVE?

@paulb777
Copy link
Member

paulb777 commented Jan 9, 2025

@MichaelVerdon Based on the description above, is it possible to write a unit test that returns the NSURLSession in different chunks to reproduce the problem with the assumption of matching braces in the code?

@MichaelVerdon
Copy link

I will give this a shot @paulb777 thanks for the suggestion.

@paulb777
Copy link
Member

@MichaelVerdon One possible solution maybe be to make the URLRequest with ?alt=sse to better chunk the responses. We do this in the Vertex SDK here - https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseVertexAI/Sources/GenerateContentRequest.swift#L50

@pyrtsa
Copy link
Author

pyrtsa commented Feb 6, 2025

Just a ping that we keep seeing these crashes in production. @MichaelVerdon Any luck fixing it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants