root/usr/src/uts/common/fs/smbsrv/smb_pathname.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2019 Nexenta by DDN, Inc. All rights reserved.
 * Copyright 2021-2023 RackTop Systems, Inc.
 */

#include <smbsrv/smb_kproto.h>
#include <smbsrv/smb_fsops.h>
#include <sys/pathname.h>
#include <sys/priv_const.h>
#include <sys/policy.h>
#include <sys/sdt.h>

static char *smb_pathname_catia_v5tov4(smb_request_t *, char *, char *, int);
static char *smb_pathname_catia_v4tov5(smb_request_t *, char *, char *, int);
static int smb_pathname_lookup(pathname_t *, pathname_t *, int,
    vnode_t **, vnode_t *, vnode_t *, smb_attr_t *attr, cred_t *);
static char *smb_pathname_strdup(smb_request_t *, const char *);
static char *smb_pathname_strcat(smb_request_t *, char *, const char *);
static void smb_pathname_preprocess(smb_request_t *, smb_pathname_t *);
static void smb_pathname_preprocess_quota(smb_request_t *, smb_pathname_t *);
static int smb_pathname_dfs_preprocess(smb_request_t *, char *, size_t);
static void smb_pathname_preprocess_adminshare(smb_request_t *,
    smb_pathname_t *);


uint32_t
smb_is_executable(char *path)
{
        char    extension[5];
        int     len = strlen(path);

        if ((len >= 4) && (path[len - 4] == '.')) {
                (void) strcpy(extension, &path[len - 3]);
                (void) smb_strupr(extension);

                if (strcmp(extension, "EXE") == 0)
                        return (NODE_FLAGS_EXECUTABLE);

                if (strcmp(extension, "COM") == 0)
                        return (NODE_FLAGS_EXECUTABLE);

                if (strcmp(extension, "DLL") == 0)
                        return (NODE_FLAGS_EXECUTABLE);

                if (strcmp(extension, "SYM") == 0)
                        return (NODE_FLAGS_EXECUTABLE);
        }

        return (0);
}

/*
 * smb_pathname_reduce
 *
 * smb_pathname_reduce() takes a path and returns the smb_node for the
 * second-to-last component of the path.  It also returns the name of the last
 * component.  Pointers for both of these fields must be supplied by the caller.
 *
 * Upon success, 0 is returned.
 *
 * Upon error, *dir_node will be set to 0.
 *
 * *sr (in)
 * ---
 * smb_request structure pointer
 *
 * *cred (in)
 * -----
 * credential
 *
 * *path (in)
 * -----
 * pathname to be looked up
 *
 * *share_root_node (in)
 * ----------------
 * File operations which are share-relative should pass sr->tid_tree->t_snode.
 * If the call is not for a share-relative operation, this parameter must be 0
 * (e.g. the call from smbsr_setup_share()).  (Such callers will have path
 * operations done using root_smb_node.)  This parameter is used to determine
 * whether mount points can be crossed.
 *
 * share_root_node should have at least one reference on it.  This reference
 * will stay intact throughout this routine.
 *
 * *cur_node (in)
 * ---------
 * The smb_node for the current directory (for relative paths).
 * cur_node should have at least one reference on it.
 * This reference will stay intact throughout this routine.
 *
 * **dir_node (out)
 * ----------
 * Directory for the penultimate component of the original path.
 * (Note that this is not the same as the parent directory of the ultimate
 * target in the case of a link.)
 *
 * The directory smb_node is returned held.  The caller will need to release
 * the hold or otherwise make sure it will get released (e.g. in a destroy
 * routine if made part of a global structure).
 *
 * last_component (out)
 * --------------
 * The last component of the path.  (This may be different from the name of any
 * link target to which the last component may resolve.)
 *
 *
 * ____________________________
 *
 * The CIFS server lookup path needs to have logic equivalent to that of
 * smb_fsop_lookup(), smb_vop_lookup() and other smb_vop_*() routines in the
 * following areas:
 *
 *      - traversal of child mounts (handled by smb_pathname_reduce)
 *      - unmangling                (handled in smb_pathname)
 *      - "chroot" behavior of share root (handled by lookuppnvp)
 *
 * In addition, it needs to replace backslashes with forward slashes.  It also
 * ensures that link processing is done correctly, and that directory
 * information requested by the caller is correctly returned (i.e. for paths
 * with a link in the last component, the directory information of the
 * link and not the target needs to be returned).
 */

