root/sbin/mount_portal/puffs_portal.c
/*      $NetBSD: puffs_portal.c,v 1.11 2023/04/04 20:39:36 rillig Exp $ */

/*
 * Copyright (c) 2007  Antti Kantee.  All Rights Reserved.
 * Development was supported by the Finnish Cultural Foundation.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#ifndef lint
__RCSID("$NetBSD: puffs_portal.c,v 1.11 2023/04/04 20:39:36 rillig Exp $");
#endif /* !lint */

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <mntopts.h>
#include <paths.h>
#include <poll.h>
#include <puffs.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <util.h>

#include "portald.h"

struct portal_node {
        char *path;
        int fd;
};

__dead static void usage(void);

PUFFSOP_PROTOS(portal)

#define PORTAL_ROOT NULL
#define METADATASIZE (sizeof(int) + sizeof(size_t))

qelem q;
int readcfg, sigchild;
const char *cfg;

static void
usage(void)
{

        errx(1, "usage: %s [-o options] /path/portal.conf mount_point",
            getprogname());
}

static void
sighup(int sig)
{

        readcfg = 1;
}

static void
sigcry(int sig)
{

        sigchild = 1;
}

static void
portal_loopfn(struct puffs_usermount *pu)
{

        if (readcfg)
                conf_read(&q, cfg);
        readcfg = 0;

        if (sigchild) {
                sigchild = 0;
                while (waitpid(-1, NULL, WNOHANG) != -1)
                        continue;
        }
}

#define PUFBUF_FD       1
#define PUFBUF_DATA     2

#define CMSIZE (sizeof(struct cmsghdr) + sizeof(int))

/* receive file descriptor produced by our child process */
static int
readfd(struct puffs_framebuf *pufbuf, int fd, int *done)
{
        struct cmsghdr *cmp;
        struct msghdr msg;
        struct iovec iov;
        ssize_t n;
        int error, rv;

        rv = 0;
        cmp = emalloc(CMSG_SPACE(sizeof(int)));

        iov.iov_base = &error;
        iov.iov_len = sizeof(int);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        msg.msg_control = cmp;
        msg.msg_controllen = CMSG_SPACE(sizeof(int));

        n = recvmsg(fd, &msg, 0);
        if (n == -1) {
                rv = errno;
                goto out;
        }
        if (n == 0) {
                rv = ECONNRESET;
                goto out;
        }

        /* the data for the server */
        puffs_framebuf_putdata_atoff(pufbuf, 0, &error, sizeof(int));
        if (error) {
                rv = error;
                goto out;
        }
        puffs_framebuf_putdata_atoff(pufbuf, sizeof(int),
            CMSG_DATA(cmp), sizeof(int));
        *done = 1;

 out:
        free(cmp);
        return rv;
}

/*
 * receive data from provider
 *
 * XXX: should read directly into the buffer and adjust offsets
 * instead of doing memcpy
 */
static int
readdata(struct puffs_framebuf *pufbuf, int fd, int *done)
{
        char buf[1024];
        size_t max;
        ssize_t n;
        size_t moved;

        /* don't override metadata */
        if (puffs_framebuf_telloff(pufbuf) == 0)
                puffs_framebuf_seekset(pufbuf, METADATASIZE);
        puffs_framebuf_getdata_atoff(pufbuf, sizeof(int), &max, sizeof(size_t));
        moved = puffs_framebuf_tellsize(pufbuf) - METADATASIZE;
        assert(max >= moved);
        max -= moved;

        do {
                n = read(fd, buf, MIN(sizeof(buf), max));
                if (n == 0) {
                        /*
                         * Deal with EOF here by closing the file descriptor
                         * and thus causing an error on subsequent accesses.
                         * This is the last kevent notification we are going
                         * to be getting for regular files.
                         */
                        close(fd);
                        if (moved)
                                break;
                        else
                                return -1; /* caught by read */
                }
                if (n < 0) {
                        if (moved)
                                return 0;

                        if (errno != EAGAIN)
                                return errno;
                        else
                                return 0;
                }

                puffs_framebuf_putdata(pufbuf, buf, n);
                moved += n;
                max -= n;
        } while (max > 0);

        *done = 1;

        return 0;
}

