root/usr/src/uts/common/os/tlabel.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) 2006, 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/cmn_err.h>
#include <sys/systm.h>
#include <sys/cred.h>
#include <sys/modctl.h>
#include <sys/vfs.h>
#include <sys/vnode.h>
#include <sys/tiuser.h>
#include <sys/kmem.h>
#include <sys/pathname.h>
#include <sys/zone.h>
#include <sys/tsol/label.h>
#include <sys/tsol/tnet.h>
#include <sys/fs/lofs_node.h>
#include <sys/fs/zfs.h>
#include <sys/dsl_prop.h>
#include <inet/ip6.h>
#include <rpc/auth.h>
#include <rpc/clnt.h>
#include <nfs/nfs.h>
#include <nfs/nfs4.h>
#include <nfs/nfs_clnt.h>


int sys_labeling = 0;                   /* the default is "off" */

static kmem_cache_t *tslabel_cache;
ts_label_t *l_admin_low;
ts_label_t *l_admin_high;

uint32_t default_doi = DEFAULT_DOI;

/*
 * Initialize labels infrastructure.
 * This is called during startup() time (before vfs_mntroot) by thread_init().
 * It has to be called early so that the is_system_labeled() function returns
 * the right value when called by the networking code on a diskless boot.
 */
void
label_init(void)
{
        bslabel_t label;

        /*
         * sys_labeling will default to "off" unless it is overridden
         * in /etc/system.
         */

        tslabel_cache = kmem_cache_create("tslabel_cache", sizeof (ts_label_t),
            0, NULL, NULL, NULL, NULL, NULL, 0);
        bsllow(&label);
        l_admin_low = labelalloc(&label, default_doi, KM_SLEEP);
        bslhigh(&label);
        l_admin_high = labelalloc(&label, default_doi, KM_SLEEP);
}

/*
 * Allocate new ts_label_t.
 */
ts_label_t *
labelalloc(const bslabel_t *val, uint32_t doi, int flag)
{
        ts_label_t *lab = kmem_cache_alloc(tslabel_cache, flag);

        if (lab != NULL) {
                lab->tsl_ref = 1;
                lab->tsl_doi = doi;
                lab->tsl_flags = 0;
                if (val == NULL)
                        bzero(&lab->tsl_label, sizeof (bslabel_t));
                else
                        bcopy(val, &lab->tsl_label,  sizeof (bslabel_t));
        }
        return (lab);
}

/*
 * Duplicate an existing ts_label_t to a new one, with only
 * the current reference.
 */
ts_label_t *
labeldup(const ts_label_t *val, int flag)
{
        ts_label_t *lab = kmem_cache_alloc(tslabel_cache, flag);

        if (lab != NULL) {
                bcopy(val, lab, sizeof (ts_label_t));
                lab->tsl_ref = 1;
        }
        return (lab);
}

/*
 * Put a hold on a label structure.
 */
void
label_hold(ts_label_t *lab)
{
        atomic_inc_32(&lab->tsl_ref);
}

/*
 * Release previous hold on a label structure.  Free it if refcnt == 0.
 */
void
label_rele(ts_label_t *lab)
{
        if (atomic_dec_32_nv(&lab->tsl_ref) == 0)
                kmem_cache_free(tslabel_cache, lab);
}

bslabel_t *
label2bslabel(ts_label_t *lab)
{
        return (&lab->tsl_label);
}


uint32_t
label2doi(ts_label_t *lab)
{
        return (lab->tsl_doi);
}

/*
 * Compare labels. Return 1 if equal, 0 otherwise.
 */
boolean_t
label_equal(const ts_label_t *l1, const ts_label_t *l2)
{
        return ((l1->tsl_doi == l2->tsl_doi) &&
            blequal(&l1->tsl_label, &l2->tsl_label));
}

/*
 * There's no protocol today to obtain the label from the server.
 * So we rely on conventions: zones, zone names, and zone paths
 * must match across TX servers and their TX clients.  Now use
 * the exported name to find the equivalent local zone and its
 * label.  Caller is responsible for doing a label_rele of the
 * returned ts_label.
 */