int
smb_pathname_reduce(
    smb_request_t       *sr,
    cred_t              *cred,
    const char          *path,
    smb_node_t          *share_root_node,
    smb_node_t          *cur_node,
    smb_node_t          **dir_node,
    char                *last_component)
{
        smb_node_t      *root_node;
        pathname_t      ppn = {0};
        pathname_t      mnt_pn = {0};
        char            *usepath;
        int             lookup_flags = FOLLOW;
        int             trailing_slash = 0;
        int             err = 0;
        int             len;
        smb_node_t      *vss_node;
        smb_node_t      *local_cur_node;
        smb_node_t      *local_root_node;
        boolean_t       chk_vss;
        char            *gmttoken;

        ASSERT(dir_node);
        ASSERT(last_component);

        *dir_node = NULL;
        *last_component = '\0';
        vss_node = NULL;
        gmttoken = NULL;
        chk_vss = B_FALSE;

        if (sr && sr->tid_tree) {
                if (STYPE_ISIPC(sr->tid_tree->t_res_type))
                        return (EACCES);
        }

        if (SMB_TREE_IS_CASEINSENSITIVE(sr))
                lookup_flags |= FIGNORECASE;

        if (path == NULL)
                return (EINVAL);

        if (*path == '\0')
                return (ENOENT);

        usepath = kmem_alloc(SMB_MAXPATHLEN, KM_SLEEP);

        len = strlcpy(usepath, path, SMB_MAXPATHLEN);
        if (len >= SMB_MAXPATHLEN) {
                kmem_free(usepath, SMB_MAXPATHLEN);
                return (ENAMETOOLONG);
        }

        (void) strsubst(usepath, '\\', '/');

        if (share_root_node)
                root_node = share_root_node;
        else
                root_node = sr->sr_server->si_root_smb_node;

        if (cur_node == NULL)
                cur_node = root_node;

        local_cur_node = cur_node;
        local_root_node = root_node;

        if (SMB_TREE_IS_DFSROOT(sr)) {
                int is_dfs;
                if (sr->session->dialect >= SMB_VERS_2_BASE)
                        is_dfs = sr->smb2_hdr_flags &
                            SMB2_FLAGS_DFS_OPERATIONS;
                else
                        is_dfs = sr->smb_flg2 & SMB_FLAGS2_DFS;
                if (is_dfs != 0) {
                        err = smb_pathname_dfs_preprocess(sr, usepath,
                            SMB_MAXPATHLEN);
                        if (err != 0) {
                                kmem_free(usepath, SMB_MAXPATHLEN);
                                return (err);
                        }
                        len = strlen(usepath);
                }
        }

        if (sr != NULL) {
                if (sr->session->dialect >= SMB_VERS_2_BASE) {
                        chk_vss = sr->arg.open.create_timewarp;
                } else {
                        chk_vss = (sr->smb_flg2 &
                            SMB_FLAGS2_REPARSE_PATH) != 0;

                        if (chk_vss) {
                                gmttoken = kmem_alloc(SMB_VSS_GMT_SIZE,
                                    KM_SLEEP);
                                err = smb_vss_extract_gmttoken(usepath,
                                    gmttoken);
                                if (err != 0) {
                                        kmem_free(usepath, SMB_MAXPATHLEN);
                                        kmem_free(gmttoken, SMB_VSS_GMT_SIZE);
                                        return (err);
                                }
                                len = strlen(usepath);
                        }
                }
                if (chk_vss)
                        (void) pn_alloc(&mnt_pn);
        }

        if (usepath[len - 1] == '/')
                trailing_slash = 1;

        (void) strcanon(usepath, "/");

        (void) pn_alloc_sz(&ppn, SMB_MAXPATHLEN);

        if ((err = pn_set(&ppn, usepath)) != 0) {
                (void) pn_free(&ppn);
                kmem_free(usepath, SMB_MAXPATHLEN);
                if (chk_vss)
                        (void) pn_free(&mnt_pn);
                if (gmttoken != NULL)
                        kmem_free(gmttoken, SMB_VSS_GMT_SIZE);
                return (err);
        }

        /*
         * If a path does not have a trailing slash, strip off the
         * last component.  (We only need to return an smb_node for
         * the second to last component; a name is returned for the
         * last component.)
         *
         * For VSS requests, the last component might be a filesystem of its
         * own, and we need to discover that before exiting this function,
         * so allow the lookup to happen on the last component.
         * We'll correct this later when we convert to the snapshot.
         */

        if (!chk_vss) {
                if (trailing_slash) {
                        (void) strlcpy(last_component, ".", MAXNAMELEN);
                } else {
                        (void) pn_setlast(&ppn);
                        if (ppn.pn_pathlen >= MAXNAMELEN) {
                                err = ENAMETOOLONG;
                                goto end_not_vss;
                        }
                        (void) strlcpy(last_component, ppn.pn_path, MAXNAMELEN);
                        ppn.pn_path[0] = '\0';
                }
        }

        if ((strcmp(ppn.pn_buf, "/") == 0) || (ppn.pn_buf[0] == '\0')) {
                smb_node_ref(local_cur_node);
                *dir_node = local_cur_node;
        } else {
                err = smb_pathname(sr, ppn.pn_buf, lookup_flags,
                    local_root_node, local_cur_node, NULL, dir_node, cred,
                    chk_vss ? &mnt_pn : NULL);
        }

end_not_vss:
        (void) pn_free(&ppn);
        kmem_free(usepath, SMB_MAXPATHLEN);

        /*
         * We need to try and convert to snapshots, even on error.
         * This is to handle the following cases:
         * - We're on the lowest level filesystem, but a directory got renamed
         *   on the live version. We'll get ENOENT, but can still find it in
         *   the snapshot.
         * - The last component was actually a file. We need to leave the last
         *   component in in case it is, itself, a mountpoint, but that means
         *   we might get ENOTDIR if it's not actually a directory.
         *
         * Note that if you change the share-relative name of a mountpoint,
         * you won't be able to access previous versions of files under it.
         */
        if (chk_vss && *dir_node != NULL) {
                if ((err = smb_vss_lookup_nodes(sr, *dir_node, &vss_node,
                    gmttoken)) == 0) {
                        char *p = mnt_pn.pn_path;
                        size_t pathleft;

                        smb_node_release(*dir_node);
                        *dir_node = NULL;
                        pathleft = pn_pathleft(&mnt_pn);

                        if (pathleft == 0 || trailing_slash) {
                                (void) strlcpy(last_component, ".", MAXNAMELEN);
                        } else {
                                (void) pn_setlast(&mnt_pn);
                                if (ppn.pn_pathlen >= MAXNAMELEN) {
                                        err = ENAMETOOLONG;
                                        goto end_chk_vss;
                                }
                                (void) strlcpy(last_component, mnt_pn.pn_path,
                                    MAXNAMELEN);
                                mnt_pn.pn_path[0] = '\0';
                                pathleft -= strlen(last_component);
                        }

                        if (pathleft != 0) {
                                err = smb_pathname(sr, p, lookup_flags,
                                    vss_node, vss_node, NULL, dir_node, cred,
                                    NULL);
                        } else {
                                *dir_node = vss_node;
                                vss_node = NULL;
                        }
                }
        }

end_chk_vss:
        if (chk_vss)
                (void) pn_free(&mnt_pn);
        if (gmttoken != NULL)
                kmem_free(gmttoken, SMB_VSS_GMT_SIZE);

        /*
         * Prevent traversal to another file system if mount point
         * traversal is disabled.
         *
         * Note that we disregard whether the traversal of the path went
         * outside of the file system and then came back (say via a link).
         * This means that only symlinks that are expressed relatively to
         * the share root work.
         *
         * share_root_node is NULL when mapping a share, so we disregard
         * that case.
         */

        if ((err == 0) && share_root_node) {
                if (share_root_node->vp->v_vfsp != (*dir_node)->vp->v_vfsp) {
                        err = EACCES;
                        if ((sr) && (sr)->tid_tree &&
                            smb_tree_has_feature((sr)->tid_tree,
                            SMB_TREE_TRAVERSE_MOUNTS))
                                err = 0;
                }
        }

        if (err) {
                if (*dir_node) {
                        (void) smb_node_release(*dir_node);
                        *dir_node = NULL;
                }
                *last_component = 0;
        }

        if (vss_node != NULL)
                (void) smb_node_release(vss_node);
        return (err);
}

