/*
 * COPYRIGHT (c) International Business Machines Corp. 2012-2017
 *
 * This program is provided under the terms of the Common Public License,
 * version 1.0 (CPL-1.0). Any use, reproduction or distribution for this
 * software constitutes recipient's acceptance of CPL-1.0 terms which can be
 * found in the file LICENSE file or at
 * https://opensource.org/licenses/cpl1.0.php
 */

/*
 * OpenCryptoki ICSF token - Shared memory abstraction for OpenCryptoki
 *
 * Author: Marcelo Cerri (mhcerri@br.ibm.com)
 *
 * Note: the functions in this files are implemented as an abstraction layer for
 *    POSIX shared memory functions but they can be extended to support other
 *    APIs of shared memory.
 */

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <syslog.h>
#include <grp.h>

/* For logging functions: */
#include "pkcs11types.h"
#include "defs.h"
#include "host_defs.h"
#include "h_extern.h"
#include "trace.h"
#include "ock_syslog.h"

#include "shared_memory.h"

/*
 * Helper macros for logging.
 */
#define SYS_ERROR(_errno, _msg, ...)     \
 do {        \
  char _sys_error[1024];     \
  if (strerror_r(_errno, _sys_error, sizeof(_sys_error))) \
   strcpy(_sys_error, "Unknown error");  \
  syslog(LOG_ERR, "Error: " _msg " %s (errno=%d)", \
   __VA_ARGS__, _sys_error, _errno);  \
  TRACE_ERROR("Error: " _msg " %s (errno=%d)",  \
   __VA_ARGS__, _sys_error, _errno);  \
 } while (0)

/*
 * Shared context used to keep meta data into the shared memory.
 *
 * The address to data field is the address visible outside this file.
 *
 * See shm_open for more details.
 */
struct shm_context {
    /*
     * `ref` is a counter that is used to detect mismatching between
     * sm_open and sm_close calls.
     */
    int ref;

    /*
     * `name` is the shared memory identifier and it is used to destroy
     * an allocated shared memory region.
     */
    char name[SM_NAME_LEN + 1];

    /*
     * `data_len` is the length of the variable length filed `data`.
     */
    int data_len;

    /*
     * `data` points to the region that will actually be used by
     * `sm_open`s caller.
     */
    char data[];
};

/*
 * Obtain the context pointer based on a data address.
 */
static inline struct shm_context *get_shm_context(void *addr)
{
    struct shm_context *ctx;

    ctx = (struct shm_context *)((unsigned char *)addr
                                 - offsetof(struct shm_context, data));
    return ctx;
}

/*
 * If a complete path is informed, convert it to a shared memory name.
 */
static char *convert_path_to_shm_name(const char *file_path)
{
    char *name = NULL;
    size_t len = strlen(file_path) + 1;
    int i;
    char *it;

    /* Need a starting '/' */
    if (file_path[0] != '/')
        len++;

    if (len > SM_NAME_LEN) {
        TRACE_ERROR("Error: path \"%s\" too long.\n", file_path);
        return NULL;
    }

    it = name = malloc(len + 1);
    if (name == NULL) {
        TRACE_ERROR("Error: failed to allocate memory for "
                    "path \"%s\".\n", file_path);
        return NULL;
    }

    i = 0;
    *it++ = '/';
    if (file_path[0] == '/')
        i++;

    for (; file_path[i]; i++, it++) {
        if (file_path[i] == '/')
            *it = '.';
        else
            *it = file_path[i];
    }
    *it = '\0';

    TRACE_DEVEL("File path \"%s\" converted to \"%s\".\n", file_path, name);

    return name;
}

/*
 * Open a shared memory region identified by `sm_name` using permissions defined
 * by `mode` with length `len`.
 *
 * If the shared memory already exists and doesn't match the given length an
 * error is returned (if `force` is zero) or the shared memory is reinitialized
 * (if `force` is non zero).
 */
