/core/externals/update-engine/Core/KSDownloadAction.m

http://macfuse.googlecode.com/ · Objective C · 549 lines · 366 code · 79 blank · 104 comment · 61 complexity · eb5ef7708142dc104d456bb50b5e4792 MD5 · raw file

  1. // Copyright 2008 Google Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "KSDownloadAction.h"
  15. #import "KSActionProcessor.h"
  16. #import "KSActionPipe.h"
  17. #import "KSURLData.h"
  18. #import "KSURLNotification.h"
  19. #import "GTMLogger.h"
  20. #import "NSData+Hash.h"
  21. #import "GTMBase64.h"
  22. #import "KSUUID.h"
  23. #import "GTMPath.h"
  24. #import "GTMNSString+FindFolder.h"
  25. #import "KSFrameworkStats.h"
  26. #import <unistd.h>
  27. #import <sys/stat.h>
  28. // Overview of secure downloading
  29. // ------------------------------
  30. //
  31. // This action needs to download a URL, save the data to a file, and verify the
  32. // contents of the file by checking it's SHA-1 hash value. To protect ourselves
  33. // in the event that root is doing the download, we fork another process
  34. // ("ksurl") which does the actual network transactions for the download, then
  35. // when it's done we proceed to verify the download. To ensure we always use the
  36. // "same" code path, we use the separate ksurl process even if we're not running
  37. // as root.
  38. //
  39. // These security requirements have led us to the following design of this code:
  40. //
  41. // 1. Before forking the child process we mark all of our file descriptors as
  42. // close-on-exec to make sure we don't leak FDs to an unprivileged process
  43. // 2. We erase the child's environment when NSTask'ing
  44. // 3. The child (ksurl) itself will change UID to non-root if being run as root.
  45. // If ksurl was not run as root, then it continues to run as the non-root
  46. // user who invoked it.
  47. // 4. Our child ksurl downloads the file to a temporary path at an essentially
  48. // world-writable location (tempPath_).
  49. // 5. When the child finishes downloading, this process (possibly running as
  50. // root) will *copy* the downloaded file from the world-writable location to
  51. // a secure location that's only writable by this user (perhaps root) to
  52. // prevent tampering with the file (path_).
  53. // 6. Once the file is in a secure location, this process (again, possibly root)
  54. // will verify that file's SHA-1 hash value.
  55. //
  56. // Assuming the hash value is OK, then we know we have a valid file and it's
  57. // stored in a safe location. At this point, it should be OK to report that the
  58. // download was a success.
  59. @interface KSDownloadAction (PrivateMethods)
  60. // Returns YES if the file at |path| has a size of |size_| and a hash value of
  61. // |hash_|. NO otherwise.
  62. - (BOOL)isFileAtPathValid:(NSString *)path;
  63. // Returns a base64 encoded SHA-1 has of the contents of the file at |path|.
  64. - (NSString *)hashOfFileAtPath:(NSString *)path;
  65. // Returns the size of the file in bytes (as obtained from NSFileManager)
  66. - (unsigned long long)sizeOfFileAtPath:(NSString *)path;
  67. // Returns the full path to the "ksurl" command.
  68. - (NSString *)ksurlPath;
  69. // If it cares, tell our delegate about our download progress.
  70. - (void)markProgress:(float)progress;
  71. // Called by an NSDistributedNotificationCenter
  72. - (void)progressNotification:(NSNotification *)notification;
  73. // Get the permissions for a file at |path|.
  74. + (mode_t)filePosixPermissionsForPath:(GTMPath *)path;
  75. // The subfolder in [~]/Library/Caches to use for +writableTempNameForPath:
  76. + (NSString *)cacheSubfolderName;
  77. // Returns a path to a file in a "writable" location in the given |domain|.
  78. // The returned path name is generated by the last path component of
  79. // |path| (i.e., path's filename) and a UUID to make the returned name
  80. // unique. This is the path where the 'ksurl' process will download
  81. // to. The directory in [~]/Library/Caches + +cacheSubfolderName will be
  82. // created if necessary, and its permissions checked and potentially repaired.
  83. + (NSString *)writableTempNameForPath:(NSString *)path
  84. inDomain:(int)domain;
  85. @end
  86. // Marks all the file descriptors in this process as "close-on-exec". This
  87. // ensures that we don't leak FDs to untrusted child processes.
  88. static void MarkFileDescriptorsCloseOnExec(void) {
  89. long maxfd = sysconf(_SC_OPEN_MAX);
  90. if (maxfd < 0) {
  91. // COV_NF_START
  92. GTMLoggerError(@"sysconf(_SC_OPEN_MAX) failed: %s", strerror(errno));
  93. maxfd = 255; // Use a reasonable default
  94. // COV_NF_END
  95. }
  96. for (int i = 0; i < maxfd; i++)
  97. fcntl(i, FD_CLOEXEC);
  98. }
  99. @implementation KSDownloadAction
  100. // Articulation point where tests can provide their own identifier (used
  101. // to construct the default download directory path) so that the user's
  102. // existing default download directory doesn't get stomped.
  103. + (NSString *)downloadDirectoryIdentifier {
  104. NSBundle *bundle = [NSBundle bundleForClass:[KSDownloadAction class]];
  105. NSString *identifier = [bundle bundleIdentifier];
  106. return identifier;
  107. }
  108. + (void)setDirectoryPermissionsForPath:(GTMPath *)path {
  109. mode_t filePerms = [self filePosixPermissionsForPath:path];
  110. filePerms &= ~(S_IWGRP | S_IWOTH); // Strip group / other writability.
  111. filePerms |= S_IRWXU; // Make sure user can rwx
  112. int result = chmod([[path fullPath] fileSystemRepresentation], filePerms);
  113. if (result == -1) {
  114. GTMLoggerError(@"chmod(%@) failed: %s", [path fullPath], strerror(errno));
  115. }
  116. }
  117. + (NSString *)defaultDownloadDirectory {
  118. short domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
  119. // nil |identifier| means we're not living in a bundle.
  120. NSString *identifier = [self downloadDirectoryIdentifier];
  121. if (identifier == nil) identifier = @"UpdateEngine";
  122. NSString *name = [NSString stringWithFormat:@"%@.%d", identifier, geteuid()];
  123. NSString *caches = [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
  124. inDomain:domain
  125. doCreate:YES];
  126. GTMPath *cacheDirectory = [[GTMPath pathWithFullPath:caches]
  127. createDirectoryName:name mode:0700];
  128. [self setDirectoryPermissionsForPath:cacheDirectory];
  129. GTMPath *downloads =
  130. [cacheDirectory createDirectoryName:@"Downloads" mode:0700];
  131. [self setDirectoryPermissionsForPath:downloads];
  132. return [downloads fullPath];
  133. }
  134. + (id)actionWithURL:(NSURL *)url
  135. size:(unsigned long long)size
  136. hash:(NSString *)hash
  137. name:(NSString *)name {
  138. NSString *dir = [self defaultDownloadDirectory];
  139. NSString *path = [dir stringByAppendingPathComponent:name];
  140. return [self actionWithURL:url size:size hash:hash path:path];
  141. }
  142. + (id)actionWithURL:(NSURL *)url
  143. size:(unsigned long long)size
  144. hash:(NSString *)hash
  145. path:(NSString *)path {
  146. return [[[self alloc] initWithURL:url
  147. size:size
  148. hash:hash
  149. path:path] autorelease];
  150. }
  151. - (id)init {
  152. return [self initWithURL:nil size:0 hash:nil path:nil];
  153. }
  154. - (id)initWithURL:(NSURL *)url
  155. size:(unsigned long long)size
  156. hash:(NSString *)hash
  157. path:(NSString *)path {
  158. if ((self = [super init])) {
  159. url_ = [url retain];
  160. size_ = size;
  161. hash_ = [hash retain];
  162. path_ = [path copy];
  163. int domain = geteuid() == 0 ? kLocalDomain : kUserDomain;
  164. tempPath_ =
  165. [[KSDownloadAction writableTempNameForPath:path_ inDomain:domain] copy];
  166. if (url_ == nil || size_ == 0 || [hash_ length] == 0 ||
  167. [path_ length] == 0 || [tempPath_ length] == 0) {
  168. GTMLoggerDebug(@"created with illegal argument: "
  169. @"url=%@, size=%llu, hash=%@, destinationPath=%@",
  170. url_, size_, hash_, path_);
  171. [self release];
  172. return nil;
  173. }
  174. }
  175. return self;
  176. }
  177. - (void)dealloc {
  178. [url_ release];
  179. [hash_ release];
  180. [path_ release];
  181. [tempPath_ release];
  182. [[NSNotificationCenter defaultCenter] removeObserver:self];
  183. [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
  184. [downloadTask_ terminate];
  185. [downloadTask_ release];
  186. [ksurlPath_ release];
  187. [super dealloc];
  188. }
  189. - (NSURL *)url {
  190. return url_;
  191. }
  192. - (unsigned long long)size {
  193. return size_;
  194. }
  195. - (NSString *)hash {
  196. return hash_;
  197. }
  198. - (NSString *)path {
  199. return path_;
  200. }
  201. - (void)performAction {
  202. // Assert class invariants that we care about here
  203. _GTMDevAssert(url_ != nil, @"url_ must not be nil");
  204. _GTMDevAssert(hash_ != nil, @"hash_ must not be nil");
  205. _GTMDevAssert(size_ != 0, @"size_ must not be 0");
  206. _GTMDevAssert(path_ != nil, @"destination path must not be nil");
  207. _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
  208. _GTMDevAssert(downloadTask_ == nil, @"downloadTask_ must be nil");
  209. // Announce our progress is just beginning.
  210. [self markProgress:0.0];
  211. // If we've already downloaded the file, then we can short circuit the
  212. // download and just return the one that we already have.
  213. if ([self isFileAtPathValid:path_]) {
  214. GTMLoggerInfo(@"Short circuiting download of %@, path=%@, "
  215. @"size=%llu, hash=%@", url_, path_, size_, hash_);
  216. [[self outPipe] setContents:path_];
  217. [self markProgress:1.0];
  218. [[self processor] finishedProcessing:self successfully:YES];
  219. [[KSFrameworkStats sharedStats] incrementStat:kStatDownloadCacheHits];
  220. return; // Short circuit
  221. }
  222. NSString *ksurlPath = [self ksurlPath];
  223. NSArray *args = [NSArray arrayWithObjects:
  224. @"-url", [url_ description],
  225. @"-path", tempPath_,
  226. @"-size", [NSString stringWithFormat:@"%llu", size_],
  227. nil];
  228. downloadTask_ = [[NSTask alloc] init];
  229. [downloadTask_ setLaunchPath:ksurlPath];
  230. [downloadTask_ setEnvironment:[NSDictionary dictionary]];
  231. [downloadTask_ setCurrentDirectoryPath:@"/tmp/"];
  232. [downloadTask_ setArguments:args];
  233. GTMLoggerInfo(@"Running '%@ %@'", ksurlPath,
  234. [args componentsJoinedByString:@" "]);
  235. MarkFileDescriptorsCloseOnExec();
  236. [[NSNotificationCenter defaultCenter] removeObserver:self];
  237. [[NSNotificationCenter defaultCenter]
  238. addObserver:self
  239. selector:@selector(taskExited:)
  240. name:NSTaskDidTerminateNotification
  241. object:downloadTask_];
  242. [[NSDistributedNotificationCenter defaultCenter]
  243. addObserver:self
  244. selector:@selector(progressNotification:)
  245. name:KSURLProgressNotification
  246. object:tempPath_];
  247. @try {
  248. // Is known to throw if launchPath is not executable
  249. [downloadTask_ launch];
  250. [[KSFrameworkStats sharedStats] incrementStat:kStatDownloads];
  251. // COV_NF_START
  252. }
  253. @catch (id ex) {
  254. // It's not really feasible to test the case where this throws because we'd
  255. // need to delete the installation of ksurl during the unit test, which
  256. // may break further tests that rely on it. Or we could move it and move
  257. // it back, but really, ugh.
  258. GTMLoggerError(@"Failed to launch %@ %@: %@", ksurlPath,
  259. [args componentsJoinedByString:@" "], ex);
  260. [[NSNotificationCenter defaultCenter] removeObserver:self];
  261. [[NSDistributedNotificationCenter defaultCenter]
  262. removeObserver:self];
  263. [downloadTask_ release];
  264. downloadTask_ = nil;
  265. [self markProgress:1.0];
  266. [[self processor] finishedProcessing:self successfully:NO];
  267. }
  268. // COV_NF_END
  269. }
  270. - (void)terminateAction {
  271. if (![self isRunning])
  272. return;
  273. GTMLoggerInfo(@"Cancelling download task %@ (%@ %@) at the behest of %@",
  274. downloadTask_, [downloadTask_ launchPath],
  275. [[downloadTask_ arguments] componentsJoinedByString:@" "],
  276. [self processor]);
  277. [[NSNotificationCenter defaultCenter] removeObserver:self];
  278. [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
  279. [downloadTask_ terminate];
  280. [downloadTask_ waitUntilExit];
  281. [downloadTask_ release];
  282. downloadTask_ = nil;
  283. }
  284. - (void)taskExited:(NSNotification *)notification {
  285. _GTMDevAssert(path_ != nil, @"path_ must not be nil");
  286. _GTMDevAssert(tempPath_ != nil, @"tempPath_ must not be nil");
  287. BOOL verified = NO;
  288. int status = [downloadTask_ terminationStatus];
  289. if (status == 0) {
  290. // Move tempPath_ into our safe, non-public location pointed at by path_
  291. // Why do we use unlink(2) instead of NSFileManager? Because NSFM
  292. // will remove directories recursively, and if some crazy accident
  293. // happend where one of these paths pointed to a dir (say, "/"),
  294. // we'd rather it fail than recursively remove things.
  295. NSFileManager *fm = [NSFileManager defaultManager];
  296. unlink([path_ fileSystemRepresentation]); // Remove destination path
  297. if (![fm copyPath:tempPath_ toPath:path_ handler:nil]) {
  298. GTMLoggerError(@"Failed to rename %@ -> %@: errno=%d", // COV_NF_LINE
  299. tempPath_, path_, errno); // COV_NF_LINE
  300. }
  301. unlink([tempPath_ fileSystemRepresentation]); // Clean up source path
  302. verified = [self isFileAtPathValid:path_];
  303. }
  304. if (verified)
  305. [[self outPipe] setContents:path_];
  306. else
  307. [[KSFrameworkStats sharedStats] incrementStat:kStatFailedDownloads];
  308. GTMLoggerInfo(@"Task %d finished status=%d, verified=%d",
  309. [downloadTask_ processIdentifier], status, verified);
  310. [self markProgress:1.0];
  311. [[self processor] finishedProcessing:self successfully:verified];
  312. [[NSNotificationCenter defaultCenter] removeObserver:self];
  313. [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
  314. [downloadTask_ release];
  315. downloadTask_ = nil;
  316. }
  317. - (NSString *)description {
  318. return [NSString stringWithFormat:@"<%@:%p url=%@ size=%llu hash=%@ ...>",
  319. [self class], self, url_, size_, hash_];
  320. }
  321. @end // KSDownloadAction
  322. @implementation KSDownloadAction (PrivateMethods)
  323. // Articulation point for tests.
  324. + (NSString *)cacheSubfolderName {
  325. return @"UpdateEngine-Temp";
  326. }
  327. + (NSString *)writableTempNameForPath:(NSString *)path
  328. inDomain:(int)domain {
  329. if (path == nil) return nil;
  330. NSString *uniqueName = [NSString stringWithFormat:@"%@-%@",
  331. [path lastPathComponent], [KSUUID uuidString]];
  332. NSString *cacheDir =
  333. [NSString gtm_stringWithPathForFolder:kCachedDataFolderType
  334. subfolderName:[self cacheSubfolderName]
  335. inDomain:domain
  336. doCreate:YES];
  337. mode_t cachePerms =
  338. [KSDownloadAction filePosixPermissionsForPath:
  339. [GTMPath pathWithFullPath:cacheDir]];
  340. if (domain == kLocalDomain) {
  341. // The cacheDir for the local domain (/Library/Caches) needs to be
  342. // world-writable, so that a ksurl running as "nobody" (on behalf of a
  343. // root-originated update) is able to create a download directory.
  344. // Make sure this is the case.
  345. if ((cachePerms & S_IWOTH) == 0) {
  346. // OR-in 007 permissions
  347. chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXO);
  348. }
  349. } else {
  350. // The cacheDir for the user domain (~/Library/Caches) need to be
  351. /// owner-writable for the same reasons as above.
  352. if ((cachePerms & S_IWUSR) == 0) {
  353. chmod([cacheDir fileSystemRepresentation], cachePerms | S_IRWXU);
  354. }
  355. }
  356. return [cacheDir stringByAppendingPathComponent:uniqueName];
  357. }
  358. + (mode_t)filePosixPermissionsForPath:(GTMPath *)path {
  359. mode_t filePerms = [[path attributes] filePosixPermissions];
  360. return filePerms;
  361. }
  362. - (BOOL)isFileAtPathValid:(NSString *)path {
  363. if (path == nil) return NO;
  364. unsigned long long size = [self sizeOfFileAtPath:path];
  365. NSString *hash = [self hashOfFileAtPath:path];
  366. return (size_ == size) && ([hash_ isEqualToString:hash]);
  367. }
  368. // Reads in a whole file and returns the base64 encoded SHA-1 hash of the data.
  369. // If we start working with HUGE files, then we may need to change this method
  370. // to stream the data from the file and hash it w/o reading the whole thing in.
  371. // For now, this should work fine.
  372. - (NSString *)hashOfFileAtPath:(NSString *)path {
  373. if (path == nil) return nil;
  374. NSData *data = [NSData dataWithContentsOfFile:path];
  375. NSData *hash = [data SHA1Hash];
  376. return [GTMBase64 stringByEncodingData:hash];
  377. }
  378. - (unsigned long long)sizeOfFileAtPath:(NSString *)path {
  379. if (path == nil) return 0;
  380. return [[[NSFileManager defaultManager] fileAttributesAtPath:path
  381. traverseLink:NO] fileSize];
  382. }
  383. // Return a directory in which to put ksurl.
  384. // Performs no error checking or creation.
  385. - (NSString *)ksurlDirectoryName {
  386. NSString *directory = [NSString stringWithFormat:@"/tmp/.ksda.%X", geteuid()];
  387. return directory;
  388. }
  389. // Return YES if |directory| is valid for us to place ksurl into, else NO.
  390. - (BOOL)isValidAndSafeDirectory:(NSString *)directory {
  391. NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
  392. NSNumber *properOwner = [NSNumber numberWithUnsignedLong:geteuid()];
  393. NSDictionary *attributes = nil;
  394. BOOL isDir = NO;
  395. if ([[NSFileManager defaultManager]
  396. fileExistsAtPath:directory
  397. isDirectory:&isDir] &&
  398. isDir) {
  399. attributes = [[NSFileManager defaultManager]
  400. fileAttributesAtPath:directory
  401. traverseLink:NO];
  402. if ([[attributes objectForKey:NSFilePosixPermissions]
  403. isEqual:properPermission] &&
  404. [[attributes objectForKey:NSFileOwnerAccountID]
  405. isEqual:properOwner]) {
  406. return YES;
  407. }
  408. }
  409. return NO;
  410. }
  411. // Return a valid directory for ksurl creation.
  412. // Create if needed. Delete and recreate if it looks bad.
  413. - (NSString *)ksurlValidatedDirectory {
  414. NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
  415. NSString *directory = [self ksurlDirectoryName];
  416. if ([self isValidAndSafeDirectory:directory]) {
  417. return directory;
  418. }
  419. // Doesn't exist, or exists with wrong permission/owner. Delete and create.
  420. [[NSFileManager defaultManager] removeFileAtPath:directory
  421. handler:nil];
  422. NSDictionary *attr =
  423. [NSDictionary dictionaryWithObject:properPermission
  424. forKey:NSFilePosixPermissions];
  425. [[NSFileManager defaultManager] createDirectoryAtPath:directory
  426. attributes:attr];
  427. // reconfirm in case it failed
  428. if ([self isValidAndSafeDirectory:directory]) {
  429. return directory;
  430. } else {
  431. return nil;
  432. }
  433. }
  434. - (NSString *)ksurlPath {
  435. if (ksurlPath_ != nil)
  436. return ksurlPath_;
  437. // KSURLData and KSURLData_len come from KSURLData.m, a file
  438. // generated by xxd'ing the ksurl binary.
  439. NSData *ksdata = [NSData dataWithBytesNoCopy:KSURLData
  440. length:KSURLData_len
  441. freeWhenDone:NO];
  442. NSString *directory = [self ksurlValidatedDirectory];
  443. if (directory == nil) {
  444. GTMLoggerInfo(@"Can't safely create directory for ksurl.");
  445. return nil;
  446. }
  447. NSString *destination = [directory
  448. stringByAppendingPathComponent:@"ksurl"];
  449. NSNumber *properPermission = [NSNumber numberWithUnsignedLong:0755];
  450. NSDictionary *attr =
  451. [NSDictionary dictionaryWithObject:properPermission
  452. forKey:NSFilePosixPermissions];
  453. if ([[NSFileManager defaultManager] createFileAtPath:destination
  454. contents:ksdata
  455. attributes:attr] == NO) {
  456. GTMLoggerInfo(@"Can't create ksurl.");
  457. return nil;
  458. }
  459. ksurlPath_ = [destination retain];
  460. return ksurlPath_;
  461. }
  462. - (void)markProgress:(float)progress {
  463. [[self processor] runningAction:self progress:progress];
  464. }
  465. - (void)progressNotification:(NSNotification *)notification {
  466. NSNumber *progress = [[notification userInfo] objectForKey:KSURLProgressKey];
  467. if (progress)
  468. [self markProgress:[progress floatValue]];
  469. }
  470. @end // KSDownloadAction (PrivateMethods)