]> git.saurik.com Git - apple/security.git/blob - OSX/sec/Security/SecItemRateLimit.m
Security-59754.80.3.tar.gz
[apple/security.git] / OSX / sec / Security / SecItemRateLimit.m
1 /*
2 * Copyright (c) 2020 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 "SecItemRateLimit.h"
25 #import "SecItemRateLimit_tests.h"
26
27 #import <utilities/debugging.h>
28 #import <utilities/SecInternalReleasePriv.h>
29 #import "ipc/securityd_client.h"
30
31 #import <sys/codesign.h>
32 #import <os/feature_private.h>
33
34 // Dressed-down version of RateLimiter which directly computes the rate of one bucket and resets when it runs out of tokens
35
36 // Broken out so the test-only reset method can reinit this
37 static SecItemRateLimit* ratelimit;
38
39 @implementation SecItemRateLimit {
40 bool _forceEnabled;
41 dispatch_queue_t _dataQueue;
42 }
43
44 + (instancetype)instance {
45 static dispatch_once_t onceToken;
46 dispatch_once(&onceToken, ^{
47 ratelimit = [SecItemRateLimit new];
48 });
49 return ratelimit;
50 }
51
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
56 _rwCapacity = 25;
57 _rwRate = 1.0;
58 _roBucket = nil;
59 _rwBucket = nil;
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);
63 }
64
65 return self;
66 }
67
68 - (bool)isEnabled {
69 return _forceEnabled || [self shouldCountAPICalls];
70 }
71
72 - (void)forceEnabled:(bool)force {
73 _forceEnabled = force;
74 secnotice("secitemratelimit", "%sorcing SIRL to be enabled (effective: %i)", force ? "F" : "Not f", [self isEnabled]);
75 }
76
77 - (bool)isReadOnlyAPICallWithinLimits {
78 if (![self consumeTokenFromBucket:false]) {
79 secnotice("secitemratelimit", "Readonly API rate exceeded");
80 return false;
81 } else {
82 return true;
83 }
84 }
85
86 - (bool)isModifyingAPICallWithinLimits {
87 if (![self consumeTokenFromBucket:true]) {
88 secnotice("secitemratelimit", "Modifying API rate exceeded");
89 return false;
90 } else {
91 return true;
92 }
93 }
94
95 - (bool)consumeTokenFromBucket:(bool)readwrite {
96 if (![self shouldCountAPICalls] && !_forceEnabled) {
97 return true;
98 }
99
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;
105
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;
111 }
112
113 *bucket = [*bucket dateByAddingTimeInterval:1.0 / *rate];
114 ok = [*bucket timeIntervalSinceDate:now] <= 0;
115
116 // Get a new bucket next time so we only complain every now and then
117 if (!ok) {
118 *bucket = nil;
119 *capacity *= _limitMultiplier;
120 *rate *= _limitMultiplier;
121 }
122 });
123
124 return ok;
125 }
126
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");
133 return;
134 }
135
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");
139 return;
140 }
141
142 if (!os_feature_enabled(Security, SecItemRateLimiting)) {
143 secnotice("secitemratelimit", "SIRL disabled via feature flag");
144 return;
145 }
146
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"));
151
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"));
157
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"));
161 #if TARGET_OS_OSX
162 [exempt addObjectsFromArray:@[@"com.apple.keychainaccess", @"com.apple.Safari"]];
163 #elif TARGET_OS_IOS
164 [exempt addObjectsFromArray:@[@"com.apple.mobilesafari", @"com.apple.Preferences"]];
165 #endif
166 if ([exempt containsObject:(__bridge NSString*)identifier]) {
167 secnotice("secitemratelimit", "%@ exempt from SIRL", identifier);
168 goto cleanup;
169 }
170
171 secnotice("secitemratelimit", "valid/debugged platform binary %@ on internal release, enabling SIRL", identifier);
172 shouldCount = true;
173
174 cleanup:
175 CFReleaseNull(task);
176 CFReleaseNull(identifier);
177 });
178 return shouldCount;
179 }
180
181 // Testing
182 + (instancetype)getStaticRateLimit {
183 return [SecItemRateLimit instance];
184 }
185
186 // Testing and super thread-UNsafe. Caveat emptor.
187 + (void)resetStaticRateLimit {
188 ratelimit = [SecItemRateLimit new];
189 }
190
191 @end
192
193 bool isReadOnlyAPIRateWithinLimits(void) {
194 return [[SecItemRateLimit instance] isReadOnlyAPICallWithinLimits];
195 }
196
197 bool isModifyingAPIRateWithinLimits(void) {
198 return [[SecItemRateLimit instance] isModifyingAPICallWithinLimits];
199 }