|
| 1 | +/* |
| 2 | + * Copyright 2018 Google |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#import "GDTClock.h" |
| 18 | + |
| 19 | +#import <sys/sysctl.h> |
| 20 | + |
| 21 | +// Using a monotonic clock is necessary because CFAbsoluteTimeGetCurrent(), NSDate, and related all |
| 22 | +// are subject to drift. That it to say, multiple consecutive calls do not always result in a |
| 23 | +// time that is in the future. Clocks may be adjusted by the user, NTP, or any number of external |
| 24 | +// factors. This class attempts to determine the wall-clock time at the time of the event by |
| 25 | +// capturing the kernel start and time since boot to determine a wallclock time in UTC. |
| 26 | +// |
| 27 | +// Timezone offsets at the time of a snapshot are also captured in order to provide local-time |
| 28 | +// details. Other classes in this library depend on comparing times at some time in the future to |
| 29 | +// a time captured in the past, and this class needs to provide a mechanism to do that. |
| 30 | +// |
| 31 | +// TL;DR: This class attempts to accomplish two things: 1. Provide accurate event times. 2. Provide |
| 32 | +// a monotonic clock mechanism to accurately check if some clock snapshot was before or after |
| 33 | +// by using a shared reference point (kernel boot time). |
| 34 | +// |
| 35 | +// Note: Much of the mach time stuff doesn't work properly in the simulator. So this class can be |
| 36 | +// difficult to unit test. |
| 37 | + |
| 38 | +/** Returns the kernel boottime property from sysctl. |
| 39 | + * |
| 40 | + * Inspired by https://stackoverflow.com/a/40497811 |
| 41 | + * |
| 42 | + * @return The KERN_BOOTTIME property from sysctl, in nanoseconds. |
| 43 | + */ |
| 44 | +static int64_t KernelBootTimeInNanoseconds() { |
| 45 | + // Caching the result is not possible because clock drift would not be accounted for. |
| 46 | + struct timeval boottime; |
| 47 | + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; |
| 48 | + size_t size = sizeof(boottime); |
| 49 | + int rc = sysctl(mib, 2, &boottime, &size, NULL, 0); |
| 50 | + if (rc != 0) { |
| 51 | + return 0; |
| 52 | + } |
| 53 | + return (int64_t)boottime.tv_sec * NSEC_PER_MSEC + (int64_t)boottime.tv_usec; |
| 54 | +} |
| 55 | + |
| 56 | +/** Returns value of gettimeofday, in nanoseconds. |
| 57 | + * |
| 58 | + * Inspired by https://stackoverflow.com/a/40497811 |
| 59 | + * |
| 60 | + * @return The value of gettimeofday, in nanoseconds. |
| 61 | + */ |
| 62 | +static int64_t UptimeInNanoseconds() { |
| 63 | + int64_t before_now; |
| 64 | + int64_t after_now; |
| 65 | + struct timeval now; |
| 66 | + |
| 67 | + before_now = KernelBootTimeInNanoseconds(); |
| 68 | + // Addresses a race condition in which the system time has updated, but the boottime has not. |
| 69 | + do { |
| 70 | + gettimeofday(&now, NULL); |
| 71 | + after_now = KernelBootTimeInNanoseconds(); |
| 72 | + } while (after_now != before_now); |
| 73 | + return (int64_t)now.tv_sec * NSEC_PER_MSEC + (int64_t)now.tv_usec - before_now; |
| 74 | +} |
| 75 | + |
| 76 | +// TODO: Consider adding a 'trustedTime' property that can be populated by the response from a BE. |
| 77 | +@implementation GDTClock |
| 78 | + |
| 79 | +- (instancetype)init { |
| 80 | + self = [super init]; |
| 81 | + if (self) { |
| 82 | + _kernelBootTime = KernelBootTimeInNanoseconds(); |
| 83 | + _uptime = UptimeInNanoseconds(); |
| 84 | + _timeMillis = |
| 85 | + (int64_t)((CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) * NSEC_PER_USEC); |
| 86 | + CFTimeZoneRef timeZoneRef = CFTimeZoneCopySystem(); |
| 87 | + _timezoneOffsetSeconds = CFTimeZoneGetSecondsFromGMT(timeZoneRef, 0); |
| 88 | + CFRelease(timeZoneRef); |
| 89 | + } |
| 90 | + return self; |
| 91 | +} |
| 92 | + |
| 93 | ++ (GDTClock *)snapshot { |
| 94 | + return [[GDTClock alloc] init]; |
| 95 | +} |
| 96 | + |
| 97 | ++ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture { |
| 98 | + GDTClock *snapshot = [self snapshot]; |
| 99 | + snapshot->_timeMillis += millisInTheFuture; |
| 100 | + return snapshot; |
| 101 | +} |
| 102 | + |
| 103 | +- (BOOL)isAfter:(GDTClock *)otherClock { |
| 104 | + // These clocks are trivially comparable when they share a kernel boot time. |
| 105 | + if (_kernelBootTime == otherClock->_kernelBootTime) { |
| 106 | + return _uptime > otherClock->_uptime; |
| 107 | + } else { |
| 108 | + int64_t kernelBootTimeDiff = otherClock->_kernelBootTime - _kernelBootTime; |
| 109 | + // This isn't a great solution, but essentially, if the other clock's boot time is 'later', NO |
| 110 | + // is returned. This can be altered by changing the system time and rebooting. |
| 111 | + return kernelBootTimeDiff < 0 ? YES : NO; |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +- (NSUInteger)hash { |
| 116 | + return [@(_kernelBootTime) hash] ^ [@(_uptime) hash] ^ [@(_timeMillis) hash] ^ |
| 117 | + [@(_timezoneOffsetSeconds) hash]; |
| 118 | +} |
| 119 | + |
| 120 | +- (BOOL)isEqual:(id)object { |
| 121 | + return [self hash] == [object hash]; |
| 122 | +} |
| 123 | + |
| 124 | +#pragma mark - NSSecureCoding |
| 125 | + |
| 126 | +/** NSKeyedCoder key for timeMillis property. */ |
| 127 | +static NSString *const kGDTClockTimeMillisKey = @"GDTClockTimeMillis"; |
| 128 | + |
| 129 | +/** NSKeyedCoder key for timezoneOffsetMillis property. */ |
| 130 | +static NSString *const kGDTClockTimezoneOffsetSeconds = @"GDTClockTimezoneOffsetSeconds"; |
| 131 | + |
| 132 | +/** NSKeyedCoder key for _kernelBootTime ivar. */ |
| 133 | +static NSString *const kGDTClockKernelBootTime = @"GDTClockKernelBootTime"; |
| 134 | + |
| 135 | +/** NSKeyedCoder key for _uptime ivar. */ |
| 136 | +static NSString *const kGDTClockUptime = @"GDTClockUptime"; |
| 137 | + |
| 138 | ++ (BOOL)supportsSecureCoding { |
| 139 | + return YES; |
| 140 | +} |
| 141 | + |
| 142 | +- (instancetype)initWithCoder:(NSCoder *)aDecoder { |
| 143 | + self = [super init]; |
| 144 | + if (self) { |
| 145 | + // TODO: If the kernelBootTime is more recent, we need to change the kernel boot time and |
| 146 | + // uptimeMillis ivars |
| 147 | + _timeMillis = [aDecoder decodeInt64ForKey:kGDTClockTimeMillisKey]; |
| 148 | + _timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDTClockTimezoneOffsetSeconds]; |
| 149 | + _kernelBootTime = [aDecoder decodeInt64ForKey:kGDTClockKernelBootTime]; |
| 150 | + _uptime = [aDecoder decodeInt64ForKey:kGDTClockUptime]; |
| 151 | + } |
| 152 | + return self; |
| 153 | +} |
| 154 | + |
| 155 | +- (void)encodeWithCoder:(NSCoder *)aCoder { |
| 156 | + [aCoder encodeInt64:_timeMillis forKey:kGDTClockTimeMillisKey]; |
| 157 | + [aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDTClockTimezoneOffsetSeconds]; |
| 158 | + [aCoder encodeInt64:_kernelBootTime forKey:kGDTClockKernelBootTime]; |
| 159 | + [aCoder encodeInt64:_uptime forKey:kGDTClockUptime]; |
| 160 | +} |
| 161 | + |
| 162 | +@end |
0 commit comments