diff --git a/include/uapi/linux/io_uring.h b/include/uapi/linux/io_uring.h
index 38f0d6b10eaf..82d8dae49645 100644
--- a/include/uapi/linux/io_uring.h
+++ b/include/uapi/linux/io_uring.h
@@ -278,6 +278,7 @@ enum io_uring_op {
 	IORING_OP_FTRUNCATE,
 	IORING_OP_BIND,
 	IORING_OP_LISTEN,
+	IORING_OP_CLONE,
 
 	/* this goes last, obviously */
 	IORING_OP_LAST,
diff --git a/io_uring/Makefile b/io_uring/Makefile
index 53167bef37d7..06ad15c07c57 100644
--- a/io_uring/Makefile
+++ b/io_uring/Makefile
@@ -12,7 +12,7 @@ obj-$(CONFIG_IO_URING)		+= io_uring.o opdef.o kbuf.o rsrc.o notif.o \
 					sqpoll.o xattr.o nop.o fs.o splice.o \
 					sync.o msg_ring.o advise.o openclose.o \
 					epoll.o statx.o timeout.o fdinfo.o \
-					cancel.o waitid.o register.o \
+					cancel.o waitid.o register.o spawn.o \
 					truncate.o memmap.o
 obj-$(CONFIG_IO_WQ)		+= io-wq.o
 obj-$(CONFIG_FUTEX)		+= futex.o
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 0fd8709401fc..b82ea1cc393f 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -97,6 +97,7 @@
 #include "uring_cmd.h"
 #include "msg_ring.h"
 #include "memmap.h"
+#include "spawn.h"
 
 #include "timeout.h"
 #include "poll.h"
@@ -1706,7 +1707,7 @@ static bool io_assign_file(struct io_kiocb *req, const struct io_issue_def *def,
 	return !!req->file;
 }
 
-static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
+int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
 {
 	const struct io_issue_def *def = &io_issue_defs[req->opcode];
 	const struct cred *creds = NULL;
diff --git a/io_uring/io_uring.h b/io_uring/io_uring.h
index 4dd051d29cb0..302c8f92b812 100644
--- a/io_uring/io_uring.h
+++ b/io_uring/io_uring.h
@@ -497,4 +497,6 @@ static inline bool io_has_work(struct io_ring_ctx *ctx)
 	return test_bit(IO_CHECK_CQ_OVERFLOW_BIT, &ctx->check_cq) ||
 	       io_local_work_pending(ctx);
 }
+
+int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags);
 #endif
diff --git a/io_uring/opdef.c b/io_uring/opdef.c
index 3de75eca1c92..1bab2e517e55 100644
--- a/io_uring/opdef.c
+++ b/io_uring/opdef.c
@@ -36,6 +36,7 @@
 #include "waitid.h"
 #include "futex.h"
 #include "truncate.h"
