root/usr/src/lib/libc/port/gen/getxby_door.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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include "lint.h"
#include <mtlib.h>
#include <sys/types.h>
#include <errno.h>
#include <pwd.h>
#include <nss_dbdefs.h>
#include <stdio.h>
#include <string.h>
#include <synch.h>
#include <sys/param.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <getxby_door.h>
#include <sys/door.h>
#include <procfs.h>
#include <door.h>
#include <sys/mman.h>
#include "libc.h"
#include "tsd.h"
#include "base_conversion.h"

/* nss<->door hints */
static mutex_t  hints_lock = DEFAULTMUTEX;
static size_t   door_bsize = 0;
static size_t   door_nbsize = 0;
static int      proc_is_cache = -1;

/* library<->nscd door interaction apis */

/*
 *
 * Routine that actually performs the door call.
 * Note that we cache a file descriptor.  We do
 * the following to prevent disasters:
 *
 * 1) Never use 0,1 or 2; if we get this from the open
 *    we dup it upwards.
 *
 * 2) Set the close on exec flags so descriptor remains available
 *    to child processes.
 *
 * 3) Verify that the door is still the same one we had before
 *    by using door_info on the client side.
 *
 *      Note that we never close the file descriptor if it isn't one
 *      we allocated; we check this with door info.  The rather tricky
 *      logic is designed to be fast in the normal case (fd is already
 *      allocated and is ok) while handling the case where the application
 *      closed it underneath us or where the nscd dies or re-execs itself
 *      and we're a multi-threaded application.  Note that we cannot protect
 *      the application if it closes the fd and it is multi-threaded.
 *
 *  int _nsc_trydoorcall(void *dptr, size_t *bufsize, size_t *actualsize);
 *
 *      *dptr           IN: points to arg buffer OUT: points to results buffer
 *      *bufsize        IN: overall size of buffer OUT: overall size of buffer
 *      *actualsize     IN: size of call data OUT: size of return data
 *
 *  Note that *dptr may change if provided space as defined by *bufsize is
 *  inadequate.  In this case the door call mmaps more space and places
 *  the answer there and sets dptr to contain a pointer to the space, which
 *  should be freed with munmap.
 *
 *  Returns 0 if the door call reached the server, -1 if contact was not made.
 *
 */

/*
 * Max size for list of db names supported by the private nscd
 * No implied max here, any size will do, fixed size chosen to
 * reduce yet another malloc
 */

#define BD_BUFSIZE      1024
#define BD_SEP          ','

typedef struct _nsc_door_t {
        int             doorfd;
        mutex_t         door_lock;
        door_info_t     doori;
} nsc_door_t;

static nsc_door_t       nsc_door[2] = {
        { -1, DEFAULTMUTEX, { 0 } },            /* front (fattached) door */
        { -1, DEFAULTMUTEX, { 0 } },            /* back (private) door */
};

/* assumed to be locked by using nsc_door[1] mutex */
static char     *nsc_db_buf = NULL;
static char     **nsc_db_list = NULL;

/*
 * Check for a valid and matching db in the list.
 * assume list is in the locked state.
 */

static int
_nsc_use_backdoor(char *db)
{
        char    **ndb;

        if (db && nsc_db_buf != NULL && nsc_db_list != NULL) {
                for (ndb = nsc_db_list; *ndb; ndb++) {
                        if (strcmp(db, *ndb) == 0)
                                return (1);
                }
        }
        return (0);
}

/*
 * flush private db lists
 */
static void
_nsc_flush_private_db()
{
        if (nsc_db_buf != NULL) {
                libc_free((void *)nsc_db_buf);
                nsc_db_buf = NULL;
        }
        if (nsc_db_list != NULL) {
                libc_free((void *)nsc_db_list);
                nsc_db_list = NULL;
        }
}

/*
 * init/update nsc_db_buf given buff containing list of
 * db's to be processed by a private nscd.
 * This function assumes it has a well formed string from nscd.
 */

static int
_nsc_init_private_db(char *dblist)
{
        char    *cp, **lp;
        int     buflen = 0;
        int     arrlen = 0;

        if (dblist == NULL)
                return (0);

        /* reset db list */
        _nsc_flush_private_db();

        /* rebuild fresh list */
        buflen = strlen(dblist) + 1;
        for (cp = dblist; *cp; cp++)
                if (*cp == BD_SEP)
                        arrlen++;
        if (cp == dblist)
                return (0);
        arrlen += 2;
        nsc_db_buf = (char *)libc_malloc(buflen);
        if (nsc_db_buf == (char *)NULL)
                return (0);
        nsc_db_list = (char **)libc_malloc(arrlen * sizeof (char *));
        if (nsc_db_list == (char **)NULL) {
                libc_free((void *)nsc_db_buf);
                nsc_db_buf = NULL;
                return (0);
        }
        (void) memcpy(nsc_db_buf, dblist, buflen);
        lp = nsc_db_list;
        *lp++ = nsc_db_buf;
        for (cp = nsc_db_buf; *cp; ) {
                if (*cp == BD_SEP) {
                        *cp++ = '\0';
                        *lp++ = cp;
                } else
                        cp++;
        }
        *lp = NULL;
        return (1);
}