static int
portal_frame_rf(struct puffs_usermount *pu, struct puffs_framebuf *pufbuf,
        int fd, int *done)
{
        int type;

        if (puffs_framebuf_getdata_atoff(pufbuf, 0, &type, sizeof(int)) == -1)
                return EINVAL;

        if (type == PUFBUF_FD)
                return readfd(pufbuf, fd, done);
        else if (type == PUFBUF_DATA)
                return readdata(pufbuf, fd, done);
        else
                abort();
}

static int
portal_frame_wf(struct puffs_usermount *pu, struct puffs_framebuf *pufbuf,
        int fd, int *done)
{
        void *win;
        size_t pbsize, pboff, winlen;
        ssize_t n;
        int error;

        pboff = puffs_framebuf_telloff(pufbuf);
        pbsize = puffs_framebuf_tellsize(pufbuf);
        error = 0;

        do {
                assert(pbsize > pboff);
                winlen = pbsize - pboff;
                if (puffs_framebuf_getwindow(pufbuf, pboff, &win, &winlen)==-1)
                        return errno;
                n = write(fd, win, winlen);
                if (n == 0) {
                        if (pboff != 0)
                                break;
                        else
                                return -1; /* caught by node_write */
                }
                if (n < 0) {
                        if (pboff != 0)
                                break;

                        if (errno != EAGAIN)
                                return errno;
                        return 0;
                }

                pboff += n;
                puffs_framebuf_seekset(pufbuf, pboff);
        } while (pboff != pbsize);

        *done = 1;
        puffs_framebuf_putdata_atoff(pufbuf, 0, &pboff, sizeof(size_t));
        return error;
}

/* transfer file descriptor to master file server */
static int
sendfd(int s, int fd, int error)
{
        struct cmsghdr *cmp;
        struct msghdr msg;
        struct iovec iov;
        ssize_t n;
        int rv;

        rv = 0;
        cmp = emalloc(CMSG_LEN(sizeof(int)));

        iov.iov_base = &error;
        iov.iov_len = sizeof(int);

        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
        if (error == 0) {
                cmp->cmsg_level = SOL_SOCKET;
                cmp->cmsg_type = SCM_RIGHTS;
                cmp->cmsg_len = CMSG_LEN(sizeof(int));

                msg.msg_control = cmp;
                msg.msg_controllen = CMSG_LEN(sizeof(int));
                *(int *)CMSG_DATA(cmp) = fd;
        } else {
                msg.msg_control = NULL;
                msg.msg_controllen = 0;
        }

        n = sendmsg(s, &msg, 0);
        if (n == -1)
                rv = errno;
        else if (n < (ssize_t)sizeof(int))
                rv = EPROTO;

        free(cmp);
        return rv;
}

/*
 * Produce I/O file descriptor by forking (like original portald).
 * 
 * child: run provider and transfer produced fd to parent
 * parent: yield until child produces fd.  receive it and store it.
 */
static int
provide(struct puffs_usermount *pu, struct portal_node *portn,
        struct portal_cred *portc, char **v)
{
        struct puffs_cc *pcc = puffs_cc_getcc(pu);
        struct puffs_framebuf *pufbuf;
        int s[2];
        int fd, error;
        int data;

        pufbuf = puffs_framebuf_make();
        if (pufbuf == NULL)
                return ENOMEM;

        data = PUFBUF_FD;
        if (puffs_framebuf_putdata(pufbuf, &data, sizeof(int)) == -1)
                goto bad;

        if (socketpair(AF_LOCAL, SOCK_STREAM, 0, s) == -1)
                goto bad;

        switch (fork()) {
        case -1:
                close(s[0]); close(s[1]);
                goto bad;
        case 0:
                close(s[0]);
                error = activate_argv(portc, portn->path, v, &fd);
                sendfd(s[1], fd, error);
                exit(0);
        default:
                close(s[1]);
                puffs_framev_addfd(pu, s[0], PUFFS_FBIO_READ);
                puffs_framev_enqueue_directreceive(pcc, s[0], pufbuf, 0);
                puffs_framev_removefd(pu, s[0], 0);
                close(s[0]);

                if (puffs_framebuf_tellsize(pufbuf) < sizeof(int)) {
                        errno = EIO;
                        goto bad;
                }
                puffs_framebuf_getdata_atoff(pufbuf, 0, &error, sizeof(int));
                if (error) {
                        errno = error;
                        goto bad;
                }

                if (puffs_framebuf_tellsize(pufbuf) != 2*sizeof(int)) {
                        errno = EIO;
                        goto bad;
                }

                puffs_framebuf_getdata_atoff(pufbuf, sizeof(int),
                    &fd, sizeof(int));
                puffs_framebuf_destroy(pufbuf);

                data = 1;
                if (ioctl(fd, FIONBIO, &data) == -1)
                        return errno;

                if (puffs_framev_addfd(pu, fd, PUFFS_FBIO_WRITE) == -1)
                        return errno;

                portn->fd = fd;
                return 0;
        }

 bad:
        puffs_framebuf_destroy(pufbuf);
        return errno;
}

int
main(int argc, char *argv[])
{
        struct puffs_usermount *pu;
        struct puffs_ops *pops;
        mntoptparse_t mp;
        int pflags, mntflags;
        int detach;
        int ch;

        setprogname(argv[0]);

        mntflags = pflags = 0;
        detach = 1;
        while ((ch = getopt(argc, argv, "o:s")) != -1) {
                switch (ch) {
                case 'o':
                        mp = getmntopts(optarg, puffsmopts, &mntflags, &pflags);
                        if (mp == NULL)
                                err(1, "getmntopts");
                        freemntopts(mp);
                        break;
                case 's': /* stay on top */
                        detach = 0;
                        break;
                default:
                        usage();
                        /*NOTREACHED*/
                }
        }
        pflags |= PUFFS_KFLAG_NOCACHE | PUFFS_KFLAG_LOOKUP_FULLPNBUF;
        if (pflags & PUFFS_FLAG_OPDUMP)
                detach = 0;
        argc -= optind;
        argv += optind;

        if (argc != 2)
                usage();

        PUFFSOP_INIT(pops);

        PUFFSOP_SETFSNOP(pops, unmount);
        PUFFSOP_SETFSNOP(pops, sync);
        PUFFSOP_SETFSNOP(pops, statvfs);

        PUFFSOP_SET(pops, portal, node, lookup);
        PUFFSOP_SET(pops, portal, node, getattr);
        PUFFSOP_SET(pops, portal, node, setattr);
        PUFFSOP_SET(pops, portal, node, open);
        PUFFSOP_SET(pops, portal, node, read);
        PUFFSOP_SET(pops, portal, node, write);
        PUFFSOP_SET(pops, portal, node, seek);
        PUFFSOP_SET(pops, portal, node, poll);
        PUFFSOP_SET(pops, portal, node, inactive);
        PUFFSOP_SET(pops, portal, node, reclaim);

        pu = puffs_init(pops, _PATH_PUFFS, "portal", NULL, pflags);
        if (pu == NULL)
                err(1, "init");

        if (signal(SIGHUP, sighup) == SIG_ERR)
                warn("cannot set sighup handler");
        if (signal(SIGCHLD, sigcry) == SIG_ERR)
                err(1, "cannot set sigchild handler");
        if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
                err(1, "cannot ignore sigpipe");

        readcfg = 0;
        cfg = argv[0];
        if (*cfg != '/')
                errx(1, "need absolute path for config");
        q.q_forw = q.q_back = &q;
        if (conf_read(&q, cfg) == -1)
                err(1, "cannot read cfg \"%s\"", cfg);

        puffs_ml_setloopfn(pu, portal_loopfn);
        puffs_framev_init(pu, portal_frame_rf, portal_frame_wf, NULL,NULL,NULL);

        if (detach)
                if (puffs_daemon(pu, 1, 1) == -1)
                        err(1, "puffs_daemon");

        if (puffs_mount(pu,  argv[1], mntflags, PORTAL_ROOT) == -1)
                err(1, "mount");
        if (puffs_mainloop(pu) == -1)
                err(1, "mainloop");

        return 0;
}