ts_label_t *
getflabel_cipso(vfs_t *vfsp)
{
        zone_t  *reszone;
        zone_t  *new_reszone;
        char    *nfspath, *respath;
        refstr_t        *resource_ref;
        boolean_t       treat_abs = B_FALSE;

        if (vfsp->vfs_resource == NULL)
                return (NULL);                  /* error */
        resource_ref = vfs_getresource(vfsp);

        nfspath = (char *)refstr_value(resource_ref);
        respath = strchr(nfspath, ':');         /* skip server name */
        if (respath)
                respath++;                      /* skip over ":" */
        if (*respath != '/') {
                /* treat path as absolute but it doesn't have leading '/' */
                treat_abs = B_TRUE;
        }

        reszone = zone_find_by_any_path(respath, treat_abs);
        if (reszone == global_zone) {
                refstr_rele(resource_ref);
                label_hold(l_admin_low);
                zone_rele(reszone);
                return (l_admin_low);
        }

        /*
         * Skip over zonepath (not including "root"), e.g. /zone/internal
         */
        respath += reszone->zone_rootpathlen - 7;
        if (treat_abs)
                respath--;                      /* no leading '/' to skip */
        if (strncmp(respath, "/root/", 6) == 0) {
                /* Check if we now have something like "/zone/public/" */

                respath += 5;                   /* skip "/root" first */
                new_reszone = zone_find_by_any_path(respath, B_FALSE);
                if (new_reszone != global_zone) {
                        zone_rele(reszone);
                        reszone = new_reszone;
                } else {
                        zone_rele(new_reszone);
                }
        }

        refstr_rele(resource_ref);
        label_hold(reszone->zone_slabel);
        zone_rele(reszone);

        return (reszone->zone_slabel);
}

/*
 * Get the label if any of a zfs filesystem.  Get the dataset, then
 * get its mlslabel property, convert as needed, and return it.  If
 * there's no mlslabel or it is the default one, return NULL.
 */
static ts_label_t *
getflabel_zfs(vfs_t *vfsp)
{
        int             error;
        ts_label_t      *tsl = NULL;
        refstr_t        *resource_ref;
        bslabel_t       ds_sl;
        char            ds_hexsl[MAXNAMELEN];
        const char      *osname;

        resource_ref = vfs_getresource(vfsp);
        osname = refstr_value(resource_ref);

        error = dsl_prop_get(osname, zfs_prop_to_name(ZFS_PROP_MLSLABEL),
            1, sizeof (ds_hexsl), &ds_hexsl, NULL);
        refstr_rele(resource_ref);

        if ((error) || (strcasecmp(ds_hexsl, ZFS_MLSLABEL_DEFAULT) == 0))
                return (NULL);
        if (hexstr_to_label(ds_hexsl, &ds_sl) != 0)
                return (NULL);

        tsl = labelalloc(&ds_sl, default_doi, KM_SLEEP);
        return (tsl);
}

static ts_label_t *
getflabel_nfs(vfs_t *vfsp)
{
        bslabel_t       *server_sl;
        ts_label_t      *srv_label;
        tsol_tpc_t      *tp;
        int             addr_type;
        void            *ipaddr;
        struct servinfo *svp;
        struct netbuf   *addr;
        struct knetconfig *knconf;
        mntinfo_t       *mi;

        mi = VFTOMI(vfsp);
        svp = mi->mi_curr_serv;
        addr = &svp->sv_addr;
        knconf = svp->sv_knconf;

        if (strcmp(knconf->knc_protofmly, NC_INET) == 0) {
                addr_type = IPV4_VERSION;
                /* LINTED: following cast to ipaddr is OK */
                ipaddr = &((struct sockaddr_in *)addr->buf)->sin_addr;
        } else if (strcmp(knconf->knc_protofmly, NC_INET6) == 0) {
                addr_type = IPV6_VERSION;
                /* LINTED: following cast to ipaddr is OK */
                ipaddr = &((struct sockaddr_in6 *)addr->buf)->sin6_addr;
        } else {
                goto errout;
        }

        tp = find_tpc(ipaddr, addr_type, B_FALSE);
        if (tp == NULL)
                goto errout;

        if (tp->tpc_tp.host_type == SUN_CIPSO) {
                TPC_RELE(tp);
                return (getflabel_cipso(vfsp));
        }

        if (tp->tpc_tp.host_type != UNLABELED)
                goto errout;

        server_sl = &tp->tpc_tp.tp_def_label;
        srv_label = labelalloc(server_sl, default_doi, KM_SLEEP);

        TPC_RELE(tp);

        return (srv_label);

errout:
        return (NULL);
}

