6a8a23549a
Use an Error** for request to let the caller handle error reporting. This will also allow to inform the frontend of a backend error. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Reviewed-by: Stefan Berger <stefanb@linux.vnet.ibm.com> Signed-off-by: Stefan Berger <stefanb@linux.vnet.ibm.com>
412 lines
12 KiB
C
412 lines
12 KiB
C
/*
|
|
* passthrough TPM driver
|
|
*
|
|
* Copyright (c) 2010 - 2013 IBM Corporation
|
|
* Authors:
|
|
* Stefan Berger <stefanb@us.ibm.com>
|
|
*
|
|
* Copyright (C) 2011 IAIK, Graz University of Technology
|
|
* Author: Andreas Niederl
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu-common.h"
|
|
#include "qemu/error-report.h"
|
|
#include "qemu/sockets.h"
|
|
#include "sysemu/tpm_backend.h"
|
|
#include "tpm_int.h"
|
|
#include "hw/hw.h"
|
|
#include "qapi/clone-visitor.h"
|
|
#include "tpm_util.h"
|
|
|
|
#define DEBUG_TPM 0
|
|
|
|
#define DPRINTF(fmt, ...) do { \
|
|
if (DEBUG_TPM) { \
|
|
fprintf(stderr, fmt, ## __VA_ARGS__); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define TYPE_TPM_PASSTHROUGH "tpm-passthrough"
|
|
#define TPM_PASSTHROUGH(obj) \
|
|
OBJECT_CHECK(TPMPassthruState, (obj), TYPE_TPM_PASSTHROUGH)
|
|
|
|
/* data structures */
|
|
struct TPMPassthruState {
|
|
TPMBackend parent;
|
|
|
|
TPMPassthroughOptions *options;
|
|
const char *tpm_dev;
|
|
int tpm_fd;
|
|
bool tpm_executing;
|
|
bool tpm_op_canceled;
|
|
int cancel_fd;
|
|
|
|
TPMVersion tpm_version;
|
|
size_t tpm_buffersize;
|
|
};
|
|
|
|
typedef struct TPMPassthruState TPMPassthruState;
|
|
|
|
#define TPM_PASSTHROUGH_DEFAULT_DEVICE "/dev/tpm0"
|
|
|
|
/* functions */
|
|
|
|
static void tpm_passthrough_cancel_cmd(TPMBackend *tb);
|
|
|
|
static int tpm_passthrough_unix_read(int fd, uint8_t *buf, uint32_t len)
|
|
{
|
|
int ret;
|
|
reread:
|
|
ret = read(fd, buf, len);
|
|
if (ret < 0) {
|
|
if (errno != EINTR && errno != EAGAIN) {
|
|
return -1;
|
|
}
|
|
goto reread;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void tpm_passthrough_unix_tx_bufs(TPMPassthruState *tpm_pt,
|
|
const uint8_t *in, uint32_t in_len,
|
|
uint8_t *out, uint32_t out_len,
|
|
bool *selftest_done, Error **errp)
|
|
{
|
|
ssize_t ret;
|
|
bool is_selftest;
|
|
|
|
/* FIXME: protect shared variables or use other sync mechanism */
|
|
tpm_pt->tpm_op_canceled = false;
|
|
tpm_pt->tpm_executing = true;
|
|
*selftest_done = false;
|
|
|
|
is_selftest = tpm_util_is_selftest(in, in_len);
|
|
|
|
ret = qemu_write_full(tpm_pt->tpm_fd, in, in_len);
|
|
if (ret != in_len) {
|
|
if (!tpm_pt->tpm_op_canceled || errno != ECANCELED) {
|
|
error_setg_errno(errp, errno, "tpm_passthrough: error while "
|
|
"transmitting data to TPM");
|
|
}
|
|
goto err_exit;
|
|
}
|
|
|
|
tpm_pt->tpm_executing = false;
|
|
|
|
ret = tpm_passthrough_unix_read(tpm_pt->tpm_fd, out, out_len);
|
|
if (ret < 0) {
|
|
if (!tpm_pt->tpm_op_canceled || errno != ECANCELED) {
|
|
error_setg_errno(errp, errno, "tpm_passthrough: error while "
|
|
"reading data from TPM");
|
|
}
|
|
} else if (ret < sizeof(struct tpm_resp_hdr) ||
|
|
tpm_cmd_get_size(out) != ret) {
|
|
ret = -1;
|
|
error_setg_errno(errp, errno, "tpm_passthrough: received invalid "
|
|
"response packet from TPM");
|
|
}
|
|
|
|
if (is_selftest && (ret >= sizeof(struct tpm_resp_hdr))) {
|
|
*selftest_done = tpm_cmd_get_errcode(out) == 0;
|
|
}
|
|
|
|
err_exit:
|
|
if (ret < 0) {
|
|
tpm_util_write_fatal_error_response(out, out_len);
|
|
}
|
|
|
|
tpm_pt->tpm_executing = false;
|
|
}
|
|
|
|
static void tpm_passthrough_handle_request(TPMBackend *tb, TPMBackendCmd *cmd,
|
|
Error **errp)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(tb);
|
|
|
|
DPRINTF("tpm_passthrough: processing command %p\n", cmd);
|
|
|
|
tpm_passthrough_unix_tx_bufs(tpm_pt, cmd->in, cmd->in_len,
|
|
cmd->out, cmd->out_len, &cmd->selftest_done,
|
|
errp);
|
|
}
|
|
|
|
static void tpm_passthrough_reset(TPMBackend *tb)
|
|
{
|
|
DPRINTF("tpm_passthrough: CALL TO TPM_RESET!\n");
|
|
|
|
tpm_passthrough_cancel_cmd(tb);
|
|
}
|
|
|
|
static bool tpm_passthrough_get_tpm_established_flag(TPMBackend *tb)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
static int tpm_passthrough_reset_tpm_established_flag(TPMBackend *tb,
|
|
uint8_t locty)
|
|
{
|
|
/* only a TPM 2.0 will support this */
|
|
return 0;
|
|
}
|
|
|
|
static void tpm_passthrough_cancel_cmd(TPMBackend *tb)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(tb);
|
|
int n;
|
|
|
|
/*
|
|
* As of Linux 3.7 the tpm_tis driver does not properly cancel
|
|
* commands on all TPM manufacturers' TPMs.
|
|
* Only cancel if we're busy so we don't cancel someone else's
|
|
* command, e.g., a command executed on the host.
|
|
*/
|
|
if (tpm_pt->tpm_executing) {
|
|
if (tpm_pt->cancel_fd >= 0) {
|
|
tpm_pt->tpm_op_canceled = true;
|
|
n = write(tpm_pt->cancel_fd, "-", 1);
|
|
if (n != 1) {
|
|
error_report("Canceling TPM command failed: %s",
|
|
strerror(errno));
|
|
}
|
|
} else {
|
|
error_report("Cannot cancel TPM command due to missing "
|
|
"TPM sysfs cancel entry");
|
|
}
|
|
}
|
|
}
|
|
|
|
static TPMVersion tpm_passthrough_get_tpm_version(TPMBackend *tb)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(tb);
|
|
|
|
return tpm_pt->tpm_version;
|
|
}
|
|
|
|
static size_t tpm_passthrough_get_buffer_size(TPMBackend *tb)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(tb);
|
|
int ret;
|
|
|
|
ret = tpm_util_get_buffer_size(tpm_pt->tpm_fd, tpm_pt->tpm_version,
|
|
&tpm_pt->tpm_buffersize);
|
|
if (ret < 0) {
|
|
tpm_pt->tpm_buffersize = 4096;
|
|
}
|
|
return tpm_pt->tpm_buffersize;
|
|
}
|
|
|
|
/*
|
|
* Unless path or file descriptor set has been provided by user,
|
|
* determine the sysfs cancel file following kernel documentation
|
|
* in Documentation/ABI/stable/sysfs-class-tpm.
|
|
* From /dev/tpm0 create /sys/class/tpm/tpm0/device/cancel
|
|
* before 4.0: /sys/class/misc/tpm0/device/cancel
|
|
*/
|
|
static int tpm_passthrough_open_sysfs_cancel(TPMPassthruState *tpm_pt)
|
|
{
|
|
int fd = -1;
|
|
char *dev;
|
|
char path[PATH_MAX];
|
|
|
|
if (tpm_pt->options->cancel_path) {
|
|
fd = qemu_open(tpm_pt->options->cancel_path, O_WRONLY);
|
|
if (fd < 0) {
|
|
error_report("tpm_passthrough: Could not open TPM cancel path: %s",
|
|
strerror(errno));
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
dev = strrchr(tpm_pt->tpm_dev, '/');
|
|
if (!dev) {
|
|
error_report("tpm_passthrough: Bad TPM device path %s",
|
|
tpm_pt->tpm_dev);
|
|
return -1;
|
|
}
|
|
|
|
dev++;
|
|
if (snprintf(path, sizeof(path), "/sys/class/tpm/%s/device/cancel",
|
|
dev) < sizeof(path)) {
|
|
fd = qemu_open(path, O_WRONLY);
|
|
if (fd < 0) {
|
|
if (snprintf(path, sizeof(path), "/sys/class/misc/%s/device/cancel",
|
|
dev) < sizeof(path)) {
|
|
fd = qemu_open(path, O_WRONLY);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fd < 0) {
|
|
error_report("tpm_passthrough: Could not guess TPM cancel path");
|
|
} else {
|
|
tpm_pt->options->cancel_path = g_strdup(path);
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
static int
|
|
tpm_passthrough_handle_device_opts(TPMPassthruState *tpm_pt, QemuOpts *opts)
|
|
{
|
|
const char *value;
|
|
|
|
value = qemu_opt_get(opts, "cancel-path");
|
|
if (value) {
|
|
tpm_pt->options->cancel_path = g_strdup(value);
|
|
tpm_pt->options->has_cancel_path = true;
|
|
}
|
|
|
|
value = qemu_opt_get(opts, "path");
|
|
if (value) {
|
|
tpm_pt->options->has_path = true;
|
|
tpm_pt->options->path = g_strdup(value);
|
|
}
|
|
|
|
tpm_pt->tpm_dev = value ? value : TPM_PASSTHROUGH_DEFAULT_DEVICE;
|
|
tpm_pt->tpm_fd = qemu_open(tpm_pt->tpm_dev, O_RDWR);
|
|
if (tpm_pt->tpm_fd < 0) {
|
|
error_report("Cannot access TPM device using '%s': %s",
|
|
tpm_pt->tpm_dev, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
if (tpm_util_test_tpmdev(tpm_pt->tpm_fd, &tpm_pt->tpm_version)) {
|
|
error_report("'%s' is not a TPM device.",
|
|
tpm_pt->tpm_dev);
|
|
return -1;
|
|
}
|
|
|
|
tpm_pt->cancel_fd = tpm_passthrough_open_sysfs_cancel(tpm_pt);
|
|
if (tpm_pt->cancel_fd < 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static TPMBackend *tpm_passthrough_create(QemuOpts *opts)
|
|
{
|
|
Object *obj = object_new(TYPE_TPM_PASSTHROUGH);
|
|
|
|
if (tpm_passthrough_handle_device_opts(TPM_PASSTHROUGH(obj), opts)) {
|
|
object_unref(obj);
|
|
return NULL;
|
|
}
|
|
|
|
return TPM_BACKEND(obj);
|
|
}
|
|
|
|
static int tpm_passthrough_startup_tpm(TPMBackend *tb, size_t buffersize)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(tb);
|
|
|
|
if (buffersize && buffersize < tpm_pt->tpm_buffersize) {
|
|
error_report("Requested buffer size of %zu is smaller than host TPM's "
|
|
"fixed buffer size of %zu",
|
|
buffersize, tpm_pt->tpm_buffersize);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static TpmTypeOptions *tpm_passthrough_get_tpm_options(TPMBackend *tb)
|
|
{
|
|
TpmTypeOptions *options = g_new0(TpmTypeOptions, 1);
|
|
|
|
options->type = TPM_TYPE_OPTIONS_KIND_PASSTHROUGH;
|
|
options->u.passthrough.data = QAPI_CLONE(TPMPassthroughOptions,
|
|
TPM_PASSTHROUGH(tb)->options);
|
|
|
|
return options;
|
|
}
|
|
|
|
static const QemuOptDesc tpm_passthrough_cmdline_opts[] = {
|
|
TPM_STANDARD_CMDLINE_OPTS,
|
|
{
|
|
.name = "cancel-path",
|
|
.type = QEMU_OPT_STRING,
|
|
.help = "Sysfs file entry for canceling TPM commands",
|
|
},
|
|
{
|
|
.name = "path",
|
|
.type = QEMU_OPT_STRING,
|
|
.help = "Path to TPM device on the host",
|
|
},
|
|
{ /* end of list */ },
|
|
};
|
|
|
|
static void tpm_passthrough_inst_init(Object *obj)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(obj);
|
|
|
|
tpm_pt->options = g_new0(TPMPassthroughOptions, 1);
|
|
tpm_pt->tpm_fd = -1;
|
|
tpm_pt->cancel_fd = -1;
|
|
}
|
|
|
|
static void tpm_passthrough_inst_finalize(Object *obj)
|
|
{
|
|
TPMPassthruState *tpm_pt = TPM_PASSTHROUGH(obj);
|
|
|
|
tpm_passthrough_cancel_cmd(TPM_BACKEND(obj));
|
|
|
|
if (tpm_pt->tpm_fd >= 0) {
|
|
qemu_close(tpm_pt->tpm_fd);
|
|
}
|
|
if (tpm_pt->cancel_fd >= 0) {
|
|
qemu_close(tpm_pt->cancel_fd);
|
|
}
|
|
qapi_free_TPMPassthroughOptions(tpm_pt->options);
|
|
}
|
|
|
|
static void tpm_passthrough_class_init(ObjectClass *klass, void *data)
|
|
{
|
|
TPMBackendClass *tbc = TPM_BACKEND_CLASS(klass);
|
|
|
|
tbc->type = TPM_TYPE_PASSTHROUGH;
|
|
tbc->opts = tpm_passthrough_cmdline_opts;
|
|
tbc->desc = "Passthrough TPM backend driver";
|
|
tbc->create = tpm_passthrough_create;
|
|
tbc->startup_tpm = tpm_passthrough_startup_tpm;
|
|
tbc->reset = tpm_passthrough_reset;
|
|
tbc->cancel_cmd = tpm_passthrough_cancel_cmd;
|
|
tbc->get_tpm_established_flag = tpm_passthrough_get_tpm_established_flag;
|
|
tbc->reset_tpm_established_flag =
|
|
tpm_passthrough_reset_tpm_established_flag;
|
|
tbc->get_tpm_version = tpm_passthrough_get_tpm_version;
|
|
tbc->get_buffer_size = tpm_passthrough_get_buffer_size;
|
|
tbc->get_tpm_options = tpm_passthrough_get_tpm_options;
|
|
tbc->handle_request = tpm_passthrough_handle_request;
|
|
}
|
|
|
|
static const TypeInfo tpm_passthrough_info = {
|
|
.name = TYPE_TPM_PASSTHROUGH,
|
|
.parent = TYPE_TPM_BACKEND,
|
|
.instance_size = sizeof(TPMPassthruState),
|
|
.class_init = tpm_passthrough_class_init,
|
|
.instance_init = tpm_passthrough_inst_init,
|
|
.instance_finalize = tpm_passthrough_inst_finalize,
|
|
};
|
|
|
|
static void tpm_passthrough_register(void)
|
|
{
|
|
type_register_static(&tpm_passthrough_info);
|
|
}
|
|
|
|
type_init(tpm_passthrough_register)
|