]> git.saurik.com Git - apple/security.git/blob - experiment/SecExperiment.m
Security-59754.41.1.tar.gz
[apple/security.git] / experiment / SecExperiment.m
1 //
2 // SecExperiment.m
3 // Security
4 //
5
6 #include <xpc/xpc.h>
7 #include <os/log.h>
8 #include <Security/SecTrustPriv.h>
9 #include <uuid/uuid.h>
10
11 #define OS_OBJECT_HAVE_OBJC_SUPPORT 1
12
13 #define SEC_EXP_NULL_BAD_INPUT ((void *_Nonnull)NULL)
14 #define SEC_EXP_NULL_OUT_OF_MEMORY SEC_EXP_NULL_BAD_INPUT
15
16 #define SEC_EXP_NIL_BAD_INPUT ((void *_Nonnull)nil)
17 #define SEC_EXP_NIL_OUT_OF_MEMORY SEC_EXP_NIL_BAD_INPUT
18
19 #define SEC_EXP_CONCRETE_CLASS_NAME(external_type) SecExpConcrete_##external_type
20 #define SEC_EXP_CONCRETE_PREFIX_STR "SecExpConcrete_"
21
22 #define SEC_EXP_OBJECT_DECL_INTERNAL_OBJC(external_type) \
23 @class SEC_EXP_CONCRETE_CLASS_NAME(external_type); \
24 typedef SEC_EXP_CONCRETE_CLASS_NAME(external_type) *external_type##_t
25
26 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, _protocol, visibility, ...) \
27 @protocol OS_OBJECT_CLASS(external_type) <_protocol> \
28 @end \
29 visibility \
30 @interface SEC_EXP_CONCRETE_CLASS_NAME(external_type) : NSObject<OS_OBJECT_CLASS(external_type)> \
31 _Pragma("clang diagnostic push") \
32 _Pragma("clang diagnostic ignored \"-Wobjc-interface-ivars\"") \
33 __VA_ARGS__ \
34 _Pragma("clang diagnostic pop") \
35 @end \
36 typedef int _useless_typedef_oio_##external_type
37
38 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL(external_type, _protocol, ...) \
39 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, _protocol, ,__VA_ARGS__)
40
41 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(external_type, ...) \
42 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL(external_type, NSObject, ##__VA_ARGS__)
43
44 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_VISIBILITY(external_type, visibility, ...) \
45 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, NSObject, visibility, ##__VA_ARGS__)
46
47 SEC_EXP_OBJECT_DECL_INTERNAL_OBJC(sec_experiment);
48
49 #define SEC_EXP_OBJECT_IMPL 1
50 #import "SecExperimentPriv.h"
51 #import "SecExperimentInternal.h"
52 #import "SecCFRelease.h"
53 #import <Foundation/Foundation.h>
54 #import <CoreFoundation/CFXPCBridge.h>
55 #import <System/sys/codesign.h>
56 #import <sys/errno.h>
57
58 #define SEC_EXPERIMENT_SAMPLING_RATE 100.0
59 #define HASH_INITIAL_VALUE 0
60 #define HASH_MULTIPLIER 31
61
62 const char *kSecExperimentDefaultsDomain = "com.apple.security.experiment";
63 const char *kSecExperimentDefaultsDisableSampling = "disableSampling";
64 const char *kSecExperimentTLSProbe = "TLSProbeExperiment";
65
66 const NSString *SecExperimentConfigurationKeyFleetSampleRate = @"FleetSampleRate";
67 const NSString *SecExperimentConfigurationKeyDeviceSampleRate = @"DeviceSampleRate";
68 const NSString *SecExperimentConfigurationKeyExperimentIdentifier = @"ExpName";
69 const NSString *SecExperimentConfigurationKeyConfigurationData = @"ConfigData";
70
71 static os_log_t
72 sec_experiment_copy_log_handle(void)
73 {
74 static dispatch_once_t onceToken = 0;
75 static os_log_t experiment_log = nil;
76 dispatch_once(&onceToken, ^{
77 experiment_log = os_log_create("com.apple.security", "experiment");
78 });
79 return experiment_log;
80 }
81
82 #define sec_experiment_log_info(fmt, ...) \
83 do { \
84 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
85 if (_log_handle) { \
86 os_log_info(_log_handle, fmt, ##__VA_ARGS__); \
87 } \
88 } while (0);
89
90 #define sec_experiment_log_debug(fmt, ...) \
91 do { \
92 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
93 if (_log_handle) { \
94 os_log_debug(_log_handle, fmt, ##__VA_ARGS__); \
95 } \
96 } while (0);
97
98 #define sec_experiment_log_error(fmt, ...) \
99 do { \
100 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
101 if (_log_handle) { \
102 os_log_error(_log_handle, fmt, ##__VA_ARGS__); \
103 } \
104 } while (0);
105
106 // Computes hash of input and returns a value between 1-100
107 static uint32_t
108 sec_experiment_hash_multiplicative(const uint8_t *key, size_t len)
109 {
110 if (!key) {
111 return 0;
112 }
113
114 uint32_t hash = HASH_INITIAL_VALUE;
115 for (uint32_t i = 0; i < len; ++i) {
116 hash = HASH_MULTIPLIER * hash + key[i];
117 }
118
119 return hash % 101; // value between 0-100
120 }
121
122 static uint32_t
123 sec_experiment_host_hash(void)
124 {
125 static uuid_string_t hostuuid = {};
126 static uint32_t hash = 0;
127 static dispatch_once_t onceToken = 0;
128 dispatch_once(&onceToken, ^{
129 struct timespec timeout = {0, 0};
130 uuid_t uuid = {};
131 if (gethostuuid(uuid, &timeout) == 0) {
132 uuid_unparse(uuid, hostuuid);
133 hash = sec_experiment_hash_multiplicative((const uint8_t *)hostuuid, strlen(hostuuid));
134 } else {
135 onceToken = 0;
136 }
137 });
138 return hash;
139 }
140
141 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(sec_experiment,
142 {
143 @public
144 SecExperiment *innerExperiment;
145 size_t numRuns;
146 size_t successRuns;
147 });
148
149 @implementation SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment)
150
151 - (instancetype)initWithName:(const char *)name
152 {
153 if (name == NULL) {
154 return SEC_EXP_NIL_BAD_INPUT;
155 }
156
157 if ((self = [super init])) {
158 self->innerExperiment = [[SecExperiment alloc] initWithName:name];
159 } else {
160 return SEC_EXP_NIL_OUT_OF_MEMORY;
161 }
162 return self;
163 }
164
165 - (instancetype)initWithInnerExperiment:(SecExperiment *)experiment
166 {
167 if (experiment == NULL) {
168 return SEC_EXP_NIL_BAD_INPUT;
169 }
170
171 if ((self = [super init])) {
172 self->innerExperiment = experiment;
173 } else {
174 return SEC_EXP_NIL_OUT_OF_MEMORY;
175 }
176 return self;
177 }
178
179 - (const char *)name
180 {
181 return [innerExperiment.name UTF8String];
182 }
183
184 - (const char *)identifier
185 {
186 return [innerExperiment.identifier UTF8String];
187 }
188
189 - (BOOL)experimentIsAllowedForProcess
190 {
191 return [innerExperiment experimentIsAllowedForProcess];
192 }
193
194 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
195 {
196 return [innerExperiment isSamplingDisabledWithDefault:defaultValue];
197 }
198
199 - (BOOL)isSamplingDisabled
200 {
201 return [innerExperiment isSamplingDisabled];
202 }
203
204 - (SecExperimentConfig *)copyExperimentConfiguration
205 {
206 return [innerExperiment copyExperimentConfiguration];
207 }
208
209 @end
210
211 @interface SecExperiment()
212 @property NSString *name;
213 @property (nonatomic) BOOL samplingDisabled;
214 @property SecExperimentConfig *cachedConfig;
215 @end
216
217 @implementation SecExperiment
218
219 - (instancetype)initWithName:(const char *)name
220 {
221 if (name == NULL) {
222 return SEC_EXP_NIL_BAD_INPUT;
223 }
224
225 if ((self = [super init])) {
226 self.name = [NSString stringWithUTF8String:name];
227 } else {
228 return SEC_EXP_NIL_OUT_OF_MEMORY;
229 }
230 return self;
231 }
232
233 - (BOOL)experimentIsAllowedForProcess
234 {
235 __block NSArray<NSString *> *whitelistedProcesses = @[
236 @"nsurlsessiond",
237 @"com.apple.WebKit.Networking",
238 @"experimentTool",
239 @"network_test",
240 ];
241
242 static BOOL isAllowed = NO;
243 static dispatch_once_t onceToken = 0;
244 dispatch_once(&onceToken, ^{
245 uint32_t flags = 0;
246 int ret = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags));
247 if (ret) {
248 // Fail closed if we're not able to determine the type of binary.
249 return;
250 }
251
252 if (!(flags & CS_PLATFORM_BINARY)) {
253 // Allow SecExperiment on all non-platform binaries, e.g., third party apps.
254 isAllowed = YES;
255 return;
256 }
257
258 // Otherwise, this is a platform binary. Check against the set of whitelisted processes.
259 NSString *process = [NSString stringWithFormat:@"%s", getprogname()];
260 [whitelistedProcesses enumerateObjectsUsingBlock:^(NSString * _Nonnull whitelistedProcess, NSUInteger idx, BOOL * _Nonnull stop) {
261 if ([whitelistedProcess isEqualToString:process]) {
262 isAllowed = YES;
263 *stop = YES; // Stop searching the whitelist
264 }
265 }];
266 });
267
268 return isAllowed;
269 }
270
271 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
272 {
273 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
274 if (defaults != nil) {
275 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
276 if (experimentDefaults != nil) {
277 NSString *key = [NSString stringWithUTF8String:kSecExperimentDefaultsDisableSampling];
278 if (experimentDefaults[key] != nil) {
279 return [experimentDefaults[key] boolValue];
280 }
281 }
282 }
283
284 return defaultValue;
285 }
286
287 - (BOOL)isSamplingDisabled
288 {
289 return [self isSamplingDisabledWithDefault:self.samplingDisabled];
290 }
291
292 - (NSDictionary *)copyExperimentConfigurationFromUserDefaults
293 {
294 NSDictionary *result = nil;
295
296 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
297 if (defaults != nil) {
298 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
299 if (experimentDefaults != nil) {
300 NSString *key = self.name;
301 if (experimentDefaults[key] != nil) {
302 result = experimentDefaults[key];
303 }
304 }
305 }
306
307 return result;
308 }
309
310 - (NSDictionary *)copyRemoteExperimentAsset
311 {
312 CFErrorRef error = NULL;
313 NSDictionary *config = NULL;
314 NSDictionary *asset = CFBridgingRelease(SecTrustOTASecExperimentCopyAsset(&error));
315 if (asset) {
316 config = [asset valueForKey:self.name];
317 }
318 CFReleaseNull(error);
319 return config;
320 }
321
322 - (NSDictionary *)copyRandomExperimentConfigurationFromAsset:(NSDictionary *)asset
323 {
324 NSArray *array = [asset valueForKey:@"ConfigArray"];
325 if (array == nil) {
326 return nil;
327 }
328 return [array objectAtIndex:(arc4random() % [array count])];
329 }
330
331 - (SecExperimentConfig *)copyExperimentConfiguration
332 {
333 if (self.cachedConfig) {
334 // If we've fetched an experiment config already, use it for the duration of this object's lifetime.
335 return self.cachedConfig;
336 }
337
338 NSDictionary *defaultsDictionary = [self copyExperimentConfigurationFromUserDefaults];
339 if (defaultsDictionary != nil) {
340 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:defaultsDictionary];
341 return self.cachedConfig;
342 }
343
344 NSDictionary *remoteAsset = [self copyRemoteExperimentAsset];
345 if (remoteAsset != nil) {
346 NSDictionary *randomConfig = [self copyRandomExperimentConfigurationFromAsset:remoteAsset];
347 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:randomConfig];
348 return self.cachedConfig;
349 }
350
351 return nil;
352 }
353
354 - (NSString *)identifier
355 {
356 if (self.cachedConfig != nil) {
357 return [self.cachedConfig identifier];
358 } else {
359 return nil;
360 }
361 }
362
363 @end
364
365 @interface SecExperimentConfig()
366 @property NSString *identifier;
367 @property NSDictionary *config;
368 @property uint32_t fleetSampleRate;
369 @property uint32_t deviceSampleRate;
370 @property NSDictionary *configurationData;
371 @end
372
373 @implementation SecExperimentConfig
374
375 - (instancetype)initWithConfiguration:(NSDictionary *)configuration
376 {
377 if (configuration == nil) {
378 return SEC_EXP_NIL_BAD_INPUT;
379 }
380
381 if ((self = [super init])) {
382 // Parse out experiment information from the configuration dictionary
383 self.config = configuration;
384 self.identifier = [configuration objectForKey:SecExperimentConfigurationKeyExperimentIdentifier];
385
386 NSNumber *deviceSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyDeviceSampleRate];
387 if (deviceSampleRate != nil) {
388 self.deviceSampleRate = [deviceSampleRate unsignedIntValue];
389 }
390
391 NSNumber *fleetSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyFleetSampleRate];
392 if (fleetSampleRate != nil) {
393 self.fleetSampleRate = [fleetSampleRate unsignedIntValue];
394 }
395
396 self.configurationData = [configuration objectForKey:SecExperimentConfigurationKeyConfigurationData];
397 } else {
398 return SEC_EXP_NIL_OUT_OF_MEMORY;
399 }
400
401 return self;
402 }
403
404 - (uint32_t)hostHash
405 {
406 return sec_experiment_host_hash();
407 }
408
409 - (BOOL)shouldRunWithSamplingRate:(NSNumber *)sampleRate
410 {
411 if (!sampleRate) {
412 return NO;
413 }
414
415 uint32_t sample = arc4random();
416 return ((float)sample < ((float)UINT32_MAX / [sampleRate unsignedIntegerValue]));
417 }
418
419 - (BOOL)isSampled
420 {
421 uint32_t hostIdHash = [self hostHash];
422 if ((hostIdHash == 0) || (self.fleetSampleRate < hostIdHash)) {
423 return NO;
424 }
425
426 return [self shouldRunWithSamplingRate:@(self.deviceSampleRate)];
427 }
428
429 @end
430
431 sec_experiment_t
432 sec_experiment_create(const char *name)
433 {
434 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithName:name];
435 }
436
437 sec_experiment_t
438 sec_experiment_create_with_inner_experiment(SecExperiment *experiment)
439 {
440 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithInnerExperiment:experiment];
441 }
442
443 void
444 sec_experiment_set_sampling_disabled(sec_experiment_t experiment, bool sampling_disabled)
445 {
446 experiment->innerExperiment.samplingDisabled = sampling_disabled;
447 }
448
449 const char *
450 sec_experiment_get_identifier(sec_experiment_t experiment)
451 {
452 return [experiment identifier];
453 }
454
455 xpc_object_t
456 sec_experiment_copy_configuration(sec_experiment_t experiment)
457 {
458 if (experiment == nil) {
459 return nil;
460 }
461
462 // Check first for defaults configured
463 SecExperimentConfig *experimentConfiguration = [experiment copyExperimentConfiguration];
464 if (experimentConfiguration != nil) {
465 NSDictionary *configurationData = [experimentConfiguration configurationData];
466 if (![experiment isSamplingDisabled]) {
467 if ([experimentConfiguration isSampled]) {
468 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
469 } else {
470 sec_experiment_log_info("Configuration '%{public}s' for experiment '%{public}s' not sampled to run",
471 [experiment name], [[experimentConfiguration identifier] UTF8String]);
472 return nil;
473 }
474 } else {
475 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
476 }
477 }
478
479 return nil;
480 }
481
482 bool
483 sec_experiment_run_internal(sec_experiment_t experiment, bool sampling_disabled, dispatch_queue_t queue, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block, bool synchronous)
484 {
485 if (experiment == NULL || run_block == nil) {
486 return false;
487 }
488
489 if (![experiment experimentIsAllowedForProcess]) {
490 sec_experiment_log_info("Not running experiments for disallowed process");
491 return false;
492 }
493
494 dispatch_block_t experiment_block = ^{
495 bool experiment_sampling_disabled = [experiment isSamplingDisabledWithDefault:sampling_disabled];
496 sec_experiment_set_sampling_disabled(experiment, [experiment isSamplingDisabledWithDefault:sampling_disabled]);
497 xpc_object_t config = sec_experiment_copy_configuration(experiment);
498 const char *identifier = sec_experiment_get_identifier(experiment);
499 if (config != nil) {
500 experiment->numRuns++;
501 if (run_block(identifier, config)) {
502 experiment->successRuns++;
503 sec_experiment_log_info("Configuration '%s' for experiment '%s' succeeded", identifier, [experiment name]);
504 } else {
505 sec_experiment_log_info("Configuration '%s' for experiment '%s' failed", identifier, [experiment name]);
506 }
507 } else {
508 sec_experiment_log_info("Configuration '%s' for experiment '%s' not configured to run with sampling %s", identifier,
509 [experiment name], experiment_sampling_disabled ? "disabled" : "enabled");
510 if (skip_block) {
511 skip_block(sec_experiment_get_identifier(experiment));
512 }
513 }
514 };
515
516 if (synchronous || !queue) {
517 sec_experiment_log_info("Starting experiment '%s' synchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
518 experiment_block();
519 } else {
520 sec_experiment_log_info("Starting experiment '%s' asynchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
521 dispatch_async(queue, experiment_block);
522 }
523
524 return true;
525 }
526
527 bool
528 sec_experiment_run(const char *experiment_name, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block)
529 {
530 // Sampling is always enabled for SecExperiment callers. Appliations may override this by setting the
531 // `disableSampling` key in the `com.apple.security.experiment` defaults domain.
532 sec_experiment_t experiment = sec_experiment_create(experiment_name);
533 if (experiment) {
534 return sec_experiment_run_internal(experiment, false, NULL, run_block, skip_block, true);
535 } else {
536 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
537 return false;
538 }
539 }
540
541 bool
542 sec_experiment_run_async(const char *experiment_name, dispatch_queue_t queue, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block)
543 {
544 sec_experiment_t experiment = sec_experiment_create(experiment_name);
545 if (experiment) {
546 return sec_experiment_run_internal(experiment, false, queue, run_block, skip_block, false);
547 } else {
548 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
549 return false;
550 }
551 }
552
553 bool
554 sec_experiment_run_with_sampling_disabled(const char *experiment_name, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block, bool sampling_disabled)
555 {
556 sec_experiment_t experiment = sec_experiment_create(experiment_name);
557 if (experiment) {
558 return sec_experiment_run_internal(experiment, sampling_disabled, NULL, run_block, skip_block, true);
559 } else {
560 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
561 return false;
562 }
563 }
564
565 bool
566 sec_experiment_run_async_with_sampling_disabled(const char *experiment_name, dispatch_queue_t queue, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block, bool sampling_disabled)
567 {
568 sec_experiment_t experiment = sec_experiment_create(experiment_name);
569 if (experiment) {
570 return sec_experiment_run_internal(experiment, sampling_disabled, queue, run_block, skip_block, false);
571 } else {
572 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
573 return false;
574 }
575 }
576
577 size_t
578 sec_experiment_get_run_count(sec_experiment_t experiment)
579 {
580 return experiment->numRuns;
581 }
582
583 size_t
584 sec_experiment_get_successful_run_count(sec_experiment_t experiment)
585 {
586 return experiment->successRuns;
587 }