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