]> git.saurik.com Git - apple/security.git/blob - experiment/SecExperiment.m
Security-59306.101.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 self = [super init];
158 if (self == nil) {
159 return SEC_EXP_NIL_OUT_OF_MEMORY;
160 } else {
161 self->innerExperiment = [[SecExperiment alloc] initWithName:name];
162 }
163 return self;
164 }
165
166 - (instancetype)initWithInnerExperiment:(SecExperiment *)experiment
167 {
168 if (experiment == NULL) {
169 return SEC_EXP_NIL_BAD_INPUT;
170 }
171
172 self = [super init];
173 if (self == nil) {
174 return SEC_EXP_NIL_OUT_OF_MEMORY;
175 } else {
176 self->innerExperiment = experiment;
177 }
178 return self;
179 }
180
181 - (const char *)name
182 {
183 return [innerExperiment.name UTF8String];
184 }
185
186 - (const char *)identifier
187 {
188 return [innerExperiment.identifier UTF8String];
189 }
190
191 - (BOOL)experimentIsAllowedForProcess
192 {
193 return [innerExperiment experimentIsAllowedForProcess];
194 }
195
196 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
197 {
198 return [innerExperiment isSamplingDisabledWithDefault:defaultValue];
199 }
200
201 - (BOOL)isSamplingDisabled
202 {
203 return [innerExperiment isSamplingDisabled];
204 }
205
206 - (SecExperimentConfig *)copyExperimentConfiguration
207 {
208 return [innerExperiment copyExperimentConfiguration];
209 }
210
211 @end
212
213 @interface SecExperiment()
214 @property NSString *name;
215 @property (nonatomic) BOOL samplingDisabled;
216 @property SecExperimentConfig *cachedConfig;
217 @end
218
219 @implementation SecExperiment
220
221 - (instancetype)initWithName:(const char *)name
222 {
223 if (name == NULL) {
224 return SEC_EXP_NIL_BAD_INPUT;
225 }
226
227 self = [super init];
228 if (self == nil) {
229 return SEC_EXP_NIL_OUT_OF_MEMORY;
230 } else {
231 self.name = [NSString stringWithUTF8String:name];
232 }
233 return self;
234 }
235
236 - (BOOL)experimentIsAllowedForProcess
237 {
238 __block NSArray<NSString *> *whitelistedProcesses = @[
239 @"nsurlsessiond",
240 @"com.apple.WebKit.Networking",
241 @"experimentTool",
242 @"network_test",
243 ];
244
245 static BOOL isAllowed = NO;
246 static dispatch_once_t onceToken = 0;
247 dispatch_once(&onceToken, ^{
248 uint32_t flags = 0;
249 int ret = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags));
250 if (ret) {
251 // Fail closed if we're not able to determine the type of binary.
252 return;
253 }
254
255 if (!(flags & CS_PLATFORM_BINARY)) {
256 // Allow SecExperiment on all non-platform binaries, e.g., third party apps.
257 isAllowed = YES;
258 return;
259 }
260
261 // Otherwise, this is a platform binary. Check against the set of whitelisted processes.
262 NSString *process = [NSString stringWithFormat:@"%s", getprogname()];
263 [whitelistedProcesses enumerateObjectsUsingBlock:^(NSString * _Nonnull whitelistedProcess, NSUInteger idx, BOOL * _Nonnull stop) {
264 if ([whitelistedProcess isEqualToString:process]) {
265 isAllowed = YES;
266 *stop = YES; // Stop searching the whitelist
267 }
268 }];
269 });
270
271 return isAllowed;
272 }
273
274 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
275 {
276 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
277 if (defaults != nil) {
278 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
279 if (experimentDefaults != nil) {
280 NSString *key = [NSString stringWithUTF8String:kSecExperimentDefaultsDisableSampling];
281 if (experimentDefaults[key] != nil) {
282 return [experimentDefaults[key] boolValue];
283 }
284 }
285 }
286
287 return defaultValue;
288 }
289
290 - (BOOL)isSamplingDisabled
291 {
292 return [self isSamplingDisabledWithDefault:self.samplingDisabled];
293 }
294
295 - (NSDictionary *)copyExperimentConfigurationFromUserDefaults
296 {
297 NSDictionary *result = nil;
298
299 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
300 if (defaults != nil) {
301 NSMutableDictionary *experimentDefaults = [[defaults persistentDomainForName:[NSString stringWithUTF8String:kSecExperimentDefaultsDomain]] mutableCopy];
302 if (experimentDefaults != nil) {
303 NSString *key = self.name;
304 if (experimentDefaults[key] != nil) {
305 result = experimentDefaults[key];
306 }
307 }
308 }
309
310 return result;
311 }
312
313 - (NSDictionary *)copyRemoteExperimentAsset
314 {
315 CFErrorRef error = NULL;
316 NSDictionary *config = NULL;
317 NSDictionary *asset = CFBridgingRelease(SecTrustOTASecExperimentCopyAsset(&error));
318 if (asset) {
319 config = [asset valueForKey:self.name];
320 }
321 CFReleaseNull(error);
322 return config;
323 }
324
325 - (NSDictionary *)copyRandomExperimentConfigurationFromAsset:(NSDictionary *)asset
326 {
327 NSArray *array = [asset valueForKey:@"ConfigArray"];
328 if (array == nil) {
329 return nil;
330 }
331 return [array objectAtIndex:(arc4random() % [array count])];
332 }
333
334 - (SecExperimentConfig *)copyExperimentConfiguration
335 {
336 if (self.cachedConfig) {
337 // If we've fetched an experiment config already, use it for the duration of this object's lifetime.
338 return self.cachedConfig;
339 }
340
341 NSDictionary *defaultsDictionary = [self copyExperimentConfigurationFromUserDefaults];
342 if (defaultsDictionary != nil) {
343 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:defaultsDictionary];
344 return self.cachedConfig;
345 }
346
347 NSDictionary *remoteAsset = [self copyRemoteExperimentAsset];
348 if (remoteAsset != nil) {
349 NSDictionary *randomConfig = [self copyRandomExperimentConfigurationFromAsset:remoteAsset];
350 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:randomConfig];
351 return self.cachedConfig;
352 }
353
354 return nil;
355 }
356
357 - (NSString *)identifier
358 {
359 if (self.cachedConfig != nil) {
360 return [self.cachedConfig identifier];
361 } else {
362 return nil;
363 }
364 }
365
366 @end
367
368 @interface SecExperimentConfig()
369 @property NSString *identifier;
370 @property NSDictionary *config;
371 @property uint32_t fleetSampleRate;
372 @property uint32_t deviceSampleRate;
373 @property NSDictionary *configurationData;
374 @end
375
376 @implementation SecExperimentConfig
377
378 - (instancetype)initWithConfiguration:(NSDictionary *)configuration
379 {
380 if (configuration == nil) {
381 return SEC_EXP_NIL_BAD_INPUT;
382 }
383
384 self = [super init];
385 if (self == nil) {
386 return SEC_EXP_NIL_OUT_OF_MEMORY;
387 } else {
388 // Parse out experiment information from the configuration dictionary
389 self.config = configuration;
390 self.identifier = [configuration objectForKey:SecExperimentConfigurationKeyExperimentIdentifier];
391
392 NSNumber *deviceSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyDeviceSampleRate];
393 if (deviceSampleRate != nil) {
394 self.deviceSampleRate = [deviceSampleRate unsignedIntValue];
395 }
396
397 NSNumber *fleetSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyFleetSampleRate];
398 if (fleetSampleRate != nil) {
399 self.fleetSampleRate = [fleetSampleRate unsignedIntValue];
400 }
401
402 self.configurationData = [configuration objectForKey:SecExperimentConfigurationKeyConfigurationData];
403 }
404 return self;
405 }
406
407 - (uint32_t)hostHash
408 {
409 return sec_experiment_host_hash();
410 }
411
412 - (BOOL)shouldRunWithSamplingRate:(NSNumber *)sampleRate
413 {
414 if (!sampleRate) {
415 return NO;
416 }
417
418 uint32_t sample = arc4random();
419 return ((float)sample < ((float)UINT32_MAX / [sampleRate unsignedIntegerValue]));
420 }
421
422 - (BOOL)isSampled
423 {
424 uint32_t hostIdHash = [self hostHash];
425 if ((hostIdHash == 0) || (self.fleetSampleRate < hostIdHash)) {
426 return NO;
427 }
428
429 return [self shouldRunWithSamplingRate:@(self.deviceSampleRate)];
430 }
431
432 @end
433
434 sec_experiment_t
435 sec_experiment_create(const char *name)
436 {
437 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithName:name];
438 }
439
440 sec_experiment_t
441 sec_experiment_create_with_inner_experiment(SecExperiment *experiment)
442 {
443 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithInnerExperiment:experiment];
444 }
445
446 void
447 sec_experiment_set_sampling_disabled(sec_experiment_t experiment, bool sampling_disabled)
448 {
449 experiment->innerExperiment.samplingDisabled = sampling_disabled;
450 }
451
452 const char *
453 sec_experiment_get_identifier(sec_experiment_t experiment)
454 {
455 return [experiment identifier];
456 }
457
458 xpc_object_t
459 sec_experiment_copy_configuration(sec_experiment_t experiment)
460 {
461 if (experiment == nil) {
462 return nil;
463 }
464
465 // Check first for defaults configured
466 SecExperimentConfig *experimentConfiguration = [experiment copyExperimentConfiguration];
467 if (experimentConfiguration != nil) {
468 NSDictionary *configurationData = [experimentConfiguration configurationData];
469 if (![experiment isSamplingDisabled]) {
470 if ([experimentConfiguration isSampled]) {
471 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
472 } else {
473 sec_experiment_log_info("Configuration '%{public}s' for experiment '%{public}s' not sampled to run",
474 [experiment name], [[experimentConfiguration identifier] UTF8String]);
475 return nil;
476 }
477 } else {
478 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
479 }
480 }
481
482 return nil;
483 }
484
485 bool
486 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)
487 {
488 if (experiment == NULL || run_block == nil) {
489 return false;
490 }
491
492 if (![experiment experimentIsAllowedForProcess]) {
493 sec_experiment_log_info("Not running experiments for disallowed process");
494 return false;
495 }
496
497 dispatch_block_t experiment_block = ^{
498 bool experiment_sampling_disabled = [experiment isSamplingDisabledWithDefault:sampling_disabled];
499 sec_experiment_set_sampling_disabled(experiment, [experiment isSamplingDisabledWithDefault:sampling_disabled]);
500 xpc_object_t config = sec_experiment_copy_configuration(experiment);
501 const char *identifier = sec_experiment_get_identifier(experiment);
502 if (config != nil) {
503 experiment->numRuns++;
504 if (run_block(identifier, config)) {
505 experiment->successRuns++;
506 sec_experiment_log_info("Configuration '%s' for experiment '%s' succeeded", identifier, [experiment name]);
507 } else {
508 sec_experiment_log_info("Configuration '%s' for experiment '%s' failed", identifier, [experiment name]);
509 }
510 } else {
511 sec_experiment_log_info("Configuration '%s' for experiment '%s' not configured to run with sampling %s", identifier,
512 [experiment name], experiment_sampling_disabled ? "disabled" : "enabled");
513 if (skip_block) {
514 skip_block(sec_experiment_get_identifier(experiment));
515 }
516 }
517 };
518
519 if (synchronous || !queue) {
520 sec_experiment_log_info("Starting experiment '%s' synchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
521 experiment_block();
522 } else {
523 sec_experiment_log_info("Starting experiment '%s' asynchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
524 dispatch_async(queue, experiment_block);
525 }
526
527 return true;
528 }
529
530 bool
531 sec_experiment_run(const char *experiment_name, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block)
532 {
533 // Sampling is always enabled for SecExperiment callers. Appliations may override this by setting the
534 // `disableSampling` key in the `com.apple.security.experiment` defaults domain.
535 sec_experiment_t experiment = sec_experiment_create(experiment_name);
536 if (experiment) {
537 return sec_experiment_run_internal(experiment, false, NULL, run_block, skip_block, true);
538 } else {
539 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
540 return false;
541 }
542 }
543
544 bool
545 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)
546 {
547 sec_experiment_t experiment = sec_experiment_create(experiment_name);
548 if (experiment) {
549 return sec_experiment_run_internal(experiment, false, queue, run_block, skip_block, false);
550 } else {
551 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
552 return false;
553 }
554 }
555
556 bool
557 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)
558 {
559 sec_experiment_t experiment = sec_experiment_create(experiment_name);
560 if (experiment) {
561 return sec_experiment_run_internal(experiment, sampling_disabled, NULL, run_block, skip_block, true);
562 } else {
563 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
564 return false;
565 }
566 }
567
568 bool
569 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)
570 {
571 sec_experiment_t experiment = sec_experiment_create(experiment_name);
572 if (experiment) {
573 return sec_experiment_run_internal(experiment, sampling_disabled, queue, run_block, skip_block, false);
574 } else {
575 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
576 return false;
577 }
578 }
579
580 size_t
581 sec_experiment_get_run_count(sec_experiment_t experiment)
582 {
583 return experiment->numRuns;
584 }
585
586 size_t
587 sec_experiment_get_successful_run_count(sec_experiment_t experiment)
588 {
589 return experiment->successRuns;
590 }