static struct portal_node *
makenode(const char *path)
{
        struct portal_node *portn;

        portn = emalloc(sizeof(struct portal_node));
        portn->path = estrdup(path);
        portn->fd = -1;

        return portn;
}

static void
credtr(struct portal_cred *portc, const struct puffs_cred *puffc, int mode)
{
        memset(portc, 0, sizeof(struct portal_cred));

        portc->pcr_flag = mode;
        puffs_cred_getuid(puffc, &portc->pcr_uid);
        puffs_cred_getgid(puffc, &portc->pcr_gid);
        puffs_cred_getgroups(puffc, portc->pcr_groups,
            (short *)&portc->pcr_ngroups);
}

/*
 * XXX: we could also simply already resolve the name at this stage
 * instead of deferring it to open.  But doing it in open is how the
 * original portald does it, and I don't want to introduce any funny
 * incompatibilities.
 */
int
portal_node_lookup(struct puffs_usermount *pu, puffs_cookie_t opc,
        struct puffs_newinfo *pni, const struct puffs_cn *pcn)
{
        struct portal_node *portn;

        assert(opc == PORTAL_ROOT);

        if (pcn->pcn_nameiop != NAMEI_LOOKUP
            && pcn->pcn_nameiop != NAMEI_CREATE)
                return EOPNOTSUPP;

        portn = makenode(pcn->pcn_name);
        puffs_newinfo_setcookie(pni, portn);
        puffs_newinfo_setvtype(pni, VREG);

        pcn->pcn_flags &= ~NAMEI_REQUIREDIR;
        pcn->pcn_consume = strlen(pcn->pcn_name) - pcn->pcn_namelen;

        return 0;
}

unsigned int fakeid = 3;

/* XXX: libpuffs'ize */
int
portal_node_getattr(struct puffs_usermount *pu, puffs_cookie_t opc,
        struct vattr *va, const struct puffs_cred *pcr)
{
        struct timeval tv;
        struct timespec ts;
        int res = 0;

