View Javadoc
1   /*
2    * Copyright 2025 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.channel.uring;
17  
18  import io.netty.channel.Channel;
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelOutboundBuffer;
22  import io.netty.channel.ChannelPipeline;
23  import io.netty.channel.ChannelPromise;
24  import io.netty.channel.IoRegistration;
25  import io.netty.channel.unix.DomainSocketAddress;
26  import io.netty.channel.unix.DomainSocketChannel;
27  import io.netty.channel.unix.DomainSocketChannelConfig;
28  import io.netty.channel.unix.DomainSocketReadMode;
29  import io.netty.channel.unix.Errors;
30  import io.netty.channel.unix.FileDescriptor;
31  import io.netty.channel.unix.PeerCredentials;
32  import io.netty.util.internal.logging.InternalLogger;
33  import io.netty.util.internal.logging.InternalLoggerFactory;
34  
35  import java.io.IOException;
36  import java.net.SocketAddress;
37  
38  /**
39   * {@link DomainSocketChannel} implementation that uses linux io_uring
40   */
41  public final class IoUringDomainSocketChannel extends AbstractIoUringStreamChannel implements DomainSocketChannel {
42      private static final InternalLogger logger = InternalLoggerFactory.getInstance(IoUringDomainSocketChannel.class);
43  
44      private final IoUringDomainSocketChannelConfig config;
45  
46      private volatile DomainSocketAddress local;
47      private volatile DomainSocketAddress remote;
48  
49      public IoUringDomainSocketChannel() {
50          super(null, LinuxSocket.newSocketDomain(), false);
51          config = new IoUringDomainSocketChannelConfig(this);
52      }
53  
54      IoUringDomainSocketChannel(Channel parent, FileDescriptor fd) {
55          this(parent, new LinuxSocket(fd.intValue()));
56      }
57  
58      IoUringDomainSocketChannel(Channel parent, LinuxSocket fd) {
59          super(parent, fd, true);
60          local = fd.localDomainSocketAddress();
61          remote = fd.remoteDomainSocketAddress();
62          config = new IoUringDomainSocketChannelConfig(this);
63      }
64  
65      @Override
66      public DomainSocketChannelConfig config() {
67          return config;
68      }
69  
70      @Override
71      public DomainSocketAddress localAddress() {
72          return local;
73      }
74  
75      @Override
76      public DomainSocketAddress remoteAddress() {
77          return remote;
78      }
79  
80      /**
81       * Returns the unix credentials (uid, gid, pid) of the peer
82       * <a href=https://man7.org/linux/man-pages/man7/socket.7.html>SO_PEERCRED</a>
83       */
84      public PeerCredentials peerCredentials() throws IOException {
85          return socket.getPeerCredentials();
86      }
87  
88      @Override
89      protected Object filterOutboundMessage(Object msg) {
90          if (msg instanceof FileDescriptor) {
91              return msg;
92          }
93          return super.filterOutboundMessage(msg);
94      }
95  
96      @Override
97      protected AbstractUringUnsafe newUnsafe() {
98          return new IoUringDomainSocketUnsafe();
99      }
100 
101     @Override
102     protected boolean allowMultiShotPollIn() {
103         // UNIX domain sockets do not support IORING_CQE_F_SOCK_NONEMPTY and POLL_ADD_MULTI is edge-triggered
104         // so we should disable it
105         return false;
106     }
107 
108     @Override
109     protected boolean socketIsEmpty(int flags) {
110         return IoUring.isUnixDomainSocketInqSupported() && super.socketIsEmpty(flags);
111     }
112 
113     @Override
114     protected boolean shouldCompleteReadLoop(int flags, boolean multishot) {
115         if (IoUring.isUnixDomainSocketInqSupported()) {
116             return socketIsEmpty(flags);
117         }
118         // Older kernels cannot report IORING_CQE_F_SOCK_NONEMPTY for UDS, so the read-loop boundary cannot be
119         // determined reliably.
120         // Multishot recv does not produce an EAGAIN completion while it remains armed, so
121         // complete the read loop for each multishot completion. A one-shot recv can continue until EAGAIN.
122         return multishot;
123     }
124 
125     private final class IoUringDomainSocketUnsafe extends IoUringStreamUnsafe {
126 
127         private MsgHdrMemory writeMsgHdrMemory;
128         private MsgHdrMemory readMsgHdrMemory;
129         private FileDescriptor pendingSendFd;
130 
131         @Override
132         protected int scheduleWriteSingle(Object msg) {
133             if (msg instanceof FileDescriptor) {
134                 // we can reuse the same memory for any fd
135                 // because we never have more than a single outstanding write.
136                 if (writeMsgHdrMemory == null) {
137                     writeMsgHdrMemory = new MsgHdrMemory();
138                 }
139                 int fd = Native.duplicateFd(((FileDescriptor) msg).intValue());
140                 if (fd < 0) {
141                     handleWriteError(Errors.newIOException("fcntl(F_DUPFD_CLOEXEC)", fd));
142                     return 0;
143                 }
144                 // Keep a private fd until SENDMSG completes, even if a failed write listener closes the original.
145                 pendingSendFd = new FileDescriptor(fd);
146                 try {
147                     IoUringIoOps ioUringIoOps = prepSendFdIoOps(pendingSendFd, writeMsgHdrMemory);
148                     writeId = submitWrite(ioUringIoOps);
149                     writeOpCode = Native.IORING_OP_SENDMSG;
150                     if (writeId == 0) {
151                         MsgHdrMemory memory = writeMsgHdrMemory;
152                         writeMsgHdrMemory = null;
153                         memory.release();
154                         return 0;
155                     }
156                     return 1;
157                 } finally {
158                     if (writeId == 0) {
159                         closePendingSendFd();
160                     }
161                 }
162             }
163             return super.scheduleWriteSingle(msg);
164         }
165 
166         private void closePendingSendFd() {
167             FileDescriptor fd = pendingSendFd;
168             pendingSendFd = null;
169             try {
170                 fd.close();
171             } catch (IOException e) {
172                 logger.debug("Error while closing a duplicated file descriptor", e);
173             }
174         }
175 
176         @Override
177         boolean writeComplete0(byte op, int res, int flags, long data, int outstanding) {
178             if (op == Native.IORING_OP_SENDMSG) {
179                 writeId = 0;
180                 writeOpCode = 0;
181                 closePendingSendFd();
182                 ChannelOutboundBuffer channelOutboundBuffer = unsafe().outboundBuffer();
183                 // A failed batch may still return an error after shutdownOutput(). It must not close the input.
184                 if (channelOutboundBuffer == null || res == Native.ERRNO_ECANCELED_NEGATIVE) {
185                     return true;
186                 }
187                 try {
188                     int nativeCallResult = res >= 0 ? res : Errors.ioResult("io_uring sendmsg", res);
189                     if (nativeCallResult >= 0) {
190                         channelOutboundBuffer.remove();
191                     }
192                 } catch (Throwable throwable) {
193                    handleWriteError(throwable);
194                 }
195                 return true;
196             }
197             return super.writeComplete0(op, res, flags, data, outstanding);
198         }
199 
200         private IoUringIoOps prepSendFdIoOps(FileDescriptor fileDescriptor, MsgHdrMemory msgHdrMemory) {
201             msgHdrMemory.setScmRightsFd(fileDescriptor.intValue());
202             return IoUringIoOps.newSendmsg(
203                     fd().intValue(), (byte) 0, 0, msgHdrMemory.address(), msgHdrMemory.idx());
204         }
205 
206         @Override
207         protected int scheduleRead0(boolean first, boolean socketIsEmpty) {
208             DomainSocketReadMode readMode = config.getReadMode();
209             switch (readMode) {
210                 case FILE_DESCRIPTORS:
211                     return scheduleRecvReadFd();
212                 case BYTES:
213                     return super.scheduleRead0(first, socketIsEmpty);
214                 default:
215                     throw new Error("Unexpected read mode: " + readMode);
216             }
217         }
218 
219         private int scheduleRecvReadFd() {
220             // we can reuse the same memory for any fd
221             // because we only submit one outstanding read
222             if (readMsgHdrMemory == null) {
223                 readMsgHdrMemory = new MsgHdrMemory();
224             }
225             readMsgHdrMemory.prepRecvReadFd();
226             IoRegistration registration = registration();
227             IoUringIoOps ioUringIoOps = IoUringIoOps.newRecvmsg(
228                     fd().intValue(), (byte) 0, 0, readMsgHdrMemory.address(), readMsgHdrMemory.idx());
229             readId = registration.submit(ioUringIoOps);
230             readOpCode = Native.IORING_OP_RECVMSG;
231             if (readId == 0) {
232                 MsgHdrMemory memory = readMsgHdrMemory;
233                 readMsgHdrMemory = null;
234                 memory.release();
235                 return 0;
236             }
237             return 1;
238         }
239 
240         @Override
241         protected void readComplete0(byte op, int res, int flags, short data, int outstanding) {
242             if (op == Native.IORING_OP_RECVMSG) {
243                 readId = 0;
244                 if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
245                     return;
246                 }
247                 final IoUringRecvByteAllocatorHandle allocHandle = recvBufAllocHandle();
248                 final ChannelPipeline pipeline = pipeline();
249                 try {
250                     int nativeCallResult = res >= 0 ? res : Errors.ioResult("io_uring recvmsg", res);
251                     if (nativeCallResult == 0) {
252                         // EOF: the peer closed the connection, so there is no control message to inspect.
253                         shutdownInput(true);
254                         return;
255                     }
256                     int nativeFd = readMsgHdrMemory.getScmRightsFd();
257                     allocHandle.lastBytesRead(nativeFd);
258                     allocHandle.incMessagesRead(1);
259                     pipeline.fireChannelRead(new FileDescriptor(nativeFd));
260                 } catch (Throwable throwable) {
261                     handleReadException(pipeline, null, throwable, false, allocHandle);
262                 } finally {
263                     allocHandle.readComplete();
264                     pipeline.fireChannelReadComplete();
265                 }
266                 return;
267             }
268             super.readComplete0(op, res, flags, data, outstanding);
269         }
270 
271         @Override
272         public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
273             // Make sure to assign local/remote first before triggering the callback, to prevent potential NPE issues.
274             ChannelPromise channelPromise = newPromise().addListener(new ChannelFutureListener() {
275                 @Override
276                 public void operationComplete(ChannelFuture future) throws Exception {
277                     if (future.isSuccess()) {
278                         local = localAddress != null
279                                 ? (DomainSocketAddress) localAddress
280                                 : socket.localDomainSocketAddress();
281                         remote = (DomainSocketAddress) remoteAddress;
282                         promise.setSuccess();
283                     } else {
284                         promise.setFailure(future.cause());
285                     }
286                 }
287             });
288             super.connect(remoteAddress, localAddress, channelPromise);
289         }
290 
291         @Override
292         public void unregistered() {
293             super.unregistered();
294             if (readMsgHdrMemory != null) {
295                 readMsgHdrMemory.release();
296                 readMsgHdrMemory = null;
297             }
298             if (writeMsgHdrMemory != null) {
299                 writeMsgHdrMemory.release();
300                 writeMsgHdrMemory = null;
301             }
302         }
303     }
304 
305     @Override
306     boolean isPollInFirst() {
307         DomainSocketReadMode readMode = config.getReadMode();
308         switch (readMode) {
309             case BYTES:
310                 return super.isPollInFirst();
311             case FILE_DESCRIPTORS:
312                 return false;
313             default:
314                 throw new Error("Unexpected read mode: " + readMode);
315         }
316     }
317 }