/*
 * QEMU PowerPC pSeries Logical Partition (aka sPAPR) hardware System Emulator
 *
 * PAPR Virtual TPM
 *
 * Copyright (c) 2015, 2017, 2019 IBM Corporation.
 *
 * Authors:
 *    Stefan Berger <stefanb@linux.vnet.ibm.com>
 *
 * This code is licensed under the GPL version 2 or later. See the
 * COPYING file in the top-level directory.
 *
 */

#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "qapi/error.h"
#include "hw/qdev-properties.h"
#include "migration/vmstate.h"

#include "sysemu/tpm_backend.h"
#include "sysemu/tpm_util.h"
#include "tpm_prop.h"

#include "hw/ppc/spapr.h"
#include "hw/ppc/spapr_vio.h"
#include "trace.h"
#include "qom/object.h"

#define DEBUG_SPAPR 0

typedef struct SpaprTpmState SpaprTpmState;
DECLARE_INSTANCE_CHECKER(SpaprTpmState, VIO_SPAPR_VTPM,
                         TYPE_TPM_SPAPR)

typedef struct TpmCrq {
    uint8_t valid;  /* 0x80: cmd; 0xc0: init crq */
                    /* 0x81-0x83: CRQ message response */
    uint8_t msg;    /* see below */
    uint16_t len;   /* len of TPM request; len of TPM response */
    uint32_t data;  /* rtce_dma_handle when sending TPM request */
    uint64_t reserved;
} TpmCrq;

#define SPAPR_VTPM_VALID_INIT_CRQ_COMMAND  0xC0
#define SPAPR_VTPM_VALID_COMMAND           0x80
#define SPAPR_VTPM_MSG_RESULT              0x80

/* msg types for valid = SPAPR_VTPM_VALID_INIT_CRQ */
#define SPAPR_VTPM_INIT_CRQ_RESULT           0x1
#define SPAPR_VTPM_INIT_CRQ_COMPLETE_RESULT  0x2

/* msg types for valid = SPAPR_VTPM_VALID_CMD */
#define SPAPR_VTPM_GET_VERSION               0x1
#define SPAPR_VTPM_TPM_COMMAND               0x2
#define SPAPR_VTPM_GET_RTCE_BUFFER_SIZE      0x3
#define SPAPR_VTPM_PREPARE_TO_SUSPEND        0x4

/* response error messages */
#define SPAPR_VTPM_VTPM_ERROR                0xff

/* error codes */
#define SPAPR_VTPM_ERR_COPY_IN_FAILED        0x3
#define SPAPR_VTPM_ERR_COPY_OUT_FAILED       0x4

#define TPM_SPAPR_BUFFER_MAX                 4096

struct SpaprTpmState {
    SpaprVioDevice vdev;

    TpmCrq crq; /* track single TPM command */

    uint8_t state;
#define SPAPR_VTPM_STATE_NONE         0
#define SPAPR_VTPM_STATE_EXECUTION    1
#define SPAPR_VTPM_STATE_COMPLETION   2

    unsigned char *buffer;

    uint32_t numbytes; /* number of bytes to deliver on resume */

    TPMBackendCmd cmd;

    TPMBackend *be_driver;
    TPMVersion be_tpm_version;

    size_t be_buffer_size;
};

/*
 * Send a request to the TPM.
 */
static void tpm_spapr_tpm_send(SpaprTpmState *s)
{
    tpm_util_show_buffer(s->buffer, s->be_buffer_size, "To TPM");

    s->state = SPAPR_VTPM_STATE_EXECUTION;
    s->cmd = (TPMBackendCmd) {
        .locty = 0,
        .in = s->buffer,
        .in_len = MIN(tpm_cmd_get_size(s->buffer), s->be_buffer_size),
        .out = s->buffer,
        .out_len = s->be_buffer_size,
    };

    tpm_backend_deliver_request(s->be_driver, &s->cmd);
}

