diff --git a/block.c b/block.c
index c390ec6461..887c0b105e 100644
--- a/block.c
+++ b/block.c
@@ -26,6 +26,7 @@
 #include "block/trace.h"
 #include "block/block_int.h"
 #include "block/blockjob.h"
+#include "block/fuse.h"
 #include "block/nbd.h"
 #include "block/qdict.h"
 #include "qemu/error-report.h"
@@ -4077,6 +4078,9 @@ void bdrv_close_all(void)
 {
     assert(job_next(NULL) == NULL);
     nbd_export_close_all();
+#ifdef CONFIG_FUSE
+    fuse_export_close_all();
+#endif
 
     /* Drop references from requests still in flight, such as canceled block
      * jobs whose AIO context has not been polled yet */
diff --git a/block/Makefile.objs b/block/Makefile.objs
index e394fe0b6c..b02846a6e7 100644
--- a/block/Makefile.objs
+++ b/block/Makefile.objs
@@ -43,6 +43,7 @@ block-obj-y += crypto.o
 
 block-obj-y += aio_task.o
 block-obj-y += backup-top.o
+block-obj-$(CONFIG_FUSE) += fuse.o
 
 common-obj-y += stream.o
 
@@ -67,3 +68,5 @@ qcow.o-libs        := -lz
 linux-aio.o-libs   := -laio
 parallels.o-cflags := $(LIBXML2_CFLAGS)
 parallels.o-libs   := $(LIBXML2_LIBS)
+fuse.o-cflags      := $(FUSE_CFLAGS)
+fuse.o-libs        := $(FUSE_LIBS)
diff --git a/block/fuse.c b/block/fuse.c
new file mode 100644
index 0000000000..3a22579dca
--- /dev/null
+++ b/block/fuse.c
@@ -0,0 +1,260 @@
+/*
+ * Present a block device as a raw image through FUSE
+ *
+ * Copyright (c) 2019 Max Reitz <mreitz@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 2 or later of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define FUSE_USE_VERSION 31
+
+#include "qemu/osdep.h"
+#include "block/aio.h"
+#include "block/block.h"
+#include "block/fuse.h"
+#include "block/qapi.h"
+#include "qapi/error.h"
+#include "qapi/qapi-commands-block.h"
+#include "sysemu/block-backend.h"
+
+#include <fuse.h>
+#include <fuse_lowlevel.h>
+
+
+typedef struct BdrvFuseSession {
+    struct fuse_session *fuse_session;
+    struct fuse_buf fuse_buf;
+    BlockBackend *blk;
+    uint64_t perm, shared_perm;
+    bool mounted, fd_handler_set_up;
+    bool writable;
+} BdrvFuseSession;
+
+static GHashTable *sessions;
+static const struct fuse_lowlevel_ops fuse_ops;
+
+static void init_fuse(void);
+static int setup_fuse_session(BdrvFuseSession *session, const char *mountpoint,
+                              Error **errp);
+static void read_from_fuse_session(void *opaque);
+static void close_fuse_session(BdrvFuseSession *session);
+static void drop_fuse_session_from_hash_table(gpointer value);
+
+static bool is_regular_file(const char *path, Error **errp);
+
+
+void qmp_fuse_export_add(const char *node_name, const char *mountpoint,
+                         bool has_writable, bool writable,
+                         Error **errp)
+{
+    BlockDriverState *bs;
+    BdrvFuseSession *session = NULL;
+
+    if (!has_writable) {
+        writable = false;
+    }
+
+    init_fuse();
+
+    /*
+     * It is important to do this check before calling is_regular_file() --
+     * that function will do a stat(), which we would have to handle if we
+     * already exported something on @mountpoint.  But we cannot, because
+     * we are currently caught up here.
+     */
+    if (g_hash_table_contains(sessions, mountpoint)) {
+        error_setg(errp, "There already is a FUSE export on '%s'", mountpoint);
+        goto fail;
+    }
+
+    if (!is_regular_file(mountpoint, errp)) {
+        goto fail;
+    }
+
+    bs = bdrv_find_node(node_name);
+    if (!bs) {
+        error_setg(errp, "Node '%s' not found", node_name);
+        goto fail;
+    }
+
+    session = g_new(BdrvFuseSession, 1);
+    *session = (BdrvFuseSession){
+        .fuse_buf = {
+            .mem = NULL,
+        },
+
+        .writable = writable,
+    };
+
+    session->perm = BLK_PERM_CONSISTENT_READ;
+    if (writable) {
+        session->perm |= BLK_PERM_WRITE;
+    }
+    session->shared_perm = BLK_PERM_ALL;
+
+    session->blk = blk_new(bdrv_get_aio_context(bs),
+                           session->perm, session->shared_perm);
+    if (blk_insert_bs(session->blk, bs, errp) < 0) {
+        goto fail;
+    }
+
+    if (setup_fuse_session(session, mountpoint, errp) < 0) {
+        goto fail;
+    }
+
+    g_hash_table_insert(sessions, g_strdup(mountpoint), session);
+    return;
+
+fail:
+    close_fuse_session(session);
+}
+
+/**
+ * Drop all FUSE exports.
+ */
+void fuse_export_close_all(void)
+{
+    if (sessions) {
+        g_hash_table_destroy(sessions);
+    }
+}
+
+
+/**
+ * Ensure that the global FUSE context is set up.
+ */
+static void init_fuse(void)
+{
+    if (sessions) {
+        return;
+    }
+
+    sessions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+                                     drop_fuse_session_from_hash_table);
+}
+
+/**
+ * Create session->fuse_session and mount it.
+ */
+static int setup_fuse_session(BdrvFuseSession *session, const char *mountpoint,
+                              Error **errp)
+{
+    const char *fuse_argv[2];
+    struct fuse_args fuse_args;
+    int ret;
+
+    fuse_argv[0] = ""; /* Dummy program name */
+    fuse_argv[1] = NULL;
+    fuse_args = (struct fuse_args)FUSE_ARGS_INIT(1, (char **)fuse_argv);
+
+    session->fuse_session = fuse_session_new(&fuse_args, &fuse_ops,
+                                             sizeof(fuse_ops), session);
+    if (!session->fuse_session) {
+        error_setg(errp, "Failed to set up FUSE session");
+        return -EIO;
+    }
+
+    ret = fuse_session_mount(session->fuse_session, mountpoint);
+    if (ret < 0) {
+        error_setg(errp, "Failed to mount FUSE session to export");
+        return -EIO;
+    }
+    session->mounted = true;
+
+    aio_set_fd_handler(blk_get_aio_context(session->blk),
+                       fuse_session_fd(session->fuse_session), true,
+                       read_from_fuse_session, NULL, NULL, session);
+    session->fd_handler_set_up = true;
+
+    return 0;
+}
+
+/**
+ * Callback to be invoked when the FUSE session FD can be read from.
+ * (This is basically the FUSE event loop.)
+ */
+static void read_from_fuse_session(void *opaque)
+{
+    BdrvFuseSession *session = opaque;
+    int ret;
+
+    ret = fuse_session_receive_buf(session->fuse_session, &session->fuse_buf);
+    if (ret < 0) {
+        return;
+    }
+
+    fuse_session_process_buf(session->fuse_session, &session->fuse_buf);
+}
+
+/**
+ * Drop a FUSE session (unmount it and free all associated resources).
+ * It is not removed from the @sessions hash table.
+ */
+static void close_fuse_session(BdrvFuseSession *session)
+{
+    if (!session) {
+        return;
+    }
+
+    if (session->fuse_session) {
+        if (session->mounted) {
+            fuse_session_unmount(session->fuse_session);
+        }
+        if (session->fd_handler_set_up) {
+            aio_set_fd_handler(blk_get_aio_context(session->blk),
+                               fuse_session_fd(session->fuse_session), true,
+                               NULL, NULL, NULL, NULL);
+        }
+        fuse_session_destroy(session->fuse_session);
+    }
+    blk_unref(session->blk);
+
+    g_free(session);
+}
+
+/**
+ * Wrapper around close_fuse_session() for use with
+ * g_hash_table_new_full().  This allows dropping sessions by removing
+ * them from the @sessions hash table.
+ */
+static void drop_fuse_session_from_hash_table(gpointer value)
+{
+    return close_fuse_session(value);
+}
+
+
+/**
+ * Check whether @path points to a regular file.  If not, put an
+ * appropriate message into *errp.
+ */
+static bool is_regular_file(const char *path, Error **errp)
+{
+    struct stat statbuf;
+    int ret;
+
+    ret = stat(path, &statbuf);
+    if (ret < 0) {
+        error_setg_errno(errp, errno, "Failed to stat '%s'", path);
+        return false;
+    }
+
+    if (!S_ISREG(statbuf.st_mode)) {
+        error_setg(errp, "'%s' is not a regular file", path);
+        return false;
+    }
+
+    return true;
+}
+
+static const struct fuse_lowlevel_ops fuse_ops = {
+};
diff --git a/include/block/fuse.h b/include/block/fuse.h
new file mode 100644
index 0000000000..1d24dded50
--- /dev/null
+++ b/include/block/fuse.h
@@ -0,0 +1,24 @@
+/*
+ * Present a block device as a raw image through FUSE
+ *
+ * Copyright (c) 2019 Max Reitz <mreitz@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 2 or later of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef BLOCK_FUSE_H
+#define BLOCK_FUSE_H
+
+void fuse_export_close_all(void);
+
+#endif
diff --git a/qapi/block.json b/qapi/block.json
index 145c268bb6..03f8d1b537 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -317,6 +317,29 @@
 ##
 { 'command': 'nbd-server-stop' }
 
+##
+# @fuse-export-add:
+#
+# Exports a block graph node on some (file) mountpoint as a raw image.
+#
+# @node-name: Node to be exported
+#
+# @mountpoint: Path on which to export the block device via FUSE.
+#              This must point to an existing regular file.
+#
+# @writable: Whether clients should be able to write to the block
+#            device via the FUSE export. (default: false)
+#
+# Since: 5.0
+##
+{ 'command': 'fuse-export-add',
+  'data': {
+      'node-name': 'str',
+      'mountpoint': 'str',
+      '*writable': 'bool'
+  },
+  'if': 'defined(CONFIG_FUSE)' }
+
 ##
 # @DEVICE_TRAY_MOVED:
 #
