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