/*
 * smb_pathname()
 * wrapper to lookuppnvp().  Handles name unmangling.
 *
 * *dir_node is the true directory of the target *node.
 *
 * If any component but the last in the path is not found, ENOTDIR instead of
 * ENOENT will be returned.
 *
 * Path components are processed one at a time so that smb_nodes can be
 * created for each component.  This allows the n_dnode field in the
 * smb_node to be properly populated.
 *
 * Because of the above, links are also processed in this routine
 * (i.e., we do not pass the FOLLOW flag to lookuppnvp()).  This
 * will allow smb_nodes to be created for each component of a link.
 *
 * Mangle checking is per component. If a name is mangled, when the
 * unmangled name is passed to smb_pathname_lookup() do not pass
 * FIGNORECASE, since the unmangled name is the real on-disk name.
 * Otherwise pass FIGNORECASE if it's set in flags. This will cause the
 * file system to return "first match" in the event of a case collision.
 *
 * If CATIA character translation is enabled it is applied to each
 * component before passing the component to smb_pathname_lookup().
 * After smb_pathname_lookup() the reverse translation is applied.
 */

int
smb_pathname(smb_request_t *sr, char *path, int flags,
    smb_node_t *root_node, smb_node_t *cur_node, smb_node_t **dir_node,
    smb_node_t **ret_node, cred_t *cred, pathname_t *mnt_pn)
{
        char            *component, *real_name, *namep;
        pathname_t      pn, rpn, upn, link_pn;
        smb_node_t      *dnode, *fnode, *mnt_node;
        smb_attr_t      attr;
        vnode_t         *rootvp, *vp;
        size_t          pathleft;
        int             err = 0;
        int             nlink = 0;
        int             local_flags;
        uint32_t        abe_flag = 0;
        char            namebuf[MAXNAMELEN];
        vnode_t *fsrootvp = NULL;

        if (path == NULL)
                return (EINVAL);

        ASSERT(root_node);
        ASSERT(cur_node);
        ASSERT(ret_node);

        *ret_node = NULL;

        if (dir_node)
                *dir_node = NULL;

        (void) pn_alloc_sz(&upn, SMB_MAXPATHLEN);

        if ((err = pn_set(&upn, path)) != 0) {
                (void) pn_free(&upn);
                return (err);
        }

        if (mnt_pn != NULL && (err = pn_set(mnt_pn, path) != 0)) {
                (void) pn_free(&upn);
                return (err);
        }

        if (SMB_TREE_SUPPORTS_ABE(sr))
                abe_flag = SMB_ABE;

        (void) pn_alloc(&pn);
        (void) pn_alloc(&rpn);

        component = kmem_alloc(MAXNAMELEN, KM_SLEEP);
        real_name = kmem_alloc(MAXNAMELEN, KM_SLEEP);

        if (mnt_pn != NULL) {
                mnt_node = cur_node;
                smb_node_ref(cur_node);
        } else
                mnt_node = NULL;
        fnode = NULL;
        dnode = cur_node;
        smb_node_ref(dnode);
        rootvp = root_node->vp;

        while ((pathleft = pn_pathleft(&upn)) != 0) {
                if (fnode) {
                        smb_node_release(dnode);
                        dnode = fnode;
                        fnode = NULL;
                }

                if ((err = pn_getcomponent(&upn, component)) != 0)
                        break;

                if ((namep = smb_pathname_catia_v5tov4(sr, component,
                    namebuf, sizeof (namebuf))) == NULL) {
                        err = EILSEQ;
                        break;
                }

                if ((err = pn_set(&pn, namep)) != 0)
                        break;

                /* We want the DOS attributes. */
                bzero(&attr, sizeof (attr));
                attr.sa_mask = SMB_AT_DOSATTR;

                local_flags = flags & FIGNORECASE;
                err = smb_pathname_lookup(&pn, &rpn, local_flags,
                    &vp, rootvp, dnode->vp, &attr, cred);

                if (err) {
                        if (!SMB_TREE_SUPPORTS_SHORTNAMES(sr) ||
                            !smb_maybe_mangled(component))
                                break;

                        if ((err = smb_unmangle(dnode, component,
                            real_name, MAXNAMELEN, abe_flag)) != 0)
                                break;

                        if ((namep = smb_pathname_catia_v5tov4(sr, real_name,
                            namebuf, sizeof (namebuf))) == NULL) {
                                err = EILSEQ;
                                break;
                        }

                        if ((err = pn_set(&pn, namep)) != 0)
                                break;

                        local_flags = 0;
                        err = smb_pathname_lookup(&pn, &rpn, local_flags,
                            &vp, rootvp, dnode->vp, &attr, cred);
                        if (err)
                                break;
                }

                /*
                 * This check MUST be done before symlink check
                 * since a reparse point is of type VLNK but should
                 * not be handled like a regular symlink.
                 */
                if (attr.sa_dosattr & FILE_ATTRIBUTE_REPARSE_POINT) {
                        err = EREMOTE;
                        VN_RELE(vp);
                        break;
                }

                if ((vp->v_type == VLNK) &&
                    ((flags & FOLLOW) || pn_pathleft(&upn))) {

                        if (++nlink > MAXSYMLINKS) {
                                err = ELOOP;
                                VN_RELE(vp);
                                break;
                        }

                        (void) pn_alloc(&link_pn);
                        err = pn_getsymlink(vp, &link_pn, cred);
                        VN_RELE(vp);

                        if (err == 0) {
                                if (pn_pathleft(&link_pn) == 0)
                                        (void) pn_set(&link_pn, ".");
                                err = pn_insert(&upn, &link_pn,
                                    strlen(component));
                        }
                        pn_free(&link_pn);

                        if (err)
                                break;

                        if (upn.pn_pathlen == 0) {
                                err = ENOENT;
                                break;
                        }

                        if (upn.pn_path[0] == '/') {
                                fnode = root_node;
                                smb_node_ref(fnode);
                        }

                        if (pn_fixslash(&upn))
                                flags |= FOLLOW;

                } else {
                        if (flags & FIGNORECASE) {
                                if (strcmp(rpn.pn_path, "/") != 0)
                                        pn_setlast(&rpn);
                                namep = rpn.pn_path;
                        } else {
                                namep = pn.pn_path;
                        }

                        namep = smb_pathname_catia_v4tov5(sr, namep,
                            namebuf, sizeof (namebuf));

                        fnode = smb_node_lookup(sr, NULL, cred, vp, namep,
                            dnode, NULL);
                        VN_RELE(vp);

                        if (fnode == NULL) {
                                err = ENOMEM;
                                break;
                        }
                }

                while (upn.pn_path[0] == '/') {
                        upn.pn_path++;
                        upn.pn_pathlen--;
                }

                /*
                 * If the node we looked up is the root of a filesystem,
                 * snapshot the lookup so we can replay this after discovering
                 * the lowest mounted filesystem.
                 */
                if (mnt_pn != NULL &&
                    fnode != NULL &&
                    (err = VFS_ROOT(fnode->vp->v_vfsp, &fsrootvp)) == 0) {
                        if (fsrootvp == fnode->vp) {
                                mnt_pn->pn_pathlen = pn_pathleft(&upn);
                                mnt_pn->pn_path = mnt_pn->pn_buf +
                                    ((ptrdiff_t)upn.pn_path -
                                    (ptrdiff_t)upn.pn_buf);

                                smb_node_ref(fnode);
                                if (mnt_node != NULL)
                                        smb_node_release(mnt_node);
                                mnt_node = fnode;

                        }
                        VN_RELE(fsrootvp);
                }
        }

        if ((pathleft) && (err == ENOENT))
                err = ENOTDIR;

        if (mnt_node == NULL)
                mnt_pn = NULL;

        /*
         * We always want to return a node when we're doing VSS
         * (mnt_pn != NULL)
         */
        if (mnt_pn == NULL && err != 0) {
                if (fnode)
                        smb_node_release(fnode);
                if (dnode)
                        smb_node_release(dnode);
        } else {
                if (mnt_pn != NULL) {
                        *ret_node = mnt_node;
                        if (fnode != NULL)
                                smb_node_release(fnode);
                } else {
                        *ret_node = fnode;
                }

                if (dir_node)
                        *dir_node = dnode;
                else
                        smb_node_release(dnode);
        }

        kmem_free(component, MAXNAMELEN);
        kmem_free(real_name, MAXNAMELEN);
        (void) pn_free(&pn);
        (void) pn_free(&rpn);
        (void) pn_free(&upn);

        return (err);
}