/*
 * _nsc_initdoor_fp attempts to validate the given door and
 * confirm that it is still available for use.  The options are:
 *      Front door:
 *              If it's not open, attempt to open or error
 *              If it's open attempt to validate.
 *              If it's not validatable, reset fd and try again.
 *              Other wise it open and validated, return success
 *      Per user (back) door:
 *              This door is passed to the client through th front door
 *              attempt to validate it.  If it can't be validated, it
 *              must be reset. Then send a NSS_ALTRESET error, so nscd can
 *              forward another fd if desired.
 */

static nss_status_t
_nsc_initdoor_fp(nsc_door_t *dp)
{

        door_info_t             my_door;

        if (dp == NULL) {
                errno = ENOTCONN;
                return (NSS_ERROR);
        }

        /*
         * the first time in we try and open and validate the front door.
         * A front door request may return an alternate private back door
         * that the client should use instead.
         *
         * To validate a door the door must have been created with
         * the name service door cookie. The front door is file
         * attached, owned by root and readonly by user, group and
         * other.  If any of these validations fail we refuse to use
         * the door.  A back door is delivered from the front door
         * via a door_desc_t, and have the same cooke notification.
         */

        lmutex_lock(&dp->door_lock);

try_again:

        if (dp->doorfd == -1 && dp == &nsc_door[0]) {   /* open front door */
                int             tbc[3];
                int             i;

                dp->doorfd = open64(NAME_SERVICE_DOOR, O_RDONLY, 0);
                if (dp->doorfd == -1) {
                        lmutex_unlock(&dp->door_lock);
                        return (NSS_ERROR);
                }

                /*
                 * dup up the file descriptor if we have 0 - 2
                 * to avoid problems with shells stdin/out/err
                 */
                i = 0;

                while (dp->doorfd < 3) { /* we have a reserved fd */
                        tbc[i++] = dp->doorfd;
                        if ((dp->doorfd = dup(dp->doorfd)) < 0) {
                                while (i--)
                                        (void) close(tbc[i]);
                                dp->doorfd = -1;
                                lmutex_unlock(&dp->door_lock);
                                return (NSS_ERROR);
                        }
                }

                while (i--)
                        (void) close(tbc[i]);

                /*
                 * mark this door descriptor as close on exec
                 */
                (void) fcntl(dp->doorfd, F_SETFD, FD_CLOEXEC);
                if (__door_info(dp->doorfd, &dp->doori) < 0 ||
                    (dp->doori.di_attributes & DOOR_REVOKED) ||
                    dp->doori.di_data != (uintptr_t)NAME_SERVICE_DOOR_COOKIE) {
                        /*
                         * we should close doorfd because we just opened it
                         */
                        (void) close(dp->doorfd);
                        dp->doorfd = -1;
                        (void) memset((void *)&dp->doori,
                            '\0', sizeof (door_info_t));
                        lmutex_unlock(&dp->door_lock);
                        errno = ECONNREFUSED;
                        return (NSS_ERROR);
                }
        } else {
                if (__door_info(dp->doorfd, &my_door) < 0 ||
                    my_door.di_data != (uintptr_t)NAME_SERVICE_DOOR_COOKIE ||
                    my_door.di_uniquifier != dp->doori.di_uniquifier) {
                        /*
                         * don't close it -
                         * someone else has clobbered fd
                         */
                        dp->doorfd = -1;
                        (void) memset((void *)&dp->doori,
                            '\0', sizeof (door_info_t));
                        if (dp == &nsc_door[1]) {       /* reset back door */
                                /* flush invalid db list */
                                _nsc_flush_private_db();
                                lmutex_unlock(&dp->door_lock);
                                return (NSS_ALTRESET);
                        }
                        goto try_again;
                }

                if (my_door.di_attributes & DOOR_REVOKED) {
                        (void) close(dp->doorfd);       /* nscd exited .... */
                        dp->doorfd = -1;        /* try and restart connection */
                        (void) memset((void *)&dp->doori,
                            '\0', sizeof (door_info_t));
                        if (dp == &nsc_door[1]) {       /* back door reset */
                                /* flush invalid db list */
                                _nsc_flush_private_db();
                                lmutex_unlock(&dp->door_lock);
                                return (NSS_ALTRESET);
                        }
                        goto try_again;
                }
        }

        lmutex_unlock(&dp->door_lock);
        return (NSS_SUCCESS);
}

