]>
Commit | Line | Data |
---|---|---|
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 | ||
50 | namespace Security { | |
51 | ||
52 | namespace SecTranslocate { | |
53 | ||
54 | using namespace std; | |
55 | ||
56 | /* String Constants for XPC dictionary passing */ | |
57 | /* XPC Function keys */ | |
58 | const char* kSecTranslocateXPCFuncCreate = "create"; | |
59 | const char* kSecTranslocateXPCFuncCheckIn = "check-in"; | |
60 | ||
61 | /* XPC message argument keys */ | |
62 | const char* kSecTranslocateXPCMessageFunction = "function"; | |
63 | const char* kSecTranslocateXPCMessageOriginalPath = "original"; | |
64 | const char* kSecTranslocateXPCMessageDestinationPath = "dest"; | |
65 | const char* kSecTranslocateXPCMessagePid = "pid"; | |
66 | ||
67 | /*XPC message reply keys */ | |
68 | const char* kSecTranslocateXPCReplyError = "error"; | |
69 | const char* kSecTranslocateXPCReplySecurePath = "result"; | |
70 | ||
71 | //Functions used only within this file | |
72 | static void setMountPointQuarantineIfNecessary(const string &mountPoint, const string &originalPath); | |
73 | static string getMountpointFromAppPath(const string &appPath, const string &originalPath); | |
74 | ||
75 | static vector<struct statfs> getMountTableSnapshot(); | |
76 | static string mountExistsForUser(const string &translationDirForUser, const string &originalPath, const string &destMount); | |
77 | static void validateMountpoint(const string &mountpoint, bool owned=false); | |
78 | static string makeNewMountpoint(const string &translationDir); | |
79 | static string newAppPath (const string &mountPoint, const TranslocationPath &originalPath); | |
80 | static void cleanupTranslocationDirForUser(const string &userDir); | |
81 | static int removeMountPoint(const string &mountpoint, bool force = false); | |
82 | ||
83 | /* calculate whether a translocation should occur and where from */ | |
84 | TranslocationPath::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 */ | |
179 | string 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.*/ | |
204 | ExtendedAutoFileDesc 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. */ | |
252 | string 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 | */ | |
353 | static 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 | ||
391 | end: | |
392 | return result; | |
393 | } | |
394 | ||
395 | /* Read the mount table and return it in a vector */ | |
396 | static 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 */ | |
458 | static 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 */ | |
529 | static 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 */ | |
638 | static 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*/ | |
654 | static 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 */ | |
686 | static 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. */ | |
742 | string 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 */ | |
826 | static 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 */ | |
873 | static 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. */ | |
895 | bool 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 | */ | |
952 | bool 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 */ | |
1003 | void 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 |