public inbox for io-uring@vger.kernel.org
 help / color / mirror / Atom feed
From: Daniele Di Proietto <daniele.di.proietto@gmail.com>
To: io-uring@vger.kernel.org
Cc: Jens Axboe <axboe@kernel.dk>,
	Daniele Di Proietto <daniele.di.proietto@gmail.com>
Subject: [PATCH liburing] Add support for IORING_OP_DUP
Date: Tue, 10 Mar 2026 15:51:11 +0000	[thread overview]
Message-ID: <20260310155111.2501074-1-daniele.di.proietto@gmail.com> (raw)

The new operation duplicates an existing file (regular fd or direct
descriptor) into a specific file descriptor.

With the IOSQE_FIXED_FILE sqe flag, it's like
IORING_OP_FIXED_FD_INSTALL, but with a specific destination file
descriptor.

Without the IOSQE_FIXED_FILE sqe flag, it's like dup3().

Signed-off-by: Daniele Di Proietto <daniele.di.proietto@gmail.com>
---
 man/io_uring_enter.2            |  24 ++++
 man/io_uring_prep_dup.3         |  71 ++++++++++
 src/include/liburing.h          |   8 ++
 src/include/liburing/io_uring.h |  10 ++
 test/Makefile                   |   1 +
 test/dup.c                      | 240 ++++++++++++++++++++++++++++++++
 6 files changed, 354 insertions(+)
 create mode 100644 man/io_uring_prep_dup.3
 create mode 100644 test/dup.c

diff --git a/man/io_uring_enter.2 b/man/io_uring_enter.2
index bd4f0613..105f78c4 100644
--- a/man/io_uring_enter.2
+++ b/man/io_uring_enter.2
@@ -272,6 +272,8 @@ struct io_uring_sqe {
 		__u32		futex_flags;
 		__u32		install_fd_flags;
 		__u32		nop_flags;
+		__u32		pipe_flags;
+		__u32		dup_flags;
 	};
 	__u64	user_data;	/* data to be passed back at completion time */
 	/* pack this to avoid bogus arm OABI complaints */
