root/usr/src/cmd/smbsrv/smbd/smbd_pipesvc.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2015 Nexenta Systems, Inc.  All rights reserved.
 * Copyright 2022-2023 RackTop Systems, Inc.
 */

/*
 * This is the named pipe service for smbd.
 */

#include <sys/types.h>
#include <sys/stat.h>

#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
#include <synch.h>
#include <unistd.h>
#include <fcntl.h>
#include <door.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <ucred.h>
#include <priv.h>

#include <smbsrv/libsmb.h>
#include <smbsrv/libmlsvc.h>
#include <smbsrv/smb_xdr.h>
#include "smbd.h"

struct pipe_listener {
        const char *name;
        int max_allowed;
        int max_seen;
        int current;
        pthread_t tid;
};

static void *pipesvc_listener(void *);
static void *pipesvc_worker(void *);
static int pipe_send(ndr_pipe_t *, void *, size_t);
static int pipe_recv(ndr_pipe_t *, void *, size_t);

mutex_t  pipesvc_mutex = DEFAULTMUTEX;
int pipesvc_workers_max = 500;
int pipesvc_workers_cur = 0;

uint16_t pipe_max_msgsize = SMB_PIPE_MAX_MSGSIZE;

/*
 * Allow more opens on SRVSVC because that's used by many clients
 * to get the share list, etc.
 */
#define SRVSVC_MAX_OPENS        200
#define DEF_MAX_OPENS           50

#define NLISTENERS      11
static struct pipe_listener
pipe_listeners[NLISTENERS] = {
        { "eventlog",   DEF_MAX_OPENS, 0, 0 },
        { "lsarpc",     DEF_MAX_OPENS, 0, 0 },
        { "lsass",      DEF_MAX_OPENS, 0, 0 },
        { "netdfs",     DEF_MAX_OPENS, 0, 0 },
        { "netlogon",   DEF_MAX_OPENS, 0, 0 },
        { "samr",       DEF_MAX_OPENS, 0, 0 },
        { "spoolss",    DEF_MAX_OPENS, 0, 0 },
        { "srvsvc",     SRVSVC_MAX_OPENS, 0, 0 },
        { "svcctl",     DEF_MAX_OPENS, 0, 0 },
        { "winreg",     DEF_MAX_OPENS, 0, 0 },
        { "wkssvc",     DEF_MAX_OPENS, 0, 0 },
};

static ndr_pipe_t *
np_new(struct pipe_listener *pl, int fid)
{
        ndr_pipe_t *np;
        size_t len;

        /*
         * Allocating ndr_pipe_t + smb_netuserinfo_t as one.
         * We could just make that part of ndr_pipe_t, but
         * that struct is opaque to libmlrpc.
         */
        len = sizeof (*np) + sizeof (smb_netuserinfo_t);
        np = malloc(len);
        if (np == NULL)
                return (NULL);

        bzero(np, len);
        np->np_listener = pl;
        np->np_endpoint = pl->name;
        np->np_user = (void*)(np + 1);
        np->np_send = pipe_send;
        np->np_recv = pipe_recv;
        np->np_fid = fid;
        np->np_max_xmit_frag = pipe_max_msgsize;
        np->np_max_recv_frag = pipe_max_msgsize;

        return (np);
}

static void
np_free(ndr_pipe_t *np)
{
        (void) close(np->np_fid);
        free(np);
}

/*
 * Create the smbd opipe door service.
 * Returns the door descriptor on success.  Otherwise returns -1.
 */
int
smbd_pipesvc_start(void)
{
        pthread_t tid;
        pthread_attr_t tattr;
        struct pipe_listener *pl;
        int i, rc;

        if (mlsvc_init() != 0) {
                smbd_report("msrpc initialization failed");
                return (-1);
        }

        (void) pthread_attr_init(&tattr);
        (void) pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED);

        for (i = 0; i < NLISTENERS; i++) {
                pl = &pipe_listeners[i];
                pl->max_seen = 0;

                if (strcasecmp(pl->name, "spoolss") == 0 &&
                    smb_config_getbool(SMB_CI_PRINT_ENABLE) == B_FALSE)
                        continue;

                rc = pthread_create(&tid, &tattr, pipesvc_listener, pl);
                if (rc != 0)
                        break;
                pipe_listeners[i].tid = tid;
        }

        if (rc != 0) {
                smbd_report("pipesvc pthread_create, %d", rc);
        }

        (void) pthread_attr_destroy(&tattr);

        return (rc);
}

void
smbd_pipesvc_stop(void)
{
        int i;

        (void) mutex_lock(&pipesvc_mutex);
        for (i = 0; i < NLISTENERS; i++) {
                if (pipe_listeners[i].tid == 0)
                        continue;
                (void) pthread_kill(pipe_listeners[i].tid, SIGTERM);
                pipe_listeners[i].tid = 0;
        }
        (void) mutex_unlock(&pipesvc_mutex);
}

static void *
pipesvc_listener(void *varg)
{
        struct sockaddr_un sa;
        int err, listen_fd, newfd, snlen;
        struct pipe_listener *pl = varg;
        ndr_pipe_t *np;
        pthread_t tid;
        int rc;

        listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (listen_fd < 0) {
                smbd_report("pipesvc_listener, so_create: %d", errno);
                return (NULL);
        }

        bzero(&sa, sizeof (sa));
        sa.sun_family = AF_UNIX;
        (void) snprintf(sa.sun_path, sizeof (sa.sun_path),
            "%s/%s", SMB_PIPE_DIR, pl->name);

        /* Bind it to a listening name. */
        (void) unlink(sa.sun_path);
        if (bind(listen_fd, (struct sockaddr *)&sa, sizeof (sa)) < 0) {
                smbd_report("pipesvc_listener, so_bind: %d", errno);
                (void) close(listen_fd);
                return (NULL);
        }

        if (listen(listen_fd, SOMAXCONN) < 0) {
                smbd_report("pipesvc_listener, listen: %d", errno);
                (void) close(listen_fd);
                return (NULL);
        }

        for (;;) {

                snlen = sizeof (sa);
                newfd = accept(listen_fd, (struct sockaddr *)&sa, &snlen);
                if (newfd < 0) {
                        err = errno;
                        switch (err) {
                        case ECONNABORTED:
                                continue;
                        case EINTR:
                                /* normal termination */
                                goto out;
                        default:
                                smbd_report("pipesvc_listener, "
                                    "accept failed: %d", errno);
                        }
                        smbd_report("pipesvc_listener, accept: %d", err);
                        break;
                }

                np = np_new(pl, newfd);
                if (np == NULL) {
                        smbd_report("pipesvc_listener, alloc1 failed");
                        (void) close(newfd);
                        smbd_nomem();
                }

                rc = pthread_create(&tid, NULL, pipesvc_worker, np);
                if (rc != 0) {
                        smbd_report("pipesvc_listener, pthread_create: %d",
                            errno);
                        np_free(np);
                        smbd_nomem();
                }
                (void) pthread_detach(tid);

                /* Note: np_free in pipesvc_worker */
                np = NULL;
        }

out:
        (void) close(listen_fd);
        pl->tid = 0;
        return (NULL);
}

#ifndef FKSMBD
/*
 * Decide whether we should trust the (in-band) user information
 * that the client sends us over the named pipe.  The (in-kernel)
 * SMB service calls this with the credential of the logged-on
 * SMB user.  The privileges are normally:
 *        effective: basic,file_dac_search
 *      inheritable: basic
 *        permitted: basic,file_dac_search,sys_smb
 *            limit: all
 * This tests the permitted set for the presence of PRIV_SYS_SMB,
 * which should only be granted to the SMB server.
 */
static boolean_t
pipe_has_priv(ndr_pipe_t *np)
{
        ucred_t *uc = NULL;
        const priv_set_t *ps = NULL;
        boolean_t ret = B_FALSE;
        pid_t  clpid;

        if (getpeerucred(np->np_fid, &uc) != 0) {
                smbd_report("pipesvc: getpeerucred err %d", errno);
                return (B_FALSE);
        }
        clpid = ucred_getpid(uc);
        ps = ucred_getprivset(uc, PRIV_PERMITTED);
        if (ps == NULL) {
                smbd_report("pipesvc: ucred_getprivset failed");
                goto out;
        }

        /*
         * Require sys_smb priv.
         */
        if (priv_ismember(ps, PRIV_SYS_SMB)) {
                ret = B_TRUE;
                goto out;
        }

        if (smbd.s_debug) {
                smbd_report("pipesvc: non-privileged client "
                    "PID = %d UID = %d",
                    (int)clpid, ucred_getruid(uc));
        }

out:
        /* ps is free'd with the ucred */
        if (uc != NULL)
                ucred_free(uc);

        return (ret);
}
#endif

