diff --git a/accel/kvm/vmi.c b/accel/kvm/vmi.c
index 883c666a2a..57ded2f69c 100644
--- a/accel/kvm/vmi.c
+++ b/accel/kvm/vmi.c
@@ -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;
 }
diff --git a/include/sysemu/vmi-handshake.h b/include/sysemu/vmi-handshake.h
new file mode 100644
index 0000000000..19bdfb6740
--- /dev/null
+++ b/include/sysemu/vmi-handshake.h
@@ -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 */
