]> git.saurik.com Git - apple/security.git/blob - OSX/libsecurity_translocate/lib/SecTranslocateShared.cpp
Security-59754.41.1.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 #include <memory>
28
29 #include <sys/stat.h>
30 #include <unistd.h>
31 #include <sys/param.h>
32 #include <sys/mount.h>
33 #include <sys/ucred.h>
34 #include <dispatch/dispatch.h>
35 #include <string.h>
36 #include <dirent.h>
37
38 #define __APPLE_API_PRIVATE
39 #include <quarantine.h>
40 #undef __APPLE_API_PRIVATE
41
42 #include <security_utilities/cfutilities.h>
43 #include <security_utilities/unix++.h>
44 #include <security_utilities/logging.h>
45 #include <Security/SecStaticCode.h>
46
47 #include "SecTranslocateShared.hpp"
48 #include "SecTranslocateUtilities.hpp"
49 #include "SecTranslocateEnumUtils.hpp"
50
51 #define NULLM_UNVEIL 0x1ULL << 2
52 struct null_mount_conf {
53 uint64_t flags;
54 };
55
56
57 namespace Security {
58
59 namespace SecTranslocate {
60
61 using namespace std;
62
63 /* String Constants for XPC dictionary passing */
64 /* XPC Function keys */
65 const char* kSecTranslocateXPCFuncCreate = "create";
66 const char* kSecTranslocateXPCFuncCheckIn = "check-in";
67
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";
74
75 /*XPC message reply keys */
76 const char* kSecTranslocateXPCReplyError = "error";
77 const char* kSecTranslocateXPCReplySecurePath = "result";
78
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);
82
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);
92
93 /* calculate whether a translocation should occur and where from */
94 TranslocationPath::TranslocationPath(string originalPath, TranslocationOptions opts)
95 {
96
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
101 bits to decide.
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
105 do not translocate
106 else if it is unquarantined
107 do not translocate
108 else
109 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
110 do not translocate
111 else
112 find the outermost acceptable code bundle
113 if not QTN_FLAG_TRANSLOCATE or QTN_FLAG_DO_NOT_TRANSLOCATE
114 don't translocate
115 else
116 translocate
117
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.
122 */
123
124 ExtendedAutoFileDesc fd(originalPath);
125
126 options = opts;
127 should = false;
128 realOriginalPath = fd.getRealPath();
129
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())
134 {
135 ExtendedAutoFileDesc &&outermost = findOuterMostCodeBundleForFD(fd);
136
137 should = outermost.isQuarantined() && outermost.shouldTranslocate();
138 pathToTranslocate = outermost.getRealPath();
139
140 /* Calculate the path that will be needed to give the caller the path they asked for originally but in the translocated place */
141 if (should)
142 {
143 vector<string> originalComponents = splitPath(realOriginalPath);
144 vector<string> toTranslocateComponents = splitPath(pathToTranslocate);
145
146 if (toTranslocateComponents.size() == 0 ||
147 toTranslocateComponents.size() > originalComponents.size())
148 {
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);
153 }
154
155 componentNameToTranslocate = toTranslocateComponents.back();
156
157 for(size_t cnt = 0; cnt < originalComponents.size(); cnt++)
158 {
159 if (cnt < toTranslocateComponents.size())
160 {
161 if (toTranslocateComponents[cnt] != originalComponents[cnt])
162 {
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);
167 }
168 }
169 else
170 {
171 /*
172 want pathInsideTranslocationPoint to look like:
173 a/b/c
174 i.e. internal / but not at the front or back.
175 */
176 if(pathInsideTranslocationPoint.empty())
177 {
178 pathInsideTranslocationPoint = originalComponents[cnt];
179 }
180 else
181 {
182 pathInsideTranslocationPoint += "/" + originalComponents[cnt];
183 }
184 }
185 }
186 }
187 }
188 }
189
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
194 {
195 string seperator = translocationPoint.back() != '/' ? "/" : "";
196
197 if (should)
198 {
199 if(!pathInsideTranslocationPoint.empty())
200 {
201 return translocationPoint + seperator + pathInsideTranslocationPoint;
202 }
203 else
204 {
205 return translocationPoint;
206 }
207 }
208 else
209 {
210 //If we weren't supposed to translocate return the original path.
211 return realOriginalPath;
212 }
213 }
214
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)
219 {
220 if( fd.isMountPoint() || !fd.isQuarantined())
221 {
222 return fd;
223 }
224 vector<string> path = splitPath(fd.getRealPath());
225 size_t currentIndex = path.size() - 1;
226 size_t lastGoodIndex = currentIndex;
227
228 string pathToCheck = joinPathUpTo(path, currentIndex);
229 /*
230 Proposed algorithm (pseudo-code):
231 lastGood := path := canonicalized path to be launched
232
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
238 return lastGood
239 */
240 while(currentIndex)
241 {
242 ExtendedAutoFileDesc currFd(pathToCheck);
243
244 if (currFd.isMountPoint() || !currFd.isQuarantined() || !currFd.isUserApproved())
245 {
246 break;
247 }
248
249 SecStaticCodeRef staticCodeRef = NULL;
250
251 if( SecStaticCodeCreateWithPath(CFTempURL(currFd.getRealPath()), kSecCSDefaultFlags, &staticCodeRef) == errSecSuccess)
252 {
253 lastGoodIndex = currentIndex;
254 CFRelease(staticCodeRef);
255 }
256
257 currentIndex--;
258 pathToCheck = joinPathUpTo(path, currentIndex);
259 }
260
261 return ExtendedAutoFileDesc(joinPathUpTo(path, lastGoodIndex));
262 }
263
264 GenericTranslocationPath::GenericTranslocationPath(const string& path, TranslocationOptions opts) {
265 ExtendedAutoFileDesc fd(path);
266 realOriginalPath = fd.getRealPath();
267 should = false;
268 options = opts;
269
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()) {
273 return;
274 }
275
276 componentNameToTranslocate = splitPath(path).back();
277
278 should = true;
279 }
280
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)
284 {
285 if (!fd.isFileSystemType(NULLFS_FSTYPE) ||
286 isDir == NULL ||
287 !fd.isInPrefixDir(fd.getMountPoint()))
288 {
289 Syslog::error("SecTranslocate::getOriginalPath called with invalid params: fs_type = %s, isDir = %p, realPath = %s, mountpoint = %s",
290 fd.getFsType().c_str(),
291 isDir,
292 fd.getRealPath().c_str(),
293 fd.getMountPoint().c_str());
294 UnixError::throwMe(EINVAL);
295 }
296
297 *isDir = fd.isA(S_IFDIR);
298
299 vector<string> mountFromPath = splitPath(fd.getMountFromPath());
300 vector<string> mountPointPath = splitPath(fd.getMountPoint());
301 vector<string> translocatedRealPath = splitPath(fd.getRealPath());
302
303 if (mountPointPath.size() > translocatedRealPath.size())
304 {
305 Syslog::warning("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
306 UnixError::throwMe(EINVAL);
307 }
308
309 string originalPath = fd.getMountFromPath();
310
311 int i;
312
313 for( i = 0; i<translocatedRealPath.size(); i++)
314 {
315 /* match the mount point directories to the real path directories */
316 if( i < mountPointPath.size())
317 {
318 if(translocatedRealPath[i] != mountPointPath[i])
319 {
320 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
321 UnixError::throwMe(EINVAL);
322 }
323 }
324 /* check for the d directory */
325 else if( i == mountPointPath.size())
326 {
327 if( translocatedRealPath[i] != "d")
328 {
329 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
330 UnixError::throwMe(EINVAL);
331 }
332 }
333 /* check for the app name */
334 else if( i == mountPointPath.size() + 1)
335 {
336 if( translocatedRealPath[i] != mountFromPath.back())
337 {
338 Syslog::error("SecTranslocate: invalid translocated path %s", fd.getRealPath().c_str());
339 UnixError::throwMe(EINVAL);
340 }
341 }
342 /* we are past the app name so add what ever is left */
343 else
344 {
345 originalPath +="/"+translocatedRealPath[i];
346 }
347 }
348
349 if( i == mountPointPath.size() || i == mountPointPath.size() + 1)
350 {
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);
354 }
355
356 /* Make sure what we built actually exists */
357 ExtendedAutoFileDesc originalFD(originalPath);
358 if(!originalFD.pathIsAbsolute())
359 {
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);
364 }
365
366 return originalPath;
367 }
368
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
373 */
374 static string getMountpointFromAppPath(const string &appPath, const string &originalPath)
375 {
376 /* assume that appPath looks like:
377 /my/user/temp/dir/AppTranslocation/MY-UUID/d/foo.app
378
379 and assume original path looks like:
380 /my/user/dir/foo.app
381
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
385 */
386 string result;
387
388 vector<string> app = splitPath(appPath); // throws if empty or not absolute
389 vector<string> original = splitPath(originalPath); //throws if empty or not absolute
390
391 if (original.size() == 0) // had to have at least one directory, can't null mount /
392 {
393 Syslog::error("SecTranslocate: invalid original path: %s", originalPath.c_str());
394 UnixError::throwMe(EINVAL);
395 }
396
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
399 {
400 app.pop_back();
401 if(app.back() == "d") //last directory of app path is preceded by /d/
402 {
403 app.pop_back();
404 result = joinPath(app);
405 goto end;
406 }
407 }
408
409 Syslog::error("SecTranslocate: invalid app path: %s", appPath.c_str());
410 UnixError::throwMe(EINVAL);
411
412 end:
413 return result;
414 }
415
416 /* Read the mount table and return it in a vector */
417 static vector<struct statfs> getMountTableSnapshot()
418 {
419 vector<struct statfs> mntInfo;
420 int fs_cnt_first = 0;
421 int fs_cnt_second = 0;
422 int retry = 2;
423
424 /*Strategy here is:
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
429 */
430
431 while(retry)
432 {
433 fs_cnt_first = getfsstat(NULL, 0 , MNT_WAIT);
434 if(fs_cnt_first <= 0)
435 {
436 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno);
437 UnixError::throwMe();
438 }
439
440 if( fs_cnt_first == fs_cnt_second)
441 {
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. */
444 break;
445 }
446
447 mntInfo.resize(fs_cnt_first*2);
448
449 fs_cnt_second = getfsstat(mntInfo.data(), (int)(mntInfo.size() * sizeof(struct statfs)), MNT_WAIT);
450 if (fs_cnt_second <= 0)
451 {
452 Syslog::warning("SecTranslocate: error(%d) getting mount table info.", errno);
453 UnixError::throwMe();
454 }
455
456 if( fs_cnt_second == mntInfo.size())
457 {
458 retry--;
459 }
460 else
461 {
462 mntInfo.resize(fs_cnt_second); // trim the vector to what we actually need
463 break;
464 }
465 }
466
467 if( retry == 0)
468 {
469 Syslog::warning("SecTranslocate: mount table is growing very quickly");
470 }
471
472 return mntInfo;
473 }
474
475 static bool pathExistsInMountTable(const GenericTranslocationPath& path, const string& mountpoint)
476 {
477 vector <struct statfs> mntbuf = getMountTableSnapshot();
478
479 /* Save the untranslocated inode number*/
480 ExtendedAutoFileDesc::UnixStat untranslocatedStat;
481
482 if (stat(path.getOriginalRealPath().c_str(), &untranslocatedStat))
483 {
484 errno_t err = errno;
485 Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
486 err,
487 path.getOriginalRealPath().c_str());
488 UnixError::throwMe(err);
489 }
490
491 for (auto &i : mntbuf)
492 {
493 string mountOnName = i.f_mntonname;
494
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
498 )
499 {
500 /*
501 find the inode number for mountOnName
502 */
503 string pathToTranslocatedApp = mountOnName+"/d/"+path.getComponentNameToTranslocate();
504
505 ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
506
507 if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
508 {
509 /* We should have access to this path and it should be real so complain if thats not true. */
510 errno_t err = errno;
511 Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp.c_str(), err);
512 UnixError::throwMe(err);
513 }
514
515 if(untranslocatedStat.st_ino != oldTranslocatedStat.st_ino)
516 {
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);
520 continue;
521 }
522
523 return true;
524 }
525 }
526
527 return false;
528 }
529
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)
535 {
536 string result; // start empty
537
538 if(!destMountPoint.empty())
539 {
540 /* Validate that destMountPoint path is well formed and for this user
541 well formed means it is === translationDirForUser/<1 directory>
542 */
543 vector<string> splitDestMount = splitPath(destMountPoint);
544
545 if(splitDestMount.size() < 2) //translationDirForUser is never /
546 {
547 Syslog::warning("SecTranslocate: invalid destination mount point: %s",
548 destMountPoint.c_str());
549 UnixError::throwMe(EINVAL);
550 }
551
552 splitDestMount.pop_back(); // knock off one directory
553
554 string destBaseDir = joinPath(splitDestMount)+"/"; //translationDirForUser has a / at the end
555
556 if (translationDirForUser != destBaseDir)
557 {
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);
563 }
564 }
565
566 vector <struct statfs> mntbuf = getMountTableSnapshot();
567
568 /* Save the untranslocated inode number*/
569 ExtendedAutoFileDesc::UnixStat untranslocatedStat;
570
571 if (stat(originalPath.getPathToTranslocate().c_str(), &untranslocatedStat))
572 {
573 errno_t err = errno;
574 Syslog::warning("SecTranslocate: failed to stat original path (%d): %s",
575 err,
576 originalPath.getPathToTranslocate().c_str());
577 UnixError::throwMe(err);
578 }
579
580 for (auto &i : mntbuf)
581 {
582 string mountOnName = i.f_mntonname;
583 size_t lastNonSlashPos = mountOnName.length() - 1; //start at the end of the string
584
585 /* find the last position of the last non slash character */
586 for(; lastNonSlashPos != 0 && mountOnName[lastNonSlashPos] == '/' ; lastNonSlashPos--);
587
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 / */
591
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
596 {
597 if(!destMountPoint.empty())
598 {
599 if (mountOnName != destMountPoint)
600 {
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",
603 i.f_mntonname,
604 destMountPoint.c_str());
605 UnixError::throwMe(EEXIST);
606 }
607 }
608 /*
609 find the inode number for mountOnName+/d/appname
610 */
611 string pathToTranslocatedApp = mountOnName+"/d/"+originalPath.getComponentNameToTranslocate();
612
613 ExtendedAutoFileDesc::UnixStat oldTranslocatedStat;
614
615 if (stat(pathToTranslocatedApp.c_str(), &oldTranslocatedStat))
616 {
617 /* We should have access to this path and it should be real so complain if thats not true. */
618 errno_t err = errno;
619 Syslog::warning("SecTranslocate: expected app not inside mountpoint: %s (error: %d)", pathToTranslocatedApp.c_str(), err);
620 UnixError::throwMe(err);
621 }
622
623 if(untranslocatedStat.st_ino != oldTranslocatedStat.st_ino)
624 {
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);
628 continue;
629 }
630
631 result = mountOnName;
632 break;
633 }
634 }
635
636 return result;
637 }
638
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)
641 {
642 /* Requirements:
643 1. can be opened
644 2. is a directory
645 3. is not already a mountpoint
646 4. is an absolute path
647 */
648 bool isDir = false;
649 bool isMount = false;
650 bool isEmpty = true;
651
652 try {
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
657 checks in nullfs))*/
658 DIR* dir = opendir(mountpoint.c_str());
659 int error = 0;
660
661 if (dir == NULL)
662 {
663 error = errno;
664 Syslog::warning("SecTranslocate: mountpoint is not a directory or doesn't exist: %s",
665 mountpoint.c_str());
666 UnixError::throwMe(error);
667 }
668
669 isDir = true;
670
671 struct dirent *d;
672 struct dirent dirbuf;
673 int cnt = 0;
674 int err = 0;
675 while(((err = readdir_r(dir, &dirbuf, &d)) == 0) &&
676 d != NULL)
677 {
678 /* skip . and .. but break if there is more than that */
679 if(++cnt > 2)
680 {
681 isEmpty = false;
682 break;
683 }
684 }
685
686 error = errno;
687 (void)closedir(dir);
688
689 if(err)
690 {
691 Syslog::warning("SecTranslocate: error while checking that mountpoint is empty");
692 UnixError::throwMe(error);
693 }
694
695 if(!isEmpty)
696 {
697 Syslog::warning("Sectranslocate: mountpoint is not empty: %s",
698 mountpoint.c_str());
699 UnixError::throwMe(EBUSY);
700 }
701
702 /* now check that the path is not a mountpoint */
703 ExtendedAutoFileDesc fd(mountpoint);
704
705 if(!fd.pathIsAbsolute())
706 {
707 Syslog::warning("SecTranslocate: mountpoint isn't fully resolved\n\tExpected: %s\n\tActual: %s",
708 fd.getRealPath().c_str(),
709 mountpoint.c_str());
710 UnixError::throwMe(EINVAL);
711 }
712
713 isMount = fd.isMountPoint();
714
715 if(isMount)
716 {
717 Syslog::warning("SecTranslocate:Translocation failed, new mountpoint is already a mountpoint (%s)",
718 mountpoint.c_str());
719 UnixError::throwMe(EINVAL);
720 }
721 }
722 catch(...)
723 {
724 if(owned)
725 {
726 if (!isMount)
727 {
728 if (isDir)
729 {
730 if(isEmpty)
731 {
732 rmdir(mountpoint.c_str());
733 }
734 /* Already logged the else case above */
735 }
736 else
737 {
738 Syslog::warning("SecTranslocate: unexpected file detected at mountpoint location (%s). Deleting.",
739 mountpoint.c_str());
740 unlink(mountpoint.c_str());
741 }
742 }
743 }
744 rethrow_exception(current_exception());
745 }
746 }
747
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)
750 {
751 AutoFileDesc fd(getFDForDirectory(translationDir));
752
753 string uuid = makeUUID();
754
755 UnixError::check(mkdirat(fd, uuid.c_str(), 0500));
756
757 string mountpoint = translationDir+uuid;
758
759 validateMountpoint(mountpoint);
760
761 return mountpoint;
762 }
763
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)
766 {
767 struct statfs sfsbuf;
768 int error = 0;
769
770 UnixError::check(statfs(originalPath.c_str(), &sfsbuf));
771 qtn_file_t original_attr = qtn_file_alloc();
772
773 if (original_attr != NULL)
774 {
775 if (qtn_file_init_with_mount_point(original_attr, sfsbuf.f_mntonname) == 0)
776 {
777 error = qtn_file_apply_to_mount_point(original_attr, mountPoint.c_str());
778 }
779 qtn_file_free(original_attr);
780 }
781 else
782 {
783 error = errno;
784 }
785
786 if (error)
787 {
788 Syslog::warning("SecTranslocate: Failed to apply quarantine information\n\tMountpoint: %s\n\tOriginal Path: %s",
789 mountPoint.c_str(),
790 originalPath.c_str());
791 UnixError::throwMe(error);
792 }
793 }
794
795 static string newAppPathFrom (const string &mountPoint, const string &outPath)
796 {
797 string midPath = mountPoint+"/d";
798
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);
803
804 if(!outFd.isFileSystemType(NULLFS_FSTYPE) ||
805 !mountFd.isFileSystemType(NULLFS_FSTYPE) ||
806 !midFd.isFileSystemType(NULLFS_FSTYPE))
807 {
808 Syslog::warning("SecTranslocate::App exists at expected translocation path (%s) but isn't a nullfs mount (%s)",
809 outPath.c_str(),
810 outFd.getFsType().c_str());
811 UnixError::throwMe(EINVAL);
812 }
813
814 if(!outFd.pathIsAbsolute() ||
815 !mountFd.pathIsAbsolute() ||
816 !midFd.pathIsAbsolute() )
817 {
818 Syslog::warning("SecTranslocate::App path isn't resolved\n\tGot: %s\n\tExpected: %s",
819 outFd.getRealPath().c_str(),
820 outPath.c_str());
821 UnixError::throwMe(EINVAL);
822 }
823
824 fsid_t outFsid = outFd.getFsid();
825 fsid_t midFsid = midFd.getFsid();
826 fsid_t mountFsid = mountFd.getFsid();
827
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)
831 {
832 Syslog::warning("SecTranslocate:: the fsid is not consistent between app, /d/ and mountpoint");
833 UnixError::throwMe(EINVAL);
834 }
835
836 return outFd.getRealPath();
837 }
838
839
840 static string newAppPath (const string &mountPoint, const GenericTranslocationPath &originalPath)
841 {
842 string outPath = mountPoint+"/d/"+originalPath.getComponentNameToTranslocate();
843 return newAppPathFrom(mountPoint, outPath);
844 }
845
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)
849 {
850 string outPath = originalPath.getTranslocatedPathToOriginalPath(mountPoint+"/d/"+originalPath.getComponentNameToTranslocate());
851 return newAppPathFrom(mountPoint, outPath);
852 }
853
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;
860 }
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');
864 return data;
865 }
866
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)
871 {
872 string newPath;
873 exception_ptr exception(0);
874
875 string mountpoint;
876 bool owned = false;
877 try
878 {
879 const string &toTranslocate = originalPath.getPathToTranslocate();
880 string baseDirForUser = translocationDirForUser(); //throws
881 string destMountPoint;
882 if(!destPath.empty())
883 {
884 destMountPoint = getMountpointFromAppPath(destPath, toTranslocate); //throws or returns a mountpoint
885 }
886
887 mountpoint = mountExistsForUser(baseDirForUser, originalPath, destMountPoint); //throws, detects invalid destMountPoint string
888
889 if (!mountpoint.empty())
890 {
891 /* A mount point exists already so bail*/
892 newPath = newAppPath(mountpoint, originalPath);
893 return newPath; /* exit the block */
894 }
895 if (destMountPoint.empty())
896 {
897 mountpoint = makeNewMountpoint(baseDirForUser); //throws
898 owned = true;
899 }
900 else
901 {
902 AutoFileDesc fd(getFDForDirectory(destMountPoint, &owned)); //throws, makes the directory if it doesn't exist
903
904 validateMountpoint(destMountPoint, owned); //throws
905 mountpoint = destMountPoint;
906 }
907
908 auto mount_data = getMountData(toTranslocate, originalPath.getOptions());
909 UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, mount_data.data()));
910
911 setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws
912
913 newPath = newAppPath(mountpoint, originalPath); //throws
914
915 if (!destPath.empty())
916 {
917 if (newPath != originalPath.getTranslocatedPathToOriginalPath(destPath))
918 {
919 Syslog::warning("SecTranslocate: created app translocation point did not equal requested app translocation point\n\texpected: %s\n\tcreated: %s",
920 newPath.c_str(),
921 destPath.c_str());
922 /* the app at originalPath didn't match the one at destPath */
923 UnixError::throwMe(EINVAL);
924 }
925 }
926 // log that we created a new mountpoint (we don't log when we are re-using)
927 Syslog::warning("SecTranslocateCreateSecureDirectoryForURL: created %s",
928 newPath.c_str());
929 }
930 catch (...)
931 {
932 exception = current_exception();
933
934 if (!mountpoint.empty())
935 {
936 if (owned)
937 {
938 /* try to unmount/delete (best effort)*/
939 unmount(mountpoint.c_str(), 0);
940 rmdir(mountpoint.c_str());
941 }
942 }
943 }
944
945 /* rethrow outside the dispatch block */
946 if (exception)
947 {
948 rethrow_exception(exception);
949 }
950
951 return newPath;
952 }
953
954 string translocatePathForUser(const GenericTranslocationPath &originalPath, const string &destPath)
955 {
956 string newPath;
957 exception_ptr exception(0);
958
959 string mountpoint = destPath;
960 bool owned = false;
961 try
962 {
963 const string &toTranslocate = originalPath.getOriginalRealPath();
964 if (pathExistsInMountTable(originalPath, destPath))
965 {
966 /* A mount point exists already so bail*/
967 newPath = newAppPath(mountpoint, originalPath);
968 return newPath; /* exit the block */
969 }
970
971 AutoFileDesc fd(getFDForDirectory(mountpoint, &owned)); //throws, makes the directory if it doesn't exist
972
973 validateMountpoint(mountpoint, owned); //throws
974
975 auto mount_data = getMountData(toTranslocate, originalPath.getOptions());
976 UnixError::check(mount(NULLFS_FSTYPE, mountpoint.c_str(), MNT_RDONLY, mount_data.data()));
977
978 setMountPointQuarantineIfNecessary(mountpoint, toTranslocate); //throws
979
980 newPath = newAppPath(mountpoint, originalPath); //throws
981
982 // log that we created a new mountpoint (we don't log when we are re-using)
983 Syslog::warning("SecTranslocateCreateGeneric: created %s",
984 newPath.c_str());
985 }
986 catch (...)
987 {
988 exception = current_exception();
989
990 if (!mountpoint.empty())
991 {
992 if (owned)
993 {
994 /* try to unmount/delete (best effort)*/
995 unmount(mountpoint.c_str(), 0);
996 rmdir(mountpoint.c_str());
997 }
998 }
999 }
1000
1001 /* rethrow outside the dispatch block */
1002 if (exception)
1003 {
1004 rethrow_exception(exception);
1005 }
1006
1007 return newPath;
1008 }
1009
1010 /* Loop through the directory in the specified user directory and delete any that aren't mountpoints */
1011 static void cleanupTranslocationDirForUser(const string &userDir)
1012 {
1013 DIR* translocationDir = opendir(userDir.c_str());
1014
1015 if( translocationDir )
1016 {
1017 struct dirent de;
1018 struct statfs sfbuf;
1019 struct dirent * result = NULL;
1020
1021 while (readdir_r(translocationDir, &de, &result) == 0 && result)
1022 {
1023 if(result->d_type == DT_DIR)
1024 {
1025 if (result->d_name[0] == '.')
1026 {
1027 if(result->d_namlen == 1 ||
1028 (result->d_namlen == 2 &&
1029 result->d_name[1] == '.'))
1030 {
1031 /* skip . and .. */
1032 continue;
1033 }
1034 }
1035 string nextDir = userDir+string(result->d_name);
1036 if (0 == statfs(nextDir.c_str(), &sfbuf) &&
1037 nextDir == sfbuf.f_mntonname)
1038 {
1039 /* its a mount point so continue */
1040 continue;
1041 }
1042
1043 /* not a mountpoint so delete it */
1044 if(unlinkat(dirfd(translocationDir), result->d_name, AT_REMOVEDIR))
1045 {
1046 Syslog::warning("SecTranslocate: failed to delete directory during cleanup (error %d)\n\tUser Dir: %s\n\tDir to delete: %s",
1047 errno,
1048 userDir.c_str(),
1049 result->d_name);
1050 }
1051 }
1052 }
1053 closedir(translocationDir);
1054 }
1055 }
1056
1057 /* Unmount and delete a directory */
1058 static int removeMountPoint(const string &mountpoint, bool force)
1059 {
1060 int error = 0;
1061
1062 if (0 == unmount(mountpoint.c_str(), force ? MNT_FORCE : 0) &&
1063 0 == rmdir(mountpoint.c_str()))
1064 {
1065 Syslog::warning("SecTranslocate: removed mountpoint: %s",
1066 mountpoint.c_str());
1067 }
1068 else
1069 {
1070 error = errno;
1071 Syslog::warning("SecTranslocate: failed to unmount/remove mount point (errno: %d): %s",
1072 error, mountpoint.c_str());
1073 }
1074
1075 return error;
1076 }
1077
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)
1081 {
1082 bool result = false;
1083 int error = 0;
1084 /* steps
1085 1. verify it is a nullfs mountpoint (with app path)
1086 2. unmount it
1087 3. delete it
1088 4. loop through all the other directories in the app translation directory looking for directories not mounted on and delete them.
1089 */
1090
1091 string baseDirForUser = translocationDirForUser(); // throws
1092 bool shouldUnmount = false;
1093 string translocatedMountpoint;
1094
1095 { //Use a block to get rid of the file descriptor before we try to unmount.
1096 ExtendedAutoFileDesc fd(translocatedPath);
1097 translocatedMountpoint = fd.getMountPoint();
1098 /*
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.
1101 */
1102 shouldUnmount = fd.isFileSystemType(NULLFS_FSTYPE);
1103 }
1104
1105 if (shouldUnmount)
1106 {
1107 error = removeMountPoint(translocatedMountpoint);
1108 result = error == 0;
1109 }
1110
1111 if (!result && !error)
1112 {
1113 Syslog::warning("SecTranslocate: mountpoint does not belong to user(%d): %s",
1114 getuid(),
1115 translocatedPath.c_str());
1116 error = EPERM;
1117 }
1118
1119 cleanupTranslocationDirForUser(baseDirForUser);
1120
1121 if (error)
1122 {
1123 UnixError::throwMe(error);
1124 }
1125
1126 return result;
1127 }
1128
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.
1133
1134 It is the caller's responsibility to synchronize the operation on the dispatch queue.
1135 */
1136 bool destroyTranslocatedPathsForUserOnVolume(const string &volumePath)
1137 {
1138 bool cleanupError = false;
1139 string baseDirForUser = translocationDirForUser();
1140 vector <struct statfs> mountTable = getMountTableSnapshot();
1141 struct statfs sb;
1142 fsid_t unmountingFsid;
1143 int haveUnmountingFsid = statfs(volumePath.c_str(), &sb);
1144 int haveMntFromState = 0;
1145
1146 memset(&unmountingFsid, 0, sizeof(unmountingFsid));
1147
1148 if(haveUnmountingFsid == 0) {
1149 unmountingFsid = sb.f_fsid;
1150 }
1151
1152 for (auto &mnt : mountTable)
1153 {
1154 /*
1155 we need to look at each translocation mount and check
1156 1. is it ours
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.
1159 */
1160 if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 &&
1161 strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0)
1162 {
1163 haveMntFromState = statfs(mnt.f_mntfromname, &sb);
1164
1165 if (haveMntFromState != 0)
1166 {
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);
1170 }
1171 else if (haveUnmountingFsid == 0)
1172 {
1173 fsid_t toCheckFsid = sb.f_fsid;
1174 if( memcmp(&unmountingFsid, &toCheckFsid, sizeof(fsid_t)) == 0)
1175 {
1176 if(removeMountPoint(mnt.f_mntonname) != 0)
1177 {
1178 cleanupError = true;
1179 }
1180 }
1181 }
1182 }
1183 }
1184
1185 return !cleanupError;
1186 }
1187
1188 /* This is intended to be used periodically to clean up translocation points that aren't used anymore */
1189 void tryToDestroyUnusedTranslocationMounts()
1190 {
1191 vector <struct statfs> mountTable = getMountTableSnapshot();
1192 string baseDirForUser = translocationDirForUser();
1193
1194 for (auto &mnt : mountTable)
1195 {
1196 if (strcmp(mnt.f_fstypename, NULLFS_FSTYPE) == 0 &&
1197 strncmp(mnt.f_mntonname, baseDirForUser.c_str(), baseDirForUser.length()) == 0)
1198 {
1199 ExtendedAutoFileDesc volumeToCheck(mnt.f_mntfromname, O_RDONLY, FileDesc::modeMissingOk);
1200
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());
1204 }
1205 }
1206 }
1207
1208 } //namespace SecTranslocate
1209 }// namespace Security