/*
 * Try the door request once only, to the specified connection.
 * return the results or error.
 */

static nss_status_t
_nsc_try1door(nsc_door_t *dp, void **dptr, size_t *ndata,
                        size_t *adata, int *pdesc)
{
        door_arg_t              param;
        int                     ret;
        nss_pheader_t           *rp;

        ret = _nsc_initdoor_fp(dp);
        if (ret != NSS_SUCCESS)
                return (ret);

        param.rbuf = (char *)*dptr;
        param.rsize = *ndata;
        param.data_ptr = (char *)*dptr;
        param.data_size = *adata;
        param.desc_ptr = NULL;
        param.desc_num = 0;
        ret = __door_call(dp->doorfd, &param);
        if (ret < 0) {
                return (NSS_ERROR);
        }
        *adata = param.data_size;
        *ndata = param.rsize;
        *dptr = (void *)param.data_ptr;
        rp = (nss_pheader_t *)((void *)param.rbuf);
        if (pdesc != NULL && rp && rp->p_status == NSS_ALTRETRY &&
            param.desc_ptr != NULL && param.desc_num > 0) {
                if ((param.desc_ptr->d_attributes & DOOR_DESCRIPTOR) &&
                    param.desc_ptr->d_data.d_desc.d_descriptor >= 0 &&
                    param.desc_ptr->d_data.d_desc.d_id != 0) {
                        /* have an alt descriptor */
                        *pdesc = param.desc_ptr->d_data.d_desc.d_descriptor;
                        /* got a NSS_ALTRETRY command */
                        return (NSS_ALTRETRY);
                }
                errno = EINVAL;
                return (NSS_ERROR);             /* other error? */
        }
        if (*adata == 0 || *dptr == NULL) {
                errno = ENOTCONN;
                return (NSS_ERROR);
        }

        if (rp->p_status == NSS_ALTRESET ||
            rp->p_status == NSS_ALTRETRY ||
            rp->p_status == NSS_TRYLOCAL)
                return (rp->p_status);

        return (NSS_SUCCESS);
}

/*
 * Backwards compatible API
 */

nss_status_t
_nsc_trydoorcall(void **dptr, size_t *ndata, size_t *adata)
{
        return (_nsc_try1door(&nsc_door[0], dptr, ndata, adata, NULL));
}

/*
 * Send the request to the designated door, based on the supplied db
 * Retry on the alternate door fd if possible.
 */

