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