static int tpm_spapr_process_cmd(SpaprTpmState *s, uint64_t dataptr)
{
    long rc;

    /* a max. of be_buffer_size bytes can be transported */
    rc = spapr_vio_dma_read(&s->vdev, dataptr,
                            s->buffer, s->be_buffer_size);
    if (rc) {
        error_report("tpm_spapr_got_payload: DMA read failure");
    }
    /* let vTPM handle any malformed request */
    tpm_spapr_tpm_send(s);

    return rc;
}

static inline int spapr_tpm_send_crq(struct SpaprVioDevice *dev, TpmCrq *crq)
{
    return spapr_vio_send_crq(dev, (uint8_t *)crq);
}

static int tpm_spapr_do_crq(struct SpaprVioDevice *dev, uint8_t *crq_data)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(dev);
    TpmCrq local_crq;
    TpmCrq *crq = &s->crq; /* requests only */
    int rc;
    uint8_t valid = crq_data[0];
    uint8_t msg = crq_data[1];

    trace_tpm_spapr_do_crq(valid, msg);

    switch (valid) {
    case SPAPR_VTPM_VALID_INIT_CRQ_COMMAND: /* Init command/response */

        /* Respond to initialization request */
        switch (msg) {
        case SPAPR_VTPM_INIT_CRQ_RESULT:
            trace_tpm_spapr_do_crq_crq_result();
            memset(&local_crq, 0, sizeof(local_crq));
            local_crq.valid = SPAPR_VTPM_VALID_INIT_CRQ_COMMAND;
            local_crq.msg = SPAPR_VTPM_INIT_CRQ_RESULT;
            spapr_tpm_send_crq(dev, &local_crq);
            break;

        case SPAPR_VTPM_INIT_CRQ_COMPLETE_RESULT:
            trace_tpm_spapr_do_crq_crq_complete_result();
            memset(&local_crq, 0, sizeof(local_crq));
            local_crq.valid = SPAPR_VTPM_VALID_INIT_CRQ_COMMAND;
            local_crq.msg = SPAPR_VTPM_INIT_CRQ_COMPLETE_RESULT;
            spapr_tpm_send_crq(dev, &local_crq);
            break;
        }

        break;
    case SPAPR_VTPM_VALID_COMMAND: /* Payloads */
        switch (msg) {
        case SPAPR_VTPM_TPM_COMMAND:
            trace_tpm_spapr_do_crq_tpm_command();
            if (s->state == SPAPR_VTPM_STATE_EXECUTION) {
                return H_BUSY;
            }
            memcpy(crq, crq_data, sizeof(*crq));

            rc = tpm_spapr_process_cmd(s, be32_to_cpu(crq->data));

            if (rc == H_SUCCESS) {
                crq->valid = be16_to_cpu(0);
            } else {
                local_crq.valid = SPAPR_VTPM_MSG_RESULT;
                local_crq.msg = SPAPR_VTPM_VTPM_ERROR;
                local_crq.len = cpu_to_be16(0);
                local_crq.data = cpu_to_be32(SPAPR_VTPM_ERR_COPY_IN_FAILED);
                spapr_tpm_send_crq(dev, &local_crq);
            }
            break;

        case SPAPR_VTPM_GET_RTCE_BUFFER_SIZE:
            trace_tpm_spapr_do_crq_tpm_get_rtce_buffer_size(s->be_buffer_size);
            local_crq.valid = SPAPR_VTPM_VALID_COMMAND;
            local_crq.msg = SPAPR_VTPM_GET_RTCE_BUFFER_SIZE |
                            SPAPR_VTPM_MSG_RESULT;
            local_crq.len = cpu_to_be16(s->be_buffer_size);
            spapr_tpm_send_crq(dev, &local_crq);
            break;

        case SPAPR_VTPM_GET_VERSION:
            local_crq.valid = SPAPR_VTPM_VALID_COMMAND;
            local_crq.msg = SPAPR_VTPM_GET_VERSION | SPAPR_VTPM_MSG_RESULT;
            local_crq.len = cpu_to_be16(0);
            switch (s->be_tpm_version) {
            case TPM_VERSION_1_2:
                local_crq.data = cpu_to_be32(1);
                break;
            case TPM_VERSION_2_0:
                local_crq.data = cpu_to_be32(2);
                break;
            default:
                g_assert_not_reached();
                break;
            }
            trace_tpm_spapr_do_crq_get_version(be32_to_cpu(local_crq.data));
            spapr_tpm_send_crq(dev, &local_crq);
            break;

        case SPAPR_VTPM_PREPARE_TO_SUSPEND:
            trace_tpm_spapr_do_crq_prepare_to_suspend();
            local_crq.valid = SPAPR_VTPM_VALID_COMMAND;
            local_crq.msg = SPAPR_VTPM_PREPARE_TO_SUSPEND |
                            SPAPR_VTPM_MSG_RESULT;
            spapr_tpm_send_crq(dev, &local_crq);
            break;

        default:
            trace_tpm_spapr_do_crq_unknown_msg_type(crq->msg);
        }
        break;
    default:
        trace_tpm_spapr_do_crq_unknown_crq(valid, msg);
    };

    return H_SUCCESS;
}