/*
 * Holds on dvp and rootvp (if not rootdir) are required by lookuppnvp()
 * and will be released within lookuppnvp().
 */
static int
smb_pathname_lookup(pathname_t *pn, pathname_t *rpn, int flags,
    vnode_t **vp, vnode_t *rootvp, vnode_t *dvp, smb_attr_t *attr, cred_t *cred)
{
        int err;

        *vp = NULL;
        VN_HOLD(dvp);
        if (rootvp != rootdir)
                VN_HOLD(rootvp);

#ifdef _KERNEL
        /*
         * When BYPASS_TRAVERSE_CHECKING is enabled, avoid EXECUTE access
         * checks. See: smb_vop_lookup().
         */
        if (smb_vop_priv_check(cred, PRIV_FILE_DAC_SEARCH, B_FALSE, dvp))
                flags |= LOOKUP_NOACLCHECK;
#endif

        err = lookuppnvp(pn, rpn, flags, NULL, vp, rootvp, dvp, cred);
        if ((err == 0) && (attr != NULL))
                (void) smb_vop_getattr(*vp, NULL, attr, 0, zone_kcred());

        return (err);
}

/*
 * CATIA Translation of a pathname component prior to passing it to lookuppnvp
 *
 * If the translated component name contains a '/' NULL is returned.
 * The caller should treat this as error EILSEQ. It is not valid to
 * have a directory name with a '/'.
 */
