2 * Copyright (c) 2020 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
24 #import "SecItemRateLimit.h"
25 #import "SecItemRateLimit_tests.h"
27 #import <utilities/debugging.h>
28 #import <utilities/SecInternalReleasePriv.h>
29 #import "ipc/securityd_client.h"
31 #import <sys/codesign.h>
32 #import <os/feature_private.h>
34 // Dressed-down version of RateLimiter which directly computes the rate of one bucket and resets when it runs out of tokens
36 // Broken out so the test-only reset method can reinit this
37 static SecItemRateLimit* ratelimit;
39 @implementation SecItemRateLimit {
41 dispatch_queue_t _dataQueue;
44 + (instancetype)instance {
45 static dispatch_once_t onceToken;
46 dispatch_once(&onceToken, ^{
47 ratelimit = [SecItemRateLimit new];
52 - (instancetype)init {
53 if (self = [super init]) {
54 _roCapacity = 25; // allow burst of this size
55 _roRate = 3.0; // allow sustained rate of this many per second
60 _forceEnabled = false;
61 _limitMultiplier = 5.0; // Multiply capacity and rate by this much after exceeding limit
62 _dataQueue = dispatch_queue_create("com.apple.keychain.secitemratelimit.dataqueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
69 return _forceEnabled || [self shouldCountAPICalls];
72 - (void)forceEnabled:(bool)force {
73 _forceEnabled = force;
74 secnotice("secitemratelimit", "%sorcing SIRL to be enabled (effective: %i)", force ? "F" : "Not f", [self isEnabled]);
77 - (bool)isReadOnlyAPICallWithinLimits {
78 if (![self consumeTokenFromBucket:false]) {
79 secnotice("secitemratelimit", "Readonly API rate exceeded");
86 - (bool)isModifyingAPICallWithinLimits {
87 if (![self consumeTokenFromBucket:true]) {
88 secnotice("secitemratelimit", "Modifying API rate exceeded");
95 - (bool)consumeTokenFromBucket:(bool)readwrite {
96 if (![self shouldCountAPICalls] && !_forceEnabled) {
100 __block bool ok = false;
101 dispatch_sync(_dataQueue, ^{
102 int* capacity = readwrite ? &_rwCapacity : &_roCapacity;
103 double* rate = readwrite ? &_rwRate : &_roRate;
104 NSDate* __strong* bucket = readwrite ? &_rwBucket : &_roBucket;
106 NSDate* now = [NSDate now];
107 NSDate* fullBucket = [now dateByAddingTimeInterval:-(*capacity * (1.0 / *rate))];
108 // bucket has more tokens than a 'full' bucket? This prevents occasional-but-bursty activity slipping through
109 if (!*bucket || [*bucket timeIntervalSinceDate: fullBucket] < 0) {
110 *bucket = fullBucket;
113 *bucket = [*bucket dateByAddingTimeInterval:1.0 / *rate];
114 ok = [*bucket timeIntervalSinceDate:now] <= 0;
116 // Get a new bucket next time so we only complain every now and then
119 *capacity *= _limitMultiplier;
120 *rate *= _limitMultiplier;
127 - (bool)shouldCountAPICalls {
128 static bool shouldCount = false;
129 static dispatch_once_t shouldCountToken;
130 dispatch_once(&shouldCountToken, ^{
131 if (!SecIsInternalRelease()) {
132 secnotice("secitemratelimit", "Not internal release, disabling SIRL");
136 // gSecurityd is the XPC elision mechanism for testing; don't want simcrashes during tests
137 if (gSecurityd != nil) {
138 secnotice("secitemratelimit", "gSecurityd non-nil, disabling SIRL for testing");
142 if (!os_feature_enabled(Security, SecItemRateLimiting)) {
143 secnotice("secitemratelimit", "SIRL disabled via feature flag");
147 SecTaskRef task = SecTaskCreateFromSelf(NULL);
148 NSMutableArray* exempt = [NSMutableArray arrayWithArray:@[@"com.apple.pcsstatus", @"com.apple.protectedcloudstorage.protectedcloudkeysyncing", @"com.apple.cloudd", @"com.apple.pcsctl"]];
149 CFStringRef identifier = NULL;
150 require_action_quiet(task, cleanup, secerror("secitemratelimit: unable to get task from self, disabling SIRL"));
152 // If not a valid or debugged platform binary, don't count
153 uint32_t flags = SecTaskGetCodeSignStatus(task);
154 require_action_quiet((flags & (CS_VALID | CS_PLATFORM_BINARY | CS_PLATFORM_PATH)) == (CS_VALID | CS_PLATFORM_BINARY) ||
155 (flags & (CS_DEBUGGED | CS_PLATFORM_BINARY | CS_PLATFORM_PATH)) == (CS_DEBUGGED | CS_PLATFORM_BINARY),
156 cleanup, secnotice("secitemratelimit", "Not valid/debugged platform binary, disabling SIRL"));
158 // Some processes have legitimate need to query or modify a large number of items
159 identifier = SecTaskCopySigningIdentifier(task, NULL);
160 require_action_quiet(identifier, cleanup, secerror("secitemratelimit: unable to get signing identifier, disabling SIRL"));
162 [exempt addObjectsFromArray:@[@"com.apple.keychainaccess", @"com.apple.Safari"]];
164 [exempt addObjectsFromArray:@[@"com.apple.mobilesafari", @"com.apple.Preferences"]];
166 if ([exempt containsObject:(__bridge NSString*)identifier]) {
167 secnotice("secitemratelimit", "%@ exempt from SIRL", identifier);
171 secnotice("secitemratelimit", "valid/debugged platform binary %@ on internal release, enabling SIRL", identifier);
176 CFReleaseNull(identifier);
182 + (instancetype)getStaticRateLimit {
183 return [SecItemRateLimit instance];
186 // Testing and super thread-UNsafe. Caveat emptor.
187 + (void)resetStaticRateLimit {
188 ratelimit = [SecItemRateLimit new];
193 bool isReadOnlyAPIRateWithinLimits(void) {
194 return [[SecItemRateLimit instance] isReadOnlyAPICallWithinLimits];
197 bool isModifyingAPIRateWithinLimits(void) {
198 return [[SecItemRateLimit instance] isModifyingAPICallWithinLimits];