/ OSX / sec / ipc / SecdWatchdog.m
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:&currentRusage]) {
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