X-Git-Url: https://git.saurik.com/apple/xnu.git/blobdiff_plain/39236c6e673c41db228275375ab7fdb0f837b292..743345f9a4b36f7e2f9ba37691e70c50baecb56e:/bsd/vfs/vfs_cache.c diff --git a/bsd/vfs/vfs_cache.c b/bsd/vfs/vfs_cache.c index 65cb02d22..b7876475d 100644 --- a/bsd/vfs/vfs_cache.c +++ b/bsd/vfs/vfs_cache.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000-2012 Apple Inc. All rights reserved. + * Copyright (c) 2000-2015 Apple Inc. All rights reserved. * * @APPLE_OSREFERENCE_LICENSE_HEADER_START@ * @@ -178,7 +178,214 @@ static unsigned int crc32tab[256]; #define NCHHASH(dvp, hash_val) \ (&nchashtbl[(dvp->v_id ^ (hash_val)) & nchashmask]) +/* + * This function tries to check if a directory vp is a subdirectory of dvp + * only from valid v_parent pointers. It is called with the name cache lock + * held and does not drop the lock anytime inside the function. + * + * It returns a boolean that indicates whether or not it was able to + * successfully infer the parent/descendent relationship via the v_parent + * pointers, or if it could not infer such relationship and that the decision + * must be delegated to the owning filesystem. + * + * If it does not defer the decision, i.e. it was successfuly able to determine + * the parent/descendent relationship, *is_subdir tells the caller if vp is a + * subdirectory of dvp. + * + * If the decision is deferred, *next_vp is where it stopped i.e. *next_vp + * is the vnode whose parent is to be determined from the filesystem. + * *is_subdir, in this case, is not indicative of anything and should be + * ignored. + * + * The return value and output args should be used as follows : + * + * defer = cache_check_vnode_issubdir(vp, dvp, is_subdir, next_vp); + * if (!defer) { + * if (*is_subdir) + * vp is subdirectory; + * else + * vp is not a subdirectory; + * } else { + * if (*next_vp) + * check this vnode's parent from the filesystem + * else + * error (likely because of forced unmount). + * } + * + */ +static boolean_t +cache_check_vnode_issubdir(vnode_t vp, vnode_t dvp, boolean_t *is_subdir, + vnode_t *next_vp) +{ + vnode_t tvp = vp; + int defer = FALSE; + + *is_subdir = FALSE; + *next_vp = NULLVP; + while (1) { + mount_t tmp; + + if (tvp == dvp) { + *is_subdir = TRUE; + break; + } else if (tvp == rootvnode) { + /* *is_subdir = FALSE */ + break; + } + + tmp = tvp->v_mount; + while ((tvp->v_flag & VROOT) && tmp && tmp->mnt_vnodecovered && + tvp != dvp && tvp != rootvnode) { + tvp = tmp->mnt_vnodecovered; + tmp = tvp->v_mount; + } + + /* + * If dvp is not at the top of a mount "stack" then + * vp is not a subdirectory of dvp either. + */ + if (tvp == dvp || tvp == rootvnode) { + /* *is_subdir = FALSE */ + break; + } + + if (!tmp) { + defer = TRUE; + *next_vp = NULLVP; + break; + } + + if ((tvp->v_flag & VISHARDLINK) || !(tvp->v_parent)) { + defer = TRUE; + *next_vp = tvp; + break; + } + + tvp = tvp->v_parent; + } + + return (defer); +} + +/* maximum times retry from potentially transient errors in vnode_issubdir */ +#define MAX_ERROR_RETRY 3 + +/* + * This function checks if a given directory (vp) is a subdirectory of dvp. + * It walks backwards from vp and if it hits dvp in its parent chain, + * it is a subdirectory. If it encounters the root directory, it is not + * a subdirectory. + * + * This function returns an error if it is unsuccessful and 0 on success. + * + * On entry (and exit) vp has an iocount and if this function has to take + * any iocounts on other vnodes in the parent chain traversal, it releases them. + */ +int +vnode_issubdir(vnode_t vp, vnode_t dvp, int *is_subdir, vfs_context_t ctx) +{ + vnode_t start_vp, tvp; + vnode_t vp_with_iocount; + int error = 0; + char dotdotbuf[] = ".."; + int error_retry_count = 0; /* retry count for potentially transient + errors */ + + *is_subdir = FALSE; + tvp = start_vp = vp; + /* + * Anytime we acquire an iocount in this function, we save the vnode + * in this variable and release it before exiting. + */ + vp_with_iocount = NULLVP; + + while (1) { + boolean_t defer; + vnode_t pvp; + uint32_t vid; + struct componentname cn; + boolean_t is_subdir_locked = FALSE; + + if (tvp == dvp) { + *is_subdir = TRUE; + break; + } else if (tvp == rootvnode) { + /* *is_subdir = FALSE */ + break; + } + + NAME_CACHE_LOCK_SHARED(); + + defer = cache_check_vnode_issubdir(tvp, dvp, &is_subdir_locked, + &tvp); + + if (defer && tvp) + vid = vnode_vid(tvp); + + NAME_CACHE_UNLOCK(); + + if (!defer) { + *is_subdir = is_subdir_locked; + break; + } + + if (!tvp) { + if (error_retry_count++ < MAX_ERROR_RETRY) { + tvp = vp; + continue; + } + error = ENOENT; + break; + } + + if (tvp != start_vp) { + if (vp_with_iocount) { + vnode_put(vp_with_iocount); + vp_with_iocount = NULLVP; + } + + error = vnode_getwithvid(tvp, vid); + if (error) { + if (error_retry_count++ < MAX_ERROR_RETRY) { + tvp = vp; + error = 0; + continue; + } + break; + } + + vp_with_iocount = tvp; + } + + bzero(&cn, sizeof(cn)); + cn.cn_nameiop = LOOKUP; + cn.cn_flags = ISLASTCN | ISDOTDOT; + cn.cn_context = ctx; + cn.cn_pnbuf = &dotdotbuf[0]; + cn.cn_pnlen = sizeof(dotdotbuf); + cn.cn_nameptr = cn.cn_pnbuf; + cn.cn_namelen = 2; + + pvp = NULLVP; + if ((error = VNOP_LOOKUP(tvp, &pvp, &cn, ctx))) + break; + + if (!(tvp->v_flag & VISHARDLINK) && tvp->v_parent != pvp) { + (void)vnode_update_identity(tvp, pvp, NULL, 0, 0, + VNODE_UPDATE_PARENT); + } + + if (vp_with_iocount) + vnode_put(vp_with_iocount); + + vp_with_iocount = tvp = pvp; + } + if (vp_with_iocount) + vnode_put(vp_with_iocount); + + return (error); +} /* * This function builds the path to a filename in "buff". The @@ -218,6 +425,9 @@ build_path(vnode_t first_vp, char *buff, int buflen, int *outlen, int flags, vfs if (first_vp == NULLVP) return (EINVAL); + + if (buflen <= 1) + return (ENOSPC); /* * Grab the process fd so we can evaluate fd_rdir. @@ -339,8 +549,12 @@ again: * and disallow further path construction */ if ((vp->v_parent == NULLVP) && (rootvnode != vp)) { - /* Only '/' is allowed to have a NULL parent pointer */ - ret = EINVAL; + /* + * Only '/' is allowed to have a NULL parent + * pointer. Upper level callers should ideally + * re-drive name lookup on receiving a ENOENT. + */ + ret = ENOENT; /* The code below will exit early if 'tvp = vp' == NULL */ } @@ -459,6 +673,26 @@ bad_news: vp = vp->v_parent; } + if (vp && (flags & BUILDPATH_CHECKACCESS)) { + vid = vp->v_id; + + NAME_CACHE_UNLOCK(); + + if (vp != first_vp && vp != vp_with_iocount) { + if (vp_with_iocount) { + vnode_put(vp_with_iocount); + vp_with_iocount = NULLVP; + } + if (vnode_getwithvid(vp, vid)) + goto again; + vp_with_iocount = vp; + } + if ((ret = vnode_authorize(vp, NULL, KAUTH_VNODE_SEARCH, ctx))) + goto out; /* no peeking */ + + NAME_CACHE_LOCK_SHARED(); + } + /* * When a mount point is crossed switch the vp. * Continue until we find the root or we find @@ -484,26 +718,6 @@ bad_news: if (tvp == NULLVP) goto out_unlock; vp = tvp; - - if (vp && (flags & BUILDPATH_CHECKACCESS)) { - vid = vp->v_id; - - NAME_CACHE_UNLOCK(); - - if (vp != first_vp && vp != vp_with_iocount) { - if (vp_with_iocount) { - vnode_put(vp_with_iocount); - vp_with_iocount = NULLVP; - } - if (vnode_getwithvid(vp, vid)) - goto again; - vp_with_iocount = vp; - } - if ((ret = vnode_authorize(vp, NULL, KAUTH_VNODE_SEARCH, ctx))) - goto out; /* no peeking */ - - NAME_CACHE_LOCK_SHARED(); - } } out_unlock: NAME_CACHE_UNLOCK(); @@ -693,7 +907,7 @@ vnode_update_identity(vnode_t vp, vnode_t dvp, const char *name, int name_len, u while ( (ncp = LIST_FIRST(&vp->v_nclinks)) ) cache_delete(ncp, 1); - while ( (ncp = LIST_FIRST(&vp->v_ncchildren)) ) + while ( (ncp = TAILQ_FIRST(&vp->v_ncchildren)) ) cache_delete(ncp, 1); /* @@ -1053,6 +1267,7 @@ cache_lookup_path(struct nameidata *ndp, struct componentname *cnp, vnode_t dp, mount_t mp; unsigned int hash; int error = 0; + boolean_t dotdotchecked = FALSE; #if CONFIG_TRIGGERS vnode_t trigger_vp; @@ -1166,9 +1381,15 @@ skiprsrcfork: /* * NAME_CACHE_LOCK holds these fields stable + * + * We can't cache KAUTH_VNODE_SEARCHBYANYONE for root correctly + * so we make an ugly check for root here. root is always + * allowed and breaking out of here only to find out that is + * authorized by virtue of being root is very very expensive. */ if ((dp->v_cred != ucred || !(dp->v_authorized_actions & KAUTH_VNODE_SEARCH)) && - !(dp->v_authorized_actions & KAUTH_VNODE_SEARCHBYANYONE)) + !(dp->v_authorized_actions & KAUTH_VNODE_SEARCHBYANYONE) && + !vfs_context_issuser(ctx)) break; /* @@ -1221,9 +1442,53 @@ skiprsrcfork: */ if (cnp->cn_namelen == 1 && cnp->cn_nameptr[0] == '.') vp = dp; - else if ( (cnp->cn_flags & ISDOTDOT) ) - vp = dp->v_parent; - else { + else if ( (cnp->cn_flags & ISDOTDOT) ) { + /* + * If this is a chrooted process, we need to check if + * the process is trying to break out of its chrooted + * jail. We do that by trying to determine if dp is + * a subdirectory of ndp->ni_rootdir. If we aren't + * able to determine that by the v_parent pointers, we + * will leave the fast path. + * + * Since this function may see dotdot components + * many times and it has the name cache lock held for + * the entire duration, we optimise this by doing this + * check only once per cache_lookup_path call. + * If dotdotchecked is set, it means we've done this + * check once already and don't need to do it again. + */ + if (!dotdotchecked && (ndp->ni_rootdir != rootvnode)) { + vnode_t tvp = dp; + boolean_t defer = FALSE; + boolean_t is_subdir = FALSE; + + defer = cache_check_vnode_issubdir(tvp, + ndp->ni_rootdir, &is_subdir, &tvp); + + if (defer) { + /* defer to Filesystem */ + break; + } else if (!is_subdir) { + /* + * This process is trying to break out + * of its chrooted jail, so all its + * dotdot accesses will be translated to + * its root directory. + */ + vp = ndp->ni_rootdir; + } else { + /* + * All good, let this dotdot access + * proceed normally + */ + vp = dp->v_parent; + } + dotdotchecked = TRUE; + } else { + vp = dp->v_parent; + } + } else { if ( (vp = cache_lookup_locked(dp, cnp)) == NULLVP) break; @@ -1245,11 +1510,11 @@ skiprsrcfork: } if ( (mp = vp->v_mountedhere) && ((cnp->cn_flags & NOCROSSMOUNT) == 0)) { - - if (mp->mnt_realrootvp == NULLVP || mp->mnt_generation != mount_generation || - mp->mnt_realrootvp_vid != mp->mnt_realrootvp->v_id) - break; - vp = mp->mnt_realrootvp; + vnode_t tmp_vp = mp->mnt_realrootvp; + if (tmp_vp == NULLVP || mp->mnt_generation != mount_generation || + mp->mnt_realrootvp_vid != tmp_vp->v_id) + break; + vp = tmp_vp; } #if CONFIG_TRIGGERS @@ -1258,10 +1523,8 @@ skiprsrcfork: * trigger in hand, resolve it. Note that we don't need to * leave the fast path if the mount has already happened. */ - if ((vp->v_resolve != NULL) && - (vp->v_resolve->vr_resolve_func != NULL)) { + if (vp->v_resolve) break; - } #endif /* CONFIG_TRIGGERS */ @@ -1403,7 +1666,7 @@ cache_lookup_locked(vnode_t dvp, struct componentname *cnp) struct namecache *ncp; struct nchashhead *ncpp; long namelen = cnp->cn_namelen; - unsigned int hashval = (cnp->cn_hash & NCHASHMASK); + unsigned int hashval = cnp->cn_hash; if (nc_disabled) { return NULL; @@ -1487,7 +1750,7 @@ cache_lookup(struct vnode *dvp, struct vnode **vpp, struct componentname *cnp) if (cnp->cn_hash == 0) cnp->cn_hash = hash_string(cnp->cn_nameptr, cnp->cn_namelen); - hashval = (cnp->cn_hash & NCHASHMASK); + hashval = cnp->cn_hash; if (nc_disabled) { return 0; @@ -1560,12 +1823,9 @@ relook: /* * We found a "negative" match, ENOENT notifies client of this match. - * The nc_whiteout field records whether this is a whiteout. */ NCHSTAT(ncs_neghits); - if (ncp->nc_whiteout) - cnp->cn_flags |= ISWHITEOUT; NAME_CACHE_UNLOCK(); return (ENOENT); } @@ -1702,12 +1962,30 @@ cache_enter_locked(struct vnode *dvp, struct vnode *vp, struct componentname *cn ncp->nc_vp = vp; ncp->nc_dvp = dvp; ncp->nc_hashval = cnp->cn_hash; - ncp->nc_whiteout = FALSE; if (strname == NULL) ncp->nc_name = add_name_internal(cnp->cn_nameptr, cnp->cn_namelen, cnp->cn_hash, FALSE, 0); else ncp->nc_name = strname; + + // + // If the bytes of the name associated with the vnode differ, + // use the name associated with the vnode since the file system + // may have set that explicitly in the case of a lookup on a + // case-insensitive file system where the case of the looked up + // name differs from what is on disk. For more details, see: + // FSEvents doesn't always decompose diacritical unicode chars in the paths of the changed directories + // + const char *vn_name = vp ? vp->v_name : NULL; + unsigned int len = vn_name ? strlen(vn_name) : 0; + if (vn_name && ncp && ncp->nc_name && strncmp(ncp->nc_name, vn_name, len) != 0) { + unsigned int hash = hash_string(vn_name, len); + + vfs_removename(ncp->nc_name); + ncp->nc_name = add_name_internal(vn_name, len, hash, FALSE, 0); + ncp->nc_hashval = hash; + } + /* * make us the newest entry in the cache * i.e. we'll be the last to be stolen @@ -1738,13 +2016,10 @@ cache_enter_locked(struct vnode *dvp, struct vnode *vp, struct componentname *cn } else { /* * this is a negative cache entry (vp == NULL) - * stick it on the negative cache list - * and record the whiteout state + * stick it on the negative cache list. */ TAILQ_INSERT_TAIL(&neghead, ncp, nc_un.nc_negentry); - if (cnp->cn_flags & ISWHITEOUT) - ncp->nc_whiteout = TRUE; ncs_negtotal++; if (ncs_negtotal > desiredNegNodes) { @@ -1761,7 +2036,10 @@ cache_enter_locked(struct vnode *dvp, struct vnode *vp, struct componentname *cn * add us to the list of name cache entries that * are children of dvp */ - LIST_INSERT_HEAD(&dvp->v_ncchildren, ncp, nc_child); + if (vp) + TAILQ_INSERT_TAIL(&dvp->v_ncchildren, ncp, nc_child); + else + TAILQ_INSERT_HEAD(&dvp->v_ncchildren, ncp, nc_child); } @@ -1933,7 +2211,7 @@ cache_delete(struct namecache *ncp, int age_entry) TAILQ_REMOVE(&neghead, ncp, nc_un.nc_negentry); ncs_negtotal--; } - LIST_REMOVE(ncp, nc_child); + TAILQ_REMOVE(&(ncp->nc_dvp->v_ncchildren), ncp, nc_child); LIST_REMOVE(ncp, nc_hash); /* @@ -1968,7 +2246,7 @@ cache_purge(vnode_t vp) kauth_cred_t tcred = NULL; if ((LIST_FIRST(&vp->v_nclinks) == NULL) && - (LIST_FIRST(&vp->v_ncchildren) == NULL) && + (TAILQ_FIRST(&vp->v_ncchildren) == NULL) && (vp->v_cred == NOCRED) && (vp->v_parent == NULLVP)) return; @@ -1981,7 +2259,7 @@ cache_purge(vnode_t vp) while ( (ncp = LIST_FIRST(&vp->v_nclinks)) ) cache_delete(ncp, 1); - while ( (ncp = LIST_FIRST(&vp->v_ncchildren)) ) + while ( (ncp = TAILQ_FIRST(&vp->v_ncchildren)) ) cache_delete(ncp, 1); /* @@ -2011,9 +2289,12 @@ cache_purge_negatives(vnode_t vp) NAME_CACHE_LOCK(); - LIST_FOREACH_SAFE(ncp, &vp->v_ncchildren, nc_child, next_ncp) - if (ncp->nc_vp == NULL) - cache_delete(ncp , 1); + TAILQ_FOREACH_SAFE(ncp, &vp->v_ncchildren, nc_child, next_ncp) { + if (ncp->nc_vp) + break; + + cache_delete(ncp, 1); + } NAME_CACHE_UNLOCK(); } @@ -2144,6 +2425,9 @@ add_name_internal(const char *name, uint32_t len, u_int hashval, boolean_t need_ uint32_t lock_index; char *ptr; + if (len > MAXPATHLEN) + len = MAXPATHLEN; + /* * if the length already accounts for the null-byte, then * subtract one so later on we don't index past the end