        puffs_vattr_null(va);
        if (opc == PORTAL_ROOT) {
                va->va_type = VDIR;
                va->va_mode = 0777;
                va->va_nlink = 2;
                va->va_uid = va->va_gid = 0;
#if 0   /* XXX Huh? */
                va->va_fileid = fakeid++;
#else
                va->va_fileid = 2;      /*XXX; ROOTINO*/
#endif
                va->va_size = va->va_bytes = 0;
                va->va_gen = 0;
                va->va_rdev = PUFFS_VNOVAL;
                va->va_blocksize = DEV_BSIZE;

                gettimeofday(&tv, NULL);
                TIMEVAL_TO_TIMESPEC(&tv, &ts);
                va->va_atime = va->va_ctime = va->va_mtime =
                    va->va_birthtime = ts;
        } else {
                /* cheat for now */
                int error;
                int newfd;
                struct stat st;
                struct portal_node *portn = opc;
                struct portal_cred portc;
                char **v = conf_match(&q, portn->path);

                if (v == NULL)
                        return ENOENT;
                if (portn->fd == -1) {
                        credtr(&portc, pcr, 0777);
                        error = provide(pu, portn, &portc, v);
                        if (error)
                                return error;
                        newfd = 1;
                } else
                        newfd = 0;

                if (fstat(portn->fd, &st) == -1)
                        res = errno;
                else {
#if 0
                        va->va_type = S_ISDIR(st.st_mode) ? VDIR : VREG; /* XXX */
#else
                        va->va_type = puffs_mode2vt(st.st_mode);
#endif
                        /* puffs supplies S_IFMT bits later */
                        va->va_mode = (st.st_mode & ~S_IFMT);

                        va->va_nlink = st.st_nlink ? st.st_nlink : 1;
                        va->va_uid = st.st_uid;
                        va->va_gid = st.st_gid;
                        va->va_fileid = st.st_ino ? st.st_ino : fakeid++;
                        va->va_size = va->va_bytes = st.st_size;
                        va->va_gen = 0;
                        va->va_rdev = st.st_rdev;
                        va->va_blocksize = st.st_blksize;
                        va->va_atime = st.st_atim;
                        va->va_ctime = st.st_ctim;
                        va->va_mtime = st.st_mtim;
                        va->va_birthtime = st.st_birthtim;
                }
                if (newfd) {
                        puffs_framev_removefd(pu, portn->fd, 0);
                        close(portn->fd);
                        portn->fd = -1;
                }
        }

        return res;
}

/* for writing, just pretend we care */
int
portal_node_setattr(struct puffs_usermount *pu, puffs_cookie_t opc,
        const struct vattr *va, const struct puffs_cred *pcr)
{

        return 0;
}

int
portal_node_open(struct puffs_usermount *pu, puffs_cookie_t opc, int mode,
        const struct puffs_cred *pcr)
{
        struct portal_node *portn = opc;
        struct portal_cred portc;
        char **v;

        if (opc == PORTAL_ROOT)
                return 0;

        if (mode & O_NONBLOCK)
                return EOPNOTSUPP;

        v = conf_match(&q, portn->path);
        if (v == NULL)
                return ENOENT;

        credtr(&portc, pcr, mode);
        return provide(pu, portn, &portc, v);
}

int
portal_node_read(struct puffs_usermount *pu, puffs_cookie_t opc,
        uint8_t *buf, off_t offset, size_t *resid,
        const struct puffs_cred *pcr, int ioflag)
{
        struct puffs_cc *pcc = puffs_cc_getcc(pu);
        struct portal_node *portn = opc;
        struct puffs_framebuf *pufbuf;
        size_t xfersize, winsize, boff;
        void *win;
        int rv, error;
        int data, dummy;

        assert(opc != PORTAL_ROOT);
        error = 0;

        /* if we can't (re-)enable it, treat it as EOF */
        rv = puffs_framev_enablefd(pu, portn->fd, PUFFS_FBIO_READ);
        if (rv == -1)
                return 0;

        pufbuf = puffs_framebuf_make();
        data = PUFBUF_DATA;
        puffs_framebuf_putdata(pufbuf, &data, sizeof(int));
        puffs_framebuf_putdata(pufbuf, resid, sizeof(size_t));

        /* if we are doing nodelay, do read directly */
        if (ioflag & PUFFS_IO_NDELAY) {
                rv = readdata(pufbuf, portn->fd, &dummy);
                if (rv != 0) {
                        error = rv;
                        goto out;
                }
        } else {
                rv = puffs_framev_enqueue_directreceive(pcc,
                    portn->fd, pufbuf, 0);

                if (rv == -1) {
                        error = errno;
                        goto out;
                }
        }

        xfersize = puffs_framebuf_tellsize(pufbuf) - METADATASIZE;
        if (xfersize == 0) {
                assert(ioflag & PUFFS_IO_NDELAY);
                error = EAGAIN;
                goto out;
        }

        *resid -= xfersize;
        boff = 0;
        while (xfersize > 0) {
                winsize = xfersize;
                rv = puffs_framebuf_getwindow(pufbuf, METADATASIZE,
                    &win, &winsize);
                assert(rv == 0);
                assert(winsize > 0);

                memcpy(buf + boff, win, winsize);
                xfersize -= winsize;
                boff += winsize;
        }

 out:
        puffs_framev_disablefd(pu, portn->fd, PUFFS_FBIO_READ);
        puffs_framebuf_destroy(pufbuf);

        /* a trickery, from readdata() */
        if (error == -1)
                return 0;
        return error;
}

