Skip to content

Commit 6b695fb

Browse files
authored
Fix memory monitor safety improvements (#725)
* Fix memory safety issues in memory monitor * Add null safety checks and file protection to memory monitor * Add tests for mmap/munmap and memory monitor enable/disable cycles * Update KSCrashMonitor_Memory.m
1 parent d1c8500 commit 6b695fb

File tree

5 files changed

+160
-13
lines changed

5 files changed

+160
-13
lines changed

Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// KSCrashMonitor_Memory.h
2+
// KSCrashMonitor_Memory.m
33
//
44
// Created by Alexander Cohen on 2024-05-20.
55
//
@@ -54,8 +54,8 @@
5454

5555
static const int32_t KSCrash_Memory_Magic = 'kscm';
5656

57-
static const uint8_t KSCrash_Memory_Version_1 = 1;
58-
const uint8_t KSCrash_Memory_CurrentVersion = KSCrash_Memory_Version_1;
57+
const uint8_t KSCrash_Memory_Version_1_0 = 1;
58+
const uint8_t KSCrash_Memory_CurrentVersion = KSCrash_Memory_Version_1_0;
5959

6060
const uint8_t KSCrash_Memory_NonFatalReportLevelNone = KSCrashAppMemoryStateTerminal + 1;
6161

@@ -77,6 +77,8 @@
7777
static void notifyPostSystemEnable(void);
7878
static void ksmemory_read(const char *path);
7979
static void ksmemory_map(const char *path);
80+
static void ksmemory_unmap(void);
81+
static void ksmemory_applyNoFileProtection(NSString *path);
8082

8183
// ============================================================================
8284
#pragma mark - Globals -
@@ -140,12 +142,21 @@ static void _ks_memory_update(void (^block)(KSCrash_Memory *mem))
140142
static void _ks_memory_set(KSCrash_Memory *mem)
141143
{
142144
os_unfair_lock_lock(&g_memoryLock);
145+
void *old = g_memory;
143146
g_memory = mem;
144147
os_unfair_lock_unlock(&g_memoryLock);
148+
149+
if (old) {
150+
ksfu_munmap(old, sizeof(KSCrash_Memory));
151+
}
145152
}
146153

147154
static void _ks_memory_update_from_app_memory(KSCrashAppMemory *const memory)
148155
{
156+
if (!memory) {
157+
return;
158+
}
159+
149160
_ks_memory_update(^(KSCrash_Memory *mem) {
150161
*mem = (KSCrash_Memory) {
151162
.magic = KSCrash_Memory_Magic,
@@ -268,9 +279,10 @@ static void setEnabled(bool isEnabled)
268279
}];
269280

270281
} else {
271-
g_memoryTracker = nil;
272282
[KSCrashAppStateTracker.sharedInstance removeObserver:g_appStateObserver];
273283
g_appStateObserver = nil;
284+
g_memoryTracker = nil;
285+
ksmemory_unmap();
274286
}
275287
}
276288

@@ -291,14 +303,16 @@ static void addContextualInfoToEvent(KSCrash_MonitorContext *eventContext)
291303
// since we're in a signal or something that can only
292304
// use async safe functions, we can't lock.
293305
// It's "ok" though, since no other threads should be running.
294-
g_memory->fatal = eventContext->requirements.isFatal;
306+
if (g_memory) {
307+
g_memory->fatal = eventContext->requirements.isFatal;
308+
}
295309
} else {
296310
_ks_memory_update(^(KSCrash_Memory *mem) {
297311
mem->fatal = eventContext->requirements.isFatal;
298312
});
299313
}
300314

