]> git.saurik.com Git - apple/security.git/blob - OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp
Security-59306.120.7.tar.gz
[apple/security.git] / OSX / libsecurity_translocate / lib / SecTranslocateShared.cpp
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #include <vector>
25 #include <string>
26 #include <exception>
27
28 #include <sys/stat.h>
29 #include <unistd.h>
30 #include <sys/param.h>
31 #include <sys/mount.h>
32 #include <sys/ucred.h>
33 #include <dispatch/dispatch.h>
34 #include <string.h>
35 #include <dirent.h>
36
37 #define __APPLE_API_PRIVATE
38 #include <quarantine.h>
39 #undef __APPLE_API_PRIVATE
40
41 #include <security_utilities/cfutilities.h>
42 #include <security_utilities/unix++.h>
43 #include <security_utilities/logging.h>
44 #include <Security/SecStaticCode.h>
45
46 #include "SecTranslocateShared.hpp"
47 #include "SecTranslocateUtilities.hpp"
48
49
50 namespace Security {
51
52 namespace SecTranslocate {
53
54 using namespace std;
55
56 /* String Constants for XPC dictionary passing */
57 /* XPC Function keys */
58 const char* kSecTranslocateXPCFuncCreate = "create";
59 const char* kSecTranslocateXPCFuncCheckIn = "check-in";
60
61 /* XPC message argument keys */
62 const char* kSecTranslocateXPCMessageFunction = "function";
63 const char* kSecTranslocateXPCMessageOriginalPath = "original";
64 const char* kSecTranslocateXPCMessageDestinationPath = "dest";
65 const char* kSecTranslocateXPCMessagePid = "pid";
66
67 /*XPC message reply keys */
68 const char* kSecTranslocateXPCReplyError = "error";
69 const char* kSecTranslocateXPCReplySecurePath = "result";
70
71 //Functions used only within this file
72 static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath);
73 static string getMountpointFromAppPath(const string &appPath, const string &originalPath);
74
75 static vector<struct statfs> getMountTableSnapshot();
76 static string mountExistsForUser(const string &translationDirForUser,
77 const TranslocationPath &originalPath,
78 const string &destMount);
79 static void validateMountpoint(const string &mountpoint, bool owned=false);
80 static string makeNewMountpoint(const string &translationDir);
81 static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath);
82 static void cleanupTranslocationDirForUser(const string &userDir);
83 static int removeMountPoint(const string &mountpoint, bool force = false);
84
85 /* calculate whether a translocation should occur and where from */
86 TranslocationPath::TranslocationPath(string originalPath)
87 {
88
89 /* To support testing of translocation the policy is as follows:
90 1. When the quarantine translocation sysctl is off, always translocate
91 if we aren't already on a translocated mount point.
92 2. When the quarantine translocation sysctl is on, use the quarantine
93 bits to decide.
94 when asking if a path should run translocated need to:
95 check the current quarantine state of the path asked about
96 if it is already on a nullfs mount
97 do not translocate
98 else if it is unquarantined
99 do not translocate
100 else
101 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
102 do not translocate
103 else
104 find the outermost acceptable code bundle
105 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
106 don't translocate
107 else
108 translocate
109
110 See findOuterMostCodeBundleForFD for more info about what an acceptable outermost bundle is
111 in particular it should be noted that the outermost acceptable bundle for a quarantined inner
112 bundle can not be unquarantined. If the inner bundle is quarantined then any bundle containing it
113 must also have been quarantined.
114 */
115
116 ExtendedAutoFileDesc fd(originalPath);
117
118 should = false;
119 realOriginalPath = fd.getRealPath();
120
121 /* don't translocate if it already is */
122 /* only consider translocation if the thing being asked about is marked for translocation */
123 /* Nullfs can't translocate other mount's roots so abort if its a mountpoint */
124 if(!fd.isFileSystemType(NULLFS_FSTYPE) && fd.isQuarantined() && fd.shouldTranslocate() && !fd.isMountPoint())
125 {
126 ExtendedAutoFileDesc &&outermost = findOuterMostCodeBundleForFD(fd);
127
128 should = outermost.isQuarantined() && outermost.shouldTranslocate();
129 pathToTranslocate = outermost.getRealPath();
130
131 /* Calculate the path that will be needed to give the caller the path they asked for originally but in the translocated place */
132 if (should)
133 {
134 vector<string> originalComponents = splitPath(realOriginalPath);
135 vector<string> toTranslocateComponents = splitPath(pathToTranslocate);
136
137 if (toTranslocateComponents.size() == 0 ||
138 toTranslocateComponents.size() > originalComponents.size())
139 {
140 Syslog::error("SecTranslocate, TranslocationPath, path calculation failed:\n\toriginal: %s\n\tcalculated: %s",
141 realOriginalPath.c_str(),
142 pathToTranslocate.c_str());
143 UnixError::throwMe(EINVAL);
144 }
145
146 componentNameToTranslocate = toTranslocateComponents.back();
147
148 for(size_t cnt = 0; cnt < originalComponents.size(); cnt++)
149 {
150 if (cnt < toTranslocateComponents.size())
151 {
152 if (toTranslocateComponents[cnt] != originalComponents[cnt])
153 {
154 Syslog::error("SecTranslocate, TranslocationPath, translocation path calculation failed:\n\toriginal: %s\n\tcalculated: %s",
155 realOriginalPath.c_str(),
156 pathToTranslocate.c_str());
157 UnixError::throwMe(EINVAL);
158 }
159 }
160 else
161 {
162 /*
163 want pathInsideTranslocationPoint to look like:
164 a/b/c
165 i.e. internal / but not at the front or back.
166 */
167 if(pathInsideTranslocationPoint.empty())
168 {
169 pathInsideTranslocationPoint = originalComponents[cnt];
170 }
171 else
172 {
173 pathInsideTranslocationPoint += "/" + originalComponents[cnt];
174 }
175 }
176 }
177 }
178 }
179 }
180
181 /* if we should translocate and a stored path inside the translocation point exists, then add it to the
182 passed in string. If no path inside is stored, then return the passed in string if translocation
183 should occur, and the original path for the TranslocationPath if translocation shouldn't occur */
184 string TranslocationPath::getTranslocatedPathToOriginalPath(const string &translocationPoint) const
185 {
186 string seperator = translocationPoint.back() != '/' ? "/" : "";
187
188 if (should)
189 {
190 if(!pathInsideTranslocationPoint.empty())
191 {
192 return translocationPoint + seperator + pathInsideTranslocationPoint;
193 }
194 else
195 {
196 return translocationPoint;
197 }
198 }
199 else
200 {
201 //If we weren't supposed to translocate return the original path.
202 return realOriginalPath;
203 }
204 }
205
206 /* Given an fd for a path find the outermost acceptable code bundle and return an fd for that.
207 an acceptable outermost bundle is quarantined, user approved, and a code bundle.
208 If nothing is found outside the path to the fd provided, then passed in fd or a copy there of is returned.*/
209 ExtendedAutoFileDesc TranslocationPath::findOuterMostCodeBundleForFD(ExtendedAutoFileDesc &fd)
210 {
211 if( fd.isMountPoint() || !fd.isQuarantined())
212 {
213 return fd;
214 }
215 vector<string> path = splitPath(fd.getRealPath());
216 size_t currentIndex = path.size() - 1;
217 size_t lastGoodIndex = currentIndex;
218
219 string pathToCheck = joinPathUpTo(path, currentIndex);
220 /*
221 Proposed algorithm (pseudo-code):
222 lastGood := path := canonicalized path to be launched
223
224 while path is not a mount point
225 if path is quarantined and not user-approved then exit loop # Gatekeeper has not cleared this code
226 if SecStaticCodeCreateWithPath(path) succeeds # used as an “is a code bundle” oracle
227 then lastGood := path
228 path := parent directory of path
229 return lastGood
230 */
231 while(currentIndex)
232 {
233 ExtendedAutoFileDesc currFd(pathToCheck);
234
235 if (currFd.isMountPoint() || !currFd.isQuarantined() || !currFd.isUserApproved())
236 {
237 break;
238 }
239
240 SecStaticCodeRef staticCodeRef = NULL;
241
242 if( SecStaticCodeCreateWithPath(CFTempURL(currFd.getRealPath()), kSecCSDefaultFlags, &staticCodeRef) == errSecSuccess)
243 {
244 lastGoodIndex = currentIndex;
245 CFRelease(staticCodeRef);
246 }
247
248 currentIndex--;
249 pathToCheck = joinPathUpTo(path, currentIndex);
250 }
251
252 return ExtendedAutoFileDesc(joinPathUpTo(path, lastGoodIndex));
253 }
254
255 /* Given an fd to a translocated file, build the path to the original file
256 Throws if the fd isn't in a nullfs mount for the calling user. */
257 string getOriginalPath(const ExtendedAutoFileDesc& fd, bool* isDir)
258 {
259 if (!fd.isFileSystemType(NULLFS_FSTYPE) ||
260 isDir == NULL ||
261 !fd.isInPrefixDir(fd.getMountPoint()))
262 {
263 Syslog::error("SecTranslocate::getOriginalPath called with invalid params: fs_type = %s, isDir = %p, realPath = %s, mountpoint = %s",
264 fd.getFsType().c_str(),
265 isDir,
266 fd.getRealPath().c_str(),
267 fd.getMountPoint().c_str());
268 UnixError::throwMe(EINVAL);
269 }
270
271 string translocationBaseDir = translocationDirForUser();
272
273 if(!fd.isInPrefixDir(translocationBaseDir))
274 {
275 Syslog::error("SecTranslocate::getOriginal path called with path (%s) that doesn't belong to user (%d)",
276 fd.getRealPath().c_str(),
277 getuid());
278 UnixError::throwMe(EPERM);
279 }
280
281 *isDir = fd.isA(S_IFDIR);
282
283 vector<string> mountFromPath = splitPath(fd.getMountFromPath());
284 vector<string> mountPointPath = splitPath(fd.getMountPoint());
285 vector<string> translocatedRealPath = splitPath(fd.getRealPath());
286
287 if (mountPointPath.size() > translocatedRealPath.size())
288 {
289 Syslog::warning("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
290 UnixError::throwMe(EINVAL);
291 }
292
293 string originalPath = fd.getMountFromPath();
294
295 int i;
296
297 for( i = 0; i<translocatedRealPath.size(); i++)
298 {
299 /* match the mount point directories to the real path directories */
300 if( i < mountPointPath.size())
301 {
302 if(translocatedRealPath[i] != mountPointPath[i])
303 {
304 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
305 UnixError::throwMe(EINVAL);
306 }
307 }
308 /* check for the d directory */
309 else if( i == mountPointPath.size())
310 {
311 if( translocatedRealPath[i] != "d")
312 {
313 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
314 UnixError::throwMe(EINVAL);
315 }
316 }
317 /* check for the app name */
318 else if( i == mountPointPath.size() + 1)
319 {
320 if( translocatedRealPath[i] != mountFromPath.back())
321 {
322 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
323 UnixError::throwMe(EINVAL);
324 }
325 }
326 /* we are past the app name so add what ever is left */
327 else
328 {
329 originalPath +="/"+translocatedRealPath[i];
330 }
331 }
332
333 if( i == mountPointPath.size() || i == mountPointPath.size() + 1)
334 {
335 //Asked for the original path of the mountpoint or /d/
336 Syslog::warning("SecTranslocate: asked for the original path of a virtual directory: %s", fd.getRealPath().c_str());
337 UnixError::throwMe(ENOENT);
338 }
339
340 /* Make sure what we built actually exists */
341 ExtendedAutoFileDesc originalFD(originalPath);
342 if(!originalFD.pathIsAbsolute())
343 {
344 Syslog::error("SecTranslocate: Calculated original path contains symlinks:\n\tExpected: %s\n\tRequested: %s",
345 originalFD.getRealPath().c_str(),
346 originalPath.c_str());
347 UnixError::throwMe(EINVAL);
348 }
349
350 return originalPath;
351 }
352
353 /* Given a path that should be a translocation path, and the path to an app do the following:
354 1. Validate that the translocation path (appPath) is a valid translocation path
355 2. Validate that the translocation path (appPath) is valid for the app specified by originalPath
356 3. Calculate what the mountpoint path would be given the app path
357 */
358 static string getMountpointFromAppPath(const string &appPath, const string &originalPath)
359 {
360 /* assume that appPath looks like:
361 /my/user/temp/dir/AppTranslocation/MY-UUID/d/foo.app
362
363 and assume original path looks like:
364 /my/user/dir/foo.app
365
366 In this function we find and return /my/user/temp/dir/AppTranslocation/MY-UUID/
367 we also verify that the stuff after that in appPath was /d/foo.app if the last directory
368 in originalPath was /foo.app
369 */
370 string result;
371
372 vector<string> app = splitPath(appPath); // throws if empty or not absolute
373 vector<string> original = splitPath(originalPath); //throws if empty or not absolute
374
375 if (original.size() == 0) // had to have at least one directory, can't null mount /
376 {
377 Syslog::error("SecTranslocate: invalid original path: %s", originalPath.c_str());
378 UnixError::throwMe(EINVAL);
379 }
380
381 if (app.size() >= 3 && //the app path must have at least 3 directories, can't null mount onto /
382 app.back() == original.back()) //last directory of both match
383 {
384 app.pop_back();
385 if(app.back() == "d") //last directory of app path is preceded by /d/
386 {
387 app.pop_back();
388 result = joinPath(app);
389 goto end;
390 }
391 }
392
393 Syslog::error("SecTranslocate: invalid app path: %s", appPath.c_str());
394 UnixError::throwMe(EINVAL);
395
396 end:
397 return result;
398 }
399
400 /* Read the mount table and return it in a vector */
401 static vector<struct statfs> getMountTableSnapshot()
402 {
403 vector<struct statfs> mntInfo;
404 int fs_cnt_first = 0;
405 int fs_cnt_second = 0;
406 int retry = 2;
407
408 /*Strategy here is:
409 1. check the current mount table size
410 2. allocate double the required space
411 3. actually read the mount table
412 4. if the read actually filled up that double size try again once otherwise we are done
413 */
414
415 while(retry)
416 {
417 fs_cnt_first = getfsstat(NULL, 0 , MNT_WAIT);
418 if(fs_cnt_first <= 0)
419 {
420 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno);
421 UnixError::throwMe();
422 }
423
424 if( fs_cnt_first == fs_cnt_second)
425 {
426 /* this path only applies on a retry. If our second attempt to get the size is
427 the same as what we already read then break. */
428 break;
429 }
430
431 mntInfo.resize(fs_cnt_first*2);
432
433 fs_cnt_second = getfsstat(mntInfo.data(), (int)(mntInfo.size() * sizeof(struct statfs)), MNT_WAIT);
434 if (fs_cnt_second <= 0)
435 {
436 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno);
437 UnixError::throwMe();
438 }
439
440 if( fs_cnt_second == mntInfo.size())
441 {
442 retry--;
443 }
444 else
445 {
446 mntInfo.resize(fs_cnt_second); // trim the vector to what we actually need
447 break;
448 }
449 }
450
451 if( retry == 0)
452 {
453 Syslog::warning("SecTranslocate: mount table is growing very quickly");
454 }
455
456 return mntInfo;
457 }
458
459 /* Given the directory where app translocations go for this user, the path to the app to be translocated
460 and an optional destination mountpoint path. Check the mount table to see if a mount point already
461 user, for this app. If a destMountPoint is provided, make sure it is for this user, and that
462 exists for this the mountpoint found in the mount table is the same as the one requested */
463 static string mountExistsForUser(const string &translationDirForUser, const TranslocationPath &originalPath, const string &destMountPoint)
464 {
465 string result; // start empty
466
467 if(!destMountPoint.empty())
468 {
469 /* Validate that destMountPoint path is well formed and for this user
470 well formed means it is === translationDirForUser/<1 directory>
471 */
472 vector<string> splitDestMount = splitPath(destMountPoint);
473
474 if(splitDestMount.size() < 2) //translationDirForUser is never /
475 {
476 Syslog::warning("SecTranslocate: invalid destination mount point: %s",
477 destMountPoint.c_str());
478 UnixError::throwMe(EINVAL);
479 }
480
481 splitDestMount.pop_back(); // knock off one directory
482
483 string destBaseDir = joinPath(splitDestMount)+"/"; //translationDirForUser has a / at the end
484
485 if (translationDirForUser != destBaseDir)
486 {
487 Syslog::warning("SecTranslocate: invalid destination mount point for user\n\tExpected: %s\n\tRequested: %s",
488 translationDirForUser.c_str(),
489 destBaseDir.c_str());
490 /* requested destination isn't valid for the user */
491 UnixError::throwMe(EINVAL);
492 }
493 }
494
495 vector <struct statfs> mntbuf = getMountTableSnapshot();
496
497 /* Save the untranslocated inode number*/
498 ExtendedAutoFileDesc::UnixStat untranslocatedStat;
499
500 if (stat(originalPath.getPathToTranslocate().c_str(), &untranslocatedStat))
501 {
502 errno_t err = errno;
503 Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
504 err,
505 originalPath.getPathToTranslocate().c_str());
506 UnixError::throwMe(err);
507 }
508
509 for (auto &i : mntbuf)
510 {
511 string mountOnName = i.f_mntonname;
512 size_t lastNonSlashPos = mountOnName.length() - 1; //start at the end of the string
513
514 /* find the last position of the last non slash character */
515 for(; lastNonSlashPos != 0 && mountOnName[lastNonSlashPos] == '/' ; lastNonSlashPos--);
516
517 /* we want an exact match for originalPath and a prefix match for translationDirForUser
518 also make sure that this is a nullfs mount and that the mount point name is longer than the
519 translation directory with something other than / */
520
521 if (i.f_mntfromname == originalPath.getPathToTranslocate() && //mount is for the requested path
522 strcmp(i.f_fstypename, NULLFS_FSTYPE) == 0 && // mount is a nullfs mount
523 lastNonSlashPos > translationDirForUser.length()-1 && // no shenanigans, there must be more directory here than just the translation dir
524 strncmp(i.f_mntonname, translationDirForUser.c_str(), translationDirForUser.length()) == 0) //mount is inside the translocation dir
525 {
526 if(!destMountPoint.empty())
527 {
528 if (mountOnName != destMountPoint)
529 {
530 /* a mount exists for this path, but its not the one requested */
531 Syslog::warning("SecTranslocate: requested destination doesn't match existing\n\tExpected: %s\n\tRequested: %s",
532 i.f_mntonname,
533 destMountPoint.c_str());
534 UnixError::throwMe(EEXIST);
535 }
536 }
537 /*
538 find the inode number for mountOnName+/d/appname
539 */
540 string pathToTranslocatedApp = mountOnName+"/d/"+originalPath.getComponentNameToTranslocate();
541
542 ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
543
544 if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
545 {
546 /* We should have access to this path and it should be real so complain if thats not true. */
547 errno_t err = errno;
548 Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp.c_str(), err);
549 UnixError::throwMe(err);
550 }
551
552 if(untranslocatedStat.st_ino != oldTranslocatedStat.st_ino)
553 {
554 /* We have two Apps with the same name at the same path but different inodes. This means that the
555 translocated path is broken and should be removed */
556 destroyTranslocatedPathForUser(pathToTranslocatedApp);
557 continue;
558 }
559
560 result = mountOnName;
561 break;
562 }
563 }
564
565 return result;
566 }
567
568 /* Given what we think is a valid mountpoint, perform a sanity check, and clean up if we are wrong */
569 static void validateMountpoint(const string &mountpoint, bool owned)
570 {
571 /* Requirements:
572 1. can be opened
573 2. is a directory
574 3. is not already a mountpoint
575 4. is an absolute path
576 */
577 bool isDir = false;
578 bool isMount = false;
579 bool isEmpty = true;
580
581 try {
582 /* first make sure this is a directory and that it is empty
583 (it could be dangerous to mount over a directory that contains something,
584 unfortunately this is still racy, and mount() is path based so we can't lock
585 down the directory until the mount succeeds (lock down is because of the entitlement
586 checks in nullfs))*/
587 DIR* dir = opendir(mountpoint.c_str());
588 int error = 0;
589
590 if (dir == NULL)
591 {
592 error = errno;
593 Syslog::warning("SecTranslocate: mountpoint is not a directory or doesn't exist: %s",
594 mountpoint.c_str());
595 UnixError::throwMe(error);
596 }
597
598 isDir = true;
599
600 struct dirent *d;
601 struct dirent dirbuf;
602 int cnt = 0;
603 int err = 0;
604 while(((err = readdir_r(dir, &dirbuf, &d)) == 0) &&
605 d != NULL)
606 {
607 /* skip . and .. but break if there is more than that */
608 if(++cnt > 2)
609 {
610 isEmpty = false;
611 break;
612 }
613 }
614
615 error = errno;
616 (void)closedir(dir);
617
618 if(err)
619 {
620 Syslog::warning("SecTranslocate: error while checking that mountpoint is empty");
621 UnixError::throwMe(error);
622 }
623
624 if(!isEmpty)
625 {
626 Syslog::warning("Sectranslocate: mountpoint is not empty: %s",
627 mountpoint.c_str());
628 UnixError::throwMe(EBUSY);
629 }
630
631 /* now check that the path is not a mountpoint */
632 ExtendedAutoFileDesc fd(mountpoint);
633
634 if(!fd.pathIsAbsolute())
635 {
636 Syslog::warning("SecTranslocate: mountpoint isn't fully resolved\n\tExpected: %s\n\tActual: %s",
637 fd.getRealPath().c_str(),
638 mountpoint.c_str());
639 UnixError::throwMe(EINVAL);
640 }
641
642 isMount = fd.isMountPoint();
643
644 if(isMount)
645 {
646 Syslog::warning("SecTranslocate:Translocation failed, new mountpoint is already a mountpoint (%s)",
647 mountpoint.c_str());
648 UnixError::throwMe(EINVAL);
649 }
650 }
651 catch(...)
652 {
653 if(owned)
654 {
655 if (!isMount)
656 {
657 if (isDir)
658 {
659 if(isEmpty)
660 {
661 rmdir(mountpoint.c_str());
662 }
663 /* Already logged the else case above */
664 }
665 else
666 {
667 Syslog::warning("SecTranslocate: unexpected file detected at mountpoint location (%s). Deleting.",
668 mountpoint.c_str());
669 unlink(mountpoint.c_str());
670 }
671 }
672 }
673 rethrow_exception(current_exception());
674 }
675 }
676
677 /* Create and validate the directory that we should mount at but don't create the mount yet */
678 static string makeNewMountpoint(const string &translationDir)
679 {
680 AutoFileDesc fd(getFDForDirectory(translationDir));
681
682 string uuid = makeUUID();
683
684 UnixError::check(mkdirat(fd, uuid.c_str(), 0500));
685
686 string mountpoint = translationDir+uuid;
687
688 validateMountpoint(mountpoint);
689
690 return mountpoint;
691 }
692
693 /* If the original path has mountpoint quarantine info, apply it to the new mountpoint*/
694 static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath)
695 {
696 struct statfs sfsbuf;
697 int error = 0;
698
699 UnixError::check(statfs(originalPath.c_str(), &sfsbuf));
700 qtn_file_t original_attr = qtn_file_alloc();
701
702 if (original_attr != NULL)
703 {
704 if (qtn_file_init_with_mount_point(original_attr, sfsbuf.f_mntonname) == 0)
705 {
706 error = qtn_file_apply_to_mount_point(original_attr, mountPoint.c_str());
707 }
708 qtn_file_free(original_attr);
709 }
710 else
711 {
712 error = errno;
713 }
714
715 if (error)
716 {
717 Syslog::warning("SecTranslocate: Failed to apply quarantine information\n\tMountpoint: %s\n\tOriginal Path: %s",
718 mountPoint.c_str(),
719 originalPath.c_str());
720 UnixError::throwMe(error);
721 }
722 }
723
724 /* Given the path to a new mountpoint and the original path to translocate, calculate the path
725 to the desired app in the new mountpoint, and sanity check that calculation */
726 static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath)
727 {
728 string midPath = mountPoint+"/d";
729 string outPath = originalPath.getTranslocatedPathToOriginalPath(midPath+"/"+originalPath.getComponentNameToTranslocate());
730
731 /* ExtendedAutoFileDesc will throw if one of these doesn't exist or isn't accessible */
732 ExtendedAutoFileDesc mountFd(mountPoint);
733 ExtendedAutoFileDesc midFd(midPath);
734 ExtendedAutoFileDesc outFd(outPath);
735
736 if(!outFd.isFileSystemType(NULLFS_FSTYPE) ||
737 !mountFd.isFileSystemType(NULLFS_FSTYPE) ||
738 !midFd.isFileSystemType(NULLFS_FSTYPE))
739 {
740 Syslog::warning("SecTranslocate::App exists at expected translocation path (%s) but isn't a nullfs mount (%s)",
741 outPath.c_str(),
742 outFd.getFsType().c_str());
743 UnixError::throwMe(EINVAL);
744 }
745
746 if(!outFd.pathIsAbsolute() ||
747 !mountFd.pathIsAbsolute() ||
748 !midFd.pathIsAbsolute() )
749 {
750 Syslog::warning("SecTranslocate::App path isn't resolved\n\tGot: %s\n\tExpected: %s",
751 outFd.getRealPath().c_str(),
752 outPath.c_str());
753 UnixError::throwMe(EINVAL);
754 }
755
756 fsid_t outFsid = outFd.getFsid();
757 fsid_t midFsid = midFd.getFsid();
758 fsid_t mountFsid = mountFd.getFsid();
759
760 /* different fsids mean that there is more than one volume between the expected mountpoint and the expected app path */
761 if (memcmp(&outFsid, &midFsid, sizeof(fsid_t)) != 0 ||
762 memcmp(&outFsid, &mountFsid, sizeof(fsid_t)) != 0)
763 {
764 Syslog::warning("SecTranslocate:: the fsid is not consistent between app, /d/ and mountpoint");
765 UnixError::throwMe(EINVAL);
766 }
767
768 return outFd.getRealPath();
769 }
770
771 /* Create an app translocation point given the original path and an optional destination path.
772 note the destination path can only be an outermost path (where the translocation would happen) and not a path to nested code
773 synchronize the process on the dispatch queue. */
774 string translocatePathForUser(const TranslocationPath &originalPath, const string &destPath)
775 {
776 string newPath;
777 exception_ptr exception(0);
778
779 string mountpoint;
780 bool owned = false;
781 try
782 {
783 const string &toTranslocate = originalPath.getPathToTranslocate();
784 string baseDirForUser = translocationDirForUser(); //throws
785 string destMountPoint;
786 if(!destPath.empty())
787 {
788 destMountPoint = getMountpointFromAppPath(destPath, toTranslocate); //throws or returns a mountpoint
789 }
790
791 mountpoint = mountExistsForUser(baseDirForUser, originalPath, destMountPoint); //throws, detects invalid destMountPoint string
792
793 if (!mountpoint.empty())
794 {
795 /* A mount point exists already so bail*/
796 newPath = newAppPath(mountpoint, originalPath);
797 return newPath; /* exit the block */
798 }
799 if (destMountPoint.empty())
800 {
801 mountpoint = makeNewMountpoint(baseDirForUser); //throws
802 owned = true;
803 }
804 else
805 {
806 AutoFileDesc fd(getFDForDirectory(destMountPoint, &owned)); //throws, makes the directory if it doesn't exist
807
808 validateMountpoint(destMountPoint, owned); //throws
809 mountpoint = destMountPoint;
810 }
811
812 UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, (void*)toTranslocate.c_str()));
813
814 setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws
815
816 newPath = newAppPath(mountpoint, originalPath); //throws
817
818 if (!destPath.empty())
819 {
820 if (newPath != originalPath.getTranslocatedPathToOriginalPath(destPath))
821 {
822 Syslog::warning("SecTranslocate: created app translocation point did not equal requested app translocation point\n\texpected: %s\n\tcreated: %s",
823 newPath.c_str(),
824 destPath.c_str());
825 /* the app at originalPath didn't match the one at destPath */
826 UnixError::throwMe(EINVAL);
827 }
828 }
829 // log that we created a new mountpoint (we don't log when we are re-using)
830 Syslog::warning("SecTranslocateCreateSecureDirectoryForURL: created %s",
831 newPath.c_str());
832 }
833 catch (...)
834 {
835 exception = current_exception();
836
837 if (!mountpoint.empty())
838 {
839 if (owned)
840 {
841 /* try to unmount/delete (best effort)*/
842 unmount(mountpoint.c_str(), 0);
843 rmdir(mountpoint.c_str());
844 }
845 }
846 }
847
848 /* rethrow outside the dispatch block */
849 if (exception)
850 {
851 rethrow_exception(exception);
852 }
853
854 return newPath;
855 }
856
857 /* Loop through the directory in the specified user directory and delete any that aren't mountpoints */
858 static void cleanupTranslocationDirForUser(const string &userDir)
859 {
860 DIR* translocationDir = opendir(userDir.c_str());
861
862 if( translocationDir )
863 {
864 struct dirent de;
865 struct statfs sfbuf;
866 struct dirent * result = NULL;
867
868 while (readdir_r(translocationDir, &de, &result) == 0 && result)
869 {
870 if(result->d_type == DT_DIR)
871 {
872 if (result->d_name[0] == '.')
873 {
874 if(result->d_namlen == 1 ||
875 (result->d_namlen == 2 &&
876 result->d_name[1] == '.'))
877 {
878 /* skip . and .. */
879 continue;
880 }
881 }
882 string nextDir = userDir+string(result->d_name);
883 if (0 == statfs(nextDir.c_str(), &sfbuf) &&
884 nextDir == sfbuf.f_mntonname)
885 {
886 /* its a mount point so continue */
887 continue;
888 }
889
890 /* not a mountpoint so delete it */
891 if(unlinkat(dirfd(translocationDir), result->d_name, AT_REMOVEDIR))
892 {
893 Syslog::warning("SecTranslocate: failed to delete directory during cleanup (error %d)\n\tUser Dir: %s\n\tDir to delete: %s",
894 errno,
895 userDir.c_str(),
896 result->d_name);
897 }
898 }
899 }
900 closedir(translocationDir);
901 }
902 }
903
904 /* Unmount and delete a directory */
905 static int removeMountPoint(const string &mountpoint, bool force)
906 {
907 int error = 0;
908
909 if (0 == unmount(mountpoint.c_str(), force ? MNT_FORCE : 0) &&
910 0 == rmdir(mountpoint.c_str()))
911 {
912 Syslog::warning("SecTranslocate: removed mountpoint: %s",
913 mountpoint.c_str());
914 }
915 else
916 {
917 error = errno;
918 Syslog::warning("SecTranslocate: failed to unmount/remove mount point (errno: %d): %s",
919 error, mountpoint.c_str());
920 }
921
922 return error;
923 }
924
925 /* Destroy the specified translocated path, and clean up the user's translocation directory.
926 It is the caller's responsibility to synchronize the operation on the dispatch queue. */
927 bool destroyTranslocatedPathForUser(const string &translocatedPath)
928 {
929 bool result = false;
930 int error = 0;
931 /* steps
932 1. verify the translocatedPath is for the user
933 2. verify it is a nullfs mountpoint (with app path)
934 3. unmount it
935 4. delete it
936 5. loop through all the other directories in the app translation directory looking for directories not mounted on and delete them.
937 */
938
939 string baseDirForUser = translocationDirForUser(); // throws
940 bool shouldUnmount = false;
941 string translocatedMountpoint;
942
943 { //Use a block to get rid of the file descriptor before we try to unmount.
944 ExtendedAutoFileDesc fd(translocatedPath);
945 translocatedMountpoint = fd.getMountPoint();
946 /*
947 To support unmount when nested apps end, just make sure that the requested path is on a translocation
948 point for this user, not that they asked for a translocation point to be removed.
949 */
950 shouldUnmount = fd.isInPrefixDir(baseDirForUser) && fd.isFileSystemType(NULLFS_FSTYPE);
951 }
952
953 if (shouldUnmount)
954 {
955 error = removeMountPoint(translocatedMountpoint);
956 result = error == 0;
957 }
958
959 if (!result && !error)
960 {
961 Syslog::warning("SecTranslocate: mountpoint does not belong to user(%d): %s",
962 getuid(),
963 translocatedPath.c_str());
964 error = EPERM;
965 }
966
967 cleanupTranslocationDirForUser(baseDirForUser);
968
969 if (error)
970 {
971 UnixError::throwMe(error);
972 }
973
974 return result;
975 }
976
977 /* Cleanup any translocation directories for this user that are either mounted from the
978 specified volume or from a volume that doesn't exist anymore. If an empty volumePath
979 is provided this has the effect of only cleaning up translocation points that point
980 to volumes that don't exist anymore.
981
982 It is the caller's responsibility to synchronize the operation on the dispatch queue.
983 */
984 bool destroyTranslocatedPathsForUserOnVolume(const string &volumePath)
985 {
986 bool cleanupError = false;
987 string baseDirForUser = translocationDirForUser();
988 vector <struct statfs> mountTable = getMountTableSnapshot();
989 struct statfs sb;
990 fsid_t unmountingFsid;
991 int haveUnmountingFsid = statfs(volumePath.c_str(), &sb);
992 int haveMntFromState = 0;
993
994 memset(&unmountingFsid, 0, sizeof(unmountingFsid));
995
996 if(haveUnmountingFsid == 0) {
997 unmountingFsid = sb.f_fsid;
998 }
999
1000 for (auto &mnt : mountTable)
1001 {
1002 /*
1003 we need to look at each translocation mount and check
1004 1. is it ours
1005 2. does its mntfromname still exist, if it doesn't unmount it
1006 3. if it does, is it the same as the volume we are cleaning up?, if so unmount it.
1007 */
1008 if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 &&
1009 strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0)
1010 {
1011 haveMntFromState = statfs(mnt.f_mntfromname, &sb);
1012
1013 if (haveMntFromState != 0)
1014 {
1015 // In this case we are trying to unmount a translocation point that points to nothing. Force it.
1016 // Not forcing it currently hangs in UBC cleanup.
1017 (void)removeMountPoint(mnt.f_mntonname , true);
1018 }
1019 else if (haveUnmountingFsid == 0)
1020 {
1021 fsid_t toCheckFsid = sb.f_fsid;
1022 if( memcmp(&unmountingFsid, &toCheckFsid, sizeof(fsid_t)) == 0)
1023 {
1024 if(removeMountPoint(mnt.f_mntonname) != 0)
1025 {
1026 cleanupError = true;
1027 }
1028 }
1029 }
1030 }
1031 }
1032
1033 return !cleanupError;
1034 }
1035
1036 /* This is intended to be used periodically to clean up translocation points that aren't used anymore */
1037 void tryToDestroyUnusedTranslocationMounts()
1038 {
1039 vector <struct statfs> mountTable = getMountTableSnapshot();
1040 string baseDirForUser = translocationDirForUser();
1041
1042 for (auto &mnt : mountTable)
1043 {
1044 if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 &&
1045 strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0)
1046 {
1047 ExtendedAutoFileDesc volumeToCheck(mnt.f_mntfromname, O_RDONLY, FileDesc::modeMissingOk);
1048
1049 // Try to destroy the mount point. If the mirroed volume (volumeToCheck) isn't open then force it.
1050 // Not forcing it currently hangs in UBC cleanup.
1051 (void)removeMountPoint(mnt.f_mntonname , !volumeToCheck.isOpen());
1052 }
1053 }
1054 }
1055
1056 } //namespace SecTranslocate
1057 }// namespace Security