static char *
smb_pathname_catia_v5tov4(smb_request_t *sr, char *name,
    char *namebuf, int buflen)
{
        char *namep;

        if (SMB_TREE_SUPPORTS_CATIA(sr)) {
                namep = smb_vop_catia_v5tov4(name, namebuf, buflen);
                if (strchr(namep, '/') != NULL)
                        return (NULL);
                return (namep);
        }

        return (name);
}

/*
 * CATIA translation of a pathname component after returning from lookuppnvp
 */
static char *
smb_pathname_catia_v4tov5(smb_request_t *sr, char *name,
    char *namebuf, int buflen)
{
        if (SMB_TREE_SUPPORTS_CATIA(sr)) {
                smb_vop_catia_v4tov5(name, namebuf, buflen);
                return (namebuf);
        }

        return (name);
}

/*
 * sr - needed to check for case sense
 * path - non mangled path needed to be looked up from the startvp
 * startvp - the vnode to start the lookup from
 * rootvp - the vnode of the root of the filesystem
 * returns the vnode found when starting at startvp and using the path
 *
 * Finds a vnode starting at startvp and parsing the non mangled path
 */

vnode_t *
smb_lookuppathvptovp(smb_request_t *sr, char *path, vnode_t *startvp,
    vnode_t *rootvp)
{
        pathname_t pn;
        vnode_t *vp = NULL;
        int lookup_flags = FOLLOW;

        if (SMB_TREE_IS_CASEINSENSITIVE(sr))
                lookup_flags |= FIGNORECASE;

        (void) pn_alloc(&pn);

        if (pn_set(&pn, path) == 0) {
                VN_HOLD(startvp);
                if (rootvp != rootdir)
                        VN_HOLD(rootvp);

                /* lookuppnvp should release the holds */
                if (lookuppnvp(&pn, NULL, lookup_flags, NULL, &vp,
                    rootvp, startvp, zone_kcred()) != 0) {
                        pn_free(&pn);
                        return (NULL);
                }
        }

        pn_free(&pn);
        return (vp);
}

/*
 * smb_pathname_init
 * Parse path: pname\\fname:sname:stype
 *
 * Elements of the smb_pathname_t structure are allocated using request
 * specific storage and will be free'd when the sr is destroyed.
 *
 * Populate pn structure elements with the individual elements
 * of pn->pn_path. pn->pn_sname will contain the whole stream name
 * including the stream type and preceding colon: :sname:%DATA
 * pn_stype will point to the stream type within pn_sname.
 *
 * If the pname element is missing pn_pname will be set to NULL.
 * If any other element is missing the pointer in pn will be NULL.
 */