static void tpm_spapr_request_completed(TPMIf *ti, int ret)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(ti);
    TpmCrq *crq = &s->crq;
    uint32_t len;
    int rc;

    s->state = SPAPR_VTPM_STATE_COMPLETION;

    /* a max. of be_buffer_size bytes can be transported */
    len = MIN(tpm_cmd_get_size(s->buffer), s->be_buffer_size);

    if (runstate_check(RUN_STATE_FINISH_MIGRATE)) {
        trace_tpm_spapr_caught_response(len);
        /* defer delivery of response until .post_load */
        s->numbytes = len;
        return;
    }

    rc = spapr_vio_dma_write(&s->vdev, be32_to_cpu(crq->data),
                             s->buffer, len);

    tpm_util_show_buffer(s->buffer, len, "From TPM");

    crq->valid = SPAPR_VTPM_MSG_RESULT;
    if (rc == H_SUCCESS) {
        crq->msg = SPAPR_VTPM_TPM_COMMAND | SPAPR_VTPM_MSG_RESULT;
        crq->len = cpu_to_be16(len);
    } else {
        error_report("%s: DMA write failure", __func__);
        crq->msg = SPAPR_VTPM_VTPM_ERROR;
        crq->len = cpu_to_be16(0);
        crq->data = cpu_to_be32(SPAPR_VTPM_ERR_COPY_OUT_FAILED);
    }

    rc = spapr_tpm_send_crq(&s->vdev, crq);
    if (rc) {
        error_report("%s: Error sending response", __func__);
    }
}

static int tpm_spapr_do_startup_tpm(SpaprTpmState *s, size_t buffersize)
{
    return tpm_backend_startup_tpm(s->be_driver, buffersize);
}

static const char *tpm_spapr_get_dt_compatible(SpaprVioDevice *dev)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(dev);

    switch (s->be_tpm_version) {
    case TPM_VERSION_1_2:
        return "IBM,vtpm";
    case TPM_VERSION_2_0:
        return "IBM,vtpm20";
    default:
        g_assert_not_reached();
    }
}

static void tpm_spapr_reset(SpaprVioDevice *dev)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(dev);

    s->state = SPAPR_VTPM_STATE_NONE;
    s->numbytes = 0;

    s->be_tpm_version = tpm_backend_get_tpm_version(s->be_driver);

    s->be_buffer_size = MIN(tpm_backend_get_buffer_size(s->be_driver),
                            TPM_SPAPR_BUFFER_MAX);

    tpm_backend_reset(s->be_driver);

    if (tpm_spapr_do_startup_tpm(s, s->be_buffer_size) < 0) {
        exit(1);
    }
}