301-
if (g_isEnabled) {
315+
if (g_isEnabled && g_memory) {
302316
// same as above re: not locking when _asyncSafeOnly_ is set.
303317
KSCrash_Memory memCopy = asyncSafeOnly ? *g_memory : _ks_memory_copy();
304318
eventContext->AppMemory.footprint = memCopy.footprint;
@@ -337,7 +351,7 @@ static void kscm_memory_check_for_oom_in_previous_session(void)
337351
// a programming error and receiving a Mach event or signal that
338352
// indicates a crash, we should process that on startup and ignore
339353
// and indication of an OOM.
340-
bool userPerceivedOOM = NO;
354+
bool userPerceivedOOM = false;
341355
if (g_FatalReportsEnabled && ksmemory_previous_session_was_terminated_due_to_memory(&userPerceivedOOM)) {
342356
// We only report an OOM that the user might have seen.
343357
// Ignore this check if we want to report all OOM, foreground and background.
@@ -459,10 +473,10 @@ static void ksmemory_read(const char *path)
459473

460474
// check the timestamp, let's say it's valid for the last week
461475
// do we really want crash reports older than a week anyway??
462-
const uint64_t kUS_in_day = 8.64e+10;
476+
const uint64_t kUS_in_day = 86400000000ULL; // 24 * 60 * 60 * 1000000
463477
const uint64_t kUS_in_week = kUS_in_day * 7;
464478
uint64_t now = ksdate_microseconds();
465-
if (memory.timestamp <= 0 || memory.timestamp == INT64_MAX || memory.timestamp < now - kUS_in_week) {
479+
if (memory.timestamp == 0 || memory.timestamp > now || memory.timestamp < now - kUS_in_week) {
466480
return;
467481
}
468482

@@ -516,9 +530,18 @@ static void ksmemory_map(const char *path)
516530
}
517531

518532
_ks_memory_set(ptr);
519-
_ks_memory_update_from_app_memory(g_memoryTracker.memory);
533+
534+
KSCrashAppMemory *currentMemory = g_memoryTracker.memory;
535+
if (currentMemory) {
536+
_ks_memory_update_from_app_memory(currentMemory);
537+
}
520538
}
521539

540+
/**
541+
Unmaps the memory-mapped file and clears the global pointer.
542+
*/
543+
static void ksmemory_unmap(void) { _ks_memory_set(NULL); }
544+
522545
/**
523546
What we're doing here is writing a file out that can be reused
524547
on restart if the data shows us there was a memory issue.
@@ -562,12 +585,27 @@ static void ksmemory_write_possible_oom(void)
562585
g_callbacks.handle(ctx);
563586
}
564587

588+
static void ksmemory_applyNoFileProtection(NSString *path)
589+
{
590+
if (!path) {
591+
return;
592+
}
593+
594+
NSDictionary *attrs = @ { NSFileProtectionKey : NSFileProtectionNone };
595+
[[NSFileManager defaultManager] setAttributes:attrs ofItemAtPath:path error:nil];
596+
}
597+
565598
void ksmemory_initialize(const char *dataPath)
566599
{
567-
g_hasPostEnable = 0;
600+
g_hasPostEnable = false;
568601
g_dataURL = [NSURL fileURLWithPath:@(dataPath)];
569602
g_memoryURL = [g_dataURL URLByAppendingPathComponent:@"memory.bin"];
570603

604+
// Ensure files we touch stay readable while the app is locked.
605+
ksmemory_applyNoFileProtection(g_dataURL.path);
606+
ksmemory_applyNoFileProtection(g_memoryURL.path);
607+
ksmemory_applyNoFileProtection(kscm_memory_oom_breadcrumb_URL().path);
608+
571609
// load up the old memory data
572610
ksmemory_read(g_memoryURL.path.UTF8String);
573611
}
@@ -579,7 +617,7 @@ bool ksmemory_previous_session_was_terminated_due_to_memory(bool *userPerceptibl
579617
// exception/event occured that terminated/crashed the app. We don't want to report
580618
// that as an OOM.
581619
if (g_previousSessionMemory.fatal) {
582-
return NO;
620+
return false;
583621
}
584622

585623
// We might care if the user might have seen the OOM

Sources/KSCrashRecordingCore/KSFileUtils.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,22 @@ void *ksfu_mmap(const char *path, size_t size)
583583
// This comes before close which is ok since it'll happen
584584
// when all fd's are closed.
585585
unlink(path);
586+
close(fd);
587+
return NULL;
586588
}
587589

588590
close(fd);
589591
return ptr;
590592
}
593+
594+
bool ksfu_munmap(void *ptr, size_t size)
595+
{
596+
if (ptr == NULL) {
597+
return true;
598+
}
599+
if (munmap(ptr, size) == -1) {
600+
KSLOG_ERROR("Could not munmap: %s", strerror(errno));
601+
return false;
602+
}
603+
return true;
604+
}

Sources/KSCrashRecordingCore/include/KSFileUtils.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,22 @@ bool ksfu_readBufferedReaderUntilChar(KSBufferedReader *reader, int ch, char *ds
274274
* @param size The size of the map.
275275
*
276276
* @return the mapped pointer if successful, NULL otherwise.
277-
* The return value must be unmapped using `munmap` when done
277+
* The return value must be unmapped using `ksfu_munmap` when done
278278
* with the returned pointer. It is ok to let the pointer live up to termination,
279279
* the system will unmap on termination if required.
280280
*/
281281
void *ksfu_mmap(const char *path, size_t size);
282282

283+
/** Unmaps a memory-mapped region previously created by ksfu_mmap.
284+
*
285+
* @param ptr The pointer returned by ksfu_mmap.
286+
*
287+
* @param size The size that was passed to ksfu_mmap.
288+
*
289+
* @return true if successful, false otherwise.
290+
*/
291+
bool ksfu_munmap(void *ptr, size_t size);
292+
283293
#ifdef __cplusplus
284294
}
285295
#endif

Tests/KSCrashRecordingCoreTests/KSFileUtils_Tests.m

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,4 +648,58 @@ - (void)testWriteBuffered_FlushAndLargeWriteOrder
648648
XCTAssertEqualObjects(actualFileContents, expectedContents);
649649
}
650650