void
smb_pathname_init(smb_request_t *sr, smb_pathname_t *pn, char *path)
{
        char *pname, *fname, *sname;
        int len;

        bzero(pn, sizeof (smb_pathname_t));
        pn->pn_path = smb_pathname_strdup(sr, path);

        smb_pathname_preprocess(sr, pn);

        /* parse pn->pn_path into its constituent parts */
        pname = pn->pn_path;

        /*
         * Split the string between the directory and filename.
         * Either part may be empty.
         *
         * Fill in pn->pn_pname (the path name)
         */
        fname = strrchr(pname, '\\');
        if (fname != NULL) {
                if (fname == pname) {
                        /*
                         * Last '/' is at start of string.
                         * No directory part (dir is root)
                         */
                        pn->pn_pname = NULL;
                } else {
                        /*
                         * Directory part ends at the last '/'
                         * Temporarily truncate and copy
                         */
                        *fname = '\0';
                        pn->pn_pname =
                            smb_pathname_strdup(sr, pname);
                        *fname = '\\';
                }
                ++fname;
                /* fname is just after the '/' */
        } else {
                /*
                 * No '/' at all in the string.
                 * It's all filename
                 */
                fname = pname;
                pn->pn_pname = NULL;
        }

        /*
         * Find end of the filename part of the string,
         * which may be the null terminator, or may be
         * the start of the optional :sname suffix.
         */
        sname = strchr(fname, ':');
        if (sname == NULL) {
                /*
                 * No :sname suffix.  We're done.
                 */
                pn->pn_fname = smb_pathname_strdup(sr, fname);
                return;
        }

        /*
         * We have a stream name, and maybe a stream type.
         * Can't use smb_is_stream_name(fname) here because
         * we need to allow sname="::$DATA"
         */
        if (sname == fname) {
                /*
                 * The ":sname" part is at the start of
                 * the file name, which means that the
                 * file name is "" and this pathname
                 * refers to a stream on the directory.
                 */
                pn->pn_fname = NULL;
        } else {
                /*
                 * The filename part ends at the ':'
                 * Temporarily truncate and copy
                 */
                *sname = '\0';
                pn->pn_fname = smb_pathname_strdup(sr, fname);
                *sname = ':';
        }

        /*
         * Special case "::$DATA" which "points to"
         * the "unnamed" stream (the file itself).
         * Basically ignore the "::$DATA"
         */
        if (strcasecmp(sname, "::$DATA") == 0) {
                ASSERT(sname >= pname &&
                    sname < (pname + strlen(pname)));
                *sname = '\0';
                return;
        }

        /*
         * sname points to ":sname:stype" in pn_path
         * If ":stype" is missing, add it, then set
         * pn_stype to point after the 2nd ':'
         *
         * Caller knows pn_stype is NOT allocated.
         * Allocations here are free'd via smb_srm_fini
         */
        pn->pn_sname = smb_pathname_strdup(sr, sname);
        pn->pn_stype = strchr(pn->pn_sname + 1, ':');
        if (pn->pn_stype) {
                (void) smb_strupr(pn->pn_stype);
        } else {
                len = strlen(pn->pn_sname);
                pn->pn_sname = smb_pathname_strcat(sr, pn->pn_sname, ":$DATA");
                pn->pn_stype = pn->pn_sname + len;
        }
        ++pn->pn_stype;
}

/*
 * smb_pathname_preprocess
 *
 * Perform common pre-processing of pn->pn_path:
 * - if the pn_path is blank, set it to '\\'
 * - perform unicode wildcard converstion.
 * - convert any '/' to '\\'
 * - eliminate duplicate slashes
 * - remove trailing slashes
 * - quota directory specific pre-processing
 */
static void
smb_pathname_preprocess(smb_request_t *sr, smb_pathname_t *pn)
{
        char *p;

        /* treat empty path as "\\" */
        if (strlen(pn->pn_path) == 0) {
                pn->pn_path = smb_pathname_strdup(sr, "\\");
                return;
        }

        if (sr->session->dialect < NT_LM_0_12)
                smb_convert_wildcards(pn->pn_path);

        /* treat '/' as '\\' */
        (void) strsubst(pn->pn_path, '/', '\\');

        (void) strcanon(pn->pn_path, "\\");

        /* remove trailing '\\' */
        p = pn->pn_path + strlen(pn->pn_path) - 1;
        if ((p != pn->pn_path) && (*p == '\\'))
                *p = '\0';

        smb_pathname_preprocess_quota(sr, pn);
        smb_pathname_preprocess_adminshare(sr, pn);
}

/*
 * smb_pathname_preprocess_quota
 *
 * There is a special file required by windows so that the quota
 * tab will be displayed by windows clients. This is created in
 * a special directory, $EXTEND, at the root of the shared file
 * system. To hide this directory prepend a '.' (dot).
 */
static void
smb_pathname_preprocess_quota(smb_request_t *sr, smb_pathname_t *pn)
{
        char *name = "$EXTEND";
        char *new_name = ".$EXTEND";
        char *p, *slash;
        int len;

        if (!smb_node_is_vfsroot(sr->tid_tree->t_snode))
                return;

        p = pn->pn_path;

        /* ignore any initial "\\" */
        p += strspn(p, "\\");
        if (smb_strcasecmp(p, name, strlen(name)) != 0)
                return;

        p += strlen(name);
        if ((*p != ':') && (*p != '\\') && (*p != '\0'))
                return;

        slash = (pn->pn_path[0] == '\\') ? "\\" : "";
        len = strlen(pn->pn_path) + 2;
        pn->pn_path = smb_srm_alloc(sr, len);
        (void) snprintf(pn->pn_path, len, "%s%s%s", slash, new_name, p);
        (void) smb_strupr(pn->pn_path);
}

/*
 * smb_pathname_preprocess_adminshare
 *
 * Convert any path with share name "C$" or "c$" (Admin share) in to lower case.
 */