int sm_open(const char *sm_name, int mode, void **p_addr, size_t len, int force)
{
    int rc;
    int fd = -1;
    void *addr = NULL;
    struct stat stat_buf;
    char *name = NULL;
    struct shm_context *ctx = NULL;
    size_t real_len = sizeof(*ctx) + len;
    int created = 0;
    struct group *grp;

    /*
     * This is used for portability purpose. Please check `shm_open`
     * man page for more details.
     */
    if ((name = convert_path_to_shm_name(sm_name)) == NULL) {
        rc = -EINVAL;
        goto done;
    }

    grp = getgrnam("pkcs11");
    if (!grp) {
        rc = -errno;
        SYS_ERROR(errno, "getgrname(\"pkcs11\"): %s\n",
                strerror(errno));
        goto done;
    }
    
    /* try and open first... */
    fd = shm_open(name, O_RDWR, mode);
    if (fd < 0) {
        /* maybe it needs to be created ... */
        fd = shm_open(name, O_RDWR | O_CREAT, mode);
        if (fd < 0) {
            rc = -errno;
            SYS_ERROR(errno, "Failed to open shared memory \"%s\".\n", name);
            goto done;
        } else {
            /* umask may have altered permissions if we created
             * the shared memory in above call, so set proper
             * permissions just in case.
             */
            if (fchmod(fd, mode) == -1) {
                rc = -errno;
                SYS_ERROR(errno, "fchmod(%s): %s\n", name, strerror(errno));
                goto done;
            }
            if (fchown(fd, -1, grp->gr_gid) != 0) {
                rc = -errno;
                SYS_ERROR(errno, "fchown of token shm segment: %s\n",
                          strerror(errno));
                goto done;
            }
        }
    }

    /*
     * fstat is used here to check if the shared memory region already
     * exists. When a shared memory region is first created, its size is
     * always zero.
     */
    if (fstat(fd, &stat_buf)) {
        rc = -errno;
        SYS_ERROR(errno, "Cannot stat \"%s\".\n", name);
        goto done;
    }

    /*
     * If the shared memory segment does not belong to the pkcs11 group or does
     * not have correct permissions, do not use it.
     */
    if (stat_buf.st_gid != grp->gr_gid ||
        (stat_buf.st_mode & ~S_IFMT) != (unsigned int)mode) {
        TRACE_ERROR("SHM segment has wrong gid/mode combination (expected: %u/0%o; got: %u/0%o)\n",
                    grp->gr_gid, mode, stat_buf.st_gid, stat_buf.st_mode);
        OCK_SYSLOG(LOG_ERR,
                   "SHM segment has wrong gid/mode combination (expected: %u/0%o; got: %u/0%o)\n",
                   grp->gr_gid, mode, stat_buf.st_gid, stat_buf.st_mode);
        rc = -EINVAL;
        goto done;
    }

    /*
     * The shared memory needs to be extended when created (when its length
     * is zero). When its length is not zero and is not equal to the
     * expected size, an error is returned if `force` is not set. If `force`
     * is set, the existing shared memory is truncated and any data on it is
     * lost.
     */
    if (stat_buf.st_size == 0
        || (force && (size_t)stat_buf.st_size != real_len)) {
        /*
         * If the shared memory region was just created, it's necessary
         * to extend it to the expected size using ftruncate.
         *
         * It's important to notice that it is resized to a length
         * greater than the value requested (`len`). The extra space is
         * used to store additional information related to the shared
         * memory, such as its size and identifier.
         */
        created = 1;
        TRACE_DEVEL("Truncating \"%s\".\n", name);
        if (ftruncate(fd, real_len) < 0) {
            rc = -errno;
            SYS_ERROR(errno, "Cannot truncate \"%s\".\n", name);
            goto done;
        }
    } else if ((size_t)stat_buf.st_size != real_len) {
        int ref;

        /* get ref count */
        addr = mmap(NULL, sizeof(*ctx), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr == NULL) {
            rc = -errno;
            SYS_ERROR(errno, "Failed to map \"%s\" to memory.\n", name);
            goto done;
        }
        ctx = addr;
        ref = ctx->ref;
        if (munmap(addr, sizeof(*ctx))) {
            rc = -errno;
            SYS_ERROR(errno, "Failed to unmap \"%s\" (%p).\n", name, (void *)ctx);
            goto done;;
        }

        /*
         * Too big real_len indicates the new token data format is used.
         * If no application is attached to the shm (ref==1) it can be
         * safely expanded/recreated. Otherwise, fail.
         */
        if (ref <= 1 && real_len > (size_t)stat_buf.st_size) {
            created = 1;
            TRACE_DEVEL("Truncating \"%s\".\n", name);
            if (ftruncate(fd, real_len) < 0) {
                rc = -errno;
                SYS_ERROR(errno, "Cannot truncate \"%s\".\n", name);
                goto done;
            }
        } else {
            rc = -1;
            TRACE_ERROR("Error: shared memory \"%s\" exists and does not "
                        "match the expected size.\n", name);
            goto done;
        }
    }

    addr = mmap(NULL, real_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == NULL) {
        rc = -errno;
        SYS_ERROR(errno, "Failed to map \"%s\" to memory.\n", name);
        goto done;
    }

    /*
     * `addr` points to the start of the shared memory region. The first
     * bytes of it are used to store the additional information about the
     * shared memory allocated. This info can be accessed using an pointer
     * to a `shm_context` structure.
     */
    ctx = addr;
    if (created) {
        strncpy(ctx->name, name, SM_NAME_LEN);
        ctx->name[SM_NAME_LEN] = '\0';
        ctx->data_len = len;
        memset(ctx->data, 0, ctx->data_len);
        ctx->ref = 0;
    }
    ctx->ref += 1;

    /*
     * The portion of the shared memory that will actually be used by the
     * called is pointed by `data`.
     *
     * The address returned (in this case `ctx->data`) is equal to
     * `addr + sizeof(struct shm_context)`, which means that the additional
     * data is skipped off.
     */
    *p_addr = ctx->data;
    rc = created ? 0 : 1;
    if (sm_sync(ctx->data)) {
        rc = -errno;
        SYS_ERROR(errno, "Failed to sync shared memory \"%s\".\n", name);
        if (created)
            sm_close(addr, 1, 0);
        goto done;
    }
    TRACE_DEVEL("open: ref = %d\n", ctx->ref);

done:
    if (fd >= 0)
        close(fd);
    if (name)
        free(name);

    return rc;
}

