/*
* Copyright (c) 2005, 2006, 2007, 2020 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Julio M. Merino Vidal, developed as part of Google's Summer of Code
* 2005 program.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* tmpfs_lookup: path name traversal routine.
*
* Arguments: dvp (directory being searched), vpp (result),
* cnp (component name - path).
*
* => Caller holds a reference and lock on dvp.
* => We return looked-up vnode (vpp) locked, with a reference held.
*/
int
tmpfs_lookup(void *v)
{
struct vop_lookup_v2_args /* {
struct vnode *a_dvp;
struct vnode **a_vpp;
struct componentname *a_cnp;
} */ *ap = v;
vnode_t *dvp = ap->a_dvp, **vpp = ap->a_vpp;
struct componentname *cnp = ap->a_cnp;
const bool lastcn = (cnp->cn_flags & ISLASTCN) != 0;
tmpfs_node_t *dnode, *tnode;
tmpfs_dirent_t *de;
int cachefound, iswhiteout;
int error;
KASSERT(VOP_ISLOCKED(dvp));
dnode = VP_TO_TMPFS_DIR(dvp);
*vpp = NULL;
/* Check accessibility of directory. */
error = VOP_ACCESS(dvp, VEXEC, cnp->cn_cred);
if (error) {
goto out;
}
/*
* If requesting the last path component on a read-only file system
* with a write operation, deny it.
*/
if (lastcn && (dvp->v_mount->mnt_flag & MNT_RDONLY) != 0 &&
(cnp->cn_nameiop == DELETE || cnp->cn_nameiop == RENAME)) {
error = EROFS;
goto out;
}
/*
* Avoid doing a linear scan of the directory if the requested
* directory/name couple is already in the cache.
*/
cachefound = cache_lookup(dvp, cnp->cn_nameptr, cnp->cn_namelen,
cnp->cn_nameiop, cnp->cn_flags,
&iswhiteout, vpp);
if (iswhiteout) {
cnp->cn_flags |= ISWHITEOUT;
}
if (cachefound && *vpp == NULLVP) {
/* Negative cache hit. */
error = ENOENT;
goto out;
} else if (cachefound) {
error = 0;
goto out;
}
/*
* Treat an unlinked directory as empty (no "." or "..")
*/
if (dnode->tn_links == 0) {
KASSERT(dnode->tn_size == 0);
error = ENOENT;
goto out;
}
if (cnp->cn_flags & ISDOTDOT) {
tmpfs_node_t *pnode;
/*
* Other lookup cases: perform directory scan.
*/
de = tmpfs_dir_lookup(dnode, cnp);
if (de == NULL || de->td_node == TMPFS_NODE_WHITEOUT) {
/*
* The entry was not found in the directory. This is valid
* if we are creating or renaming an entry and are working
* on the last component of the path name.
*/
if (lastcn && (cnp->cn_nameiop == CREATE ||
cnp->cn_nameiop == RENAME)) {
error = VOP_ACCESS(dvp, VWRITE, cnp->cn_cred);
if (error) {
goto out;
}
error = EJUSTRETURN;
} else {
error = ENOENT;
}
if (de) {
KASSERT(de->td_node == TMPFS_NODE_WHITEOUT);
cnp->cn_flags |= ISWHITEOUT;
}
goto done;
}
tnode = de->td_node;
/*
* If it is not the last path component and found a non-directory
* or non-link entry (which may itself be pointing to a directory),
* raise an error.
*/
if (!lastcn && tnode->tn_type != VDIR && tnode->tn_type != VLNK) {
error = ENOTDIR;
goto out;
}
/* Check the permissions. */
if (lastcn && (cnp->cn_nameiop == DELETE || cnp->cn_nameiop == RENAME)) {
error = VOP_ACCESS(dvp, VWRITE, cnp->cn_cred);
if (error)
goto out;
/* Get a vnode for the matching entry. */
error = vcache_get(dvp->v_mount, &tnode, sizeof(tnode), vpp);
done:
/*
* Cache the result, unless request was for creation (as it does
* not improve the performance).
*/
if (cnp->cn_nameiop != CREATE) {
cache_enter(dvp, *vpp, cnp->cn_nameptr, cnp->cn_namelen,
cnp->cn_flags);
}
out:
KASSERT(VOP_ISLOCKED(dvp));
if (uio->uio_offset + uio->uio_resid > node->tn_size) {
error = tmpfs_reg_resize(vp, uio->uio_offset + uio->uio_resid);
if (error)
goto out;
}
/*
* If we're extending the file and have data to write that would
* not leave an un-zeroed hole, we can avoid fault processing and
* zeroing of pages on allocation.
*
* Don't do this if the file is mapped and we need to touch an
* existing page, because writing a mapping of the file into itself
* could cause a deadlock on PG_BUSY.
*
* New pages will not become visible until finished here (because
* of PG_BUSY and the vnode lock).
*/
ubc_flags = UBC_WRITE | UBC_VNODE_FLAGS(vp);
#if 0
/*
* XXX disable use of UBC_FAULTBUSY for now, this check is insufficient
* because it does not zero uninitialized parts of pages in all of
* the cases where zeroing is needed.
*/
if (uio->uio_offset >= oldsize &&
((uio->uio_offset & (PAGE_SIZE - 1)) == 0 ||
((vp->v_vflag & VV_MAPPED) == 0 &&
trunc_page(uio->uio_offset) == trunc_page(oldsize)))) {
ubc_flags |= UBC_FAULTBUSY;
}
#endif
/*
* Files marked as immutable or append-only cannot be deleted.
* Likewise, files residing on directories marked as append-only
* cannot be deleted.
*/
if (node->tn_flags & (IMMUTABLE | APPEND)) {
error = EPERM;
goto out;
}
if (dnode->tn_flags & APPEND) {
error = EPERM;
goto out;
}
/* Lookup the directory entry (check the cached hint first). */
de = tmpfs_dir_cached(node);
if (de == NULL) {
struct componentname *cnp = ap->a_cnp;
de = tmpfs_dir_lookup(dnode, cnp);
}
KASSERT(de && de->td_node == node);
/*
* Remove the entry from the directory (drops the link count) and
* destroy it or replace with a whiteout.
*
* Note: the inode referred by it will not be destroyed until the
* vnode is reclaimed/recycled.
*/
tmpfs_dir_detach(dnode, de);
if (ap->a_cnp->cn_flags & DOWHITEOUT)
tmpfs_dir_attach(dnode, de, TMPFS_NODE_WHITEOUT);
else
tmpfs_free_dirent(VFS_TO_TMPFS(vp->v_mount), de);
tflags = TMPFS_UPDATE_MTIME | TMPFS_UPDATE_CTIME;
if (node->tn_links > 0) {
/* We removed a hard link. */
tflags |= TMPFS_UPDATE_CTIME;
}
ap->ctx_vp_new_nlink = node->tn_links;
tmpfs_update(dvp, tflags);
error = 0;
out:
/* Drop the reference and unlock the node. */
if (dvp == vp) {
vrele(vp);
} else {
vput(vp);
}
return error;
}
/* Allocate a new directory entry to represent the inode. */
error = tmpfs_alloc_dirent(VFS_TO_TMPFS(vp->v_mount),
cnp->cn_nameptr, cnp->cn_namelen, &de);
if (error) {
goto out;
}
/*
* Insert the entry into the directory.
* It will increase the inode link count.
*/
tmpfs_dir_attach(dnode, de, node);
tmpfs_update(dvp, TMPFS_UPDATE_MTIME | TMPFS_UPDATE_CTIME);
/*
* Directories with more than two entries ('.' and '..') cannot be
* removed. There may be whiteout entries, which we will destroy.
*/
if (node->tn_size > 0) {
/*
* If never had whiteout entries, the directory is certainly
* not empty. Otherwise, scan for any non-whiteout entry.
*/
if ((node->tn_gen & TMPFS_WHITEOUT_BIT) == 0) {
error = ENOTEMPTY;
goto out;
}
TAILQ_FOREACH(de, &node->tn_spec.tn_dir.tn_dir, td_entries) {
if (de->td_node != TMPFS_NODE_WHITEOUT) {
error = ENOTEMPTY;
goto out;
}
}
KASSERT(error == 0);
}
KASSERT(node->tn_spec.tn_dir.tn_parent == dnode);
/* Lookup the directory entry (check the cached hint first). */
de = tmpfs_dir_cached(node);
if (de == NULL) {
struct componentname *cnp = ap->a_cnp;
de = tmpfs_dir_lookup(dnode, cnp);
}
KASSERT(de && de->td_node == node);
/* Check flags to see if we are allowed to remove the directory. */
if (dnode->tn_flags & APPEND || node->tn_flags & (IMMUTABLE | APPEND)) {
error = EPERM;
goto out;
}
/* Decrement the link count for the virtual '.' entry. */
node->tn_links--;
/* Detach the directory entry from the directory. */
tmpfs_dir_detach(dnode, de);
/* Purge the cache for parent. */
cache_purge(dvp);
/*
* Destroy the directory entry or replace it with a whiteout.
*
* Note: the inode referred by it will not be destroyed until the
* vnode is reclaimed.
*/
if (ap->a_cnp->cn_flags & DOWHITEOUT)
tmpfs_dir_attach(dnode, de, TMPFS_NODE_WHITEOUT);
else
tmpfs_free_dirent(tmp, de);
/* Destroy the whiteout entries from the node. */
while ((de = TAILQ_FIRST(&node->tn_spec.tn_dir.tn_dir)) != NULL) {
KASSERT(de->td_node == TMPFS_NODE_WHITEOUT);
tmpfs_dir_detach(node, de);
tmpfs_free_dirent(tmp, de);
}
tmpfs_update(dvp, TMPFS_UPDATE_MTIME | TMPFS_UPDATE_CTIME);
int
tmpfs_readdir(void *v)
{
struct vop_readdir_args /* {
struct vnode *a_vp;
struct uio *a_uio;
kauth_cred_t a_cred;
int *a_eofflag;
off_t **a_cookies;
int *ncookies;
} */ *ap = v;
vnode_t *vp = ap->a_vp;
struct uio *uio = ap->a_uio;
int *eofflag = ap->a_eofflag;
off_t **cookies = ap->a_cookies;
int *ncookies = ap->a_ncookies;
off_t startoff, cnt;
tmpfs_node_t *node;
int error;
KASSERT(VOP_ISLOCKED(vp));
/* This operation only makes sense on directory nodes. */
if (vp->v_type != VDIR) {
return ENOTDIR;
}
node = VP_TO_TMPFS_DIR(vp);
startoff = uio->uio_offset;
cnt = 0;
/*
* Retrieve the directory entries, unless it is being destroyed.
*/
if (node->tn_links) {
error = tmpfs_dir_getdents(node, uio, &cnt);
} else {
error = 0;
}
if (eofflag != NULL) {
*eofflag = !error && uio->uio_offset == TMPFS_DIRSEQ_EOF;
}
if (error || cookies == NULL || ncookies == NULL) {
return error;
}
/* Update NFS-related variables, if any. */
tmpfs_dirent_t *de = NULL;
off_t i, off = startoff;
node = VP_TO_TMPFS_NODE(vp);
if (node->tn_links == 0) {
/*
* Mark node as dead by setting its generation to zero.
*/
atomic_and_32(&node->tn_gen, ~TMPFS_NODE_GEN_MASK);
/*
* If the file has been deleted, truncate it, otherwise VFS
* will quite rightly try to write back dirty data, which in
* the case of tmpfs/UAO means needless page deactivations.
*/
if (vp->v_type == VREG) {
error = tmpfs_reg_resize(vp, 0);
}
*ap->a_recycle = true;
} else {
tmpfs_update(vp, 0);
*ap->a_recycle = false;
}
/*
* Check for reclaimed vnode. v_interlock is not held here, but
* VI_DEADCHECK is set with vmobjlock held.
*/
iflag = atomic_load_relaxed(&vp->v_iflag);
if (__predict_false((iflag & VI_DEADCHECK) != 0)) {
mutex_enter(vp->v_interlock);
error = vdead_check(vp, VDEAD_NOWAIT);
mutex_exit(vp->v_interlock);
if (error) {
if ((flags & PGO_LOCKED) == 0)
rw_exit(vp->v_uobj.vmobjlock);
return error;
}
}
/*
* Update timestamp lazily. The update will be made real when
* a synchronous update is next made -- or by tmpfs_getattr,
* tmpfs_putpages, and tmpfs_inactive.
*/
if ((flags & PGO_NOTIMESTAMP) == 0) {
u_int tflags = 0;
if ((vp->v_mount->mnt_flag & MNT_NOATIME) == 0)
tflags |= TMPFS_UPDATE_ATIME;