How to Prevent iOS App Crashes from Malicious Text with Crash‑Protected Labels
This article explains a crash‑protection strategy for iOS apps that detects and blocks special characters causing crashes by marking risky strings during layout/rendering, counting crashes, and switching to a safe mode when repeated failures occur.
Introduction
Many iOS apps have crashed when encountering specially crafted text, such as certain Indian‑language characters that trigger iOS 11 crashes. While system updates can fix the underlying vulnerability, most users do not update promptly, so the client must implement its own protection against these malicious strings.
Solution
Because the presence of a problematic character cannot be known in advance, the app first attempts to layout and draw the string while setting a marker flag. After rendering, the flag is cleared; if the flag remains, the string is considered unsafe and will be hidden on subsequent displays.
The approach raises several issues:
During layout/rendering, another thread might crash, leaving the marker uncleared. Therefore, when a crash occurs, the app must verify whether the crashing thread was the layout/rendering thread.
How many crashes are needed to deem a string unsafe? A single crash leads to false positives because iOS occasionally crashes even on benign strings. Requiring two crashes reduces false blocks but may still be insufficient for large volumes of malicious messages. The adopted strategy is to ignore the first crash for a string and block it only after consecutive crashes, effectively handling the string after N+1 crashes.
The core implementation looks like this:
// MessageItemView.mm, CP is short for CrashProtected
@implementation MessageItemView
- (void)initContentLabel {
m_label = [[MMCPLabel alloc] init];
m_label.cpKey = [MMCPUtil generateKeyWithObject:self.messageModel];
if ([MMCPUtil isUnsafeWithKey:m_label.cpKey]) {
// Detected unsafe content, hide it
m_label.text = @"该内容无法显示";
} else {
m_label.text = self.messageModel.content;
}
}
@end // MMCPLabel.mm
@implementation MMCPLabel
@synthesize cpKey = m_cpKey;
// Intercept common layout/rendering methods
- (void)layoutSublayersOfLayer:(CALayer *)layer {
CScopedCrashCounter crashCounter(m_cpKey);
[super layoutSublayersOfLayer:layer];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CScopedCrashCounter crashCounter(m_cpKey);
[super drawLayer:layer inContext:ctx];
}
- (CGSize)sizeThatFits:(CGSize)size {
CScopedCrashCounter crashCounter(m_cpKey);
return [super sizeThatFits:size];
}
@end // MMCPUtil.mm
// C++ helper that increments a crash counter on construction and decrements on destruction
class CScopedCrashCounter {
public:
CScopedCrashCounter(NSString *cpKey) {
m_cpKey = cpKey;
[MMCPUtil increaseCrashCountWithKey:m_cpKey];
}
~CScopedCrashCounter() {
[MMCPUtil decreaseCrashCountWithKey:m_cpKey];
}
private:
NSString *m_cpKey;
};
@implementation MMCPUtil
@synthesize crashKeyMemoryMappedKV = m_crashKeyMemoryMappedKV; // Keys identified as malicious
@synthesize crashCountMemoryMappedKV = m_crashCountMemoryMappedKV; // Crash count per key
- (BOOL)isUnsafeWithKey:(NSString *)key {
return [m_crashKeyMemoryMappedKV getBoolForKey:key] == YES;
}
- (void)increaseCrashCountWithKey:(NSString *)key {
// Record the thread that caused the crash
...
int32_t count = [m_crashCountMemoryMappedKV getInt32ForKey:key];
[m_crashCountMemoryMappedKV setInt32:count + 1 forKey:key];
}
- (void)decreaseCrashCountWithKey:(NSString *)key {
int32_t count = [m_crashCountMemoryMappedKV getInt32ForKey:key];
[m_crashCountMemoryMappedKV setInt32:MAX(0, count - 1) forKey:key];
}
// Crash callback
- (void)onSignalCrash:(siginfo_t *)info {
NSString *key = [self lastCPKey:info->si_pid];
if (key == nil) return;
if (m_isLastTimeCrashedBySpecialCharacter == NO) {
[self setLastTimeCrashedBySpecialCharacter:YES];
if ([m_crashCountMemoryMappedKV getInt32ForKey:key] > 1) {
[m_crashKeyMemoryMappedKV setBool:YES forKey:key];
}
} else {
// Consecutive crashes caused by special characters – block immediately
[m_crashKeyMemoryMappedKV setBool:YES forKey:key];
}
}
@endEven with the N+1 optimization, a large number of malicious messages can still cause many crashes before the client stabilizes. To mitigate this, the app enters a safe‑mode UI after three consecutive crashes, guiding users on how to proceed.
For group chats that frequently crash, a quick‑exit button is provided on the main screen, and a recovery entry lets users restore mistakenly blocked strings.
When a special‑character crash is detected, the client reports the incident to the backend, creating a closed loop that quickly identifies new malicious patterns. This framework can also be extended to block other harmful content such as spam messages, GIFs, short videos, and links.
MemoryMappedKV
Because many parts of the app (nicknames, message bodies, avatars, etc.) need instrumentation without hurting scroll performance, a high‑performance mmap‑based key‑value store was developed. See future WeMobileDev articles for details.
WeChat Client Technology Team
Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