nss_status_t
_nsc_trydoorcall_ext(void **dptr, size_t *ndata, size_t *adata)
{
        int             ret = NSS_ALTRETRY;
        nsc_door_t      *frontd = &nsc_door[0];
        nsc_door_t      *backd = &nsc_door[1];
        int             fd;

        nss_pheader_t   *ph, ph_save;
        char            *dbl;
        char            *db = NULL;
        nss_dbd_t       *dbd;
        int             fb2frontd = 0;
        int             reset_frontd = 0;
        size_t          ndata_save = *ndata, adata_save = *adata;
        void            *dptr_save = *dptr;

        ph = (nss_pheader_t *)*dptr;
        dbd = (nss_dbd_t *)((void *)((char *)ph + ph->dbd_off));
        if (dbd->o_name != 0)
                db = (char *)dbd + dbd->o_name;

        /*
         * save away a copy of the header, in case the request needs
         * to be sent to nscd more than once. In that case, this
         * original header can be copied back to the door buffer
         * to replace the possibly changed header
         */
        ph_save = *ph;

        while (ret == NSS_ALTRETRY || ret == NSS_ALTRESET) {
                /* try private (back) door first if it exists and applies */
                if (db != NULL && backd->doorfd > 0 && fb2frontd == 0 &&
                    _nsc_use_backdoor(db)) {
                        ret = _nsc_try1door(backd, dptr, ndata, adata, NULL);
                        if (ret == NSS_ALTRESET) {
                                /*
                                 * received NSS_ALTRESET command,
                                 * retry on front door
                                 */
                                lmutex_lock(&backd->door_lock);
                                backd->doorfd = -1;
                                (void) memset((void *)&backd->doori,
                                    '\0', sizeof (door_info_t));
                                /* flush now invalid db list */
                                _nsc_flush_private_db();
                                lmutex_unlock(&backd->door_lock);
                                continue;
                        } else if (ret == NSS_ALTRETRY) {
                                /*
                                 * received NSS_ALTRETRY command,
                                 * fall back and retry on front door
                                 */
                                fb2frontd = 1;
                                if (*dptr != dptr_save)
                                        (void) munmap((void *)*dptr, *ndata);

                                /*
                                 * restore the buffer size and header
                                 * data so that the front door will
                                 * see the original request
                                 */
                                *ndata = ndata_save;
                                *adata = adata_save;
                                *dptr = dptr_save;
                                ph =  (nss_pheader_t *)*dptr;
                                *ph = ph_save;
                                /*
                                 * tell the front door server, this is
                                 * a fallback call
                                 */
                                ph->p_status = NSS_ALTRETRY;
                                continue;
                        }

                        /* return the result or error */
                        break;
                }

                /* try the front door */
                fd = -1;
                ret = _nsc_try1door(frontd, dptr, ndata, adata, &fd);

                if (ret != NSS_ALTRETRY) {
                        /*
                         * got a success or failure result.
                         * but front door should never send NSS_ALTRESET
                         */
                        if (ret == NSS_ALTRESET)
                                /* reset the front door */
                                reset_frontd = 1;
                        else
                                /*
                                 * not NSS_ALTRETRY and not NSS_ALTRESET
                                 * return the result or error
                                 */
                                break;
                } else if (fb2frontd == 1) {
                        /*
                         * front door should never send NSS_ALTRETRY
                         * in a fallback call. Reset the front door.
                         */
                        reset_frontd = 1;
                }

                if (reset_frontd == 1) {
                        lmutex_lock(&frontd->door_lock);
                        frontd->doorfd = -1;
                        (void) memset((void *)&frontd->doori,
                            '\0', sizeof (door_info_t));
                        lmutex_unlock(&frontd->door_lock);
                        /* error out */
                        ret = NSS_ERROR;
                        break;
                }

                /* process NSS_ALTRETRY request from front door */
                if (fd < 0)
                        continue;       /* no new door given, try again */

                /* update and try alternate door */
                lmutex_lock(&backd->door_lock);
                if (backd->doorfd >= 0) {
                        /* unexpected open alt door - clean up, continue */
                        _nsc_flush_private_db();
                        (void) close(backd->doorfd);
                }

                /* set up back door fd */
                backd->doorfd = fd;

                /* set up back door db list */
                ph =  (nss_pheader_t *)*dptr;
                dbl = ((char *)ph) + ph->data_off;

                if (_nsc_init_private_db(dbl) == 0) {
                        /* could not init db list, try again */
                        (void) close(backd->doorfd);
                        backd->doorfd = -1;
                        lmutex_unlock(&backd->door_lock);
                        continue;
                }
                if (door_info(backd->doorfd, &backd->doori) < 0 ||
                    (backd->doori.di_attributes & DOOR_REVOKED) ||
                    backd->doori.di_data !=
                    (uintptr_t)NAME_SERVICE_DOOR_COOKIE) {
                        /* doorfd bad, or must not really be open */
                        (void) close(backd->doorfd);
                        backd->doorfd = -1;
                        (void) memset((void *)&backd->doori,
                            '\0', sizeof (door_info_t));
                }
                (void) fcntl(backd->doorfd, F_SETFD, FD_CLOEXEC);
                lmutex_unlock(&backd->door_lock);
                /* NSS_ALTRETRY new back door */
                if (*dptr != dptr_save)
                        (void) munmap((void *)*dptr, *ndata);

                /*
                 * restore the buffer size and header
                 * data so that the back door will
                 * see the original request
                 */
                *ndata = ndata_save;
                *adata = adata_save;
                *dptr = dptr_save;
                ph =  (nss_pheader_t *)*dptr;
                *ph = ph_save;
        }
        return (ret);
}

/*
 * Get the current (but growable) buffer size for a NSS2 packet.
 * Heuristic algorithm used:
 *      1) Make sure it's at least NSS_BUFLEN_DOOR in length (16k default)
 *      2) if an incoming user buffer is > larger than the current size
 *         Make the buffer at least NSS_BUFLEN_DOOR/2+user buffer size
 *         This should account for any reasonable nss_pheader, keys
 *         extended area etc.
 *      3) keep the prototype/debugging (private)NSS_BUFLEN option
 *         to change any preconfigured value if needed(?)
 */