static void *
pipesvc_worker(void *varg)
{
        XDR xdrs;
        smb_pipehdr_t phdr;
        ndr_pipe_t *np = varg;
        struct pipe_listener *pl = np->np_listener;
        void *buf = NULL;
        uint32_t status;
        ssize_t rc;

        (void) mutex_lock(&pipesvc_mutex);
        if (pipesvc_workers_cur >= pipesvc_workers_max ||
            pl->current >= pl->max_allowed) {
                (void) mutex_unlock(&pipesvc_mutex);
                status = NT_STATUS_PIPE_NOT_AVAILABLE;
                (void) send(np->np_fid, &status, sizeof (status), 0);
                goto out_free_np;
        }
        pipesvc_workers_cur++;
        pl->current++;
        if (pl->max_seen < pl->current)
                pl->max_seen = pl->current;
        (void) mutex_unlock(&pipesvc_mutex);

        /*
         * The smbsrv kmod sends us one initial message containing an
         * XDR encoded smb_netuserinfo_t that we read and decode here,
         * all unbeknownst to libmlrpc.
         *
         * Might be nice to enhance getpeerucred() so it can give us
         * all the info smb_netuserinfo_t carries, and then use that,
         * which would allow using a more generic RPC service.
         */
        rc = pipe_recv(np, &phdr, sizeof (phdr));
        if (rc != 0) {
                smbd_report("pipesvc_worker, recv1: %d", rc);
                goto out_decr;
        }
        if (phdr.ph_magic != SMB_PIPE_HDR_MAGIC ||
            phdr.ph_uilen > 8192) {
                smbd_report("pipesvc_worker, bad hdr");
                goto out_decr;
        }
        buf = malloc(phdr.ph_uilen);
        if (buf == NULL) {
                smbd_report("pipesvc_worker, alloc1 failed");
                goto out_decr;
        }
        rc = pipe_recv(np, buf, phdr.ph_uilen);
        if (rc != 0) {
                smbd_report("pipesvc_worker, recv2: %d", rc);
                goto out_decr;
        }

        xdrmem_create(&xdrs, buf, phdr.ph_uilen, XDR_DECODE);
        if (!smb_netuserinfo_xdr(&xdrs, np->np_user)) {
                smbd_report("pipesvc_worker, bad uinfo");
                goto out_free_buf;
        }

        /*
         * Don't trust the netuserinfo unless the client side
         * has the necessary privileges
         */
#ifndef FKSMBD
        if (!pipe_has_priv(np)) {
                np->np_user->ui_flags = SMB_ATF_ANON;
        }
#endif

        /*
         * Later, could disallow opens of some pipes by
         * anonymous users, etc.  For now, reply "OK".
         */
        status = 0;
        rc = pipe_send(np, &status, sizeof (status));
        if (rc != 0) {
                smbd_report("pipesvc_worker, send1: %d", rc);
                goto out_free_buf;
        }

        /*
         * Run the RPC service loop worker, which
         * returns when it sees the pipe close.
         */
        ndr_pipe_worker(np);

        xdrs.x_op = XDR_FREE;
        (void) smb_netuserinfo_xdr(&xdrs, np->np_user);

out_free_buf:
        free(buf);
        xdr_destroy(&xdrs);

out_decr:
        (void) mutex_lock(&pipesvc_mutex);
        pipesvc_workers_cur--;
        pl->current--;
        (void) mutex_unlock(&pipesvc_mutex);

out_free_np:
        /* Cleanup what came in by varg. */
        (void) shutdown(np->np_fid, SHUT_RDWR);
        np_free(np);
        return (NULL);
}

/*
 * These are the transport get/put callback functions provided
 * via the ndr_pipe_t object to the libmlrpc`ndr_pipe_worker.
 * These are called only with known PDU sizes and should
 * loop as needed to transfer the entire message.
 */
static int
pipe_recv(ndr_pipe_t *np, void *buf, size_t len)
{
        int x;

        while (len > 0) {
                x = recv(np->np_fid, buf, len, 0);
                if (x < 0)
                        return (errno);
                if (x == 0)
                        return (EIO);
                buf = (char *)buf + x;
                len -= x;
        }

        return (0);
}

static int
pipe_send(ndr_pipe_t *np, void *buf, size_t len)
{
        int x;

        while (len > 0) {
                x = send(np->np_fid, buf, len, 0);
                if (x < 0)
                        return (errno);
                if (x == 0)
                        return (EIO);
                buf = (char *)buf + x;
                len -= x;
        }

        return (0);
}