651+
#pragma mark - Memory Mapping Tests
652+
653+
- (void)testMunmap_NullPointer
654+
{
655+
// munmap with NULL should return true (no-op)
656+
XCTAssertTrue(ksfu_munmap(NULL, 100));
657+
}
658+
659+
- (void)testMmap_InvalidPath
660+
{
661+
// mmap with non-existent path should return NULL
662+
NSString *invalidPath = @"/nonexistent/path/to/file.bin";
663+
void *ptr = ksfu_mmap(invalidPath.UTF8String, 100);
664+
XCTAssertTrue(ptr == NULL);
665+
}
666+
667+
- (void)testMmap_Munmap_RoundTrip
668+
{
669+
// Test mapping a file, writing to it, and unmapping
670+
NSString *path = [self generateTempFilePath];
671+
size_t size = 1024;
672+
673+
void *ptr = ksfu_mmap(path.UTF8String, size);
674+
XCTAssertTrue(ptr != NULL);
675+
676+
// Write some data to the mapped memory
677+
memset(ptr, 0xAB, size);
678+
679+
// Verify the data was written
680+
uint8_t *bytes = (uint8_t *)ptr;
681+
XCTAssertEqual(bytes[0], 0xAB);
682+
XCTAssertEqual(bytes[size - 1], 0xAB);
683+
684+
// Unmap should succeed
685+
XCTAssertTrue(ksfu_munmap(ptr, size));
686+
}
687+
688+
- (void)testMmap_Munmap_MultipleRoundTrips
689+
{
690+
// Test multiple map/unmap cycles to ensure no leaks or issues
691+
NSString *path = [self generateTempFilePath];
692+
size_t size = 256;
693+
694+
for (int i = 0; i < 5; i++) {
695+
void *ptr = ksfu_mmap(path.UTF8String, size);
696+
XCTAssertTrue(ptr != NULL);
697+
698+
// Write iteration number
699+
memset(ptr, (uint8_t)i, size);
700+
701+
XCTAssertTrue(ksfu_munmap(ptr, size));
702+
}
703+
}
704+
651705
@end

Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,35 @@ - (void)testNonFatalReportLevel
319319
XCTAssertEqual(ksmemory_get_nonfatal_report_level(), KSCrashAppMemoryStateUrgent);
320320
}
321321

322+
- (void)testRapidEnableDisableCycles
323+
{
324+
// Test rapid enable/disable cycles to ensure proper cleanup
325+
// This helps catch issues with memory mapping/unmapping
326+
KSCrashMonitorAPI *api = kscm_memory_getAPI();
327+
328+
for (int i = 0; i < 10; i++) {
329+
api->setEnabled(true);
330+
XCTAssertTrue(api->isEnabled());
331+
api->setEnabled(false);
332+
XCTAssertFalse(api->isEnabled());
333+
}
334+
}
335+
336+
- (void)testEnableDisableWithoutInitialize
337+
{
338+
// Test that enable/disable handles edge cases gracefully
339+
KSCrashMonitorAPI *api = kscm_memory_getAPI();
340+
341+
// Should handle being disabled when already disabled
342+
api->setEnabled(false);
343+
XCTAssertFalse(api->isEnabled());
344+
345+
// Enable then disable
346+
api->setEnabled(true);
347+
XCTAssertTrue(api->isEnabled());
348+
[NSThread sleepForTimeInterval:0.05];
349+
api->setEnabled(false);
350+
XCTAssertFalse(api->isEnabled());
351+
}
352+
322353
@end

0 commit comments

Comments
 (0)