static void
smb_pathname_preprocess_adminshare(smb_request_t *sr, smb_pathname_t *pn)
{
        if (strcasecmp(sr->tid_tree->t_sharename, "c$") == 0)
                (void) smb_strlwr(pn->pn_path);
}

/*
 * smb_pathname_strdup
 *
 * Duplicate NULL terminated string s.
 *
 * The new string is allocated using request specific storage and will
 * be free'd when the sr is destroyed.
 */
static char *
smb_pathname_strdup(smb_request_t *sr, const char *s)
{
        char *s2;
        size_t n;

        n = strlen(s) + 1;
        s2 = smb_srm_zalloc(sr, n);
        (void) strlcpy(s2, s, n);
        return (s2);
}

/*
 * smb_pathname_strcat
 *
 * Reallocate NULL terminated string s1 to accommodate
 * concatenating  NULL terminated string s2.
 * Append s2 and return resulting NULL terminated string.
 *
 * The string buffer is reallocated using request specific
 * storage and will be free'd when the sr is destroyed.
 */
static char *
smb_pathname_strcat(smb_request_t *sr, char *s1, const char *s2)
{
        size_t n;

        n = strlen(s1) + strlen(s2) + 1;
        s1 = smb_srm_rezalloc(sr, s1, n);
        (void) strlcat(s1, s2, n);
        return (s1);
}

/*
 * smb_pathname_validate
 *
 * Perform basic validation of pn:
 * - If first component of pn->path is ".." -> PATH_SYNTAX_BAD
 * - If there are wildcards in pn->pn_pname -> OBJECT_NAME_INVALID
 * - If fname is "." -> INVALID_OBJECT_NAME
 *
 * On unix .. at the root of a file system links to the root. Thus
 * an attempt to lookup "/../../.." will be the same as looking up "/"
 * CIFs clients expect the above to result in
 * NT_STATUS_OBJECT_PATH_SYNTAX_BAD. It is currently not possible
 * (and questionable if it's desirable) to deal with all cases
 * but paths beginning with \\.. are handled.
 *
 * Returns: B_TRUE if pn is valid,
 *          otherwise returns B_FALSE and sets error status in sr.
 *
 * XXX: Get rid of smbsr_error calls for SMB2
 */
boolean_t
smb_pathname_validate(smb_request_t *sr, smb_pathname_t *pn)
{
        char *path = pn->pn_path;

        /* ignore any initial "\\" */
        path += strspn(path, "\\");

        /* If first component of path is ".." -> PATH_SYNTAX_BAD */
        if ((strcmp(path, "..") == 0) || (strncmp(path, "..\\", 3) == 0)) {
                smbsr_error(sr, NT_STATUS_OBJECT_PATH_SYNTAX_BAD,
                    ERRDOS, ERROR_BAD_PATHNAME);
                return (B_FALSE);
        }

        /* If there are wildcards in pn->pn_pname -> OBJECT_NAME_INVALID */
        if (pn->pn_pname && smb_contains_wildcards(pn->pn_pname)) {
                smbsr_error(sr, NT_STATUS_OBJECT_NAME_INVALID,
                    ERRDOS, ERROR_INVALID_NAME);
                return (B_FALSE);
        }

        /* If fname is "." -> OBJECT_NAME_INVALID */
        if (pn->pn_fname && (strcmp(pn->pn_fname, ".") == 0)) {
                smbsr_error(sr, NT_STATUS_OBJECT_NAME_INVALID,
                    ERRDOS, ERROR_INVALID_NAME);
                return (B_FALSE);
        }

        return (B_TRUE);
}

/*
 * smb_validate_dirname
 *
 * smb_pathname_validate() should have already been performed on pn.
 *
 * Very basic directory name validation:  checks for colons in a path.
 * Need to skip the drive prefix since it contains a colon.
 *
 * Returns: B_TRUE if the name is valid,
 *          otherwise returns B_FALSE and sets error status in sr.
 */
boolean_t
smb_validate_dirname(smb_request_t *sr, smb_pathname_t *pn)
{
        char *name;
        char *path = pn->pn_path;

        if ((name = path) != 0) {
                name += strspn(name, "\\");

                if (strchr(name, ':') != 0) {
                        smbsr_error(sr, NT_STATUS_NOT_A_DIRECTORY,
                            ERRDOS, ERROR_INVALID_NAME);
                        return (B_FALSE);
                }
        }

        if (pn->pn_sname)
                return (smb_validate_stream_name(sr, pn));

        return (B_TRUE);
}

/*
 * smb_validate_object_name
 *
 * smb_pathname_validate() should have already been pertformed on pn.
 *
 * Very basic file name validation.
 * For filenames, we check for names of the form "AAAn:". Names that
 * contain three characters, a single digit and a colon (:) are reserved
 * as DOS device names, i.e. "COM1:".
 * Stream name validation is handed off to smb_validate_stream_name
 *
 * Returns: B_TRUE if pn->pn_fname is valid,
 *          otherwise returns B_FALSE and sets error status in sr.
 */
