mirror of
https://github.com/xemu-project/xemu.git
synced 2024-11-28 05:50:37 +00:00
da5361cc68
Destroying a mutex that another thread might have just unlocked is racy. It usually works, but you cannot do that in general and can lead to deadlocks or segfaults. Change ccid to use joinable threads instead. (Also, qemu_mutex_init/qemu_cond_init were missing). Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> Signed-off-by: Anthony Liguori <aliguori@us.ibm.com>
593 lines
18 KiB
C
593 lines
18 KiB
C
/*
|
|
* CCID Card Device. Emulated card.
|
|
*
|
|
* Copyright (c) 2011 Red Hat.
|
|
* Written by Alon Levy.
|
|
*
|
|
* This code is licensed under the GNU LGPL, version 2 or later.
|
|
*/
|
|
|
|
/*
|
|
* It can be used to provide access to the local hardware in a non exclusive
|
|
* way, or it can use certificates. It requires the usb-ccid bus.
|
|
*
|
|
* Usage 1: standard, mirror hardware reader+card:
|
|
* qemu .. -usb -device usb-ccid -device ccid-card-emulated
|
|
*
|
|
* Usage 2: use certificates, no hardware required
|
|
* one time: create the certificates:
|
|
* for i in 1 2 3; do
|
|
* certutil -d /etc/pki/nssdb -x -t "CT,CT,CT" -S -s "CN=user$i" -n user$i
|
|
* done
|
|
* qemu .. -usb -device usb-ccid \
|
|
* -device ccid-card-emulated,cert1=user1,cert2=user2,cert3=user3
|
|
*
|
|
* If you use a non default db for the certificates you can specify it using
|
|
* the db parameter.
|
|
*/
|
|
|
|
#include <eventt.h>
|
|
#include <vevent.h>
|
|
#include <vreader.h>
|
|
#include <vcard_emul.h>
|
|
|
|
#include "qemu-thread.h"
|
|
#include "qemu-char.h"
|
|
#include "monitor.h"
|
|
#include "hw/ccid.h"
|
|
|
|
#define DPRINTF(card, lvl, fmt, ...) \
|
|
do {\
|
|
if (lvl <= card->debug) {\
|
|
printf("ccid-card-emul: %s: " fmt , __func__, ## __VA_ARGS__);\
|
|
} \
|
|
} while (0)
|
|
|
|
#define EMULATED_DEV_NAME "ccid-card-emulated"
|
|
|
|
#define BACKEND_NSS_EMULATED_NAME "nss-emulated"
|
|
#define BACKEND_CERTIFICATES_NAME "certificates"
|
|
|
|
enum {
|
|
BACKEND_NSS_EMULATED = 1,
|
|
BACKEND_CERTIFICATES
|
|
};
|
|
|
|
#define DEFAULT_BACKEND BACKEND_NSS_EMULATED
|
|
|
|
typedef struct EmulatedState EmulatedState;
|
|
|
|
enum {
|
|
EMUL_READER_INSERT = 0,
|
|
EMUL_READER_REMOVE,
|
|
EMUL_CARD_INSERT,
|
|
EMUL_CARD_REMOVE,
|
|
EMUL_GUEST_APDU,
|
|
EMUL_RESPONSE_APDU,
|
|
EMUL_ERROR,
|
|
};
|
|
|
|
static const char *emul_event_to_string(uint32_t emul_event)
|
|
{
|
|
switch (emul_event) {
|
|
case EMUL_READER_INSERT:
|
|
return "EMUL_READER_INSERT";
|
|
case EMUL_READER_REMOVE:
|
|
return "EMUL_READER_REMOVE";
|
|
case EMUL_CARD_INSERT:
|
|
return "EMUL_CARD_INSERT";
|
|
case EMUL_CARD_REMOVE:
|
|
return "EMUL_CARD_REMOVE";
|
|
case EMUL_GUEST_APDU:
|
|
return "EMUL_GUEST_APDU";
|
|
case EMUL_RESPONSE_APDU:
|
|
return "EMUL_RESPONSE_APDU";
|
|
case EMUL_ERROR:
|
|
return "EMUL_ERROR";
|
|
}
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
typedef struct EmulEvent {
|
|
QSIMPLEQ_ENTRY(EmulEvent) entry;
|
|
union {
|
|
struct {
|
|
uint32_t type;
|
|
} gen;
|
|
struct {
|
|
uint32_t type;
|
|
uint64_t code;
|
|
} error;
|
|
struct {
|
|
uint32_t type;
|
|
uint32_t len;
|
|
uint8_t data[];
|
|
} data;
|
|
} p;
|
|
} EmulEvent;
|
|
|
|
#define MAX_ATR_SIZE 40
|
|
struct EmulatedState {
|
|
CCIDCardState base;
|
|
uint8_t debug;
|
|
char *backend_str;
|
|
uint32_t backend;
|
|
char *cert1;
|
|
char *cert2;
|
|
char *cert3;
|
|
char *db;
|
|
uint8_t atr[MAX_ATR_SIZE];
|
|
uint8_t atr_length;
|
|
QSIMPLEQ_HEAD(event_list, EmulEvent) event_list;
|
|
QemuMutex event_list_mutex;
|
|
QemuThread event_thread_id;
|
|
VReader *reader;
|
|
QSIMPLEQ_HEAD(guest_apdu_list, EmulEvent) guest_apdu_list;
|
|
QemuMutex vreader_mutex; /* and guest_apdu_list mutex */
|
|
QemuMutex handle_apdu_mutex;
|
|
QemuCond handle_apdu_cond;
|
|
int pipe[2];
|
|
int quit_apdu_thread;
|
|
QemuThread apdu_thread_id;
|
|
};
|
|
|
|
static void emulated_apdu_from_guest(CCIDCardState *base,
|
|
const uint8_t *apdu, uint32_t len)
|
|
{
|
|
EmulatedState *card = DO_UPCAST(EmulatedState, base, base);
|
|
EmulEvent *event = (EmulEvent *)g_malloc(sizeof(EmulEvent) + len);
|
|
|
|
assert(event);
|
|
event->p.data.type = EMUL_GUEST_APDU;
|
|
event->p.data.len = len;
|
|
memcpy(event->p.data.data, apdu, len);
|
|
qemu_mutex_lock(&card->vreader_mutex);
|
|
QSIMPLEQ_INSERT_TAIL(&card->guest_apdu_list, event, entry);
|
|
qemu_mutex_unlock(&card->vreader_mutex);
|
|
qemu_mutex_lock(&card->handle_apdu_mutex);
|
|
qemu_cond_signal(&card->handle_apdu_cond);
|
|
qemu_mutex_unlock(&card->handle_apdu_mutex);
|
|
}
|
|
|
|
static const uint8_t *emulated_get_atr(CCIDCardState *base, uint32_t *len)
|
|
{
|
|
EmulatedState *card = DO_UPCAST(EmulatedState, base, base);
|
|
|
|
*len = card->atr_length;
|
|
return card->atr;
|
|
}
|
|
|
|
static void emulated_push_event(EmulatedState *card, EmulEvent *event)
|
|
{
|
|
qemu_mutex_lock(&card->event_list_mutex);
|
|
QSIMPLEQ_INSERT_TAIL(&(card->event_list), event, entry);
|
|
qemu_mutex_unlock(&card->event_list_mutex);
|
|
if (write(card->pipe[1], card, 1) != 1) {
|
|
DPRINTF(card, 1, "write to pipe failed\n");
|
|
}
|
|
}
|
|
|
|
static void emulated_push_type(EmulatedState *card, uint32_t type)
|
|
{
|
|
EmulEvent *event = (EmulEvent *)g_malloc(sizeof(EmulEvent));
|
|
|
|
assert(event);
|
|
event->p.gen.type = type;
|
|
emulated_push_event(card, event);
|
|
}
|
|
|
|
static void emulated_push_error(EmulatedState *card, uint64_t code)
|
|
{
|
|
EmulEvent *event = (EmulEvent *)g_malloc(sizeof(EmulEvent));
|
|
|
|
assert(event);
|
|
event->p.error.type = EMUL_ERROR;
|
|
event->p.error.code = code;
|
|
emulated_push_event(card, event);
|
|
}
|
|
|
|
static void emulated_push_data_type(EmulatedState *card, uint32_t type,
|
|
const uint8_t *data, uint32_t len)
|
|
{
|
|
EmulEvent *event = (EmulEvent *)g_malloc(sizeof(EmulEvent) + len);
|
|
|
|
assert(event);
|
|
event->p.data.type = type;
|
|
event->p.data.len = len;
|
|
memcpy(event->p.data.data, data, len);
|
|
emulated_push_event(card, event);
|
|
}
|
|
|
|
static void emulated_push_reader_insert(EmulatedState *card)
|
|
{
|
|
emulated_push_type(card, EMUL_READER_INSERT);
|
|
}
|
|
|
|
static void emulated_push_reader_remove(EmulatedState *card)
|
|
{
|
|
emulated_push_type(card, EMUL_READER_REMOVE);
|
|
}
|
|
|
|
static void emulated_push_card_insert(EmulatedState *card,
|
|
const uint8_t *atr, uint32_t len)
|
|
{
|
|
emulated_push_data_type(card, EMUL_CARD_INSERT, atr, len);
|
|
}
|
|
|
|
static void emulated_push_card_remove(EmulatedState *card)
|
|
{
|
|
emulated_push_type(card, EMUL_CARD_REMOVE);
|
|
}
|
|
|
|
static void emulated_push_response_apdu(EmulatedState *card,
|
|
const uint8_t *apdu, uint32_t len)
|
|
{
|
|
emulated_push_data_type(card, EMUL_RESPONSE_APDU, apdu, len);
|
|
}
|
|
|
|
#define APDU_BUF_SIZE 270
|
|
static void *handle_apdu_thread(void* arg)
|
|
{
|
|
EmulatedState *card = arg;
|
|
uint8_t recv_data[APDU_BUF_SIZE];
|
|
int recv_len;
|
|
VReaderStatus reader_status;
|
|
EmulEvent *event;
|
|
|
|
while (1) {
|
|
qemu_mutex_lock(&card->handle_apdu_mutex);
|
|
qemu_cond_wait(&card->handle_apdu_cond, &card->handle_apdu_mutex);
|
|
qemu_mutex_unlock(&card->handle_apdu_mutex);
|
|
if (card->quit_apdu_thread) {
|
|
card->quit_apdu_thread = 0; /* debugging */
|
|
break;
|
|
}
|
|
qemu_mutex_lock(&card->vreader_mutex);
|
|
while (!QSIMPLEQ_EMPTY(&card->guest_apdu_list)) {
|
|
event = QSIMPLEQ_FIRST(&card->guest_apdu_list);
|
|
assert((unsigned long)event > 1000);
|
|
QSIMPLEQ_REMOVE_HEAD(&card->guest_apdu_list, entry);
|
|
if (event->p.data.type != EMUL_GUEST_APDU) {
|
|
DPRINTF(card, 1, "unexpected message in handle_apdu_thread\n");
|
|
g_free(event);
|
|
continue;
|
|
}
|
|
if (card->reader == NULL) {
|
|
DPRINTF(card, 1, "reader is NULL\n");
|
|
g_free(event);
|
|
continue;
|
|
}
|
|
recv_len = sizeof(recv_data);
|
|
reader_status = vreader_xfr_bytes(card->reader,
|
|
event->p.data.data, event->p.data.len,
|
|
recv_data, &recv_len);
|
|
DPRINTF(card, 2, "got back apdu of length %d\n", recv_len);
|
|
if (reader_status == VREADER_OK) {
|
|
emulated_push_response_apdu(card, recv_data, recv_len);
|
|
} else {
|
|
emulated_push_error(card, reader_status);
|
|
}
|
|
g_free(event);
|
|
}
|
|
qemu_mutex_unlock(&card->vreader_mutex);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void *event_thread(void *arg)
|
|
{
|
|
int atr_len = MAX_ATR_SIZE;
|
|
uint8_t atr[MAX_ATR_SIZE];
|
|
VEvent *event = NULL;
|
|
EmulatedState *card = arg;
|
|
|
|
while (1) {
|
|
const char *reader_name;
|
|
|
|
event = vevent_wait_next_vevent();
|
|
if (event == NULL || event->type == VEVENT_LAST) {
|
|
break;
|
|
}
|
|
if (event->type != VEVENT_READER_INSERT) {
|
|
if (card->reader == NULL && event->reader != NULL) {
|
|
/* Happens after device_add followed by card remove or insert.
|
|
* XXX: create synthetic add_reader events if vcard_emul_init
|
|
* already called, which happens if device_del and device_add
|
|
* are called */
|
|
card->reader = vreader_reference(event->reader);
|
|
} else {
|
|
if (event->reader != card->reader) {
|
|
fprintf(stderr,
|
|
"ERROR: wrong reader: quiting event_thread\n");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
switch (event->type) {
|
|
case VEVENT_READER_INSERT:
|
|
/* TODO: take a specific reader. i.e. track which reader
|
|
* we are seeing here, check it is the one we want (the first,
|
|
* or by a particular name), and ignore if we don't want it.
|
|
*/
|
|
reader_name = vreader_get_name(event->reader);
|
|
if (card->reader != NULL) {
|
|
DPRINTF(card, 2, "READER INSERT - replacing %s with %s\n",
|
|
vreader_get_name(card->reader), reader_name);
|
|
qemu_mutex_lock(&card->vreader_mutex);
|
|
vreader_free(card->reader);
|
|
qemu_mutex_unlock(&card->vreader_mutex);
|
|
emulated_push_reader_remove(card);
|
|
}
|
|
qemu_mutex_lock(&card->vreader_mutex);
|
|
DPRINTF(card, 2, "READER INSERT %s\n", reader_name);
|
|
card->reader = vreader_reference(event->reader);
|
|
qemu_mutex_unlock(&card->vreader_mutex);
|
|
emulated_push_reader_insert(card);
|
|
break;
|
|
case VEVENT_READER_REMOVE:
|
|
DPRINTF(card, 2, " READER REMOVE: %s\n",
|
|
vreader_get_name(event->reader));
|
|
qemu_mutex_lock(&card->vreader_mutex);
|
|
vreader_free(card->reader);
|
|
card->reader = NULL;
|
|
qemu_mutex_unlock(&card->vreader_mutex);
|
|
emulated_push_reader_remove(card);
|
|
break;
|
|
case VEVENT_CARD_INSERT:
|
|
/* get the ATR (intended as a response to a power on from the
|
|
* reader */
|
|
atr_len = MAX_ATR_SIZE;
|
|
vreader_power_on(event->reader, atr, &atr_len);
|
|
card->atr_length = (uint8_t)atr_len;
|
|
DPRINTF(card, 2, " CARD INSERT\n");
|
|
emulated_push_card_insert(card, atr, atr_len);
|
|
break;
|
|
case VEVENT_CARD_REMOVE:
|
|
DPRINTF(card, 2, " CARD REMOVE\n");
|
|
emulated_push_card_remove(card);
|
|
break;
|
|
case VEVENT_LAST: /* quit */
|
|
vevent_delete(event);
|
|
return NULL;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
vevent_delete(event);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void pipe_read(void *opaque)
|
|
{
|
|
EmulatedState *card = opaque;
|
|
EmulEvent *event, *next;
|
|
char dummy;
|
|
int len;
|
|
|
|
do {
|
|
len = read(card->pipe[0], &dummy, sizeof(dummy));
|
|
} while (len == sizeof(dummy));
|
|
qemu_mutex_lock(&card->event_list_mutex);
|
|
QSIMPLEQ_FOREACH_SAFE(event, &card->event_list, entry, next) {
|
|
DPRINTF(card, 2, "event %s\n", emul_event_to_string(event->p.gen.type));
|
|
switch (event->p.gen.type) {
|
|
case EMUL_RESPONSE_APDU:
|
|
ccid_card_send_apdu_to_guest(&card->base, event->p.data.data,
|
|
event->p.data.len);
|
|
break;
|
|
case EMUL_READER_INSERT:
|
|
ccid_card_ccid_attach(&card->base);
|
|
break;
|
|
case EMUL_READER_REMOVE:
|
|
ccid_card_ccid_detach(&card->base);
|
|
break;
|
|
case EMUL_CARD_INSERT:
|
|
assert(event->p.data.len <= MAX_ATR_SIZE);
|
|
card->atr_length = event->p.data.len;
|
|
memcpy(card->atr, event->p.data.data, card->atr_length);
|
|
ccid_card_card_inserted(&card->base);
|
|
break;
|
|
case EMUL_CARD_REMOVE:
|
|
ccid_card_card_removed(&card->base);
|
|
break;
|
|
case EMUL_ERROR:
|
|
ccid_card_card_error(&card->base, event->p.error.code);
|
|
break;
|
|
default:
|
|
DPRINTF(card, 2, "unexpected event\n");
|
|
break;
|
|
}
|
|
g_free(event);
|
|
}
|
|
QSIMPLEQ_INIT(&card->event_list);
|
|
qemu_mutex_unlock(&card->event_list_mutex);
|
|
}
|
|
|
|
static int init_pipe_signaling(EmulatedState *card)
|
|
{
|
|
if (pipe(card->pipe) < 0) {
|
|
DPRINTF(card, 2, "pipe creation failed\n");
|
|
return -1;
|
|
}
|
|
fcntl(card->pipe[0], F_SETFL, O_NONBLOCK);
|
|
fcntl(card->pipe[1], F_SETFL, O_NONBLOCK);
|
|
fcntl(card->pipe[0], F_SETOWN, getpid());
|
|
qemu_set_fd_handler(card->pipe[0], pipe_read, NULL, card);
|
|
return 0;
|
|
}
|
|
|
|
#define CERTIFICATES_DEFAULT_DB "/etc/pki/nssdb"
|
|
#define CERTIFICATES_ARGS_TEMPLATE\
|
|
"db=\"%s\" use_hw=no soft=(,Virtual Reader,CAC,,%s,%s,%s)"
|
|
|
|
static int wrap_vcard_emul_init(VCardEmulOptions *options)
|
|
{
|
|
static int called;
|
|
static int options_was_null;
|
|
|
|
if (called) {
|
|
if ((options == NULL) != options_was_null) {
|
|
printf("%s: warning: running emulated with certificates"
|
|
" and emulated side by side is not supported\n",
|
|
__func__);
|
|
return VCARD_EMUL_FAIL;
|
|
}
|
|
vcard_emul_replay_insertion_events();
|
|
return VCARD_EMUL_OK;
|
|
}
|
|
options_was_null = (options == NULL);
|
|
called = 1;
|
|
return vcard_emul_init(options);
|
|
}
|
|
|
|
static int emulated_initialize_vcard_from_certificates(EmulatedState *card)
|
|
{
|
|
char emul_args[200];
|
|
VCardEmulOptions *options = NULL;
|
|
|
|
snprintf(emul_args, sizeof(emul_args) - 1, CERTIFICATES_ARGS_TEMPLATE,
|
|
card->db ? card->db : CERTIFICATES_DEFAULT_DB,
|
|
card->cert1, card->cert2, card->cert3);
|
|
options = vcard_emul_options(emul_args);
|
|
if (options == NULL) {
|
|
printf("%s: warning: not using certificates due to"
|
|
" initialization error\n", __func__);
|
|
}
|
|
return wrap_vcard_emul_init(options);
|
|
}
|
|
|
|
typedef struct EnumTable {
|
|
const char *name;
|
|
uint32_t value;
|
|
} EnumTable;
|
|
|
|
EnumTable backend_enum_table[] = {
|
|
{BACKEND_NSS_EMULATED_NAME, BACKEND_NSS_EMULATED},
|
|
{BACKEND_CERTIFICATES_NAME, BACKEND_CERTIFICATES},
|
|
{NULL, 0},
|
|
};
|
|
|
|
static uint32_t parse_enumeration(char *str,
|
|
EnumTable *table, uint32_t not_found_value)
|
|
{
|
|
uint32_t ret = not_found_value;
|
|
|
|
while (table->name != NULL) {
|
|
if (strcmp(table->name, str) == 0) {
|
|
ret = table->value;
|
|
break;
|
|
}
|
|
table++;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int emulated_initfn(CCIDCardState *base)
|
|
{
|
|
EmulatedState *card = DO_UPCAST(EmulatedState, base, base);
|
|
VCardEmulError ret;
|
|
EnumTable *ptable;
|
|
|
|
QSIMPLEQ_INIT(&card->event_list);
|
|
QSIMPLEQ_INIT(&card->guest_apdu_list);
|
|
qemu_mutex_init(&card->event_list_mutex);
|
|
qemu_mutex_init(&card->vreader_mutex);
|
|
qemu_mutex_init(&card->handle_apdu_mutex);
|
|
qemu_cond_init(&card->handle_apdu_cond);
|
|
card->reader = NULL;
|
|
card->quit_apdu_thread = 0;
|
|
if (init_pipe_signaling(card) < 0) {
|
|
return -1;
|
|
}
|
|
card->backend = parse_enumeration(card->backend_str, backend_enum_table, 0);
|
|
if (card->backend == 0) {
|
|
printf("unknown backend, must be one of:\n");
|
|
for (ptable = backend_enum_table; ptable->name != NULL; ++ptable) {
|
|
printf("%s\n", ptable->name);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* TODO: a passthru backened that works on local machine. third card type?*/
|
|
if (card->backend == BACKEND_CERTIFICATES) {
|
|
if (card->cert1 != NULL && card->cert2 != NULL && card->cert3 != NULL) {
|
|
ret = emulated_initialize_vcard_from_certificates(card);
|
|
} else {
|
|
printf("%s: you must provide all three certs for"
|
|
" certificates backend\n", EMULATED_DEV_NAME);
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (card->backend != BACKEND_NSS_EMULATED) {
|
|
printf("%s: bad backend specified. The options are:\n%s (default),"
|
|
" %s.\n", EMULATED_DEV_NAME, BACKEND_NSS_EMULATED_NAME,
|
|
BACKEND_CERTIFICATES_NAME);
|
|
return -1;
|
|
}
|
|
if (card->cert1 != NULL || card->cert2 != NULL || card->cert3 != NULL) {
|
|
printf("%s: unexpected cert parameters to nss emulated backend\n",
|
|
EMULATED_DEV_NAME);
|
|
return -1;
|
|
}
|
|
/* default to mirroring the local hardware readers */
|
|
ret = wrap_vcard_emul_init(NULL);
|
|
}
|
|
if (ret != VCARD_EMUL_OK) {
|
|
printf("%s: failed to initialize vcard\n", EMULATED_DEV_NAME);
|
|
return -1;
|
|
}
|
|
qemu_thread_create(&card->event_thread_id, event_thread, card,
|
|
QEMU_THREAD_JOINABLE);
|
|
qemu_thread_create(&card->apdu_thread_id, handle_apdu_thread, card,
|
|
QEMU_THREAD_JOINABLE);
|
|
return 0;
|
|
}
|
|
|
|
static int emulated_exitfn(CCIDCardState *base)
|
|
{
|
|
EmulatedState *card = DO_UPCAST(EmulatedState, base, base);
|
|
VEvent *vevent = vevent_new(VEVENT_LAST, NULL, NULL);
|
|
|
|
vevent_queue_vevent(vevent); /* stop vevent thread */
|
|
qemu_thread_join(&card->event_thread_id);
|
|
|
|
card->quit_apdu_thread = 1; /* stop handle_apdu thread */
|
|
qemu_cond_signal(&card->handle_apdu_cond);
|
|
qemu_thread_join(&card->apdu_thread_id);
|
|
|
|
/* threads exited, can destroy all condvars/mutexes */
|
|
qemu_cond_destroy(&card->handle_apdu_cond);
|
|
qemu_mutex_destroy(&card->handle_apdu_mutex);
|
|
qemu_mutex_destroy(&card->vreader_mutex);
|
|
qemu_mutex_destroy(&card->event_list_mutex);
|
|
return 0;
|
|
}
|
|
|
|
static CCIDCardInfo emulated_card_info = {
|
|
.qdev.name = EMULATED_DEV_NAME,
|
|
.qdev.desc = "emulated smartcard",
|
|
.qdev.size = sizeof(EmulatedState),
|
|
.initfn = emulated_initfn,
|
|
.exitfn = emulated_exitfn,
|
|
.get_atr = emulated_get_atr,
|
|
.apdu_from_guest = emulated_apdu_from_guest,
|
|
.qdev.unplug = qdev_simple_unplug_cb,
|
|
.qdev.props = (Property[]) {
|
|
DEFINE_PROP_STRING("backend", EmulatedState, backend_str),
|
|
DEFINE_PROP_STRING("cert1", EmulatedState, cert1),
|
|
DEFINE_PROP_STRING("cert2", EmulatedState, cert2),
|
|
DEFINE_PROP_STRING("cert3", EmulatedState, cert3),
|
|
DEFINE_PROP_STRING("db", EmulatedState, db),
|
|
DEFINE_PROP_UINT8("debug", EmulatedState, debug, 0),
|
|
DEFINE_PROP_END_OF_LIST(),
|
|
},
|
|
};
|
|
|
|
static void ccid_card_emulated_register_devices(void)
|
|
{
|
|
ccid_card_qdev_register(&emulated_card_info);
|
|
}
|
|
|
|
device_init(ccid_card_emulated_register_devices)
|