8 #include <Security/SecTrustPriv.h>
11 #define OS_OBJECT_HAVE_OBJC_SUPPORT 1
13 #define SEC_EXP_NULL_BAD_INPUT ((void *_Nonnull)NULL)
14 #define SEC_EXP_NULL_OUT_OF_MEMORY SEC_EXP_NULL_BAD_INPUT
16 #define SEC_EXP_NIL_BAD_INPUT ((void *_Nonnull)nil)
17 #define SEC_EXP_NIL_OUT_OF_MEMORY SEC_EXP_NIL_BAD_INPUT
19 #define SEC_EXP_CONCRETE_CLASS_NAME(external_type) SecExpConcrete_##external_type
20 #define SEC_EXP_CONCRETE_PREFIX_STR "SecExpConcrete_"
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
26 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL_AND_VISBILITY(external_type, _protocol, visibility, ...) \
27 @protocol OS_OBJECT_CLASS(external_type) <_protocol> \
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\"") \
34 _Pragma("clang diagnostic pop") \
36 typedef int _useless_typedef_oio_##external_type
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__)
41 #define SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(external_type, ...) \
42 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC_WITH_PROTOCOL(external_type, NSObject, ##__VA_ARGS__)
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__)
47 SEC_EXP_OBJECT_DECL_INTERNAL_OBJC(sec_experiment);
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>
58 #define SEC_EXPERIMENT_SAMPLING_RATE 100.0
59 #define HASH_INITIAL_VALUE 0
60 #define HASH_MULTIPLIER 31
62 const char *kSecExperimentDefaultsDomain = "com.apple.security.experiment";
63 const char *kSecExperimentDefaultsDisableSampling = "disableSampling";
64 const char *kSecExperimentTLSProbe = "TLSProbeExperiment";
66 const NSString *SecExperimentConfigurationKeyFleetSampleRate = @"FleetSampleRate";
67 const NSString *SecExperimentConfigurationKeyDeviceSampleRate = @"DeviceSampleRate";
68 const NSString *SecExperimentConfigurationKeyExperimentIdentifier = @"ExpName";
69 const NSString *SecExperimentConfigurationKeyConfigurationData = @"ConfigData";
72 sec_experiment_copy_log_handle(void)
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");
79 return experiment_log;
82 #define sec_experiment_log_info(fmt, ...) \
84 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
86 os_log_info(_log_handle, fmt, ##__VA_ARGS__); \
90 #define sec_experiment_log_debug(fmt, ...) \
92 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
94 os_log_debug(_log_handle, fmt, ##__VA_ARGS__); \
98 #define sec_experiment_log_error(fmt, ...) \
100 os_log_t _log_handle = sec_experiment_copy_log_handle(); \
102 os_log_error(_log_handle, fmt, ##__VA_ARGS__); \
106 // Computes hash of input and returns a value between 1-100
108 sec_experiment_hash_multiplicative(const uint8_t *key, size_t len)
114 uint32_t hash = HASH_INITIAL_VALUE;
115 for (uint32_t i = 0; i < len; ++i) {
116 hash = HASH_MULTIPLIER * hash + key[i];
119 return hash % 101; // value between 0-100
123 sec_experiment_host_hash(void)
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};
131 if (gethostuuid(uuid, &timeout) == 0) {
132 uuid_unparse(uuid, hostuuid);
133 hash = sec_experiment_hash_multiplicative((const uint8_t *)hostuuid, strlen(hostuuid));
141 SEC_EXP_OBJECT_IMPL_INTERNAL_OBJC(sec_experiment,
144 SecExperiment *innerExperiment;
149 @implementation SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment)
151 - (instancetype)initWithName:(const char *)name
154 return SEC_EXP_NIL_BAD_INPUT;
157 if ((self = [super init])) {
158 self->innerExperiment = [[SecExperiment alloc] initWithName:name];
160 return SEC_EXP_NIL_OUT_OF_MEMORY;
165 - (instancetype)initWithInnerExperiment:(SecExperiment *)experiment
167 if (experiment == NULL) {
168 return SEC_EXP_NIL_BAD_INPUT;
171 if ((self = [super init])) {
172 self->innerExperiment = experiment;
174 return SEC_EXP_NIL_OUT_OF_MEMORY;
181 return [innerExperiment.name UTF8String];
184 - (const char *)identifier
186 return [innerExperiment.identifier UTF8String];
189 - (BOOL)experimentIsAllowedForProcess
191 return [innerExperiment experimentIsAllowedForProcess];
194 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
196 return [innerExperiment isSamplingDisabledWithDefault:defaultValue];
199 - (BOOL)isSamplingDisabled
201 return [innerExperiment isSamplingDisabled];
204 - (SecExperimentConfig *)copyExperimentConfiguration
206 return [innerExperiment copyExperimentConfiguration];
211 @interface SecExperiment()
212 @property NSString *name;
213 @property (nonatomic) BOOL samplingDisabled;
214 @property SecExperimentConfig *cachedConfig;
217 @implementation SecExperiment
219 - (instancetype)initWithName:(const char *)name
222 return SEC_EXP_NIL_BAD_INPUT;
225 if ((self = [super init])) {
226 self.name = [NSString stringWithUTF8String:name];
228 return SEC_EXP_NIL_OUT_OF_MEMORY;
233 - (BOOL)experimentIsAllowedForProcess
235 __block NSArray<NSString *> *whitelistedProcesses = @[
237 @"com.apple.WebKit.Networking",
242 static BOOL isAllowed = NO;
243 static dispatch_once_t onceToken = 0;
244 dispatch_once(&onceToken, ^{
246 int ret = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags));
248 // Fail closed if we're not able to determine the type of binary.
252 if (!(flags & CS_PLATFORM_BINARY)) {
253 // Allow SecExperiment on all non-platform binaries, e.g., third party apps.
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]) {
263 *stop = YES; // Stop searching the whitelist
271 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
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];
287 - (BOOL)isSamplingDisabled
289 return [self isSamplingDisabledWithDefault:self.samplingDisabled];
292 - (NSDictionary *)copyExperimentConfigurationFromUserDefaults
294 NSDictionary *result = nil;
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];
310 - (NSDictionary *)copyRemoteExperimentAsset
312 CFErrorRef error = NULL;
313 NSDictionary *config = NULL;
314 NSDictionary *asset = CFBridgingRelease(SecTrustOTASecExperimentCopyAsset(&error));
316 config = [asset valueForKey:self.name];
318 CFReleaseNull(error);
322 - (NSDictionary *)copyRandomExperimentConfigurationFromAsset:(NSDictionary *)asset
324 NSArray *array = [asset valueForKey:@"ConfigArray"];
328 return [array objectAtIndex:(arc4random() % [array count])];
331 - (SecExperimentConfig *)copyExperimentConfiguration
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;
338 NSDictionary *defaultsDictionary = [self copyExperimentConfigurationFromUserDefaults];
339 if (defaultsDictionary != nil) {
340 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:defaultsDictionary];
341 return self.cachedConfig;
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;
354 - (NSString *)identifier
356 if (self.cachedConfig != nil) {
357 return [self.cachedConfig identifier];
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;
373 @implementation SecExperimentConfig
375 - (instancetype)initWithConfiguration:(NSDictionary *)configuration
377 if (configuration == nil) {
378 return SEC_EXP_NIL_BAD_INPUT;
381 if ((self = [super init])) {
382 // Parse out experiment information from the configuration dictionary
383 self.config = configuration;
384 self.identifier = [configuration objectForKey:SecExperimentConfigurationKeyExperimentIdentifier];
386 NSNumber *deviceSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyDeviceSampleRate];
387 if (deviceSampleRate != nil) {
388 self.deviceSampleRate = [deviceSampleRate unsignedIntValue];
391 NSNumber *fleetSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyFleetSampleRate];
392 if (fleetSampleRate != nil) {
393 self.fleetSampleRate = [fleetSampleRate unsignedIntValue];
396 self.configurationData = [configuration objectForKey:SecExperimentConfigurationKeyConfigurationData];
398 return SEC_EXP_NIL_OUT_OF_MEMORY;
406 return sec_experiment_host_hash();
409 - (BOOL)shouldRunWithSamplingRate:(NSNumber *)sampleRate
415 uint32_t sample = arc4random();
416 return ((float)sample < ((float)UINT32_MAX / [sampleRate unsignedIntegerValue]));
421 uint32_t hostIdHash = [self hostHash];
422 if ((hostIdHash == 0) || (self.fleetSampleRate < hostIdHash)) {
426 return [self shouldRunWithSamplingRate:@(self.deviceSampleRate)];
432 sec_experiment_create(const char *name)
434 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithName:name];
438 sec_experiment_create_with_inner_experiment(SecExperiment *experiment)
440 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithInnerExperiment:experiment];
444 sec_experiment_set_sampling_disabled(sec_experiment_t experiment, bool sampling_disabled)
446 experiment->innerExperiment.samplingDisabled = sampling_disabled;
450 sec_experiment_get_identifier(sec_experiment_t experiment)
452 return [experiment identifier];
456 sec_experiment_copy_configuration(sec_experiment_t experiment)
458 if (experiment == nil) {
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);
470 sec_experiment_log_info("Configuration '%{public}s' for experiment '%{public}s' not sampled to run",
471 [experiment name], [[experimentConfiguration identifier] UTF8String]);
475 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
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)
485 if (experiment == NULL || run_block == nil) {
489 if (![experiment experimentIsAllowedForProcess]) {
490 sec_experiment_log_info("Not running experiments for disallowed process");
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);
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]);
505 sec_experiment_log_info("Configuration '%s' for experiment '%s' failed", identifier, [experiment name]);
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");
511 skip_block(sec_experiment_get_identifier(experiment));
516 if (synchronous || !queue) {
517 sec_experiment_log_info("Starting experiment '%s' synchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
520 sec_experiment_log_info("Starting experiment '%s' asynchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
521 dispatch_async(queue, experiment_block);
528 sec_experiment_run(const char *experiment_name, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block)
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);
534 return sec_experiment_run_internal(experiment, false, NULL, run_block, skip_block, true);
536 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
544 sec_experiment_t experiment = sec_experiment_create(experiment_name);
546 return sec_experiment_run_internal(experiment, false, queue, run_block, skip_block, false);
548 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
556 sec_experiment_t experiment = sec_experiment_create(experiment_name);
558 return sec_experiment_run_internal(experiment, sampling_disabled, NULL, run_block, skip_block, true);
560 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
568 sec_experiment_t experiment = sec_experiment_create(experiment_name);
570 return sec_experiment_run_internal(experiment, sampling_disabled, queue, run_block, skip_block, false);
572 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
578 sec_experiment_get_run_count(sec_experiment_t experiment)
580 return experiment->numRuns;
584 sec_experiment_get_successful_run_count(sec_experiment_t experiment)
586 return experiment->successRuns;