diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d7e89598c..83ec37ad1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Fix JSON encoding of infinite numeric values in crash reports (#7802) + ## 9.11.0 ### Features diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 6d9bff4cf95..83dbda06e16 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -317,34 +317,57 @@ sentrycrashjson_addFloatingPointElement( if (isnan(value)) { return sentrycrashjson_addNullElement(context, name); } + if (isinf(value)) { + int result = sentrycrashjson_beginElement(context, name); + unlikely_if(result != SentryCrashJSON_OK) { return result; } + return value > 0 ? addJSONData(context, "1e999", 5) : addJSONData(context, "-1e999", 6); + } + // Conservative buffer for double formatting. + char buff[64]; + int written = snprintf(buff, sizeof(buff), "%lg", value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[50]; - snprintf(buff, sizeof(buff), "%lg", value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int sentrycrashjson_addIntegerElement( SentryCrashJSONEncodeContext *const context, const char *const name, int64_t value) { + // Exact max for int64_t in decimal: 19 digits plus sign and NUL. + char buff[21]; + int written = snprintf(buff, sizeof(buff), "%" PRId64, value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[30]; - snprintf(buff, sizeof(buff), "%" PRId64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int sentrycrashjson_addUIntegerElement( SentryCrashJSONEncodeContext *const context, const char *const name, uint64_t value) { + // Exact max for uint64_t in decimal: 20 digits plus NUL. + char buff[21]; + int written = snprintf(buff, sizeof(buff), "%" PRIu64, value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[30]; - snprintf(buff, sizeof(buff), "%" PRIu64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int diff --git a/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m index a1fadfe1d49..c6ac503b579 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m @@ -26,7 +26,9 @@ // #import +#import #import +#import #import "FileBasedTestCase.h" #import "SentryCrashJSONCodec.h" @@ -884,6 +886,84 @@ - (void)testSerializeDeserializeNANDouble XCTAssertTrue([[result objectAtIndex:0] isKindOfClass:[NSNull class]]); } +- (void)testSerializeDeserializePositiveInfinityFloat +{ + NSError *error = (NSError *)self; + NSString *expected = @"[1e999]"; + float infValue = INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithFloat:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); +} + +- (void)testSerializeDeserializeNegativeInfinityFloat +{ + NSError *error = (NSError *)self; + NSString *expected = @"[-1e999]"; + float infValue = -INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithFloat:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); + XCTAssertTrue([[result objectAtIndex:0] doubleValue] < 0, @"Should be negative infinity"); +} + +- (void)testSerializeDeserializePositiveInfinityDouble +{ + NSError *error = (NSError *)self; + NSString *expected = @"[1e999]"; + double infValue = (double)INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithDouble:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); +} + +- (void)testSerializeDeserializeNegativeInfinityDouble +{ + NSError *error = (NSError *)self; + NSString *expected = @"[-1e999]"; + double infValue = -(double)INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithDouble:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); + XCTAssertTrue([[result objectAtIndex:0] doubleValue] < 0, @"Should be negative infinity"); +} + - (void)testSerializeDeserializeChar { NSError *error = (NSError *)self; @@ -1562,10 +1642,9 @@ - (void)expectData:(NSData *)data encodesObject:(id)expectedObject XCTAssertEqualObjects(object, expectedObject); } -- (id)decodeJSON:(const char *)jsonBytes +- (id)decodeJSONData:(NSData *)jsonData { NSError *error = nil; - NSData *jsonData = [NSData dataWithBytes:jsonBytes length:strlen(jsonBytes)]; id object = [SentryCrashJSONCodec decode:jsonData options:SentryCrashJSONDecodeOptionKeepPartialObject error:&error]; @@ -1574,9 +1653,15 @@ - (id)decodeJSON:(const char *)jsonBytes return object; } -- (void)expectEquivalentJSON:(const char *)jsonCompareBytes toJSON:(const char *)jsonExpectedBytes +- (id)decodeJSON:(const char *)jsonBytes +{ + NSData *jsonData = [NSData dataWithBytes:jsonBytes length:strlen(jsonBytes)]; + return [self decodeJSONData:jsonData]; +} + +- (void)expectEquivalentJSONData:(NSData *)jsonCompareData toJSON:(const char *)jsonExpectedBytes { - id objectCompare = [self decodeJSON:jsonCompareBytes]; + id objectCompare = [self decodeJSONData:jsonCompareData]; id objectExpect = [self decodeJSON:jsonExpectedBytes]; XCTAssertEqualObjects(objectCompare, objectExpect); } @@ -1684,9 +1769,8 @@ - (void)testDontCloseLastContainer sentrycrashjson_addStringElement(&context, "testing", "this", SentryCrashJSON_SIZE_AUTOMATIC); sentrycrashjson_endContainer(&context); sentrycrashjson_endEncode(&context); - [encodedData appendBytes:"\0" length:1]; - [self expectEquivalentJSON:encodedData.bytes toJSON:expectedJson]; + [self expectEquivalentJSONData:encodedData toJSON:expectedJson]; } - (NSArray *)decode:(NSString *)jsonString @@ -1711,9 +1795,60 @@ - (void)testFastUIntEncode sentrycrashjson_addUIntegerElement(&context, "uint", 1234567890); sentrycrashjson_endContainer(&context); sentrycrashjson_endEncode(&context); - [encodedData appendBytes:"\0" length:1]; - [self expectEquivalentJSON:encodedData.bytes toJSON:expectedJson]; + [self expectEquivalentJSONData:encodedData toJSON:expectedJson]; +} + +- (void)testFastDoubleEncode_DoubleLimits +{ + const char *expectedJson = "{\"min\":-1.79769e+308,\"max\":1.79769e+308}"; + + NSMutableData *encodedData = [NSMutableData data]; + SentryCrashJSONEncodeContext context = { 0 }; + sentrycrashjson_beginEncode(&context, false, addJSONData, (__bridge void *)(encodedData)); + sentrycrashjson_beginObject(&context, NULL); + XCTAssertEqual( + sentrycrashjson_addFloatingPointElement(&context, "min", -DBL_MAX), SentryCrashJSON_OK); + XCTAssertEqual( + sentrycrashjson_addFloatingPointElement(&context, "max", DBL_MAX), SentryCrashJSON_OK); + sentrycrashjson_endContainer(&context); + sentrycrashjson_endEncode(&context); + + [self expectEquivalentJSONData:encodedData toJSON:expectedJson]; +} + +- (void)testFastIntEncode_Int64Limits +{ + const char *expectedJson = "{\"min\":-9223372036854775808,\"max\":9223372036854775807}"; + + NSMutableData *encodedData = [NSMutableData data]; + SentryCrashJSONEncodeContext context = { 0 }; + sentrycrashjson_beginEncode(&context, false, addJSONData, (__bridge void *)(encodedData)); + sentrycrashjson_beginObject(&context, NULL); + XCTAssertEqual( + sentrycrashjson_addIntegerElement(&context, "min", INT64_MIN), SentryCrashJSON_OK); + XCTAssertEqual( + sentrycrashjson_addIntegerElement(&context, "max", INT64_MAX), SentryCrashJSON_OK); + sentrycrashjson_endContainer(&context); + sentrycrashjson_endEncode(&context); + + [self expectEquivalentJSONData:encodedData toJSON:expectedJson]; +} + +- (void)testFastUIntEncode_UInt64Max +{ + const char *expectedJson = "{\"uint\":18446744073709551615}"; + + NSMutableData *encodedData = [NSMutableData data]; + SentryCrashJSONEncodeContext context = { 0 }; + sentrycrashjson_beginEncode(&context, false, addJSONData, (__bridge void *)(encodedData)); + sentrycrashjson_beginObject(&context, NULL); + XCTAssertEqual( + sentrycrashjson_addUIntegerElement(&context, "uint", UINT64_MAX), SentryCrashJSON_OK); + sentrycrashjson_endContainer(&context); + sentrycrashjson_endEncode(&context); + + [self expectEquivalentJSONData:encodedData toJSON:expectedJson]; } @end