/*
 * Close (unmap) a shared memory region. `destroy` indicates if the shared
 * memory should be destroyed if no other processes are using it.
 * 'ignore_ref_count' indicates that the reference count in the shared memory
 * should not be decremented (used during termination after a fork).
 */
int sm_close(void *addr, int destroy, int ignore_ref_count)
{
    int rc;
    int ref;
    char name[SM_NAME_LEN + 1] = { 0, };
    struct shm_context *ctx = get_shm_context(addr);

    if (ctx->ref <= 0) {
        TRACE_ERROR("Error: invalid shared memory address %p (ref=%d).\n",
                    addr, ctx->ref);
        return -EINVAL;
    }

    if (!ignore_ref_count)
        ctx->ref--;
    ref = ctx->ref;

    TRACE_DEVEL("close: ref = %d\n", ref);
    if (ref == 0 && destroy) {
        memcpy(name, ctx->name, SM_NAME_LEN);
        name[SM_NAME_LEN] = '\0';
    }

    if (munmap(ctx, sizeof(*ctx) + ctx->data_len)) {
        rc = -errno;
        SYS_ERROR(errno, "Failed to unmap \"%s\" (%p).\n", name, (void *)ctx);
        return rc;
    }

    if (ref == 0 && destroy) {
        TRACE_DEVEL("Deleting shared memory \"%s\".\n", name);
        if ((rc = sm_destroy(name)) != 0)
            return rc;
    }

    return 0;
}

/*
 * Destroy a shared memory region.
 */
int sm_destroy(const char *name)
{
    int rc;

    if (shm_unlink(name)) {
        rc = -errno;
        SYS_ERROR(errno, "Failed to delete shared memory \"%s\".\n", name);
        return rc;
    }

    return 0;
}

/*
 * Force sync for a shared memory region.
 */
int sm_sync(void *addr)
{
    struct shm_context *ctx = get_shm_context(addr);

    if (ctx->ref <= 0) {
        TRACE_ERROR("Error: invalid shared memory address %p (ref=%d).\n",
                    addr, ctx->ref);
        return -EINVAL;
    }

    return msync(ctx, ctx->data_len, MS_SYNC);
}

/*
 * Get the name of the shared memory indicated by `addr` and copy it to the
 * given `buffer`.
 */
int sm_copy_name(void *addr, char *buffer, size_t len)
{
    size_t name_len;
    struct shm_context *ctx = get_shm_context(addr);

    if (ctx->ref <= 0) {
        TRACE_ERROR("Error: invalid shared memory address %p (ref=%d).\n",
                    addr, ctx->ref);
        return -EINVAL;
    }

    name_len = strlen(ctx->name);
    if (len <= name_len)
        return -ENOSPC;

    strcpy(buffer, ctx->name);

    return 0;
}

/*
 * Return the reference count for the given shared memory.
 */
int sm_get_count(void *addr)
{
    struct shm_context *ctx = get_shm_context(addr);

    if (ctx->ref <= 0) {
        TRACE_ERROR("Error: invalid shared memory address %p (ref=%d).\n",
                    addr, ctx->ref);
        return -EINVAL;
    }

    return ctx->ref;
}
