root/usr/src/lib/libproc/common/proc_fd.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 2020 OmniOS Community Edition (OmniOSce) Association.
 */

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

#include <libgen.h>
#include <limits.h>
#include <alloca.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <dirent.h>

#include "Pcontrol.h"

/*
 * Walk all file descriptors open for a process and call func() for each.
 */
int
proc_fdwalk(pid_t pid, proc_fdwalk_f *func, void *arg)
{
        struct dirent *dirent;
        DIR *fddir;
        char *dir;
        int ret = 0;

        if (asprintf(&dir, "%s/%d/fd", procfs_path, (int)pid) == -1)
                return (-1);

        if ((fddir = opendir(dir)) == NULL) {
                free(dir);
                return (-1);
        }

        free(dir);

        while ((dirent = readdir(fddir)) != NULL) {
                prfdinfo_t *info;
                char *errptr;
                int fd;

                if (!isdigit(dirent->d_name[0]))
                        continue;

                fd = (int)strtol(dirent->d_name, &errptr, 10);
                if (errptr != NULL && *errptr != '\0')
                        continue;

                if ((info = proc_get_fdinfo(pid, fd)) == NULL)
                        continue;

                ret = func(info, arg);

                free(info);

                if (ret != 0)
                        break;
        }

        (void) closedir(fddir);
        return (ret);
}

int
proc_fdinfowalk(const prfdinfo_t *info, proc_fdinfowalk_f *func, void *arg)
{
        off_t off = offsetof(prfdinfo_t, pr_misc);
        int ret = 0;

        for (;;) {
                const pr_misc_header_t *misc;
                uint_t type;
                size_t size;

                misc = (pr_misc_header_t *)((uint8_t *)info + off);

                /* Found terminating record */
                if (misc->pr_misc_size == 0)
                        break;

                off += misc->pr_misc_size;

                type = misc->pr_misc_type;
                size = misc->pr_misc_size - sizeof (pr_misc_header_t);
                misc++;

                ret = func(type, misc, size, arg);

                if (ret != 0)
                        break;
        }

        return (ret);
}

prfdinfo_t *
proc_get_fdinfo(pid_t pid, int fd)
{
        prfdinfo_t *info = NULL;
        char *fname;
        uint_t retries;
        int ifd, err = EIO;

        if (asprintf(&fname, "%s/%d/fdinfo/%d",
            procfs_path, (int)pid, fd) == -1) {
                return (NULL);
        }

        if ((ifd = open(fname, O_RDONLY)) == -1) {
                free(fname);
                return (NULL);
        }

        free(fname);

        /*
         * There is a race between stat()-ing the file and reading from
         * it where the size may change. To protect against that, we
         * walk the returned data to ensure that it is properly
         * terminated. If not, increase the buffer size and try again.
         */

        for (retries = 1; retries < 5; retries++) {
                struct stat st;
                off_t off;
                size_t l;

                if (fstat(ifd, &st) == -1) {
                        err = errno;
                        break;
                }

                st.st_size *= retries;

                if ((info = reallocf(info, st.st_size)) == NULL) {
                        err = errno;
                        break;
                }

                if (lseek(ifd, 0, SEEK_SET) != 0 ||
                    (l = read(ifd, info, st.st_size)) == -1) {
                        err = errno;
                        break;
                }

                /* Walk the data to check that is properly terminated. */

                off = offsetof(prfdinfo_t, pr_misc);

                if (l < off + sizeof (pr_misc_header_t))
                        continue;

                while (off <= l - sizeof (pr_misc_header_t)) {
                        pr_misc_header_t *misc;

                        misc = (pr_misc_header_t *)((uint8_t *)info + off);

                        if (misc->pr_misc_size == 0) {
                                /* Found terminator record */
                                (void) close(ifd);
                                return (info);
                        }

                        /* Next record */
                        off += misc->pr_misc_size;
                }
        }

        (void) close(ifd);
        free(info);

        errno = err;

        return (NULL);
}

typedef struct proc_fdinfo_misc_cbdata {
        uint_t type;
        const void *data;
        size_t len;
} pfm_data_t;

static int
proc_fdinfo_misc_cb(uint_t type, const void *data, size_t len, void *datap)
{
        pfm_data_t *cb = (pfm_data_t *)datap;

        if (type == cb->type) {
                cb->data = data;
                cb->len = len;
                return (1);
        }
        return (0);
}

