SecdWatchdog.m
1 /* 2 * Copyright (c) 2017 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24 #import "ipc/SecdWatchdog.h" 25 #include "utilities/debugging.h" 26 #include <xpc/private.h> 27 #import <xpc/private.h> 28 #import <libproc.h> 29 #import <os/log.h> 30 #import <mach/mach_time.h> 31 #import <mach/message.h> 32 #import <os/assumes.h> 33 34 #if !TARGET_OS_MAC 35 #import <CrashReporterSupport/CrashReporterSupport.h> 36 #endif 37 38 #define CPU_RUNTIME_SECONDS_BEFORE_WATCHDOG (60 * 20) 39 #define WATCHDOG_RESET_PERIOD (60 * 60 * 24) 40 #define WATCHDOG_CHECK_PERIOD (60 * 60) 41 #define WATCHDOG_CHECK_PERIOD_LEEWAY (60 * 10) 42 #define WATCHDOG_GRACEFUL_EXIT_LEEWAY (60 * 5) 43 #define WATCHDOG_DISKUSAGE_LIMIT (1000 * 1024 * 1024) // (1GiBi) 44 45 NSString* const SecdWatchdogAllowedRuntime = @"allowed-runtime"; 46 NSString* const SecdWatchdogResetPeriod = @"reset-period"; 47 NSString* const SecdWatchdogCheckPeriod = @"check-period"; 48 NSString* const SecdWatchdogGracefulExitTime = @"graceful-exit-time"; 49 50 void SecdLoadWatchDog() 51 { 52 (void)[SecdWatchdog watchdog]; 53 } 54 55 @implementation SecdWatchdog { 56 uint64_t _rusageBaseline; 57 CFTimeInterval _lastCheckTime; 58 dispatch_source_t _timer; 59 60 uint64_t _runtimeSecondsBeforeWatchdog; 61 long _resetPeriod; 62 long _checkPeriod; 63 long _checkPeriodLeeway; 64 long _gracefulExitLeeway; 65 uint64_t _diskUsageBaseLine; 66 uint64_t _diskUsageLimit; 67 68 bool _diskUsageHigh; 69 } 70 71 @synthesize diskUsageHigh = _diskUsageHigh; 72 73 + (instancetype)watchdog 74 { 75 static SecdWatchdog* watchdog = nil; 76 static dispatch_once_t onceToken; 77 dispatch_once(&onceToken, ^{ 78 watchdog = [[self alloc] init]; 79 }); 80 81 return watchdog; 82 } 83 84 - (instancetype)init 85 { 86 if (self = [super init]) { 87 _runtimeSecondsBeforeWatchdog = CPU_RUNTIME_SECONDS_BEFORE_WATCHDOG; 88 _resetPeriod = WATCHDOG_RESET_PERIOD; 89 _checkPeriod = WATCHDOG_CHECK_PERIOD; 90 _checkPeriodLeeway = WATCHDOG_CHECK_PERIOD_LEEWAY; 91 _gracefulExitLeeway = WATCHDOG_GRACEFUL_EXIT_LEEWAY; 92 _diskUsageLimit = WATCHDOG_DISKUSAGE_LIMIT; 93 _diskUsageHigh = false; 94 95 [self activateTimer]; 96 } 97 98 return self; 99 } 100 101 - (uint64_t)secondsFromMachTime:(uint64_t)machTime 102 { 103 static dispatch_once_t once; 104 static uint64_t ratio; 105 dispatch_once(&once, ^{ 106 mach_timebase_info_data_t tbi; 107 if (os_assumes_zero(mach_timebase_info(&tbi)) == KERN_SUCCESS) { 108 ratio = tbi.numer / tbi.denom; 109 } else { 110 ratio = 1; 111 } 112 }); 113 114 return (machTime * ratio)/NSEC_PER_SEC; 115 } 116 117 + (bool)watchdogrusage:(rusage_info_current *)rusage 118 { 119 if (proc_pid_rusage(getpid(), RUSAGE_INFO_CURRENT, (rusage_info_t *)rusage) != 0) { 120 return false; 121 } 122 return true; 123 } 124 125 + (bool)triggerOSFaults 126 { 127 return true; 128 } 129 130 - (void)runWatchdog 131 { 132 rusage_info_current currentRusage; 133 134 if (![[self class] watchdogrusage:¤tRusage]) { 135 return; 136 } 137 138 @synchronized (self) { 139 uint64_t spentUserTime = [self secondsFromMachTime:currentRusage.ri_user_time]; 140 if (spentUserTime > _rusageBaseline + _runtimeSecondsBeforeWatchdog) { 141 seccritical("SecWatchdog: watchdog has detected securityd/secd is using too much CPU - attempting to exit gracefully"); 142 #if !TARGET_OS_MAC 143 WriteStackshotReport(@"securityd watchdog triggered", __sec_exception_code_Watchdog); 144 #endif 145 xpc_transaction_exit_clean(); // we've used too much CPU - try to exit gracefully 146 147 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_gracefulExitLeeway * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 148 // if we still haven't exited gracefully after 5 minutes, time to die unceremoniously 149 seccritical("SecWatchdog: watchdog has failed to exit securityd/secd gracefully - exiting ungracefully"); 150 exit(EXIT_FAILURE); 151 }); 152 153 return; 154 } 155 156 if (_diskUsageHigh == false && 157 (currentRusage.ri_logical_writes > _diskUsageBaseLine + _diskUsageLimit)) 158 { 159 if ([[self class] triggerOSFaults]) { 160 os_log_fault(OS_LOG_DEFAULT, "securityd have written more then %llu", 161 (unsigned long long)_diskUsageLimit); 162 } 163 _diskUsageHigh = true; 164 } 165 166 CFTimeInterval currentTime = CFAbsoluteTimeGetCurrent(); 167 if (currentTime > _lastCheckTime + _resetPeriod) { 168 // we made it through a 24 hour period - reset our timeout to 24 hours from now, and our cpu usage threshold to another 20 minutes 169 secinfo("SecWatchdog", "resetting watchdog monitoring interval ahead another 24 hours"); 170 _lastCheckTime = currentTime; 171 _rusageBaseline = spentUserTime; 172 _diskUsageHigh = false; 173 _diskUsageBaseLine = currentRusage.ri_logical_writes; 174 } 175 } 176 177 } 178 179 - (void)activateTimer 180 { 181 @synchronized (self) { 182 183 rusage_info_current initialRusage; 184 [[self class] watchdogrusage:&initialRusage]; 185 186 _rusageBaseline = [self secondsFromMachTime:initialRusage.ri_user_time]; 187 _lastCheckTime = CFAbsoluteTimeGetCurrent(); 188 189 __weak __typeof(self) weakSelf = self; 190 _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); 191 dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, _checkPeriod * NSEC_PER_SEC, _checkPeriodLeeway * NSEC_PER_SEC); // run once every hour, but give the timer lots of leeway to be power friendly 192 dispatch_source_set_event_handler(_timer, ^{ 193 __strong __typeof(self) strongSelf = weakSelf; 194 if (!strongSelf) { 195 return; 196 } 197 [strongSelf runWatchdog]; 198 }); 199 dispatch_resume(_timer); 200 } 201 } 202 203 - (NSDictionary*)watchdogParameters 204 { 205 @synchronized (self) { 206 return @{ SecdWatchdogAllowedRuntime : @(_runtimeSecondsBeforeWatchdog), 207 SecdWatchdogResetPeriod : @(_resetPeriod), 208 SecdWatchdogCheckPeriod : @(_checkPeriod), 209 SecdWatchdogGracefulExitTime : @(_gracefulExitLeeway) }; 210 } 211 } 212 213 - (BOOL)setWatchdogParameters:(NSDictionary*)parameters error:(NSError**)error 214 { 215 NSMutableArray* failedParameters = [NSMutableArray array]; 216 @synchronized (self) { 217 __weak __typeof(self) weakSelf = self; 218 [parameters enumerateKeysAndObjectsUsingBlock:^(NSString* parameter, NSNumber* value, BOOL* stop) { 219 __strong __typeof(self) strongSelf = weakSelf; 220 if (!strongSelf) { 221 return; 222 } 223 224 if ([parameter isEqualToString:SecdWatchdogAllowedRuntime] && [value isKindOfClass:[NSNumber class]]) { 225 strongSelf->_runtimeSecondsBeforeWatchdog = value.longValue; 226 } 227 else if ([parameter isEqualToString:SecdWatchdogResetPeriod] && [value isKindOfClass:[NSNumber class]]) { 228 strongSelf->_resetPeriod = value.longValue; 229 } 230 else if ([parameter isEqualToString:SecdWatchdogCheckPeriod] && [value isKindOfClass:[NSNumber class]]) { 231 strongSelf->_checkPeriod = value.longValue; 232 } 233 else if ([parameter isEqualToString:SecdWatchdogGracefulExitTime] && [value isKindOfClass:[NSNumber class]]) { 234 strongSelf->_gracefulExitLeeway = value.longValue; 235 } 236 else { 237 [failedParameters addObject:parameter]; 238 } 239 }]; 240 241 dispatch_source_cancel(_timer); 242 _timer = NULL; 243 } 244 245 [self activateTimer]; 246 247 if (failedParameters.count > 0) { 248 if (error) { 249 *error = [NSError errorWithDomain:@"com.apple.securityd.watchdog" code:0 userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"failed to set parameters: %@", failedParameters]}]; 250 } 251 252 return NO; 253 } 254 else { 255 return YES; 256 } 257 } 258 259 @end