static size_t
_nsc_getdoorbsize(size_t min_size)
{
        if (!door_bsize) {
                lmutex_lock(&hints_lock);
                if (!door_bsize) {
                        /* future work - get nscd hint & use hint size */
                        door_bsize = ROUND_UP(door_bsize, NSS_BUFSIZ);
                        if (door_bsize < NSS_BUFLEN_DOOR) {
                                door_bsize = NSS_BUFLEN_DOOR;
                        }
                }
                lmutex_unlock(&hints_lock);
        }
        if (min_size && door_bsize < (min_size + NSS_BUFLEN_DOOR/2)) {
                lmutex_lock(&hints_lock);
                if (door_bsize < (min_size + NSS_BUFLEN_DOOR/2)) {
                        min_size += NSS_BUFLEN_DOOR;
                        door_bsize = ROUND_UP(min_size, NSS_BUFSIZ);
                }
                lmutex_unlock(&hints_lock);
        }
        return (door_bsize);
}

static void
_nsc_freedbuf(void *arg)
{
        nss_XbyY_buf_t *tsdbuf = arg;

        if (tsdbuf != NULL && tsdbuf->buffer != NULL) {
                lfree(tsdbuf->buffer, (size_t)tsdbuf->buflen);
                tsdbuf->result = NULL;
                tsdbuf->buffer = NULL;
                tsdbuf->buflen = 0;
        }
}

/*
 * _nsc_getdoorbuf - return the client side per thread door buffer
 * Elsewhere, it is assumed that the header is 0'd upon return from here.
 */

int
_nsc_getdoorbuf(void **doorptr, size_t *bufsize)
{
        nss_XbyY_buf_t *tsdbuf;
        char *bp;
        size_t dsize;

        if (doorptr == NULL || bufsize == NULL)
                return (-1);

        /* Get thread specific pointer to door buffer */
        tsdbuf = tsdalloc(_T_DOORBUF, sizeof (nss_XbyY_buf_t), _nsc_freedbuf);
        if (tsdbuf == NULL)
                return (-1);

        /* if door buffer does not exist create it */
        if (tsdbuf->buffer == NULL) {
                dsize = _nsc_getdoorbsize(*bufsize);

                /* setup a door buffer with a total length of dsize */
                bp = lmalloc(dsize);
                if (bp == NULL)
                        return (-1);
                tsdbuf->buffer = bp;
                tsdbuf->buflen = dsize;
        } else {
                /* check old buffer size and resize if needed */
                if (*bufsize) {
                        dsize = _nsc_getdoorbsize(*bufsize);
                        if (tsdbuf->buflen < dsize) {
                                lfree(tsdbuf->buffer, (size_t)tsdbuf->buflen);
                                bp = lmalloc(dsize);
                                if (bp == NULL)
                                        return (-1);
                                tsdbuf->buffer = bp;
                                tsdbuf->buflen = dsize;
                        }
                }
                /* freshly malloc'd door bufs are 0'd */
                /* 0 header for now.  Zero entire buf(?) TDB */
                (void) memset((void *)tsdbuf->buffer, 0,
                    (size_t)sizeof (nss_pheader_t));

        }
        *doorptr = (void *)tsdbuf->buffer;
        *bufsize = tsdbuf->buflen;
        return (0);
}

void
_nsc_resizedoorbuf(size_t bsize)
{
        /* signal to update if new door size is desired */
        lmutex_lock(&hints_lock);
        if (bsize > door_bsize && door_nbsize < bsize)
                door_nbsize = bsize;
        lmutex_unlock(&hints_lock);
}

/*
 * Check uid and /proc/PID/psinfo to see if this process is nscd
 * If it is set the appropriate flags and allow policy reconfiguration.
 */
int
_nsc_proc_is_cache()
{
        psinfo_t        pinfo;
        char            fname[128];
        int             ret;
        int             fd;

        if (proc_is_cache >= 0)
                return (proc_is_cache);
        lmutex_lock(&hints_lock);
        if (proc_is_cache >= 0) {
                lmutex_unlock(&hints_lock);
                return (proc_is_cache);
        }
        proc_is_cache = 0;
        /* It can't be nscd if it's not running as root... */
        if (getuid() != 0) {
                lmutex_unlock(&hints_lock);
                return (0);
        }
        ret = snprintf(fname, 128, "/proc/%d/psinfo", getpid());
        if (ret > 0 && ret < 128) {
                if ((fd = open(fname,  O_RDONLY)) >= 0) {
                        ret = read(fd, &pinfo, sizeof (psinfo_t));
                        (void) close(fd);
                        if (ret == sizeof (psinfo_t) &&
                            (strcmp(pinfo.pr_fname, "nscd") == 0)) {
                                /* process runs as root and is named nscd */
                                /* that's good enough for now */
                                proc_is_cache = 1;
                        }
                }
        }
        lmutex_unlock(&hints_lock);
        return (proc_is_cache);
}