]>
Commit | Line | Data |
---|---|---|
d64be36e A |
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 | } |