diff --git a/include/chardev/char-fe.h b/include/chardev/char-fe.h
index 9f38060..0314870 100644
--- a/include/chardev/char-fe.h
+++ b/include/chardev/char-fe.h
@@ -10,6 +10,12 @@ typedef int BackendChangeHandler(void *opaque);
  * Chardev */
 struct CharBackend {
     Chardev *chr;
+    /* chr_lock is used to ensure thread-safety of operations with
+     * the associated Chardev.
+     * There is no guarantee otherwise, generally, that *chr won't become
+     * invalid.
+     */
+    QemuMutex chr_lock;
     IOEventHandler *chr_event;
     IOCanReadHandler *chr_can_read;
     IOReadHandler *chr_read;
@@ -42,6 +48,17 @@ bool qemu_chr_fe_init(CharBackend *b, Chardev *s, Error **errp);
 void qemu_chr_fe_deinit(CharBackend *b, bool del);
 
 /**
+ * @qemu_chr_fe_connect:
+ *
+ * Connects the already initialized front end with a given Chardev.
+ * Call qemu_chr_fe_deinit() to remove the association and
+ * release the driver.
+ *
+ * Returns: false on error.
+ */
+bool qemu_chr_fe_connect(CharBackend *b, Chardev *s, Error **errp);
+
+/**
  * @qemu_chr_fe_get_driver:
  *
  * Returns the driver associated with a CharBackend or NULL if no
diff --git a/include/chardev/char.h b/include/chardev/char.h
index 8a9ade4..22fd734 100644
--- a/include/chardev/char.h
+++ b/include/chardev/char.h
@@ -93,6 +93,15 @@ void qemu_chr_parse_common(QemuOpts *opts, ChardevCommon *backend);
 Chardev *qemu_chr_new(const char *label, const char *filename);
 
 /**
+ * @qemu_chr_change:
+ *
+ * Change an existing character backend
+ *
+ * @opts the new backend options
+ */
+void qemu_chr_change(QemuOpts *opts, Error **errp);
+
+/**
  * @qemu_chr_cleanup:
  *
  * Delete all chardevs (when leaving qemu)
diff --git a/chardev/char-fe.c b/chardev/char-fe.c
index 7054863..86a878b 100644
--- a/chardev/char-fe.c
+++ b/chardev/char-fe.c
@@ -33,24 +33,28 @@
 
 int qemu_chr_fe_write(CharBackend *be, const uint8_t *buf, int len)
 {
-    Chardev *s = be->chr;
+    int ret = 0;
 
-    if (!s) {
-        return 0;
+    qemu_mutex_lock(&be->chr_lock);
+    if (be->chr) {
+        ret = qemu_chr_write(be->chr, buf, len, false);
     }
+    qemu_mutex_unlock(&be->chr_lock);
 
-    return qemu_chr_write(s, buf, len, false);
+    return ret;
 }
 
 int qemu_chr_fe_write_all(CharBackend *be, const uint8_t *buf, int len)
 {
-    Chardev *s = be->chr;
+    int ret = 0;
 
-    if (!s) {
-        return 0;
+    qemu_mutex_lock(&be->chr_lock);
+    if (be->chr) {
+        ret = qemu_chr_write(be->chr, buf, len, true);
     }
+    qemu_mutex_unlock(&be->chr_lock);
 
-    return qemu_chr_write(s, buf, len, true);
+    return ret;
 }
 
 int qemu_chr_fe_read_all(CharBackend *be, uint8_t *buf, int len)
@@ -182,7 +186,7 @@ Chardev *qemu_chr_fe_get_driver(CharBackend *be)
     return be->chr;
 }
 
-bool qemu_chr_fe_init(CharBackend *b, Chardev *s, Error **errp)
+bool qemu_chr_fe_connect(CharBackend *b, Chardev *s, Error **errp)
 {
     int tag = 0;
 
@@ -211,6 +215,16 @@ unavailable:
     return false;
 }
 
+bool qemu_chr_fe_init(CharBackend *b, Chardev *s, Error **errp)
+{
+    if (!qemu_chr_fe_connect(b, s, errp)) {
+        return false;
+    }
+
+    qemu_mutex_init(&b->chr_lock);
+    return true;
+}
+
 void qemu_chr_fe_deinit(CharBackend *b, bool del)
 {
     assert(b);
@@ -228,6 +242,7 @@ void qemu_chr_fe_deinit(CharBackend *b, bool del)
             object_unparent(OBJECT(b->chr));
         }
         b->chr = NULL;
+        qemu_mutex_destroy(&b->chr_lock);
     }
 }
 
diff --git a/chardev/char.c b/chardev/char.c
index 839eff6..a7b342f 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -951,6 +951,92 @@ ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
     return ret;
 }
 
+ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
+                                  Error **errp)
+{
+    CharBackend *be;
+    const ChardevClass *cc;
+    Chardev *chr, *chr_new;
+    bool closed_sent = false;
+    ChardevReturn *ret;
+
+    chr = qemu_chr_find(id);
+    if (!chr) {
+        error_setg(errp, "Chardev '%s' does not exist", id);
+        return NULL;
+    }
+
+    if (CHARDEV_IS_MUX(chr)) {
+        error_setg(errp, "Mux device hotswap not supported yet");
+        return NULL;
+    }
+
+    if (qemu_chr_replay(chr)) {
+        error_setg(errp,
+            "Chardev '%s' cannot be changed in record/replay mode", id);
+        return NULL;
+    }
+
+    be = chr->be;
+    if (!be) {
+        /* easy case */
+        object_unparent(OBJECT(chr));
+        return qmp_chardev_add(id, backend, errp);
+    }
+
+    if (!be->chr_be_change) {
+        error_setg(errp, "Chardev user does not support chardev hotswap");
+        return NULL;
+    }
+
+    cc = char_get_class(ChardevBackendKind_lookup[backend->type], errp);
+    if (!cc) {
+        return NULL;
+    }
+
+    chr_new = qemu_chardev_new(NULL, object_class_get_name(OBJECT_CLASS(cc)),
+                               backend, errp);
+    if (!chr_new) {
+        return NULL;
+    }
+    chr_new->label = g_strdup(id);
+
+    if (chr->be_open && !chr_new->be_open) {
+        qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
+        closed_sent = true;
+    }
+
+    qemu_mutex_lock(&be->chr_lock);
+    chr->be = NULL;
+    qemu_chr_fe_connect(be, chr_new, &error_abort);
+
+    if (be->chr_be_change(be->opaque) < 0) {
+        error_setg(errp, "Chardev '%s' change failed", chr_new->label);
+        chr_new->be = NULL;
+        qemu_chr_fe_connect(be, chr, &error_abort);
+        qemu_mutex_unlock(&be->chr_lock);
+        if (closed_sent) {
+            qemu_chr_be_event(chr, CHR_EVENT_OPENED);
+        }
+        object_unref(OBJECT(chr_new));
+        return NULL;
+    }
+    qemu_mutex_unlock(&be->chr_lock);
+
+    object_unparent(OBJECT(chr));
+    object_property_add_child(get_chardevs_root(), chr_new->label,
+                              OBJECT(chr_new), &error_abort);
+    object_unref(OBJECT(chr_new));
+
+    ret = g_new0(ChardevReturn, 1);
+    if (CHARDEV_IS_PTY(chr_new)) {
+        ret->pty = g_strdup(chr_new->filename + 4);
+        ret->has_pty = true;
+    }
+
+    return ret;
+}
+
 void qmp_chardev_remove(const char *id, Error **errp)
 {
     Chardev *chr;
diff --git a/qapi-schema.json b/qapi-schema.json
index 37c4b95..0090fbf 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -5098,6 +5098,46 @@
   'returns': 'ChardevReturn' }
 
 ##
+# @chardev-change:
+#
+# Change a character device backend
+#
+# @id: the chardev's ID, must exist
+# @backend: new backend type and parameters
+#
+# Returns: ChardevReturn.
+#
+# Since: 2.10
+#
+# Example:
+#
+# -> { "execute" : "chardev-change",
+#      "arguments" : { "id" : "baz",
+#                      "backend" : { "type" : "pty", "data" : {} } } }
+# <- { "return": { "pty" : "/dev/pty/42" } }
+#
+# -> {"execute" : "chardev-change",
+#     "arguments" : {
+#         "id" : "charchannel2",
+#         "backend" : {
+#             "type" : "socket",
+#             "data" : {
+#                 "addr" : {
+#                     "type" : "unix" ,
+#                     "data" : {
+#                         "path" : "/tmp/charchannel2.socket"
+#                     }
+#                  },
+#                  "server" : true,
+#                  "wait" : false }}}}
+# <- {"return": {}}
+#
+##
+{ 'command': 'chardev-change', 'data': {'id'      : 'str',
+                                        'backend' : 'ChardevBackend' },
+  'returns': 'ChardevReturn' }
+
+##
 # @chardev-remove:
 #
 # Remove a character device backend