/*
 * getflabel -
 *
 * Return pointer to the ts_label associated with the specified file,
 * or returns NULL if error occurs.  Caller is responsible for doing
 * a label_rele of the ts_label.
 */
ts_label_t *
getflabel(vnode_t *vp)
{
        vfs_t           *vfsp, *rvfsp;
        vnode_t         *rvp, *rvp2;
        zone_t          *zone;
        ts_label_t      *zl;
        int             err;
        boolean_t       vfs_is_held = B_FALSE;
        char            vpath[MAXPATHLEN];

        ASSERT(vp);
        vfsp = vp->v_vfsp;
        if (vfsp == NULL)
                return (NULL);

        rvp = vp;

        /*
         * Traverse lofs mounts and fattach'es to get the real vnode
         */
        if (VOP_REALVP(rvp, &rvp2, NULL) == 0)
                rvp = rvp2;

        rvfsp = rvp->v_vfsp;

        /* rvp/rvfsp now represent the real vnode/vfs we will be using */

        /* Go elsewhere to handle all nfs files. */
        if (strncmp(vfssw[rvfsp->vfs_fstype].vsw_name, "nfs", 3) == 0)
                return (getflabel_nfs(rvfsp));

        /*
         * Fast path, for objects in a labeled zone: everything except
         * for lofs/nfs will be just the label of that zone.
         */
        if ((rvfsp->vfs_zone != NULL) && (rvfsp->vfs_zone != global_zone)) {
                if ((strcmp(vfssw[rvfsp->vfs_fstype].vsw_name,
                    "lofs") != 0)) {
                        zone = rvfsp->vfs_zone;
                        zone_hold(zone);
                        goto zone_out;          /* return this label */
                }
        }

        /*
         * Get the vnode path -- it may be missing or weird for some
         * cases, like devices.  In those cases use the label of the
         * current zone.
         */
        err = vnodetopath(rootdir, rvp, vpath, sizeof (vpath), kcred);
        if ((err != 0) || (*vpath != '/')) {
                zone = curproc->p_zone;
                zone_hold(zone);
                goto zone_out;
        }

        /*
         * For zfs filesystem, return the explicit label property if a
         * meaningful one exists.
         */
        if (strncmp(vfssw[rvfsp->vfs_fstype].vsw_name, "zfs", 3) == 0) {
                ts_label_t *tsl;

                tsl = getflabel_zfs(rvfsp);

                /* if label found, return it, otherwise continue... */
                if (tsl != NULL)
                        return (tsl);
        }

        /*
         * If a mountpoint exists, hold the vfs while we reference it.
         * Otherwise if mountpoint is NULL it should not be held (e.g.,
         * a hold/release on spec_vfs would result in an attempted free
         * and panic.)
         */
        if (vfsp->vfs_mntpt != NULL) {
                VFS_HOLD(vfsp);
                vfs_is_held = B_TRUE;
        }

        zone = zone_find_by_any_path(vpath, B_FALSE);

        /*
         * If the vnode source zone is properly set to a non-global zone, or
         * any zone if the mount is R/W, then use the label of that zone.
         */
        if ((zone != global_zone) || ((vfsp->vfs_flag & VFS_RDONLY) != 0))
                goto zone_out;          /* return this label */

        /*
         * Otherwise, if we're not in the global zone, use the label of
         * our zone.
         */
        if ((zone = curproc->p_zone) != global_zone) {
                zone_hold(zone);
                goto zone_out;          /* return this label */
        }

        /*
         * We're in the global zone and the mount is R/W ... so the file
         * may actually be in the global zone -- or in the root of any zone.
         * Always build our own path for the file, to be sure it's simplified
         * (i.e., no ".", "..", "//", and so on).
         */

        zone_rele(zone);
        zone = zone_find_by_any_path(vpath, B_FALSE);

zone_out:
        if ((curproc->p_zone == global_zone) && (zone == global_zone)) {
                vfs_t           *nvfs;
                boolean_t       exported = B_FALSE;
                refstr_t        *mntpt_ref;
                char            *mntpt;

                /*
                 * File is in the global zone - check whether it's admin_high.
                 * If it's in a filesys that was exported from the global zone,
                 * it's admin_low by definition.  Otherwise, if it's in a
                 * filesys that's NOT exported to any zone, it's admin_high.
                 *
                 * And for these files if there wasn't a valid mount resource,
                 * the file must be admin_high (not exported, probably a global
                 * zone device).
                 */
                if (!vfs_is_held)
                        goto out_high;

                mntpt_ref = vfs_getmntpoint(vfsp);
                mntpt = (char *)refstr_value(mntpt_ref);

                if ((mntpt != NULL) && (*mntpt == '/')) {
                        zone_t  *to_zone;

                        to_zone = zone_find_by_any_path(mntpt, B_FALSE);
                        zone_rele(to_zone);
                        if (to_zone != global_zone) {
                                /* force admin_low */
                                exported = B_TRUE;
                        }
                }
                if (mntpt_ref)
                        refstr_rele(mntpt_ref);

                if (!exported) {
                        size_t  plen = strlen(vpath);

                        vfs_list_read_lock();
                        nvfs = vfsp->vfs_next;
                        while (nvfs != vfsp) {
                                const char      *rstr;
                                size_t          rlen = 0;

                                /*
                                 * Skip checking this vfs if it's not lofs
                                 * (the only way to export from the global
                                 * zone to a zone).
                                 */
                                if (strncmp(vfssw[nvfs->vfs_fstype].vsw_name,
                                    "lofs", 4) != 0) {
                                        nvfs = nvfs->vfs_next;
                                        continue;
                                }

                                rstr = refstr_value(nvfs->vfs_resource);
                                if (rstr != NULL)
                                        rlen = strlen(rstr);

                                /*
                                 * Check for a match: does this vfs correspond
                                 * to our global zone file path?  I.e., check
                                 * if the resource string of this vfs is a
                                 * prefix of our path.
                                 */
                                if ((rlen > 0) && (rlen <= plen) &&
                                    (strncmp(rstr, vpath, rlen) == 0) &&
                                    (vpath[rlen] == '/' ||
                                    vpath[rlen] == '\0')) {
                                        /* force admin_low */
                                        exported = B_TRUE;
                                        break;
                                }
                                nvfs = nvfs->vfs_next;
                        }
                        vfs_list_unlock();
                }

                if (!exported)
                        goto out_high;
        }

        if (vfs_is_held)
                VFS_RELE(vfsp);

        /*
         * Now that we have the "home" zone for the file, return the slabel
         * of that zone.
         */
        zl = zone->zone_slabel;
        label_hold(zl);
        zone_rele(zone);
        return (zl);

out_high:
        if (vfs_is_held)
                VFS_RELE(vfsp);

        label_hold(l_admin_high);
        zone_rele(zone);
        return (l_admin_high);
}

