Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Crashlytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Unreleased
- [fixed] Escape quotes, backslashes, and control characters when writing strings to
Crashlytics JSON records for thread names, queue labels, and binary image paths. (#16772)

# 13.0.0
- [fixed] Safely validate memory reads when executing `DW_OP_deref_size`
operations during DWARF stack unwinding (#16550).
Expand Down
39 changes: 39 additions & 0 deletions Crashlytics/Crashlytics/Helpers/FIRCLSFile.m
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,45 @@ static void FIRCLSFileWriteStringWithSuffix(FIRCLSFile* file,
const char* string,
size_t length,
char suffix) {
// Signal and Mach exception handlers use this path for thread names and queue labels.
// Keep it async-signal-safe: no allocation, Objective-C calls, or locks.
bool needsEscaping = false;
Comment thread
paulb777 marked this conversation as resolved.
for (size_t i = 0; i < length; ++i) {
const unsigned char character = (unsigned char)string[i];
if (character == '"' || character == '\\' || character < 0x20) {
needsEscaping = true;
break;
}
}

if (needsEscaping) {
static const char hexDigits[] = "0123456789abcdef";
size_t segmentStart = 0;
FIRCLSFileWriteToFileDescriptorOrBuffer(file, "\"", 1);
for (size_t i = 0; i < length; ++i) {
const unsigned char character = (unsigned char)string[i];
if (character != '"' && character != '\\' && character >= 0x20) {
continue;
}

FIRCLSFileWriteToFileDescriptorOrBuffer(file, string + segmentStart, i - segmentStart);
if (character == '"' || character == '\\') {
const char escapedCharacter[] = {'\\', (char)character};
FIRCLSFileWriteToFileDescriptorOrBuffer(file, escapedCharacter, sizeof(escapedCharacter));
} else {
const char escapedControl[] = {
'\\', 'u', '0', '0', hexDigits[character >> 4], hexDigits[character & 0x0f]};
FIRCLSFileWriteToFileDescriptorOrBuffer(file, escapedControl, sizeof(escapedControl));
}
segmentStart = i + 1;
}
FIRCLSFileWriteToFileDescriptorOrBuffer(file, string + segmentStart, length - segmentStart);
Comment thread
paulb777 marked this conversation as resolved.

char closingString[2] = {'"', suffix};
FIRCLSFileWriteToFileDescriptorOrBuffer(file, closingString, suffix == 0 ? 1 : 2);
return;
}

// 2 for quotes, 1 for suffix (if present) and 1 more for null character
const size_t maxStringSize = FIRCLSStringBufferLength - (suffix == 0 ? 3 : 4);

Expand Down
71 changes: 71 additions & 0 deletions Crashlytics/UnitTests/FIRCLSFileTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,77 @@ - (void)emptyCollectionFollowedByEntryWithFile:(FIRCLSFile *)file

#pragma mark -

- (void)testEscapesJSONStrings {
Comment thread
paulb777 marked this conversation as resolved.
[self jsonEscapingWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO];
[self jsonEscapingWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES];
}

- (void)testEscapesArrayStringsAcrossBufferFlush {
[self arrayStringEscapingWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO];
[self arrayStringEscapingWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES];
}

- (void)arrayStringEscapingWithFile:(FIRCLSFile *)file
filePath:(NSString *)filePath
buffered:(BOOL)buffered {
char value[1537];
const char pattern[] = {'t', '"', '\\', '\n', '\t', 1};
for (size_t i = 0; i < sizeof(value) - 1; ++i) {
value[i] = pattern[i % sizeof(pattern)];
}
value[sizeof(value) - 1] = 0;
NSString *expected = [NSString stringWithUTF8String:value];

FIRCLSFileWriteSectionStart(file, "thread_names");
FIRCLSFileWriteArrayStart(file);
FIRCLSFileWriteArrayEntryString(file, value);
FIRCLSFileWriteArrayEntryString(file, "after flush");
FIRCLSFileWriteArrayEnd(file);
FIRCLSFileWriteSectionEnd(file);
if (buffered) {
XCTAssertGreaterThan([NSData dataWithContentsOfFile:filePath].length, (NSUInteger)0,
@"The long escaped string must flush the 1000-byte buffer");
FIRCLSFileFlushWriteBuffer(file);
}
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:filePath];
XCTAssertNotNil(data);
if (!data) {
return;
}
NSDictionary *root = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
XCTAssertNotNil(root, @"Escaped array JSON should parse, got error %@", error);
XCTAssertEqualObjects(root[@"thread_names"], (@[ expected, @"after flush" ]));
}

- (void)jsonEscapingWithFile:(FIRCLSFile *)file
filePath:(NSString *)filePath
buffered:(BOOL)buffered {
NSString *key = @"key\"\n";
NSString *value =
[NSString stringWithFormat:@"quote\" slash\\ newline\n tab\t control %C", (unichar)1];

FIRCLSFileWriteSectionStart(file, "string_escaping");
FIRCLSFileWriteHashStart(file);
FIRCLSFileWriteHashEntryNSString(file, [key UTF8String], value);
FIRCLSFileWriteHashEnd(file);
FIRCLSFileWriteSectionEnd(file);

if (buffered) {
FIRCLSFileFlushWriteBuffer(file);
}
NSData *data = [NSData dataWithContentsOfFile:filePath];
XCTAssertNotNil(data);
if (data == nil) {
return;
}
NSError *error;
NSDictionary *root = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
XCTAssertNotNil(root, @"Escaped JSON should parse, got error %@", error);
NSDictionary *section = root[@"string_escaping"];
XCTAssertEqualObjects(section[key], value);
}

- (void)testHexEncodingString {
[self hexEncodingStringWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO];
[self hexEncodingStringWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES];
Expand Down
Loading