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;
159 return SEC_EXP_NIL_OUT_OF_MEMORY;
161 self->innerExperiment = [[SecExperiment alloc] initWithName:name];
166 - (instancetype)initWithInnerExperiment:(SecExperiment *)experiment
168 if (experiment == NULL) {
169 return SEC_EXP_NIL_BAD_INPUT;
174 return SEC_EXP_NIL_OUT_OF_MEMORY;
176 self->innerExperiment = experiment;
183 return [innerExperiment.name UTF8String];
186 - (const char *)identifier
188 return [innerExperiment.identifier UTF8String];
191 - (BOOL)experimentIsAllowedForProcess
193 return [innerExperiment experimentIsAllowedForProcess];
196 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
198 return [innerExperiment isSamplingDisabledWithDefault:defaultValue];
201 - (BOOL)isSamplingDisabled
203 return [innerExperiment isSamplingDisabled];
206 - (SecExperimentConfig *)copyExperimentConfiguration
208 return [innerExperiment copyExperimentConfiguration];
213 @interface SecExperiment()
214 @property NSString *name;
215 @property (nonatomic) BOOL samplingDisabled;
216 @property SecExperimentConfig *cachedConfig;
219 @implementation SecExperiment
221 - (instancetype)initWithName:(const char *)name
224 return SEC_EXP_NIL_BAD_INPUT;
229 return SEC_EXP_NIL_OUT_OF_MEMORY;
231 self.name = [NSString stringWithUTF8String:name];
236 - (BOOL)experimentIsAllowedForProcess
238 __block NSArray<NSString *> *whitelistedProcesses = @[
240 @"com.apple.WebKit.Networking",
245 static BOOL isAllowed = NO;
246 static dispatch_once_t onceToken = 0;
247 dispatch_once(&onceToken, ^{
249 int ret = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags));
251 // Fail closed if we're not able to determine the type of binary.
255 if (!(flags & CS_PLATFORM_BINARY)) {
256 // Allow SecExperiment on all non-platform binaries, e.g., third party apps.
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]) {
266 *stop = YES; // Stop searching the whitelist
274 - (BOOL)isSamplingDisabledWithDefault:(BOOL)defaultValue
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];
290 - (BOOL)isSamplingDisabled
292 return [self isSamplingDisabledWithDefault:self.samplingDisabled];
295 - (NSDictionary *)copyExperimentConfigurationFromUserDefaults
297 NSDictionary *result = nil;
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];
313 - (NSDictionary *)copyRemoteExperimentAsset
315 CFErrorRef error = NULL;
316 NSDictionary *config = NULL;
317 NSDictionary *asset = CFBridgingRelease(SecTrustOTASecExperimentCopyAsset(&error));
319 config = [asset valueForKey:self.name];
321 CFReleaseNull(error);
325 - (NSDictionary *)copyRandomExperimentConfigurationFromAsset:(NSDictionary *)asset
327 NSArray *array = [asset valueForKey:@"ConfigArray"];
331 return [array objectAtIndex:(arc4random() % [array count])];
334 - (SecExperimentConfig *)copyExperimentConfiguration
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;
341 NSDictionary *defaultsDictionary = [self copyExperimentConfigurationFromUserDefaults];
342 if (defaultsDictionary != nil) {
343 self.cachedConfig = [[SecExperimentConfig alloc] initWithConfiguration:defaultsDictionary];
344 return self.cachedConfig;
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;
357 - (NSString *)identifier
359 if (self.cachedConfig != nil) {
360 return [self.cachedConfig identifier];
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;
376 @implementation SecExperimentConfig
378 - (instancetype)initWithConfiguration:(NSDictionary *)configuration
380 if (configuration == nil) {
381 return SEC_EXP_NIL_BAD_INPUT;
386 return SEC_EXP_NIL_OUT_OF_MEMORY;
388 // Parse out experiment information from the configuration dictionary
389 self.config = configuration;
390 self.identifier = [configuration objectForKey:SecExperimentConfigurationKeyExperimentIdentifier];
392 NSNumber *deviceSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyDeviceSampleRate];
393 if (deviceSampleRate != nil) {
394 self.deviceSampleRate = [deviceSampleRate unsignedIntValue];
397 NSNumber *fleetSampleRate = [configuration objectForKey:SecExperimentConfigurationKeyFleetSampleRate];
398 if (fleetSampleRate != nil) {
399 self.fleetSampleRate = [fleetSampleRate unsignedIntValue];
402 self.configurationData = [configuration objectForKey:SecExperimentConfigurationKeyConfigurationData];
409 return sec_experiment_host_hash();
412 - (BOOL)shouldRunWithSamplingRate:(NSNumber *)sampleRate
418 uint32_t sample = arc4random();
419 return ((float)sample < ((float)UINT32_MAX / [sampleRate unsignedIntegerValue]));
424 uint32_t hostIdHash = [self hostHash];
425 if ((hostIdHash == 0) || (self.fleetSampleRate < hostIdHash)) {
429 return [self shouldRunWithSamplingRate:@(self.deviceSampleRate)];
435 sec_experiment_create(const char *name)
437 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithName:name];
441 sec_experiment_create_with_inner_experiment(SecExperiment *experiment)
443 return [[SEC_EXP_CONCRETE_CLASS_NAME(sec_experiment) alloc] initWithInnerExperiment:experiment];
447 sec_experiment_set_sampling_disabled(sec_experiment_t experiment, bool sampling_disabled)
449 experiment->innerExperiment.samplingDisabled = sampling_disabled;
453 sec_experiment_get_identifier(sec_experiment_t experiment)
455 return [experiment identifier];
459 sec_experiment_copy_configuration(sec_experiment_t experiment)
461 if (experiment == nil) {
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);
473 sec_experiment_log_info("Configuration '%{public}s' for experiment '%{public}s' not sampled to run",
474 [experiment name], [[experimentConfiguration identifier] UTF8String]);
478 return _CFXPCCreateXPCObjectFromCFObject((__bridge CFDictionaryRef)configurationData);
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)
488 if (experiment == NULL || run_block == nil) {
492 if (![experiment experimentIsAllowedForProcess]) {
493 sec_experiment_log_info("Not running experiments for disallowed process");
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);
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]);
508 sec_experiment_log_info("Configuration '%s' for experiment '%s' failed", identifier, [experiment name]);
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");
514 skip_block(sec_experiment_get_identifier(experiment));
519 if (synchronous || !queue) {
520 sec_experiment_log_info("Starting experiment '%s' synchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
523 sec_experiment_log_info("Starting experiment '%s' asynchronously with sampling %s", [experiment name], sampling_disabled ? "disabled" : "enabled");
524 dispatch_async(queue, experiment_block);
531 sec_experiment_run(const char *experiment_name, sec_experiment_run_block_t run_block, sec_experiment_skip_block_t skip_block)
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);
537 return sec_experiment_run_internal(experiment, false, NULL, run_block, skip_block, true);
539 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
547 sec_experiment_t experiment = sec_experiment_create(experiment_name);
549 return sec_experiment_run_internal(experiment, false, queue, run_block, skip_block, false);
551 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
559 sec_experiment_t experiment = sec_experiment_create(experiment_name);
561 return sec_experiment_run_internal(experiment, sampling_disabled, NULL, run_block, skip_block, true);
563 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
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)
571 sec_experiment_t experiment = sec_experiment_create(experiment_name);
573 return sec_experiment_run_internal(experiment, sampling_disabled, queue, run_block, skip_block, false);
575 sec_experiment_log_info("Experiment '%s' not found", experiment_name);
581 sec_experiment_get_run_count(sec_experiment_t experiment)
583 return experiment->numRuns;
587 sec_experiment_get_successful_run_count(sec_experiment_t experiment)
589 return experiment->successRuns;