boolean_t
smb_validate_object_name(smb_request_t *sr, smb_pathname_t *pn)
{
        if (pn->pn_fname &&
            strlen(pn->pn_fname) == 5 &&
            smb_isdigit(pn->pn_fname[3]) &&
            pn->pn_fname[4] == ':') {
                smbsr_error(sr, NT_STATUS_OBJECT_NAME_INVALID,
                    ERRDOS, ERROR_INVALID_NAME);
                return (B_FALSE);
        }

        if (pn->pn_sname)
                return (smb_validate_stream_name(sr, pn));

        return (B_TRUE);
}

/*
 * smb_stream_parse_name
 *
 * smb_stream_parse_name should only be called for a path that
 * contains a valid named stream.  Path validation should have
 * been performed before this function is called, typically by
 * calling smb_is_stream_name() just before this.
 *
 * Find the last component of path and split it into filename
 * and stream name.
 *
 * On return the named stream type will be present.  The stream
 * type defaults to ":$DATA", if it has not been defined
 * For example, 'stream' contains :<sname>:$DATA
 *
 * Output args: filename, stream both MAXNAMELEN
 */
void
smb_stream_parse_name(char *path, char *filename, char *stream)
{
        char *fname, *sname, *stype;
        size_t flen, slen;

        ASSERT(path);
        ASSERT(filename);
        ASSERT(stream);

        fname = strrchr(path, '\\');
        fname = (fname == NULL) ? path : fname + 1;
        sname = strchr(fname, ':');
        /* Caller makes sure there is a ':' in path. */
        VERIFY(sname != NULL);
        /* LINTED: possible ptrdiff_t overflow */
        flen = sname - fname;
        slen = strlen(sname);

        if (flen > (MAXNAMELEN-1))
                flen = (MAXNAMELEN-1);
        (void) strncpy(filename, fname, flen);
        filename[flen] = '\0';

        if (slen > (MAXNAMELEN-1))
                slen = (MAXNAMELEN-1);
        (void) strncpy(stream, sname, slen);
        stream[slen] = '\0';

        /* Add a "stream type" if there isn't one. */
        stype = strchr(stream + 1, ':');
        if (stype == NULL)
                (void) strlcat(stream, ":$DATA", MAXNAMELEN);
        else
                (void) smb_strupr(stype);
}

/*
 * smb_is_stream_name
 *
 * Determines if 'path' specifies a named stream.
 *
 * path is a NULL terminated string which could be a stream path.
 * [pathname/]fname[:stream_name[:stream_type]]
 *
 * - If there is no colon in the path or it's the last char
 *   then it's not a stream name
 *
 * - '::' is a non-stream and is commonly used by Windows to designate
 *   the unamed stream in the form "::$DATA"
 */
boolean_t
smb_is_stream_name(char *path)
{
        char *colonp;

        if (path == NULL)
                return (B_FALSE);

        colonp = strchr(path, ':');
        if ((colonp == NULL) || (*(colonp+1) == '\0'))
                return (B_FALSE);

        if (strstr(path, "::"))
                return (B_FALSE);

        return (B_TRUE);
}

/*
 * Is this stream node a "restricted" type?
 */
boolean_t
smb_strname_restricted(char *strname)
{
        char *stype;

        stype = strrchr(strname, ':');
        if (stype == NULL)
                return (B_FALSE);

        /*
         * Only ":$CA" is restricted (for now).
         */
        if (strcmp(stype, ":$CA") == 0)
                return (B_TRUE);

        return (B_FALSE);
}

/*
 * smb_validate_stream_name
 *
 * B_FALSE will be returned, and the error status ser in the sr, if:
 * - the path is not a stream name
 * - a path is specified but the fname is ommitted.
 * - the stream_type is specified but not valid.
 *
 * Note: the stream type is case-insensitive.
 */
boolean_t
smb_validate_stream_name(smb_request_t *sr, smb_pathname_t *pn)
{
        static char *strmtype[] = {
                "$CA",
                "$DATA",
                "$INDEX_ALLOCATION"
        };
        int i;

        ASSERT(pn);
        ASSERT(pn->pn_sname);

        if (pn->pn_stype != NULL) {
                for (i = 0; i < sizeof (strmtype) / sizeof (strmtype[0]); ++i) {
                        if (strcasecmp(pn->pn_stype, strmtype[i]) == 0)
                                return (B_TRUE);
                }

                smbsr_error(sr, NT_STATUS_OBJECT_NAME_INVALID,
                    ERRDOS, ERROR_INVALID_NAME);
                return (B_FALSE);
        }

        return (B_TRUE);
}

/*
 * valid DFS I/O path:
 *
 * \server-or-domain\share
 * \server-or-domain\share\path
 *
 * All the returned errors by this function needs to be
 * checked against Windows.
 */
static int
smb_pathname_dfs_preprocess(smb_request_t *sr, char *path, size_t pathsz)
{
        smb_unc_t unc;
        char *linkpath;
        int rc;

        if (sr->tid_tree == NULL)
                return (0);

        if ((rc = smb_unc_init(path, &unc)) != 0)
                return (rc);

        if (smb_strcasecmp(unc.unc_share, sr->tid_tree->t_sharename, 0)) {
                smb_unc_free(&unc);
                return (EINVAL);
        }

        linkpath = unc.unc_path;
        (void) snprintf(path, pathsz, "/%s", (linkpath) ? linkpath : "");

        smb_unc_free(&unc);
        return (0);
}