+#include "spawn.h"
 
 static int io_no_issue(struct io_kiocb *req, unsigned int issue_flags)
 {
@@ -515,6 +516,11 @@ const struct io_issue_def io_issue_defs[] = {
 		.prep			= io_eopnotsupp_prep,
 #endif
 	},
+	[IORING_OP_CLONE] = {
+		.audit_skip		= 1,
+		.prep			= io_clone_prep,
+		.issue			= io_clone,
+	},
 };
 
 const struct io_cold_def io_cold_defs[] = {
@@ -744,6 +750,9 @@ const struct io_cold_def io_cold_defs[] = {
 	[IORING_OP_LISTEN] = {
 		.name			= "LISTEN",
 	},
+	[IORING_OP_CLONE] = {
+		.name			= "CLONE",
+	},
 };
 
 const char *io_uring_get_opcode(u8 opcode)
diff --git a/io_uring/spawn.c b/io_uring/spawn.c
new file mode 100644
index 000000000000..1cd069bb6f59
--- /dev/null
+++ b/io_uring/spawn.c
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Spawning a linked series of operations onto a dedicated task.
+ *
+ * Copyright (C) 2022 Josh Triplett
+ */
+
+#include <linux/binfmts.h>
+#include <linux/nospec.h>
+#include <linux/syscalls.h>
+
+#include "io_uring.h"
+#include "rsrc.h"
+#include "spawn.h"
+
+struct io_clone {
+	struct file *file_unused;
+	struct io_kiocb *link;
+};
+
+static void fail_link(struct io_kiocb *req)
+{
+	struct io_kiocb *nxt;
+
+	while (req) {
+		req_fail_link_node(req, -ECANCELED);
+		io_req_complete_defer(req);
+
+		nxt = req->link;
+		req->link = NULL;
+		req = nxt;
+	}
+}
+
+static int io_uring_spawn_task(void *data)
+{
+	struct io_kiocb *head = data;
+	struct io_clone *c = io_kiocb_to_cmd(head, struct io_clone);
+	struct io_ring_ctx *ctx = head->ctx;
+	struct io_kiocb *req, *next;
+	int err;
+
+	set_task_comm(current, "iou-spawn");
+
+	mutex_lock(&ctx->uring_lock);
+
+	for (req = c->link; req; req = next) {
+		int hardlink = req->flags & REQ_F_HARDLINK;
+
+		next = req->link;
+		req->link = NULL;
+		req->flags &= ~(REQ_F_HARDLINK | REQ_F_LINK);
+
+		if (!(req->flags & REQ_F_FAIL)) {
+			err = io_issue_sqe(req, IO_URING_F_COMPLETE_DEFER);
+			/*
+			 * We can't requeue a request from the spawn
+			 * context.  Fail the whole chain.
+			 */
+			if (err) {
+				req_fail_link_node(req, -ECANCELED);
+				io_req_complete_defer(req);
+			}
+		}
+		if (req->flags & REQ_F_FAIL) {
+			if (!hardlink) {
+				fail_link(next);
+				break;
+			}
+		}
+	}
+
+	io_submit_flush_completions(ctx);
+	percpu_ref_put(&ctx->refs);
+
+	mutex_unlock(&ctx->uring_lock);
+
+	force_exit_sig(SIGKILL);
+	return 0;
+}
+
+int io_clone_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
+{
+	if (unlikely(sqe->fd || sqe->ioprio || sqe->addr2 || sqe->addr
+		     || sqe->len || sqe->rw_flags || sqe->buf_index
+		     || sqe->optlen || sqe->addr3))
+		return -EINVAL;
+
+	if (unlikely(!(req->flags & (REQ_F_HARDLINK|REQ_F_LINK))))
+		return -EINVAL;
+
+	if (unlikely(req->ctx->submit_state.link.head))
+		return -EINVAL;
+
+	return 0;
+}
+
+int io_clone(struct io_kiocb *req, unsigned int issue_flags)
+{
+	struct io_clone *c = io_kiocb_to_cmd(req, struct io_clone);
+	struct task_struct *tsk;
+
+	/* It is possible that we don't have any linked requests, depite
+	 * checking during ->prep().  It would be harmless to continue,
+	 * but we don't need even to create the worker thread in this
+	 * case.
+	 */
+	if (!req->link)
+		return IOU_OK;
+
+	/*
+	 * Prevent the context from going away before the spawned task
+	 * has had a chance to execute.  Dropped by io_uring_spawn_task.
+	 */
+	percpu_ref_get(&req->ctx->refs);
+
+	tsk = create_io_uring_spawn_task(io_uring_spawn_task, req);
+	if (IS_ERR(tsk)) {
+		percpu_ref_put(&req->ctx->refs);
+
+		req_set_fail(req);
+		io_req_set_res(req, PTR_ERR(tsk), 0);
+		return PTR_ERR(tsk);
+	}
+
+	/*
+	 * Steal the link from the io_uring dispatcher to have them
+	 * submitted through the new thread.  Note we can no longer fail
+	 * the clone, so the spawned task is responsible for completing
+	 * these requests.
+	 */
+	c->link = req->link;
+	req->flags &= ~(REQ_F_HARDLINK | REQ_F_LINK);
+	req->link = NULL;
+
+	wake_up_new_task(tsk);
+
+	return IOU_OK;
+
+}
diff --git a/io_uring/spawn.h b/io_uring/spawn.h
new file mode 100644
index 000000000000..9b7ddb776d1e
--- /dev/null
+++ b/io_uring/spawn.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/*
+ * Spawning a linked series of operations onto a dedicated task.
+ *
+ * Copyright © 2022 Josh Triplett
+ */
+
+int io_clone_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe);
+int io_clone(struct io_kiocb *req, unsigned int issue_flags);
