/* * Copyright (c) 2009-2010 Apple Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ /* * These routines, accessed through path_node_create() and path_node_release(), * provide an API for monitoring a path in the filesystem. The path may contain * directories and symbolic links. The path may not even exist! If the path * does exist, this code will respond to path deletions, component renaming, and * access control changes that either delete the path or make it inaccessible to a * target user/group. A notification will be provided if the path comes back into * existance or again becomes accessible. * * path_node_create() returns a path_node_t object, which contains a dispatch_source_t. * This source behaves very much like a DISPATCH_SOURCE_TYPE_VNODE, except that it also * triggers on the creation of a path. * * Internally, the work of monitoring a path is done by a set of helper vnode_t * objects. A vnode_t contains a dispatch_source_t (of type DISPATCH_SOURCE_TYPE_VNODE) * for a particular vnode. When a path_node_t is created, it creates (or shares) * vnode_t objects for each component of the desired path. For example, a path_node_t * for "/a/b/c" will create (or share, if some other path_node_t has already created) a * dispatch_source_t for "/", "/a", "/a/b", and "/a/b/c". If any of these sources is * notified of a change, the vnode_t will trigger an update for all path_node_t * objects that contain that path component. * * When a path_node_t update is triggered by a vnode_t component, the node re-evaluates * the target path that it is charged with monitoring. If the path exists and the end-point * vnode changed, then the update operation will trigger its dispatch_source_t to notify the * end-user of the change. If an intermediate path component is removed, renamed, or becomes * blocked by an access-control change, then the end-point dispatch_source_t is triggered to * indicate that the path has been deleted. However, the path_node_t remains active and * monitors the path components that still exist. Eventually, if the path is recreated or * if access controls change so that the path becomes visible to the target user, then the * end-point dispatch_source_t is triggered with a PATH_NODE_CREATE bit set in its data flags. * * path_node_releases() releases a path_node_t object and all of the vnode_t objects * that were monitoring components of its target path. */ #include #include #include #include #include #include #include #include #include #include #include #include "pathwatch.h" #define ZONEINFO_DIR "/usr/share/zoneinfo/" #define forever for(;;) #define streq(A,B) (strcmp(A,B)==0) #define DISPATCH_VNODE_ALL 0x7f #define PATH_STAT_OK 0 #define PATH_STAT_FAILED 1 #define PATH_STAT_ACCESS 2 #define VPATH_NODE_TYPE_REG 0 #define VPATH_NODE_TYPE_LINK 1 #define VPATH_NODE_TYPE_DELETED 2 #define DISPATCH_VNODE_UNAVAIL (DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE) /* Libinfo global */ extern uint32_t gL1CacheEnabled; /* * vnode_t represents a vnode. * * The dispatch source is of type DISPATCH_SOURCE_TYPE_VNODE for file descriptor fd. * The handler for the source triggers an update routine for all the path_node_t * objects in the path_node list. */ typedef struct { char *path; uint32_t type; int fd; struct timespec mtime; struct timespec ctime; dispatch_source_t src; uint32_t path_node_count; path_node_t **path_node; } vnode_t; static struct { dispatch_once_t pathwatch_init; dispatch_queue_t pathwatch_queue; uint32_t vnode_count; vnode_t **vnode; } _global = {0}; /* forward */ static void _path_node_update(path_node_t *pnode, uint32_t flags, vnode_t *vnode); /* * stat() or lstat() a path as a particular user/group. */ static int _path_stat(const char *path, int link, uid_t uid, gid_t gid) { struct stat sb; gid_t orig_gidset[NGROUPS_MAX]; int ngroups, status, stat_status; struct passwd *p; uint32_t orig_cache_enabled; /* disable L1 cache to avoid notification deadlock */ orig_cache_enabled = gL1CacheEnabled; gL1CacheEnabled = 0; /* get my group list */ memset(orig_gidset, 0, sizeof(orig_gidset)); ngroups = getgroups(NGROUPS_MAX, orig_gidset); if (ngroups < 0) { return PATH_STAT_FAILED; } /* look up user name */ p = getpwuid(uid); if (p == NULL) { gL1CacheEnabled = orig_cache_enabled; return PATH_STAT_FAILED; } /* switch to user's grouplist */ status = initgroups(p->pw_name, gid); if (status < 0) { gL1CacheEnabled = orig_cache_enabled; return PATH_STAT_FAILED; } /* reset gL1CacheEnabled */ gL1CacheEnabled = orig_cache_enabled; /* set thread credentials */ pthread_setugid_np(uid, gid); /* stat the file */ stat_status = -1; if (link != 0) { stat_status = lstat(path, &sb); } else { stat_status = stat(path, &sb); } /* unset thread credentials */ pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE); /* restore original grouplist for UID 0 */ status = syscall(SYS_initgroups, ngroups, orig_gidset, 0); if (status < 0) { return PATH_STAT_FAILED; } /* return status */ if (stat_status == 0) { return PATH_STAT_OK; } if (errno == EACCES) { return PATH_STAT_ACCESS; } return PATH_STAT_FAILED; } /* * Check access to a path by a particular user/group. * Sets ftype output parameter if it is non-NULL. */ static int _path_stat_check_access(const char *path, uid_t uid, gid_t gid, uint32_t *ftype) { struct stat sb; char buf[MAXPATHLEN + 1]; int status, t; if (path == NULL) return PATH_STAT_FAILED; if (ftype != NULL) *ftype = PATH_NODE_TYPE_GHOST; /* Paths must be absolute */ if (path[0] != '/') return PATH_STAT_FAILED; /* Root dir is readable */ if (path[1] == '\0') { if (ftype != NULL) *ftype = PATH_NODE_TYPE_DIR; return PATH_STAT_OK; } memset(&sb, 0, sizeof(struct stat)); status = lstat(path, &sb); if (status != 0) return PATH_STAT_FAILED; else if ((sb.st_mode & S_IFMT) == S_IFDIR) t = PATH_NODE_TYPE_DIR; else if ((sb.st_mode & S_IFMT) == S_IFREG) t = PATH_NODE_TYPE_FILE; else if ((sb.st_mode & S_IFMT) == S_IFLNK) t = PATH_NODE_TYPE_LINK; else t = PATH_NODE_TYPE_OTHER; if (ftype != NULL) *ftype = t; if (t == PATH_NODE_TYPE_OTHER) return PATH_STAT_FAILED; /* skip access control check if uid is zero */ if (uid == 0) return 0; /* special case: anything in /etc/zoneinfo is OK */ memset(buf, 0, sizeof(buf)); if (realpath(path, buf) == NULL) return PATH_STAT_FAILED; if (!strncasecmp(buf, ZONEINFO_DIR, sizeof(ZONEINFO_DIR) - 1)) { return PATH_STAT_OK; } /* call _path_stat to check access as the user/group provided */ if (t == PATH_NODE_TYPE_FILE) { status = _path_stat(path, 0, uid, gid); if ((status == PATH_STAT_ACCESS) && (ftype != NULL)) *ftype = PATH_NODE_TYPE_GHOST; return status; } else if (t == PATH_NODE_TYPE_LINK) { status = _path_stat(path, 1, uid, gid); if ((status == PATH_STAT_ACCESS) && (ftype != NULL)) *ftype = PATH_NODE_TYPE_GHOST; return status; } else if (t == PATH_NODE_TYPE_DIR) { snprintf(buf, MAXPATHLEN, "%s/.", path); status = _path_stat(buf, 0, uid, gid); if ((status == PATH_STAT_ACCESS) && (ftype != NULL)) *ftype = PATH_NODE_TYPE_GHOST; return status; } /* we don't ever get here, but... */ return PATH_STAT_FAILED; } /* * Uniquely add a pnode to a vnode's list of path nodes. */ static void _vnode_add_pnode(vnode_t *vnode, path_node_t *pnode) { uint32_t i; for (i = 0; i < vnode->path_node_count; i++) { if (vnode->path_node[i] == pnode) return; } for (i = 0; i < vnode->path_node_count; i++) { if (vnode->path_node[i] == NULL) { vnode->path_node[i] = pnode; return; } } if (vnode->path_node_count == 0) { vnode->path_node = (path_node_t **)calloc(1, sizeof(path_node_t *)); } else { vnode->path_node = (path_node_t **)reallocf(vnode->path_node, (vnode->path_node_count + 1) * sizeof(path_node_t *)); } assert(vnode->path_node != NULL); vnode->path_node[vnode->path_node_count++] = pnode; } /* * Free a vnode_t and cancel/release its dispatch source. */ static void _vnode_free(vnode_t *vnode) { dispatch_source_cancel(vnode->src); /* * Actually free the vnode on the pathwatch queue. This allows any * enqueued _vnode_event operations to complete before the vnode disappears. * _vnode_event() quietly returns if the source has been cancelled. */ dispatch_async(_global.pathwatch_queue, ^{ dispatch_release(vnode->src); free(vnode->path); free(vnode->path_node); free(vnode); }); } /* * Handler routine for vnode_t objects. * Invokes the _path_node_update routine for all of the vnode's pnodes. */ static void _vnode_event(vnode_t *vnode) { uint32_t i, flags; unsigned long ulf; struct stat sb; if (vnode == NULL) return; if ((vnode->src != NULL) && (dispatch_source_testcancel(vnode->src))) return; ulf = dispatch_source_get_data(vnode->src); flags = ulf; memset(&sb, 0, sizeof(struct stat)); if (fstat(vnode->fd, &sb) == 0) { if ((vnode->mtime.tv_sec != sb.st_mtimespec.tv_sec) || (vnode->mtime.tv_nsec != sb.st_mtimespec.tv_nsec)) { flags |= PATH_NODE_MTIME; vnode->mtime = sb.st_mtimespec; } if ((vnode->ctime.tv_sec != sb.st_ctimespec.tv_sec) || (vnode->ctime.tv_nsec != sb.st_ctimespec.tv_nsec)) { flags |= PATH_NODE_CTIME; vnode->ctime = sb.st_ctimespec; } } /* * Flag deleted sources. * We can't delete them here, since _path_node_update may need them. * However, _path_node_update will release them and they will get cleaned * up in a _vnode_sweep later on. */ if (flags & DISPATCH_VNODE_DELETE) vnode->type = VPATH_NODE_TYPE_DELETED; for (i = 0; i < vnode->path_node_count; i++) { _path_node_update(vnode->path_node[i], flags, vnode); } } /* * Creates a vnode_t object. */ static vnode_t * _vnode_create(const char *path, uint32_t type, path_node_t *pnode) { int fd, flags; uint32_t i; vnode_t *vnode; dispatch_source_t src; struct stat sb; if (path == NULL) path = "/"; if (path[0] == '\0') path = "/"; for (i = 0; i < _global.vnode_count; i++) { vnode = _global.vnode[i]; if (vnode == NULL) continue; if ((vnode->type == type) && (streq(path, vnode->path))) { _vnode_add_pnode(vnode, pnode); return vnode; } } vnode = NULL; flags = O_EVTONLY; if (type == VPATH_NODE_TYPE_LINK) flags |= O_SYMLINK; fd = open(path, flags, 0); if (fd < 0) return NULL; src = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, (uintptr_t)fd, DISPATCH_VNODE_ALL, _global.pathwatch_queue); if (src == NULL) { close(fd); return NULL; } vnode = (vnode_t *)calloc(1, sizeof(vnode_t)); assert(vnode != NULL); vnode->type = type; vnode->path = strdup(path); assert(vnode->path != NULL); vnode->fd = fd; vnode->src = src; memset(&sb, 0, sizeof(struct stat)); if (fstat(fd, &sb) == 0) { vnode->mtime = sb.st_mtimespec; vnode->ctime = sb.st_ctimespec; } _vnode_add_pnode(vnode, pnode); dispatch_source_set_event_handler(src, ^{ _vnode_event(vnode); }); dispatch_source_set_cancel_handler(src, ^{ close(fd); }); if (_global.vnode_count == 0) { _global.vnode = (vnode_t **)calloc(1, sizeof(vnode_t *)); } else { _global.vnode = (vnode_t **)reallocf(_global.vnode, (_global.vnode_count + 1) * sizeof(vnode_t *)); } assert(_global.vnode != NULL); _global.vnode[_global.vnode_count++] = vnode; dispatch_resume(src); return vnode; } static vnode_t * _vnode_create_real_path(const char *path, uint32_t type, path_node_t *pnode) { char real[MAXPATHLEN + 1]; if (path == NULL) return _vnode_create(path, type, pnode); if (NULL != realpath(path, real)) return _vnode_create(real, type, pnode); return NULL; } /* * Examines all the vnode_t objects (held in the _global data), * frees any that have no path nodes. */ static void _vnode_sweep() { uint32_t i, j, new_vnode_count, new_path_node_count; vnode_t **new_source, *vnode; path_node_t **new_path_node; new_source = NULL; for (i = 0; i < _global.vnode_count; i++) { vnode = _global.vnode[i]; if (vnode == NULL) continue; new_path_node_count = 0; new_path_node = NULL; for (j = 0; j < vnode->path_node_count; j++) { if (vnode->path_node[j] != NULL) new_path_node_count++; } if (new_path_node_count == vnode->path_node_count) { /* no change */ continue; } else if (new_path_node_count > 0) { new_path_node = (path_node_t **)calloc(new_path_node_count, sizeof(path_node_t *)); assert(new_path_node != NULL); new_path_node_count = 0; for (j = 0; j < vnode->path_node_count; j++) { if (vnode->path_node[j] != NULL) { new_path_node[new_path_node_count++] = vnode->path_node[j]; } } } free(vnode->path_node); vnode->path_node = new_path_node; vnode->path_node_count = new_path_node_count; } new_vnode_count = 0; for (i = 0; i < _global.vnode_count; i++) { vnode = _global.vnode[i]; if (vnode == NULL) continue; if (vnode->path_node_count > 0) new_vnode_count++; } if (new_vnode_count == _global.vnode_count) { /* no change */ return; } else if (new_vnode_count > 0) { new_source = (vnode_t **)calloc(new_vnode_count, sizeof(vnode_t *)); assert(new_source != NULL); new_vnode_count = 0; for (i = 0; i < _global.vnode_count; i++) { vnode = _global.vnode[i]; if (vnode == NULL) continue; if (vnode->path_node_count > 0) { new_source[new_vnode_count++] = vnode; } else { _vnode_free(vnode); } } } free(_global.vnode); _global.vnode = new_source; _global.vnode_count = new_vnode_count; } /* * Releases sources that have a particular node on their list. * This is a deferred release mechanism for vnode_t objects. * The calling routine must call _vnode_sweep subsequent to * calling this routine. * _vnode_sweep will actually free any vnode_t objects * that have a no path nodes. */ static void _vnode_release_for_node(path_node_t *pnode) { uint32_t i, j; vnode_t *vnode; for (i = 0; i < _global.vnode_count; i++) { vnode = _global.vnode[i]; if (vnode == NULL) continue; for (j = 0; j < vnode->path_node_count; j++) { if (vnode->path_node[j] == pnode) { vnode->path_node[j] = NULL; break; } } } } /* * Frees a path_node_t object. * Dispatched on _global.pathwatch_queue. */ static void path_node_free(path_node_t *pnode) { uint32_t i, n; if (pnode == NULL) return; /* * Remove this path node from all vnodes. */ _vnode_release_for_node(pnode); _vnode_sweep(); free(pnode->path); if (pnode->pname != NULL) { n = pnode->pname_count; pnode->pname_count = 0; for (i = 0; i < n; i++) { free(pnode->pname[i]); pnode->pname[i] = NULL; } free(pnode->pname); } free(pnode->contextp); dispatch_release(pnode->src); dispatch_release(pnode->src_queue); memset(pnode, 0, sizeof(path_node_t)); free(pnode); } /* * Frees a path_node_t object. * The work is actually done on the global pathwatch_queue to make this safe. */ void path_node_close(path_node_t *pnode) { if (pnode == NULL) return; if (pnode->src != NULL) dispatch_source_cancel(pnode->src); /* * We need to make sure that the node's event handler isn't currently * executing before freeing the node. We dispatch on the src_queue, so * that when the block executes there will be no more events in the queue. * From there, we dispatch async back to the pathwatch_queue to do the * data structure cleanup. */ dispatch_async(pnode->src_queue, ^{ dispatch_async(_global.pathwatch_queue, ^{ path_node_free(pnode); }); }); } static void _pathwatch_init() { /* Create serial queue for node creation / deletion operations */ _global.pathwatch_queue = dispatch_queue_create("pathwatch", NULL); } /* * _path_node_init is responsible for allocating a path_node_t structure, * and for creating the pname array and setting the path component. * The path is a sanatized version of the caller's path with redundant "/" * characters stripped out. The pname array contains each "/" separated * component of the path. * * For example, _path_node_init("///foo////bar//baz/") creates: * pnode->path = "/foo/bar/baz" * pnode->pname_count = 3 * pnode->pname[0] = "foo" * pnode->pname[1] = "bar" * pnode->pname[2] = "baz" */ static path_node_t * _path_node_init(const char *path) { size_t len; uint32_t i; path_node_t *pnode; const char *start, *end; char *name; if (path == NULL) path = "/"; if (path[0] != '/') return NULL; pnode = (path_node_t *)calloc(1, sizeof(path_node_t)); assert(pnode != NULL); pnode->plen = 1; start = path; while (*start == '/') start++; forever { end = strchr(start, '/'); if (end == NULL) end = strchr(start, '\0'); len = end - start; if (len == 0) break; pnode->plen += (len + 1); name = NULL; if (end == NULL) { name = strdup(start); } else { name = malloc(len + 1); assert(name != NULL); strncpy(name, start, len); name[len] = '\0'; } if (pnode->pname_count == 0) { pnode->pname = (char **)calloc(1, sizeof(char *)); } else { pnode->pname = (char **)reallocf(pnode->pname, (pnode->pname_count + 1) * sizeof(char *)); } assert(pnode->pname != NULL); pnode->pname[pnode->pname_count] = name; pnode->pname_count++; start = end; if (start != NULL) { /* skip '/' chars */ while (*start == '/') start++; } } pnode->path = calloc(1, pnode->plen); assert(pnode->path != NULL); /* * Reconstruct the path here to strip out excess "/" chars. * This ensures that path comparisons in _path_node_update are correct. */ for (i = 0; i < pnode->pname_count; i++) { strlcat(pnode->path, "/", pnode->plen); strlcat(pnode->path, pnode->pname[i], pnode->plen); } return pnode; } /* dispatched on _global.pathwatch_queue */ static void _path_node_update(path_node_t *pnode, uint32_t flags, vnode_t *vnode) { char *buf, fixed[MAXPATHLEN + 1]; uint32_t i, old_type; int status; unsigned long data; struct stat sb; if (pnode == NULL) return; if ((pnode->src != NULL) && (dispatch_source_testcancel(pnode->src))) return; old_type = pnode->type; status = _path_stat_check_access(pnode->path, pnode->uid, pnode->gid, &(pnode->type)); if (status == PATH_STAT_ACCESS) flags |= DISPATCH_VNODE_REVOKE; data = 0; if (vnode != NULL) { /* update status */ if (flags & DISPATCH_VNODE_UNAVAIL) { pnode->type = PATH_NODE_TYPE_GHOST; data |= (flags & DISPATCH_VNODE_UNAVAIL); data |= DISPATCH_VNODE_DELETE; } if ((vnode->path != NULL) && (pnode->path != NULL) && streq(vnode->path, pnode->path)) { /* this is the target VNODE - transfer flags to src data */ data |= flags; } if (old_type == PATH_NODE_TYPE_GHOST) { if (pnode->type != PATH_NODE_TYPE_GHOST) { data |= PATH_NODE_CREATE; } else { data = 0; } } data &= pnode->mask; if (data != 0) dispatch_source_merge_data(pnode->src, data); } buf = NULL; if (pnode->plen < MAXPATHLEN) buf = fixed; else buf = malloc(pnode->plen); assert(buf != NULL); /* "autorelease" current sources (_vnode_sweep() will delete those with zero refcount) */ _vnode_release_for_node(pnode); /* create new sources (may re-use existing sources) */ _vnode_create(NULL, 0, pnode); memset(buf, 0, pnode->plen); for (i = 0; i < pnode->pname_count; i++) { assert((strlen(buf) + 1) <= pnode->plen); strlcat(buf, "/", pnode->plen); assert(pnode->pname[i] != NULL); assert((strlen(buf) + strlen(pnode->pname[i])) <= pnode->plen); strlcat(buf, pnode->pname[i], pnode->plen); memset(&sb, 0, sizeof(struct stat)); if (lstat(buf, &sb) < 0) { /* the path stops existing here */ break; } if ((sb.st_mode & S_IFMT) == S_IFLNK) { /* open the symlink itself */ _vnode_create(buf, VPATH_NODE_TYPE_LINK, pnode); /* open the symlink target */ _vnode_create_real_path(buf, 0, pnode); } else { _vnode_create(buf, 0, pnode); } } /* sweep source list (deletes those with zero refcount) */ _vnode_sweep(); if (buf != fixed) free(buf); } /* * Creates a dispatch source that activates when a path changes. * Internally, creates a data structure (path_node_t) that represents the entire path. * Also creates dispatch sources (vnode_t) for each path component. These vnodes may * be shared with other path_node_t structures. */ path_node_t * path_node_create(const char *path, uid_t uid, gid_t gid, uint32_t mask, dispatch_queue_t queue) { path_node_t *pnode; dispatch_once(&(_global.pathwatch_init), ^{ _pathwatch_init(); }); pnode = _path_node_init(path); if (pnode == NULL) return NULL; pnode->uid = uid; pnode->gid = gid; dispatch_sync(_global.pathwatch_queue, ^{ _path_node_update(pnode, 0, NULL); }); dispatch_retain(queue); pnode->src = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, queue); pnode->src_queue = queue; pnode->mask = mask; return pnode; }