@@ -8,6 +8,7 @@
*/
#include "qemu/osdep.h"
+#include "qemu-common.h"
#include "qapi/error.h"
#include "qemu/error-report.h"
#include "qom/object_interfaces.h"
@@ -16,6 +17,8 @@
#include "chardev/char.h"
#include "chardev/char-fe.h"
+#include "sysemu/vmi-handshake.h"
+
typedef struct VMIntrospection {
Object parent_obj;
@@ -23,9 +26,19 @@ typedef struct VMIntrospection {
char *chardevid;
Chardev *chr;
+ CharBackend sock;
+ int sock_fd;
+
+ qemu_vmi_from_introspector hsk_in;
+ uint64_t hsk_in_read_pos;
+ uint64_t hsk_in_read_size;
+
+ int64_t vm_start_time;
Notifier machine_ready;
bool created_from_command_line;
+
+ bool kvmi_hooked;
} VMIntrospection;
#define TYPE_VM_INTROSPECTION "introspection"
@@ -50,6 +63,11 @@ static void machine_ready(Notifier *notifier, void *data)
}
}
+static void update_vm_start_time(VMIntrospection *i)
+{
+ i->vm_start_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
+}
+
static void complete(UserCreatable *uc, Error **errp)
{
VMIntrospection *i = VM_INTROSPECTION(uc);
@@ -87,6 +105,13 @@ static void prop_set_chardev(Object *obj, const char *value, Error **errp)
i->chardevid = g_strdup(value);
}
+static bool chardev_is_connected(VMIntrospection *i, Error **errp)
+{
+ Object *obj = OBJECT(i->chr);
+
+ return obj && object_property_get_bool(obj, "connected", errp);
+}
+
static void class_init(ObjectClass *oc, void *data)
{
UserCreatableClass *uc = USER_CREATABLE_CLASS(oc);
@@ -98,17 +123,60 @@ static void instance_init(Object *obj)
{
VMIntrospection *i = VM_INTROSPECTION(obj);
+ i->sock_fd = -1;
i->created_from_command_line = (qdev_hotplug == false);
+ update_vm_start_time(i);
+
object_property_add_str(obj, "chardev", NULL, prop_set_chardev, NULL);
}
+static void disconnect_chardev(VMIntrospection *i)
+{
+ if (chardev_is_connected(i, NULL)) {
+ qemu_chr_fe_disconnect(&i->sock);
+ }
+}
+
+static void unhook_kvmi(VMIntrospection *i)
+{
+ if (i->kvmi_hooked) {
+ if (kvm_vm_ioctl(kvm_state, KVM_INTROSPECTION_UNHOOK, NULL)) {
+ error_report("VMI: ioctl/KVM_INTROSPECTION_UNHOOK failed, errno %d",
+ errno);
+ }
+ i->kvmi_hooked = false;
+ }
+}
+
+static void shutdown_socket_fd(VMIntrospection *i)
+{
+ /* signal both ends (kernel, introspector) */
+ if (i->sock_fd != -1) {
+ shutdown(i->sock_fd, SHUT_RDWR);
+ i->sock_fd = -1;
+ }
+}
+
+static void disconnect_and_unhook_kvmi(VMIntrospection *i)
+{
+ shutdown_socket_fd(i);
+ disconnect_chardev(i);
+ unhook_kvmi(i);
+}
+
static void instance_finalize(Object *obj)
{
VMIntrospection *i = VM_INTROSPECTION(obj);
g_free(i->chardevid);
+ if (i->chr) {
+ shutdown_socket_fd(i);
+ qemu_chr_fe_deinit(&i->sock, true);
+ unhook_kvmi(i);
+ }
+
error_free(i->init_error);
}
@@ -132,6 +200,210 @@ static void register_types(void)
type_init(register_types);
+static bool send_handshake_info(VMIntrospection *i, Error **errp)
+{
+ qemu_vmi_to_introspector send = {};
+ const char *vm_name;
+ int r;
+
+ send.struct_size = sizeof(send);
+ send.start_time = i->vm_start_time;
+ memcpy(&send.uuid, &qemu_uuid, sizeof(send.uuid));
+ vm_name = qemu_get_vm_name();
+ if (vm_name) {
+ snprintf(send.name, sizeof(send.name), "%s", vm_name);
+ send.name[sizeof(send.name) - 1] = 0;
+ }
+
+ r = qemu_chr_fe_write_all(&i->sock, (uint8_t *)&send, sizeof(send));
+ if (r != sizeof(send)) {
+ error_setg_errno(errp, errno, "VMI: error writing to '%s'",
+ i->chardevid);
+ return false;
+ }
+
+ /* tcp_chr_write may call tcp_chr_disconnect/CHR_EVENT_CLOSED */
+ if (!chardev_is_connected(i, errp)) {
+ error_append_hint(errp, "VMI: qemu_chr_fe_write_all() failed");
+ return false;
+ }
+
+ return true;
+}
+
+static bool validate_handshake(VMIntrospection *i, Error **errp)
+{
+ uint32_t min_accepted_size;
+
+ min_accepted_size = offsetof(qemu_vmi_from_introspector, cookie_hash)
+ + QEMU_VMI_COOKIE_HASH_SIZE;
+
+ if (i->hsk_in.struct_size < min_accepted_size) {
+ error_setg(errp, "VMI: not enough or invalid handshake data");
+ return false;
+ }
+
+ /*
+ * Check hsk_in.struct_size and sizeof(hsk_in) before accessing any
+ * other fields. We might get fewer bytes from applications using
+ * old versions if we extended the qemu_vmi_from_introspector structure.
+ */
+
+ return true;
+}
+
+static bool connect_kernel(VMIntrospection *i, Error **errp)
+{
+ struct kvm_introspection_feature commands, events;
+ struct kvm_introspection_hook kernel;
+ const __s32 all_ids = -1;
+
+ memset(&kernel, 0, sizeof(kernel));
+ memcpy(kernel.uuid, &qemu_uuid, sizeof(kernel.uuid));
+ kernel.fd = i->sock_fd;
+
+ if (kvm_vm_ioctl(kvm_state, KVM_INTROSPECTION_HOOK, &kernel)) {
+ error_setg_errno(errp, -errno,
+ "VMI: ioctl/KVM_INTROSPECTION_HOOK failed");
+ if (errno == -EPERM) {
+ error_append_hint(errp,
+ "Reload the kvm module with kvm.introspection=on");
+ }
+ return false;
+ }
+
+ i->kvmi_hooked = true;
+
+ commands.allow = 1;
+ commands.id = all_ids;
+ if (kvm_vm_ioctl(kvm_state, KVM_INTROSPECTION_COMMAND, &commands)) {
+ error_setg_errno(errp, -errno,
+ "VMI: ioctl/KVM_INTROSPECTION_COMMAND failed");
+ unhook_kvmi(i);
+ return false;
+ }
+
+ events.allow = 1;
+ events.id = all_ids;
+ if (kvm_vm_ioctl(kvm_state, KVM_INTROSPECTION_EVENT, &events)) {
+ error_setg_errno(errp, -errno,
+ "VMI: ioctl/KVM_INTROSPECTION_EVENT failed");
+ unhook_kvmi(i);
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * We should read only the handshake structure,
+ * which might have a different size than what we expect.
+ */
+static int chr_can_read(void *opaque)
+{
+ VMIntrospection *i = opaque;
+
+ if (i->sock_fd == -1) {
+ return 0;
+ }
+
+ /* first, we read the incoming structure size */
+ if (i->hsk_in_read_pos == 0) {
+ return sizeof(i->hsk_in.struct_size);
+ }
+
+ /* validate the incoming structure size */
+ if (i->hsk_in.struct_size < sizeof(i->hsk_in.struct_size)) {
+ return 0;
+ }
+
+ /* read the rest of the incoming structure */
+ return i->hsk_in.struct_size - i->hsk_in_read_pos;
+}
+
+static bool enough_bytes_for_handshake(VMIntrospection *i)
+{
+ return i->hsk_in_read_pos >= sizeof(i->hsk_in.struct_size)
+ && i->hsk_in_read_size == i->hsk_in.struct_size;
+}
+
+static void validate_and_connect(VMIntrospection *i)
+{
+ Error *local_err = NULL;
+
+ if (!validate_handshake(i, &local_err) || !connect_kernel(i, &local_err)) {
+ error_append_hint(&local_err, "reconnecting\n");
+ warn_report_err(local_err);
+ disconnect_chardev(i);
+ }
+}
+
+static void chr_read(void *opaque, const uint8_t *buf, int size)
+{
+ VMIntrospection *i = opaque;
+ size_t to_read;
+
+ i->hsk_in_read_size += size;
+
+ to_read = sizeof(i->hsk_in) - i->hsk_in_read_pos;
+ if (to_read > size) {
+ to_read = size;
+ }
+
+ if (to_read) {
+ memcpy((uint8_t *)&i->hsk_in + i->hsk_in_read_pos, buf, to_read);
+ i->hsk_in_read_pos += to_read;
+ }
+
+ if (enough_bytes_for_handshake(i)) {
+ validate_and_connect(i);
+ }
+}
+
+static void chr_event_open(VMIntrospection *i)
+{
+ Error *local_err = NULL;
+
+ if (!send_handshake_info(i, &local_err)) {
+ error_append_hint(&local_err, "reconnecting\n");
+ warn_report_err(local_err);
+ disconnect_chardev(i);
+ return;
+ }
+
+ info_report("VMI: introspection tool connected");
+
+ i->sock_fd = object_property_get_int(OBJECT(i->chr), "fd", NULL);
+
+ memset(&i->hsk_in, 0, sizeof(i->hsk_in));
+ i->hsk_in_read_pos = 0;
+ i->hsk_in_read_size = 0;
+}
+
+static void chr_event_close(VMIntrospection *i)
+{
+ if (i->sock_fd != -1) {
+ warn_report("VMI: introspection tool disconnected");
+ disconnect_and_unhook_kvmi(i);
+ }
+}
+
+static void chr_event(void *opaque, QEMUChrEvent event)
+{
+ VMIntrospection *i = opaque;
+
+ switch (event) {
+ case CHR_EVENT_OPENED:
+ chr_event_open(i);
+ break;
+ case CHR_EVENT_CLOSED:
+ chr_event_close(i);
+ break;
+ default:
+ break;
+ }
+}
+
static Error *vm_introspection_init(VMIntrospection *i)
{
Error *err = NULL;
@@ -162,7 +434,25 @@ static Error *vm_introspection_init(VMIntrospection *i)
return err;
}
+ if (!qemu_chr_fe_init(&i->sock, chr, &err)) {
+ error_append_hint(&err, "VMI: device '%s' not initialized",
+ i->chardevid);
+ return err;
+ }
+
i->chr = chr;
+ qemu_chr_fe_set_handlers(&i->sock, chr_can_read, chr_read, chr_event,
+ NULL, i, NULL, true);
+
+ /*
+ * The reconnect timer is triggered by either machine init or by a chardev
+ * disconnect. For the QMP creation, when the machine is already started,
+ * use an artificial disconnect just to restart the timer.
+ */
+ if (!i->created_from_command_line) {
+ qemu_chr_fe_disconnect(&i->sock);
+ }
+
return NULL;
}
new file mode 100644
@@ -0,0 +1,45 @@
+/*
+ * QEMU VM Introspection Handshake
+ *
+ */
+
+#ifndef QEMU_VMI_HANDSHAKE_H
+#define QEMU_VMI_HANDSHAKE_H
+
+enum { QEMU_VMI_NAME_SIZE = 64 };
+enum { QEMU_VMI_COOKIE_HASH_SIZE = 20};
+
+/**
+ * qemu_vmi_to_introspector:
+ *
+ * This structure is passed to the introspection tool during the handshake.
+ *
+ * @struct_size: the structure size
+ * @uuid: the UUID
+ * @start_time: the VM start time
+ * @name: the VM name
+ */
+typedef struct qemu_vmi_to_introspector {
+ uint32_t struct_size;
+ uint8_t uuid[16];
+ uint32_t padding;
+ int64_t start_time;
+ char name[QEMU_VMI_NAME_SIZE];
+ /* ... */
+} qemu_vmi_to_introspector;
+
+/**
+ * qemu_vmi_from_introspector:
+ *
+ * This structure is passed by the introspection tool during the handshake.
+ *
+ * @struct_size: the structure size
+ * @cookie_hash: the hash of the cookie know by the introspection tool
+ */
+typedef struct qemu_vmi_from_introspector {
+ uint32_t struct_size;
+ uint8_t cookie_hash[QEMU_VMI_COOKIE_HASH_SIZE];
+ /* ... */
+} qemu_vmi_from_introspector;
+
+#endif /* QEMU_VMI_HANDSHAKE_H */
QEMU sends the name, the UUID and the VM start time and expects the hash of a secret shared with the introspection tool that can be used to authenticate it. Signed-off-by: Adalbert Lazăr <alazar@bitdefender.com> --- accel/kvm/vmi.c | 290 +++++++++++++++++++++++++++++++++ include/sysemu/vmi-handshake.h | 45 +++++ 2 files changed, 335 insertions(+) create mode 100644 include/sysemu/vmi-handshake.h