const void *
proc_fdinfo_misc(const prfdinfo_t *info, uint_t type, size_t *buflen)
{
        pfm_data_t cb;

        cb.data = NULL;
        cb.type = type;

        (void) proc_fdinfowalk(info, proc_fdinfo_misc_cb, (void *)&cb);

        if (cb.data != NULL) {
                if (buflen != NULL)
                        *buflen = cb.len;

                return (cb.data);
        }

        return (NULL);
}

static int
proc_fdinfo_dup_cb(uint_t type, const void *data, size_t len, void *datap)
{
        size_t *sz = (size_t *)datap;

        *sz += len + sizeof (pr_misc_header_t);
        return (0);
}


prfdinfo_t *
proc_fdinfo_dup(const prfdinfo_t *old)
{
        prfdinfo_t *new;
        size_t sz = offsetof(prfdinfo_t, pr_misc);

        /* Determine the size of the miscellaneous items */
        (void) proc_fdinfowalk(old, proc_fdinfo_dup_cb, (void *)&sz);

        /* Add the size of the terminator record */
        sz += sizeof (pr_misc_header_t);

        if ((new = calloc(1, sz)) == NULL)
                return (NULL);

        bcopy(old, new, sz);

        return (new);
}

void
proc_fdinfo_free(prfdinfo_t *info)
{
        free(info);
}

/*
 * Convert a prfdinfo_core_t to prfdinfo_t
 */
int
proc_fdinfo_from_core(const prfdinfo_core_t *core, prfdinfo_t **infop)
{
        prfdinfo_t *info;
        size_t len, slen = 0;

        len = offsetof(prfdinfo_t, pr_misc) + sizeof (pr_misc_header_t);
        if (*core->pr_path != '\0') {
                slen = strlen(core->pr_path) + 1;
                len += PRFDINFO_ROUNDUP(slen) + sizeof (pr_misc_header_t);
        }

        if ((info = calloc(1, len)) == NULL)
                return (-1);

        *infop = info;

        info->pr_fd = core->pr_fd;
        info->pr_mode = core->pr_mode;
        info->pr_uid = core->pr_uid;
        info->pr_gid = core->pr_gid;
        info->pr_major = core->pr_major;
        info->pr_minor = core->pr_minor;
        info->pr_rmajor = core->pr_rmajor;
        info->pr_rminor = core->pr_rminor;
        info->pr_size = core->pr_size;
        info->pr_ino = core->pr_ino;
        info->pr_fileflags = core->pr_fileflags;
        info->pr_fdflags = core->pr_fdflags;
        info->pr_offset = core->pr_offset;

        if (slen != 0) {
                pr_misc_header_t *misc;

                misc = (pr_misc_header_t *)&info->pr_misc;

                misc->pr_misc_size = sizeof (*misc) + PRFDINFO_ROUNDUP(slen);
                misc->pr_misc_type = PR_PATHNAME;
                misc++;
                bcopy(core->pr_path, misc, slen);
        }

        return (0);
}

/*
 * Convert a prfdinfo_t to prfdinfo_core_t
 */
int
proc_fdinfo_to_core(const prfdinfo_t *info, prfdinfo_core_t *core)
{
        const char *path;
        size_t pathl;

        bzero(core, sizeof (*core));

        core->pr_fd = info->pr_fd;
        core->pr_mode = info->pr_mode;
        core->pr_uid = info->pr_uid;
        core->pr_gid = info->pr_gid;
        core->pr_major = info->pr_major;
        core->pr_minor = info->pr_minor;
        core->pr_rmajor = info->pr_rmajor;
        core->pr_rminor = info->pr_rminor;
        core->pr_size = info->pr_size;
        core->pr_ino = info->pr_ino;
        core->pr_fileflags = info->pr_fileflags;
        core->pr_fdflags = info->pr_fdflags;
        core->pr_offset = info->pr_offset;

        path = proc_fdinfo_misc(info, PR_PATHNAME, &pathl);
        if (path != NULL) {
                /*
                 * Rather than provide a truncated path in the pr_path field
                 * just leave it empty if the path will not fit.
                 */
                if (pathl <= sizeof (core->pr_path) - 1)
                        bcopy(path, core->pr_path, pathl + 1);
        }

        return (0);
}