static enum TPMVersion tpm_spapr_get_version(TPMIf *ti)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(ti);

    if (tpm_backend_had_startup_error(s->be_driver)) {
        return TPM_VERSION_UNSPEC;
    }

    return tpm_backend_get_tpm_version(s->be_driver);
}

/* persistent state handling */

static int tpm_spapr_pre_save(void *opaque)
{
    SpaprTpmState *s = opaque;

    tpm_backend_finish_sync(s->be_driver);
    /*
     * we cannot deliver the results to the VM since DMA would touch VM memory
     */

    return 0;
}

static int tpm_spapr_post_load(void *opaque, int version_id)
{
    SpaprTpmState *s = opaque;

    if (s->numbytes) {
        trace_tpm_spapr_post_load();
        /* deliver the results to the VM via DMA */
        tpm_spapr_request_completed(TPM_IF(s), 0);
        s->numbytes = 0;
    }

    return 0;
}

static const VMStateDescription vmstate_spapr_vtpm = {
    .name = "tpm-spapr",
    .pre_save = tpm_spapr_pre_save,
    .post_load = tpm_spapr_post_load,
    .fields = (VMStateField[]) {
        VMSTATE_SPAPR_VIO(vdev, SpaprTpmState),

        VMSTATE_UINT8(state, SpaprTpmState),
        VMSTATE_UINT32(numbytes, SpaprTpmState),
        VMSTATE_VBUFFER_UINT32(buffer, SpaprTpmState, 0, NULL, numbytes),
        /* remember DMA address */
        VMSTATE_UINT32(crq.data, SpaprTpmState),
        VMSTATE_END_OF_LIST(),
    }
};

static Property tpm_spapr_properties[] = {
    DEFINE_SPAPR_PROPERTIES(SpaprTpmState, vdev),
    DEFINE_PROP_TPMBE("tpmdev", SpaprTpmState, be_driver),
    DEFINE_PROP_END_OF_LIST(),
};

static void tpm_spapr_realizefn(SpaprVioDevice *dev, Error **errp)
{
    SpaprTpmState *s = VIO_SPAPR_VTPM(dev);

    if (!tpm_find()) {
        error_setg(errp, "at most one TPM device is permitted");
        return;
    }

    dev->crq.SendFunc = tpm_spapr_do_crq;

    if (!s->be_driver) {
        error_setg(errp, "'tpmdev' property is required");
        return;
    }
    s->buffer = g_malloc(TPM_SPAPR_BUFFER_MAX);
}

static void tpm_spapr_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    SpaprVioDeviceClass *k = VIO_SPAPR_DEVICE_CLASS(klass);
    TPMIfClass *tc = TPM_IF_CLASS(klass);

    k->realize = tpm_spapr_realizefn;
    k->reset = tpm_spapr_reset;
    k->dt_name = "vtpm";
    k->dt_type = "IBM,vtpm";
    k->get_dt_compatible = tpm_spapr_get_dt_compatible;
    k->signal_mask = 0x00000001;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    device_class_set_props(dc, tpm_spapr_properties);
    k->rtce_window_size = 0x10000000;
    dc->vmsd = &vmstate_spapr_vtpm;

    tc->model = TPM_MODEL_TPM_SPAPR;
    tc->get_version = tpm_spapr_get_version;
    tc->request_completed = tpm_spapr_request_completed;
}

static const TypeInfo tpm_spapr_info = {
    .name          = TYPE_TPM_SPAPR,
    .parent        = TYPE_VIO_SPAPR_DEVICE,
    .instance_size = sizeof(SpaprTpmState),
    .class_init    = tpm_spapr_class_init,
    .interfaces = (InterfaceInfo[]) {
        { TYPE_TPM_IF },
        { }
    }
};

static void tpm_spapr_register_types(void)
{
    type_register_static(&tpm_spapr_info);
}

type_init(tpm_spapr_register_types)