int
portal_node_write(struct puffs_usermount *pu, puffs_cookie_t opc,
        uint8_t *buf, off_t offset, size_t *resid,
        const struct puffs_cred *pcr, int ioflag)
{
        struct puffs_cc *pcc = puffs_cc_getcc(pu);
        struct portal_node *portn = opc;
        struct puffs_framebuf *pufbuf;
        size_t written;
        int error, rv, dummy;

        assert(opc != PORTAL_ROOT);

        pufbuf = puffs_framebuf_make();
        puffs_framebuf_putdata(pufbuf, buf, *resid);

        error = 0;
        if (ioflag & PUFFS_IO_NDELAY) {
                rv = portal_frame_wf(pu, pufbuf, portn->fd, &dummy);
                if (rv) {
                        error = rv;
                        goto out;
                }
        } else  {
                rv = puffs_framev_enqueue_directsend(pcc, portn->fd, pufbuf, 0);
                if (rv == -1) {
                        error = errno;
                        goto out;
                }
        }

        rv = puffs_framebuf_getdata_atoff(pufbuf, 0, &written, sizeof(size_t));
        assert(rv == 0);
        assert(written <= *resid);
        *resid -= written;

 out:
        puffs_framebuf_destroy(pufbuf);
        if (error == -1)
                error = 0;
        return 0;
}

int
portal_node_seek(struct puffs_usermount *pu, puffs_cookie_t opc,
        off_t oldoff, off_t newoff, const struct puffs_cred *pcr)
{
        struct portal_node *portn = opc;

        if (opc == PORTAL_ROOT || portn->fd == -1)
                return EOPNOTSUPP;

        if (lseek(portn->fd, newoff, SEEK_SET) == -1)
                return errno;
        return 0;
}

int
portal_node_poll(struct puffs_usermount *pu, puffs_cookie_t opc, int *events)
{
        struct puffs_cc *pcc = puffs_cc_getcc(pu);
        struct portal_node *portn = opc;
        int what;

        what = 0;
        if (*events & POLLIN)
                what |= PUFFS_FBIO_READ;
        if (*events & POLLOUT)
                what |= PUFFS_FBIO_WRITE;
        if (*events & POLLERR)
                what |= PUFFS_FBIO_ERROR;

        if (puffs_framev_enqueue_waitevent(pcc, portn->fd, &what) == -1) {
                *events = POLLERR;
                return errno;
        }

        *events = 0;
        if (what & PUFFS_FBIO_READ)
                *events |= POLLIN;
        if (what & PUFFS_FBIO_WRITE)
                *events |= POLLOUT;
        if (what & PUFFS_FBIO_ERROR)
                *events |= POLLERR;

        return 0;
}

int
portal_node_inactive(struct puffs_usermount *pu, puffs_cookie_t opc)
{

        if (opc == PORTAL_ROOT)
                return 0;

        puffs_setback(puffs_cc_getcc(pu), PUFFS_SETBACK_NOREF_N1);
        return 0;
}

int
portal_node_reclaim(struct puffs_usermount *pu, puffs_cookie_t opc)
{
        struct portal_node *portn = opc;

        if (portn->fd != -1) {
                puffs_framev_removefd(pu, portn->fd, 0);
                close(portn->fd);
        }
        free(portn->path);
        free(portn);

        return 0;
}