2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
31 #include <sys/param.h>
32 #include <sys/mount.h>
33 #include <sys/ucred.h>
34 #include <dispatch/dispatch.h>
38 #define __APPLE_API_PRIVATE
39 #include <quarantine.h>
40 #undef __APPLE_API_PRIVATE
42 #include <security_utilities/cfutilities.h>
43 #include <security_utilities/unix++.h>
44 #include <security_utilities/logging.h>
45 #include <Security/SecStaticCode.h>
47 #include "SecTranslocateShared.hpp"
48 #include "SecTranslocateUtilities.hpp"
49 #include "SecTranslocateEnumUtils.hpp"
51 #define NULLM_UNVEIL 0x1ULL << 2
52 struct null_mount_conf
{
59 namespace SecTranslocate
{
63 /* String Constants for XPC dictionary passing */
64 /* XPC Function keys */
65 const char* kSecTranslocateXPCFuncCreate
= "create";
66 const char* kSecTranslocateXPCFuncCheckIn
= "check-in";
68 /* XPC message argument keys */
69 const char* kSecTranslocateXPCMessageFunction
= "function";
70 const char* kSecTranslocateXPCMessageOriginalPath
= "original";
71 const char* kSecTranslocateXPCMessageDestinationPath
= "dest";
72 const char* kSecTranslocateXPCMessageOptions
= "opts";
73 const char* kSecTranslocateXPCMessagePid
= "pid";
75 /*XPC message reply keys */
76 const char* kSecTranslocateXPCReplyError
= "error";
77 const char* kSecTranslocateXPCReplySecurePath
= "result";
79 //Functions used only within this file
80 static void setMountPointQuarantineIfNecessary(const string
&mountPoint
, const string
&originalPath
);
81 static string
getMountpointFromAppPath(const string
&appPath
, const string
&originalPath
);
83 static vector
<struct statfs
> getMountTableSnapshot();
84 static string
mountExistsForUser(const string
&translationDirForUser
,
85 const TranslocationPath
&originalPath
,
86 const string
&destMount
);
87 static void validateMountpoint(const string
&mountpoint
, bool owned
=false);
88 static string
makeNewMountpoint(const string
&translationDir
);
89 static string
newAppPath (const string
&mountPoint
, const TranslocationPath
&originalPath
);
90 static void cleanupTranslocationDirForUser(const string
&userDir
);
91 static int removeMountPoint(const string
&mountpoint
, bool force
= false);
93 /* calculate whether a translocation should occur and where from */
94 TranslocationPath::TranslocationPath(string originalPath
, TranslocationOptions opts
)
97 /* To support testing of translocation the policy is as follows:
98 1. When the quarantine translocation sysctl is off, always translocate
99 if we aren't already on a translocated mount point.
100 2. When the quarantine translocation sysctl is on, use the quarantine
102 when asking if a path should run translocated need to:
103 check the current quarantine state of the path asked about
104 if it is already on a nullfs mount
106 else if it is unquarantined
109 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
112 find the outermost acceptable code bundle
113 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
118 See findOuterMostCodeBundleForFD for more info about what an acceptable outermost bundle is
119 in particular it should be noted that the outermost acceptable bundle for a quarantined inner
120 bundle can not be unquarantined. If the inner bundle is quarantined then any bundle containing it
121 must also have been quarantined.
124 ExtendedAutoFileDesc
fd(originalPath
);
128 realOriginalPath
= fd
.getRealPath();
130 /* don't translocate if it already is */
131 /* only consider translocation if the thing being asked about is marked for translocation */
132 /* Nullfs can't translocate other mount's roots so abort if its a mountpoint */
133 if(!fd
.isFileSystemType(NULLFS_FSTYPE
) && fd
.isQuarantined() && fd
.shouldTranslocate() && !fd
.isMountPoint())
135 ExtendedAutoFileDesc
&&outermost
= findOuterMostCodeBundleForFD(fd
);
137 should
= outermost
.isQuarantined() && outermost
.shouldTranslocate();
138 pathToTranslocate
= outermost
.getRealPath();
140 /* Calculate the path that will be needed to give the caller the path they asked for originally but in the translocated place */
143 vector
<string
> originalComponents
= splitPath(realOriginalPath
);
144 vector
<string
> toTranslocateComponents
= splitPath(pathToTranslocate
);
146 if (toTranslocateComponents
.size() == 0 ||
147 toTranslocateComponents
.size() > originalComponents
.size())
149 Syslog::error("SecTranslocate, TranslocationPath, path calculation failed:\n\toriginal: %s\n\tcalculated: %s",
150 realOriginalPath
.c_str(),
151 pathToTranslocate
.c_str());
152 UnixError::throwMe(EINVAL
);
155 componentNameToTranslocate
= toTranslocateComponents
.back();
157 for(size_t cnt
= 0; cnt
< originalComponents
.size(); cnt
++)
159 if (cnt
< toTranslocateComponents
.size())
161 if (toTranslocateComponents
[cnt
] != originalComponents
[cnt
])
163 Syslog::error("SecTranslocate, TranslocationPath, translocation path calculation failed:\n\toriginal: %s\n\tcalculated: %s",
164 realOriginalPath
.c_str(),
165 pathToTranslocate
.c_str());
166 UnixError::throwMe(EINVAL
);
172 want pathInsideTranslocationPoint to look like:
174 i.e. internal / but not at the front or back.
176 if(pathInsideTranslocationPoint
.empty())
178 pathInsideTranslocationPoint
= originalComponents
[cnt
];
182 pathInsideTranslocationPoint
+= "/" + originalComponents
[cnt
];
190 /* if we should translocate and a stored path inside the translocation point exists, then add it to the
191 passed in string. If no path inside is stored, then return the passed in string if translocation
192 should occur, and the original path for the TranslocationPath if translocation shouldn't occur */
193 string
TranslocationPath::getTranslocatedPathToOriginalPath(const string
&translocationPoint
) const
195 string seperator
= translocationPoint
.back() != '/' ? "/" : "";
199 if(!pathInsideTranslocationPoint
.empty())
201 return translocationPoint
+ seperator
+ pathInsideTranslocationPoint
;
205 return translocationPoint
;
210 //If we weren't supposed to translocate return the original path.
211 return realOriginalPath
;
215 /* Given an fd for a path find the outermost acceptable code bundle and return an fd for that.
216 an acceptable outermost bundle is quarantined, user approved, and a code bundle.
217 If nothing is found outside the path to the fd provided, then passed in fd or a copy there of is returned.*/
218 ExtendedAutoFileDesc
TranslocationPath::findOuterMostCodeBundleForFD(ExtendedAutoFileDesc
&fd
)
220 if( fd
.isMountPoint() || !fd
.isQuarantined())
224 vector
<string
> path
= splitPath(fd
.getRealPath());
225 size_t currentIndex
= path
.size() - 1;
226 size_t lastGoodIndex
= currentIndex
;
228 string pathToCheck
= joinPathUpTo(path
, currentIndex
);
230 Proposed algorithm (pseudo-code):
231 lastGood := path := canonicalized path to be launched
233 while path is not a mount point
234 if path is quarantined and not user-approved then exit loop # Gatekeeper has not cleared this code
235 if SecStaticCodeCreateWithPath(path) succeeds # used as an “is a code bundle” oracle
236 then lastGood := path
237 path := parent directory of path
242 ExtendedAutoFileDesc
currFd(pathToCheck
);
244 if (currFd
.isMountPoint() || !currFd
.isQuarantined() || !currFd
.isUserApproved())
249 SecStaticCodeRef staticCodeRef
= NULL
;
251 if( SecStaticCodeCreateWithPath(CFTempURL(currFd
.getRealPath()), kSecCSDefaultFlags
, &staticCodeRef
) == errSecSuccess
)
253 lastGoodIndex
= currentIndex
;
254 CFRelease(staticCodeRef
);
258 pathToCheck
= joinPathUpTo(path
, currentIndex
);
261 return ExtendedAutoFileDesc(joinPathUpTo(path
, lastGoodIndex
));
264 GenericTranslocationPath::GenericTranslocationPath(const string
& path
, TranslocationOptions opts
) {
265 ExtendedAutoFileDesc
fd(path
);
266 realOriginalPath
= fd
.getRealPath();
270 /* don't translocate if it already is */
271 /* Nullfs can't translocate other mount's roots so abort if its a mountpoint */
272 if(fd
.isFileSystemType(NULLFS_FSTYPE
) || fd
.isMountPoint()) {
276 componentNameToTranslocate
= splitPath(path
).back();
281 /* Given an fd to a translocated file, build the path to the original file
282 Throws if the fd isn't in a nullfs mount. */
283 string
getOriginalPath(const ExtendedAutoFileDesc
& fd
, bool* isDir
)
285 if (!fd
.isFileSystemType(NULLFS_FSTYPE
) ||
287 !fd
.isInPrefixDir(fd
.getMountPoint()))
289 Syslog::error("SecTranslocate::getOriginalPath called with invalid params: fs_type = %s, isDir = %p, realPath = %s, mountpoint = %s",
290 fd
.getFsType().c_str(),
292 fd
.getRealPath().c_str(),
293 fd
.getMountPoint().c_str());
294 UnixError::throwMe(EINVAL
);
297 *isDir
= fd
.isA(S_IFDIR
);
299 vector
<string
> mountFromPath
= splitPath(fd
.getMountFromPath());
300 vector
<string
> mountPointPath
= splitPath(fd
.getMountPoint());
301 vector
<string
> translocatedRealPath
= splitPath(fd
.getRealPath());
303 if (mountPointPath
.size() > translocatedRealPath
.size())
305 Syslog::warning("SecTranslocate: invalid translocated path %s", fd
.getRealPath().c_str());
306 UnixError::throwMe(EINVAL
);
309 string originalPath
= fd
.getMountFromPath();
313 for( i
= 0; i
<translocatedRealPath
.size(); i
++)
315 /* match the mount point directories to the real path directories */
316 if( i
< mountPointPath
.size())
318 if(translocatedRealPath
[i
] != mountPointPath
[i
])
320 Syslog::error("SecTranslocate: invalid translocated path %s", fd
.getRealPath().c_str());
321 UnixError::throwMe(EINVAL
);
324 /* check for the d directory */
325 else if( i
== mountPointPath
.size())
327 if( translocatedRealPath
[i
] != "d")
329 Syslog::error("SecTranslocate: invalid translocated path %s", fd
.getRealPath().c_str());
330 UnixError::throwMe(EINVAL
);
333 /* check for the app name */
334 else if( i
== mountPointPath
.size() + 1)
336 if( translocatedRealPath
[i
] != mountFromPath
.back())
338 Syslog::error("SecTranslocate: invalid translocated path %s", fd
.getRealPath().c_str());
339 UnixError::throwMe(EINVAL
);
342 /* we are past the app name so add what ever is left */
345 originalPath
+="/"+translocatedRealPath
[i
];
349 if( i
== mountPointPath
.size() || i
== mountPointPath
.size() + 1)
351 //Asked for the original path of the mountpoint or /d/
352 Syslog::warning("SecTranslocate: asked for the original path of a virtual directory: %s", fd
.getRealPath().c_str());
353 UnixError::throwMe(ENOENT
);
356 /* Make sure what we built actually exists */
357 ExtendedAutoFileDesc
originalFD(originalPath
);
358 if(!originalFD
.pathIsAbsolute())
360 Syslog::error("SecTranslocate: Calculated original path contains symlinks:\n\tExpected: %s\n\tRequested: %s",
361 originalFD
.getRealPath().c_str(),
362 originalPath
.c_str());
363 UnixError::throwMe(EINVAL
);
369 /* Given a path that should be a translocation path, and the path to an app do the following:
370 1. Validate that the translocation path (appPath) is a valid translocation path
371 2. Validate that the translocation path (appPath) is valid for the app specified by originalPath
372 3. Calculate what the mountpoint path would be given the app path
374 static string
getMountpointFromAppPath(const string
&appPath
, const string
&originalPath
)
376 /* assume that appPath looks like:
377 /my/user/temp/dir/AppTranslocation/MY-UUID/d/foo.app
379 and assume original path looks like:
382 In this function we find and return /my/user/temp/dir/AppTranslocation/MY-UUID/
383 we also verify that the stuff after that in appPath was /d/foo.app if the last directory
384 in originalPath was /foo.app
388 vector
<string
> app
= splitPath(appPath
); // throws if empty or not absolute
389 vector
<string
> original
= splitPath(originalPath
); //throws if empty or not absolute
391 if (original
.size() == 0) // had to have at least one directory, can't null mount /
393 Syslog::error("SecTranslocate: invalid original path: %s", originalPath
.c_str());
394 UnixError::throwMe(EINVAL
);
397 if (app
.size() >= 3 && //the app path must have at least 3 directories, can't null mount onto /
398 app
.back() == original
.back()) //last directory of both match
401 if(app
.back() == "d") //last directory of app path is preceded by /d/
404 result
= joinPath(app
);
409 Syslog::error("SecTranslocate: invalid app path: %s", appPath
.c_str());
410 UnixError::throwMe(EINVAL
);
416 /* Read the mount table and return it in a vector */
417 static vector
<struct statfs
> getMountTableSnapshot()
419 vector
<struct statfs
> mntInfo
;
420 int fs_cnt_first
= 0;
421 int fs_cnt_second
= 0;
425 1. check the current mount table size
426 2. allocate double the required space
427 3. actually read the mount table
428 4. if the read actually filled up that double size try again once otherwise we are done
433 fs_cnt_first
= getfsstat(NULL
, 0 , MNT_WAIT
);
434 if(fs_cnt_first
<= 0)
436 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno
);
437 UnixError::throwMe();
440 if( fs_cnt_first
== fs_cnt_second
)
442 /* this path only applies on a retry. If our second attempt to get the size is
443 the same as what we already read then break. */
447 mntInfo
.resize(fs_cnt_first
*2);
449 fs_cnt_second
= getfsstat(mntInfo
.data(), (int)(mntInfo
.size() * sizeof(struct statfs
)), MNT_WAIT
);
450 if (fs_cnt_second
<= 0)
452 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno
);
453 UnixError::throwMe();
456 if( fs_cnt_second
== mntInfo
.size())
462 mntInfo
.resize(fs_cnt_second
); // trim the vector to what we actually need
469 Syslog::warning("SecTranslocate: mount table is growing very quickly");
475 static bool pathExistsInMountTable(const GenericTranslocationPath
& path
, const string
& mountpoint
)
477 vector
<struct statfs
> mntbuf
= getMountTableSnapshot();
479 /* Save the untranslocated inode number*/
480 ExtendedAutoFileDesc::UnixStat untranslocatedStat
;
482 if (stat(path
.getOriginalRealPath().c_str(), &untranslocatedStat
))
485 Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
487 path
.getOriginalRealPath().c_str());
488 UnixError::throwMe(err
);
491 for (auto &i
: mntbuf
)
493 string mountOnName
= i
.f_mntonname
;
495 if (path
.getOriginalRealPath() == i
.f_mntfromname
&& //mount is for the requested path
496 mountpoint
== mountOnName
&& //mount to is the same
497 strcmp(i
.f_fstypename
, NULLFS_FSTYPE
) == 0 // mount is a nullfs mount
501 find the inode number for mountOnName
503 string pathToTranslocatedApp
= mountOnName
+"/d/"+path
.getComponentNameToTranslocate();
505 ExtendedAutoFileDesc::UnixStat oldTranslocatedStat
;
507 if (stat(pathToTranslocatedApp
.c_str(), &oldTranslocatedStat
))
509 /* We should have access to this path and it should be real so complain if thats not true. */
511 Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp
.c_str(), err
);
512 UnixError::throwMe(err
);
515 if(untranslocatedStat
.st_ino
!= oldTranslocatedStat
.st_ino
)
517 /* We have two Apps with the same name at the same path but different inodes. This means that the
518 translocated path is broken and should be removed */
519 destroyTranslocatedPathForUser(pathToTranslocatedApp
);
530 /* Given the directory where app translocations go for this user, the path to the app to be translocated
531 and an optional destination mountpoint path. Check the mount table to see if a mount point already
532 user, for this app. If a destMountPoint is provided, make sure it is for this user, and that
533 exists for this the mountpoint found in the mount table is the same as the one requested */
534 static string
mountExistsForUser(const string
&translationDirForUser
, const TranslocationPath
&originalPath
, const string
&destMountPoint
)
536 string result
; // start empty
538 if(!destMountPoint
.empty())
540 /* Validate that destMountPoint path is well formed and for this user
541 well formed means it is === translationDirForUser/<1 directory>
543 vector
<string
> splitDestMount
= splitPath(destMountPoint
);
545 if(splitDestMount
.size() < 2) //translationDirForUser is never /
547 Syslog::warning("SecTranslocate: invalid destination mount point: %s",
548 destMountPoint
.c_str());
549 UnixError::throwMe(EINVAL
);
552 splitDestMount
.pop_back(); // knock off one directory
554 string destBaseDir
= joinPath(splitDestMount
)+"/"; //translationDirForUser has a / at the end
556 if (translationDirForUser
!= destBaseDir
)
558 Syslog::warning("SecTranslocate: invalid destination mount point for user\n\tExpected: %s\n\tRequested: %s",
559 translationDirForUser
.c_str(),
560 destBaseDir
.c_str());
561 /* requested destination isn't valid for the user */
562 UnixError::throwMe(EINVAL
);
566 vector
<struct statfs
> mntbuf
= getMountTableSnapshot();
568 /* Save the untranslocated inode number*/
569 ExtendedAutoFileDesc::UnixStat untranslocatedStat
;
571 if (stat(originalPath
.getPathToTranslocate().c_str(), &untranslocatedStat
))
574 Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
576 originalPath
.getPathToTranslocate().c_str());
577 UnixError::throwMe(err
);
580 for (auto &i
: mntbuf
)
582 string mountOnName
= i
.f_mntonname
;
583 size_t lastNonSlashPos
= mountOnName
.length() - 1; //start at the end of the string
585 /* find the last position of the last non slash character */
586 for(; lastNonSlashPos
!= 0 && mountOnName
[lastNonSlashPos
] == '/' ; lastNonSlashPos
--);
588 /* we want an exact match for originalPath and a prefix match for translationDirForUser
589 also make sure that this is a nullfs mount and that the mount point name is longer than the
590 translation directory with something other than / */
592 if (i
.f_mntfromname
== originalPath
.getPathToTranslocate() && //mount is for the requested path
593 strcmp(i
.f_fstypename
, NULLFS_FSTYPE
) == 0 && // mount is a nullfs mount
594 lastNonSlashPos
> translationDirForUser
.length()-1 && // no shenanigans, there must be more directory here than just the translation dir
595 strncmp(i
.f_mntonname
, translationDirForUser
.c_str(), translationDirForUser
.length()) == 0) //mount is inside the translocation dir
597 if(!destMountPoint
.empty())
599 if (mountOnName
!= destMountPoint
)
601 /* a mount exists for this path, but its not the one requested */
602 Syslog::warning("SecTranslocate: requested destination doesn't match existing\n\tExpected: %s\n\tRequested: %s",
604 destMountPoint
.c_str());
605 UnixError::throwMe(EEXIST
);
609 find the inode number for mountOnName+/d/appname
611 string pathToTranslocatedApp
= mountOnName
+"/d/"+originalPath
.getComponentNameToTranslocate();
613 ExtendedAutoFileDesc::UnixStat oldTranslocatedStat
;
615 if (stat(pathToTranslocatedApp
.c_str(), &oldTranslocatedStat
))
617 /* We should have access to this path and it should be real so complain if thats not true. */
619 Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp
.c_str(), err
);
620 UnixError::throwMe(err
);
623 if(untranslocatedStat
.st_ino
!= oldTranslocatedStat
.st_ino
)
625 /* We have two Apps with the same name at the same path but different inodes. This means that the
626 translocated path is broken and should be removed */
627 destroyTranslocatedPathForUser(pathToTranslocatedApp
);
631 result
= mountOnName
;
639 /* Given what we think is a valid mountpoint, perform a sanity check, and clean up if we are wrong */
640 static void validateMountpoint(const string
&mountpoint
, bool owned
)
645 3. is not already a mountpoint
646 4. is an absolute path
649 bool isMount
= false;
653 /* first make sure this is a directory and that it is empty
654 (it could be dangerous to mount over a directory that contains something,
655 unfortunately this is still racy, and mount() is path based so we can't lock
656 down the directory until the mount succeeds (lock down is because of the entitlement
658 DIR* dir
= opendir(mountpoint
.c_str());
664 Syslog::warning("SecTranslocate: mountpoint is not a directory or doesn't exist: %s",
666 UnixError::throwMe(error
);
672 struct dirent dirbuf
;
675 while(((err
= readdir_r(dir
, &dirbuf
, &d
)) == 0) &&
678 /* skip . and .. but break if there is more than that */
691 Syslog::warning("SecTranslocate: error while checking that mountpoint is empty");
692 UnixError::throwMe(error
);
697 Syslog::warning("Sectranslocate: mountpoint is not empty: %s",
699 UnixError::throwMe(EBUSY
);
702 /* now check that the path is not a mountpoint */
703 ExtendedAutoFileDesc
fd(mountpoint
);
705 if(!fd
.pathIsAbsolute())
707 Syslog::warning("SecTranslocate: mountpoint isn't fully resolved\n\tExpected: %s\n\tActual: %s",
708 fd
.getRealPath().c_str(),
710 UnixError::throwMe(EINVAL
);
713 isMount
= fd
.isMountPoint();
717 Syslog::warning("SecTranslocate:Translocation failed, new mountpoint is already a mountpoint (%s)",
719 UnixError::throwMe(EINVAL
);
732 rmdir(mountpoint
.c_str());
734 /* Already logged the else case above */
738 Syslog::warning("SecTranslocate: unexpected file detected at mountpoint location (%s). Deleting.",
740 unlink(mountpoint
.c_str());
744 rethrow_exception(current_exception());
748 /* Create and validate the directory that we should mount at but don't create the mount yet */
749 static string
makeNewMountpoint(const string
&translationDir
)
751 AutoFileDesc
fd(getFDForDirectory(translationDir
));
753 string uuid
= makeUUID();
755 UnixError::check(mkdirat(fd
, uuid
.c_str(), 0500));
757 string mountpoint
= translationDir
+uuid
;
759 validateMountpoint(mountpoint
);
764 /* If the original path has mountpoint quarantine info, apply it to the new mountpoint*/
765 static void setMountPointQuarantineIfNecessary(const string
&mountPoint
, const string
&originalPath
)
767 struct statfs sfsbuf
;
770 UnixError::check(statfs(originalPath
.c_str(), &sfsbuf
));
771 qtn_file_t original_attr
= qtn_file_alloc();
773 if (original_attr
!= NULL
)
775 if (qtn_file_init_with_mount_point(original_attr
, sfsbuf
.f_mntonname
) == 0)
777 error
= qtn_file_apply_to_mount_point(original_attr
, mountPoint
.c_str());
779 qtn_file_free(original_attr
);
788 Syslog::warning("SecTranslocate: Failed to apply quarantine information\n\tMountpoint: %s\n\tOriginal Path: %s",
790 originalPath
.c_str());
791 UnixError::throwMe(error
);
795 static string
newAppPathFrom (const string
&mountPoint
, const string
&outPath
)
797 string midPath
= mountPoint
+"/d";
799 /* ExtendedAutoFileDesc will throw if one of these doesn't exist or isn't accessible */
800 ExtendedAutoFileDesc
mountFd(mountPoint
);
801 ExtendedAutoFileDesc
midFd(midPath
);
802 ExtendedAutoFileDesc
outFd(outPath
);
804 if(!outFd
.isFileSystemType(NULLFS_FSTYPE
) ||
805 !mountFd
.isFileSystemType(NULLFS_FSTYPE
) ||
806 !midFd
.isFileSystemType(NULLFS_FSTYPE
))
808 Syslog::warning("SecTranslocate::App exists at expected translocation path (%s) but isn't a nullfs mount (%s)",
810 outFd
.getFsType().c_str());
811 UnixError::throwMe(EINVAL
);
814 if(!outFd
.pathIsAbsolute() ||
815 !mountFd
.pathIsAbsolute() ||
816 !midFd
.pathIsAbsolute() )
818 Syslog::warning("SecTranslocate::App path isn't resolved\n\tGot: %s\n\tExpected: %s",
819 outFd
.getRealPath().c_str(),
821 UnixError::throwMe(EINVAL
);
824 fsid_t outFsid
= outFd
.getFsid();
825 fsid_t midFsid
= midFd
.getFsid();
826 fsid_t mountFsid
= mountFd
.getFsid();
828 /* different fsids mean that there is more than one volume between the expected mountpoint and the expected app path */
829 if (memcmp(&outFsid
, &midFsid
, sizeof(fsid_t
)) != 0 ||
830 memcmp(&outFsid
, &mountFsid
, sizeof(fsid_t
)) != 0)
832 Syslog::warning("SecTranslocate:: the fsid is not consistent between app, /d/ and mountpoint");
833 UnixError::throwMe(EINVAL
);
836 return outFd
.getRealPath();
840 static string
newAppPath (const string
&mountPoint
, const GenericTranslocationPath
&originalPath
)
842 string outPath
= mountPoint
+"/d/"+originalPath
.getComponentNameToTranslocate();
843 return newAppPathFrom(mountPoint
, outPath
);
846 /* Given the path to a new mountpoint and the original path to translocate, calculate the path
847 to the desired app in the new mountpoint, and sanity check that calculation */
848 static string
newAppPath (const string
&mountPoint
, const TranslocationPath
&originalPath
)
850 string outPath
= originalPath
.getTranslocatedPathToOriginalPath(mountPoint
+"/d/"+originalPath
.getComponentNameToTranslocate());
851 return newAppPathFrom(mountPoint
, outPath
);
854 static std::vector
<char> getMountData(const string
& toTranslocate
, TranslocationOptions opts
) {
855 std::vector
<char> data
;
856 data
.reserve(sizeof(null_mount_conf
) + toTranslocate
.size() + 1);
857 null_mount_conf conf
= {0};
858 if ((opts
& TranslocationOptions::Unveil
) == TranslocationOptions::Unveil
) {
859 conf
.flags
= NULLM_UNVEIL
;
861 data
.insert(data
.end(), reinterpret_cast<const char*>(&conf
), reinterpret_cast<const char*>(&conf
+ 1));
862 data
.insert(data
.end(), toTranslocate
.c_str(), toTranslocate
.c_str() + toTranslocate
.size());
863 data
.push_back('\0');
867 /* Create an app translocation point given the original path and an optional destination path.
868 note the destination path can only be an outermost path (where the translocation would happen) and not a path to nested code
869 synchronize the process on the dispatch queue. */
870 string
translocatePathForUser(const TranslocationPath
&originalPath
, const string
&destPath
)
873 exception_ptr
exception(0);
879 const string
&toTranslocate
= originalPath
.getPathToTranslocate();
880 string baseDirForUser
= translocationDirForUser(); //throws
881 string destMountPoint
;
882 if(!destPath
.empty())
884 destMountPoint
= getMountpointFromAppPath(destPath
, toTranslocate
); //throws or returns a mountpoint
887 mountpoint
= mountExistsForUser(baseDirForUser
, originalPath
, destMountPoint
); //throws, detects invalid destMountPoint string
889 if (!mountpoint
.empty())
891 /* A mount point exists already so bail*/
892 newPath
= newAppPath(mountpoint
, originalPath
);
893 return newPath
; /* exit the block */
895 if (destMountPoint
.empty())
897 mountpoint
= makeNewMountpoint(baseDirForUser
); //throws
902 AutoFileDesc
fd(getFDForDirectory(destMountPoint
, &owned
)); //throws, makes the directory if it doesn't exist
904 validateMountpoint(destMountPoint
, owned
); //throws
905 mountpoint
= destMountPoint
;
908 auto mount_data
= getMountData(toTranslocate
, originalPath
.getOptions());
909 UnixError::check(mount(NULLFS_FSTYPE
, mountpoint
.c_str(), MNT_RDONLY
, mount_data
.data()));
911 setMountPointQuarantineIfNecessary(mountpoint
, toTranslocate
); //throws
913 newPath
= newAppPath(mountpoint
, originalPath
); //throws
915 if (!destPath
.empty())
917 if (newPath
!= originalPath
.getTranslocatedPathToOriginalPath(destPath
))
919 Syslog::warning("SecTranslocate: created app translocation point did not equal requested app translocation point\n\texpected: %s\n\tcreated: %s",
922 /* the app at originalPath didn't match the one at destPath */
923 UnixError::throwMe(EINVAL
);
926 // log that we created a new mountpoint (we don't log when we are re-using)
927 Syslog::warning("SecTranslocateCreateSecureDirectoryForURL: created %s",
932 exception
= current_exception();
934 if (!mountpoint
.empty())
938 /* try to unmount/delete (best effort)*/
939 unmount(mountpoint
.c_str(), 0);
940 rmdir(mountpoint
.c_str());
945 /* rethrow outside the dispatch block */
948 rethrow_exception(exception
);
954 string
translocatePathForUser(const GenericTranslocationPath
&originalPath
, const string
&destPath
)
957 exception_ptr
exception(0);
959 string mountpoint
= destPath
;
963 const string
&toTranslocate
= originalPath
.getOriginalRealPath();
964 if (pathExistsInMountTable(originalPath
, destPath
))
966 /* A mount point exists already so bail*/
967 newPath
= newAppPath(mountpoint
, originalPath
);
968 return newPath
; /* exit the block */
971 AutoFileDesc
fd(getFDForDirectory(mountpoint
, &owned
)); //throws, makes the directory if it doesn't exist
973 validateMountpoint(mountpoint
, owned
); //throws
975 auto mount_data
= getMountData(toTranslocate
, originalPath
.getOptions());
976 UnixError::check(mount(NULLFS_FSTYPE
, mountpoint
.c_str(), MNT_RDONLY
, mount_data
.data()));
978 setMountPointQuarantineIfNecessary(mountpoint
, toTranslocate
); //throws
980 newPath
= newAppPath(mountpoint
, originalPath
); //throws
982 // log that we created a new mountpoint (we don't log when we are re-using)
983 Syslog::warning("SecTranslocateCreateGeneric: created %s",
988 exception
= current_exception();
990 if (!mountpoint
.empty())
994 /* try to unmount/delete (best effort)*/
995 unmount(mountpoint
.c_str(), 0);
996 rmdir(mountpoint
.c_str());
1001 /* rethrow outside the dispatch block */
1004 rethrow_exception(exception
);
1010 /* Loop through the directory in the specified user directory and delete any that aren't mountpoints */
1011 static void cleanupTranslocationDirForUser(const string
&userDir
)
1013 DIR* translocationDir
= opendir(userDir
.c_str());
1015 if( translocationDir
)
1018 struct statfs sfbuf
;
1019 struct dirent
* result
= NULL
;
1021 while (readdir_r(translocationDir
, &de
, &result
) == 0 && result
)
1023 if(result
->d_type
== DT_DIR
)
1025 if (result
->d_name
[0] == '.')
1027 if(result
->d_namlen
== 1 ||
1028 (result
->d_namlen
== 2 &&
1029 result
->d_name
[1] == '.'))
1035 string nextDir
= userDir
+string(result
->d_name
);
1036 if (0 == statfs(nextDir
.c_str(), &sfbuf
) &&
1037 nextDir
== sfbuf
.f_mntonname
)
1039 /* its a mount point so continue */
1043 /* not a mountpoint so delete it */
1044 if(unlinkat(dirfd(translocationDir
), result
->d_name
, AT_REMOVEDIR
))
1046 Syslog::warning("SecTranslocate: failed to delete directory during cleanup (error %d)\n\tUser Dir: %s\n\tDir to delete: %s",
1053 closedir(translocationDir
);
1057 /* Unmount and delete a directory */
1058 static int removeMountPoint(const string
&mountpoint
, bool force
)
1062 if (0 == unmount(mountpoint
.c_str(), force
? MNT_FORCE
: 0) &&
1063 0 == rmdir(mountpoint
.c_str()))
1065 Syslog::warning("SecTranslocate: removed mountpoint: %s",
1066 mountpoint
.c_str());
1071 Syslog::warning("SecTranslocate: failed to unmount/remove mount point (errno: %d): %s",
1072 error
, mountpoint
.c_str());
1078 /* Destroy the specified translocated path, and clean up the user's translocation directory.
1079 It is the caller's responsibility to synchronize the operation on the dispatch queue. */
1080 bool destroyTranslocatedPathForUser(const string
&translocatedPath
)
1082 bool result
= false;
1085 1. verify it is a nullfs mountpoint (with app path)
1088 4. loop through all the other directories in the app translation directory looking for directories not mounted on and delete them.
1091 string baseDirForUser
= translocationDirForUser(); // throws
1092 bool shouldUnmount
= false;
1093 string translocatedMountpoint
;
1095 { //Use a block to get rid of the file descriptor before we try to unmount.
1096 ExtendedAutoFileDesc
fd(translocatedPath
);
1097 translocatedMountpoint
= fd
.getMountPoint();
1099 To support unmount when nested apps end, just make sure that the requested path is on a translocation
1100 point for this user, not that they asked for a translocation point to be removed.
1102 shouldUnmount
= fd
.isFileSystemType(NULLFS_FSTYPE
);
1107 error
= removeMountPoint(translocatedMountpoint
);
1108 result
= error
== 0;
1111 if (!result
&& !error
)
1113 Syslog::warning("SecTranslocate: mountpoint does not belong to user(%d): %s",
1115 translocatedPath
.c_str());
1119 cleanupTranslocationDirForUser(baseDirForUser
);
1123 UnixError::throwMe(error
);
1129 /* Cleanup any translocation directories for this user that are either mounted from the
1130 specified volume or from a volume that doesn't exist anymore. If an empty volumePath
1131 is provided this has the effect of only cleaning up translocation points that point
1132 to volumes that don't exist anymore.
1134 It is the caller's responsibility to synchronize the operation on the dispatch queue.
1136 bool destroyTranslocatedPathsForUserOnVolume(const string
&volumePath
)
1138 bool cleanupError
= false;
1139 string baseDirForUser
= translocationDirForUser();
1140 vector
<struct statfs
> mountTable
= getMountTableSnapshot();
1142 fsid_t unmountingFsid
;
1143 int haveUnmountingFsid
= statfs(volumePath
.c_str(), &sb
);
1144 int haveMntFromState
= 0;
1146 memset(&unmountingFsid
, 0, sizeof(unmountingFsid
));
1148 if(haveUnmountingFsid
== 0) {
1149 unmountingFsid
= sb
.f_fsid
;
1152 for (auto &mnt
: mountTable
)
1155 we need to look at each translocation mount and check
1157 2. does its mntfromname still exist, if it doesn't unmount it
1158 3. if it does, is it the same as the volume we are cleaning up?, if so unmount it.
1160 if (strcmp(mnt
.f_fstypename
, NULLFS_FSTYPE
) == 0 &&
1161 strncmp(mnt
.f_mntonname
, baseDirForUser
.c_str(), baseDirForUser
.length()) == 0)
1163 haveMntFromState
= statfs(mnt
.f_mntfromname
, &sb
);
1165 if (haveMntFromState
!= 0)
1167 // In this case we are trying to unmount a translocation point that points to nothing. Force it.
1168 // Not forcing it currently hangs in UBC cleanup.
1169 (void)removeMountPoint(mnt
.f_mntonname
, true);
1171 else if (haveUnmountingFsid
== 0)
1173 fsid_t toCheckFsid
= sb
.f_fsid
;
1174 if( memcmp(&unmountingFsid
, &toCheckFsid
, sizeof(fsid_t
)) == 0)
1176 if(removeMountPoint(mnt
.f_mntonname
) != 0)
1178 cleanupError
= true;
1185 return !cleanupError
;
1188 /* This is intended to be used periodically to clean up translocation points that aren't used anymore */
1189 void tryToDestroyUnusedTranslocationMounts()
1191 vector
<struct statfs
> mountTable
= getMountTableSnapshot();
1192 string baseDirForUser
= translocationDirForUser();
1194 for (auto &mnt
: mountTable
)
1196 if (strcmp(mnt
.f_fstypename
, NULLFS_FSTYPE
) == 0 &&
1197 strncmp(mnt
.f_mntonname
, baseDirForUser
.c_str(), baseDirForUser
.length()) == 0)
1199 ExtendedAutoFileDesc
volumeToCheck(mnt
.f_mntfromname
, O_RDONLY
, FileDesc::modeMissingOk
);
1201 // Try to destroy the mount point. If the mirroed volume (volumeToCheck) isn't open then force it.
1202 // Not forcing it currently hangs in UBC cleanup.
1203 (void)removeMountPoint(mnt
.f_mntonname
, !volumeToCheck
.isOpen());
1208 } //namespace SecTranslocate
1209 }// namespace Security