static int
cgetlabel(bslabel_t *label_p, vnode_t *vp)
{
        ts_label_t      *tsl;
        int             error = 0;

        if ((tsl = getflabel(vp)) == NULL)
                return (EIO);

        if (copyout((caddr_t)label2bslabel(tsl), (caddr_t)label_p,
            sizeof (*(label_p))) != 0)
                error = EFAULT;

        label_rele(tsl);
        return (error);
}

/*
 * fgetlabel(3TSOL) - get file label
 * getlabel(3TSOL) - get file label
 */
int
getlabel(const char *path, bslabel_t *label_p)
{
        struct          vnode   *vp;
        char            *spath;
        int             error;

        /* Sanity check arguments */
        if (path == NULL)
                return (set_errno(EINVAL));

        spath = kmem_zalloc(MAXPATHLEN, KM_SLEEP);
        if ((error = copyinstr(path, spath, MAXPATHLEN, NULL)) != 0) {
                kmem_free(spath, MAXPATHLEN);
                return (set_errno(error));
        }

        if (error = lookupname(spath, UIO_SYSSPACE, FOLLOW, NULLVPP, &vp)) {
                kmem_free(spath, MAXPATHLEN);
                return (set_errno(error));
        }
        kmem_free(spath, MAXPATHLEN);

        error = cgetlabel(label_p, vp);

        VN_RELE(vp);
        if (error != 0)
                return (set_errno(error));
        else
                return (0);
}

int
fgetlabel(int fd, bslabel_t *label_p)
{
        file_t          *fp;
        int             error;

        if ((fp = getf(fd)) == NULL)
                return (set_errno(EBADF));

        error = cgetlabel(label_p, fp->f_vnode);
        releasef(fd);

        if (error != 0)
                return (set_errno(error));
        else
                return (0);
}