@@ -286,7 +288,9 @@ struct io_uring_sqe {
 	union {
 		__s32	splice_fd_in;
 		__u32	file_index;
+		__u32	zcrx_ifq_idx;
 		__u32	optlen;
+		__s32	dup_new_fd;
 		struct {
 			__u16	addr_len;
 			__u16	__pad3[1];
@@ -1729,6 +1733,26 @@ for general usage details.
 
 Available since 6.19.
 
+.TP
+.B IORING_OP_DUP
+Used to duplicate a file into a specifc fd of the regular process file table.
+If
+.B IOSQE_FIXED_FILE
+is set,
+.I fd
+must contain the file index, otherwise it must contain a regular fd, similar to
+.BR dup (2) .
+The duplicated file is placed into
+.IR dup_new_fd .
+Additional flags may be passed in via
+.IR dup_flags .
+Currently supported flags are
+.BR IORING_DUP_NO_CLOEXEC ,
+which is the opposite of
+.BR O_CLOEXEC .
+
+Available since 7.1.
+
 .PP
 The
 .I flags
diff --git a/man/io_uring_prep_dup.3 b/man/io_uring_prep_dup.3
new file mode 100644
index 00000000..6e334eaa
--- /dev/null
+++ b/man/io_uring_prep_dup.3
@@ -0,0 +1,71 @@
+.TH io_uring_prep_dup 3 "February 24, 2026" "liburing-2.15" "liburing Manual"
+.SH NAME
+io_uring_prep_dup \- prepare file duplication request
+.SH SYNOPSIS
+.nf
+.B #include <liburing.h>
+.PP
+.BI "void io_uring_prep_dup(struct io_uring_sqe *" sqe ","
+.BI "                       int " oldfd ","
+.BI "                       int " newfd ","
+.BI "                       unsigned int " dup_flags ");"
+.fi
+.SH DESCRIPTION
+.PP
+The
+.BR io_uring_prep_dup (3)
+helper prepares a file duplication request. The submission queue entry
+.I sqe
+is setup to duplicate the duplicate the file
+.I oldfd
+with the specified
+.I dup_flags
+into the regular file descriptor
+.IR newfd .
+
+When the SQE flag
+.B IOSQE_FIXED_FILE
+is set,
+.I oldfd
+must be a fixed file descriptor index.
+.BR io_uring_prep_dup (3)
+behaves similarly to
+.BR io_uring_prep_fixed_fd_install (3),
+but allows specifying a specific destination fd.
+
+.I newfd
+must always be a regular file descriptor.
+
+When the SQE flag
+.B IOSQE_FIXED_FILE
+is not set, this is similar to
+.BR dup3 (2).
+
+.I dup_flags
+may be either zero, or set to
+.B IORING_DUP_NO_CLOEXEC
+to indicate that the new regular file descriptor should not be closed during
+exec. By default,
+.B O_CLOEXEC
+will be set on
+.I newfd
+otherwise. Setting this field to anything but
+those two values will result in the request being failed with
+.B -EINVAL
+in the CQE
+.I res
+field.
+
+.SH RETURN VALUE
+None
+.SH ERRORS
+The CQE
+.I res
+field will contain the result of the operation, which in this case will be the
+value of the new regular file descriptor. In case of failure, a negative value
+is returned.
+.SH SEE ALSO
+.BR io_uring_get_sqe (3),
+.BR io_uring_submit (3),
+.BR io_uring_prep_fixed_fd_install (3),
+.BR dup3 (2),
diff --git a/src/include/liburing.h b/src/include/liburing.h
index c056e71c..debf9e34 100644
--- a/src/include/liburing.h
+++ b/src/include/liburing.h
@@ -1688,6 +1688,14 @@ IOURINGINLINE void io_uring_prep_pipe_direct(struct io_uring_sqe *sqe, int *fds,
 	__io_uring_set_target_fixed_file(sqe, file_index);
 }
 
+IOURINGINLINE void io_uring_prep_dup(struct io_uring_sqe *sqe, int oldfd,
+				     int newfd, unsigned int flags)
+{
+	io_uring_prep_rw(IORING_OP_DUP, sqe, oldfd, 0, 0, 0);
+	sqe->dup_new_fd = newfd;
+	sqe->dup_flags = flags;
+}
+
 /* Read the kernel's SQ head index with appropriate memory ordering */
 IOURINGINLINE unsigned io_uring_load_sq_head(const struct io_uring *ring)
 	LIBURING_NOEXCEPT
diff --git a/src/include/liburing/io_uring.h b/src/include/liburing/io_uring.h
index 7a0b517b..73d616ac 100644
--- a/src/include/liburing/io_uring.h
+++ b/src/include/liburing/io_uring.h
@@ -74,6 +74,7 @@ struct io_uring_sqe {
 		__u32		install_fd_flags;
 		__u32		nop_flags;
 		__u32		pipe_flags;
+		__u32		dup_flags;
 	};
 	__u64	user_data;	/* data to be passed back at completion time */
 	/* pack this to avoid bogus arm OABI complaints */
@@ -90,6 +91,7 @@ struct io_uring_sqe {
 		__u32	file_index;
 		__u32	zcrx_ifq_idx;
 		__u32	optlen;
+		__s32	dup_new_fd;
 		struct {
 			__u16	addr_len;
 			__u16	__pad3[1];
@@ -312,6 +314,7 @@ enum io_uring_op {
 	IORING_OP_PIPE,
 	IORING_OP_NOP128,
 	IORING_OP_URING_CMD128,
+	IORING_OP_DUP,
 
 	/* this goes last, obviously */
 	IORING_OP_LAST,
@@ -472,6 +475,13 @@ enum io_uring_msg_ring_flags {
  */
 #define IORING_FIXED_FD_NO_CLOEXEC	(1U << 0)
 
+/*
+ * IORING_OP_DUP flags (sqe->install_fd_flags)
+ *
+ * IORING_DUP_NO_CLOEXEC	Don't mark the fd as O_CLOEXEC
+ */
+#define IORING_DUP_NO_CLOEXEC (1U << 0)
+
 /*
  * IORING_OP_NOP flags (sqe->nop_flags)
  *
diff --git a/test/Makefile b/test/Makefile
index 7b94a1f4..2960139b 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -89,6 +89,7 @@ test_srcs := \
 	defer-tw-timeout.c \
 	double-poll-crash.c \
 	drop-submit.c \
+	dup.c \
 	eeed8b54e0df.c \
 	empty-eownerdead.c \
 	eploop.c \
diff --git a/test/dup.c b/test/dup.c
new file mode 100644
index 00000000..08d11444
--- /dev/null
+++ b/test/dup.c
@@ -0,0 +1,240 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Description: test io_uring_prep_dup
+ *
+ */
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "liburing.h"
+#include "helpers.h"
+
+struct fixture {
+	/* file descriptor of a pipe connected to fd_pipe_w */
+	int fd_pipe_r;
+	/* direct descriptor of a pipe connected to fd_pipe_w */
+	int fixed_pipe_r;
+
+	/* file descriptor of a pipe connected to fd_pipe_r and fixed_pipe_r */
+	int fd_pipe_w;
+
+	/* closed file descriptor */
+	int fd_invalid;
+	/* closed file descriptor */
+	int fd_invalid_2;
+};
+
+static int setup(struct io_uring *ring, struct fixture *fixture)
+{
+	int ret, fds[2];
+
+	if (pipe(fds) < 0) {
+		perror("pipe");
+		return T_EXIT_FAIL;
+	}
+	fixture->fd_pipe_r = fds[0];
+	fixture->fd_pipe_w = fds[1];
+	ret = io_uring_register_files(ring, &fixture->fd_pipe_r, 1);
+	if (ret) {
+		fprintf(stderr, "failed register files %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+	fixture->fixed_pipe_r = 0;
+
+	if (pipe(fds) < 0) {
+		perror("pipe");
+		return T_EXIT_FAIL;
+	}
+	close(fds[0]);
+	close(fds[1]);
+
+	fixture->fd_invalid = fds[0];
+	fixture->fd_invalid_2 = fds[1];
+
+	return T_EXIT_PASS;
+}
+
+static int are_pipes_connected(int fd_r, int fd_w)
+{
+	char buf[32];
+	int err;
+
+	err = write(fd_w, "Hello", 5);
+	if (err < 0) {
+		perror("write");
+		return T_EXIT_FAIL;
+	} else if (err != 5) {
+		fprintf(stderr, "short write %d\n", err);
+		return T_EXIT_FAIL;
+	}
+
+	err = read(fd_r, buf, sizeof(buf));
+	if (err != 5) {
+		fprintf(stderr, "unexpected read ret %d\n", err);
+		return T_EXIT_FAIL;
+	}
+
+	return T_EXIT_PASS;
+}
+
+static int cloexec_check(int fd, int nocloexec)
+{
+	int flags;
+	flags = fcntl(fd, F_GETFD, 0);
+	if (flags < 0) {
+		perror("fcntl");
+		return T_EXIT_FAIL;
+	}
+
+	if (nocloexec) {
+		return (flags & FD_CLOEXEC) ? T_EXIT_FAIL : T_EXIT_PASS;
+	} else {
+		return (flags & FD_CLOEXEC) ? T_EXIT_PASS : T_EXIT_FAIL;
+	}
+}
+
+static int test_working(struct io_uring *ring, struct fixture *fixture,
+			int fixed, int nocloexec)
+{
+	struct io_uring_sqe *sqe;
+	struct io_uring_cqe *cqe;
+	int ret, oldfd, newfd;
+
+	if (fixed) {
+		oldfd = fixture->fixed_pipe_r;
+	} else {
+		oldfd = fixture->fd_pipe_r;
+	}
+	newfd = fixture->fd_invalid;
+
+	sqe = io_uring_get_sqe(ring);
+	io_uring_prep_dup(sqe, oldfd, newfd,
+			  nocloexec ? IORING_DUP_NO_CLOEXEC : 0);
+	if (fixed) {
+		sqe->flags |= IOSQE_FIXED_FILE;
+	}
+	io_uring_submit(ring);
+	ret = io_uring_wait_cqe(ring, &cqe);
+	if (ret) {
+		fprintf(stderr, "wait cqe %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+	if (cqe->res < 0) {
+		fprintf(stderr, "failed dup: %d\n", cqe->res);
+		return T_EXIT_FAIL;
+	}
+	io_uring_cqe_seen(ring, cqe);
+
+	ret = are_pipes_connected(newfd, fixture->fd_pipe_w);
+	if (ret != T_EXIT_PASS) {
+		fprintf(stderr, "dup pipes not connected\n");
+		return ret;
+	}
+	ret = cloexec_check(newfd, nocloexec);
+	if (ret != T_EXIT_PASS) {
+		fprintf(stderr, "cloexec mismatch\n");
+		return ret;
+	}
+
+	close(newfd);
+	return T_EXIT_PASS;
+}
+
+static int test_invalid_flags(struct io_uring *ring, struct fixture *fixture)
+{
+	struct io_uring_sqe *sqe;
+	struct io_uring_cqe *cqe;
+	int ret, oldfd, newfd;
+
+	oldfd = fixture->fd_pipe_r;
+	newfd = fixture->fd_invalid;
+
+	sqe = io_uring_get_sqe(ring);
+	io_uring_prep_dup(sqe, oldfd, newfd, 1 << 7);
+	io_uring_submit(ring);
+	ret = io_uring_wait_cqe(ring, &cqe);
+	if (ret) {
+		fprintf(stderr, "wait cqe %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+	if (cqe->res != -EINVAL) {
+		return T_EXIT_FAIL;
+	}
+	io_uring_cqe_seen(ring, cqe);
+
+	return T_EXIT_PASS;
+}
+
+int main(int argc, char *argv[])
+{
+	struct io_uring_probe *probe;
+	struct fixture fixture;
+	struct io_uring ring;
+	int ret;
+
+	if (argc > 1)
+		return T_EXIT_SKIP;
+
+	probe = io_uring_get_probe();
+	if (!probe) {
+		return T_EXIT_SKIP;
+	}
+	if (!io_uring_opcode_supported(probe, IORING_OP_DUP)) {
+		return T_EXIT_SKIP;
+	}
+	io_uring_free_probe(probe);
+
+	ret = io_uring_queue_init(4, &ring, 0);
+	if (ret) {
+		fprintf(stderr, "ring setup failed: %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+
+	ret = setup(&ring, &fixture);
+	if (ret != T_EXIT_PASS) {
+		fprintf(stderr, "fixture setup failed\n");
+		return T_EXIT_FAIL;
+	}
+
+	ret = test_working(&ring, &fixture, 0, 0);
+	if (ret != T_EXIT_PASS) {
+		if (ret == T_EXIT_FAIL)
+			fprintf(stderr,
+				"test_working regular cloexec failed\n");
+		return ret;
+	}
+
+	ret = test_working(&ring, &fixture, 1, 0);
+	if (ret != T_EXIT_PASS) {
+		if (ret == T_EXIT_FAIL)
+			fprintf(stderr, "test_working fixed cloexec failed\n");
+		return ret;
+	}
+
+	ret = test_working(&ring, &fixture, 0, 1);
+	if (ret != T_EXIT_PASS) {
+		if (ret == T_EXIT_FAIL)
+			fprintf(stderr,
+				"test_working regular nocloexec failed\n");
+		return ret;
+	}
+
+	ret = test_working(&ring, &fixture, 1, 1);
+	if (ret != T_EXIT_PASS) {
+		if (ret == T_EXIT_FAIL)
+			fprintf(stderr,
+				"test_working fixed nocloexec failed\n");
+		return ret;
+	}
+
+	ret = test_invalid_flags(&ring, &fixture);
+	if (ret != T_EXIT_PASS) {
+		if (ret == T_EXIT_FAIL)
+			fprintf(stderr, "test_invalid_flags failed\n");
+		return ret;
+	}
+
+	return T_EXIT_PASS;
+}
-- 
2.43.0


                 reply	other threads:[~2026-03-10 15:51 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260310155111.2501074-1-daniele.di.proietto@gmail.com \
    --to=daniele.di.proietto@gmail.com \
    --cc=axboe@kernel.dk \
    --cc=io-uring@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox