View Javadoc
1   /*
2    * Copyright 2024 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.unix.Errors;
19  
20  import java.io.IOException;
21  import java.nio.ByteBuffer;
22  
23  /**
24   * <pre>{@code
25   * struct cmsghdr {
26   *     socklen_t cmsg_len;    // data byte count, including header
27   *     int cmsg_level;  //originating protocol
28   *     int cmsg_type;   // protocol-specific type
29   *     // followed by unsigned char cmsg_data[];
30   * };
31   * }</pre>
32   */
33  final class CmsgHdr {
34  
35      private CmsgHdr() { }
36  
37      static void write(ByteBuffer cmsghdr, int cmsgHdrDataOffset,
38                        int cmsgLen, int cmsgLevel, int cmsgType, short segmentSize) {
39          int cmsghdrPosition = cmsghdr.position();
40          if (Native.SIZEOF_SIZE_T == 4) {
41              cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN, cmsgLen);
42          } else {
43              assert Native.SIZEOF_SIZE_T == 8;
44              cmsghdr.putLong(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN, cmsgLen);
45          }
46          cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEVEL, cmsgLevel);
47          cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_TYPE, cmsgType);
48          cmsghdr.putShort(cmsghdrPosition + cmsgHdrDataOffset, segmentSize);
49      }
50  
51      static void writeScmRights(ByteBuffer cmsghdr, int cmsgHdrDataOffset, int fd) {
52          int cmsghdrPosition = cmsghdr.position();
53          if (Native.SIZEOF_SIZE_T == 4) {
54              cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN, Native.CMSG_LEN_FOR_FD);
55          } else {
56              assert Native.SIZEOF_SIZE_T == 8;
57              cmsghdr.putLong(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN, Native.CMSG_LEN_FOR_FD);
58          }
59          cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEVEL, Native.SOL_SOCKET);
60          cmsghdr.putInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_TYPE, Native.SCM_RIGHTS);
61          cmsghdr.putInt(cmsghdrPosition + cmsgHdrDataOffset, fd);
62      }
63  
64      /**
65       * Reads the file descriptor delivered via a {@code SCM_RIGHTS} control message.
66       * <p>
67       * A malicious (or buggy) peer can pack more than one file descriptor into the single control message that
68       * Netty only ever sizes for one fd. On 64-bit Linux {@code CMSG_SPACE(sizeof(int))} and
69       * {@code CMSG_SPACE(2 * sizeof(int))} are both 24 bytes (due to 8 byte alignment), so the kernel does not
70       * report {@code MSG_CTRUNC} even though two fds were delivered. If that is not detected the surplus fd is
71       * never closed and leaks for the lifetime of the process (see CVE-2026-45536 for the equivalent issue that
72       * was fixed for the epoll/kqueue transports in {@code netty_unix_socket_recvFd()}).
73       * <p>
74       * To guard against this we validate the cmsg level/type, compute the real number of fds from
75       * {@code cmsg_len} and, if more than one fd (or a truncated message) was received, close every fd we got
76       * and fail instead of silently returning a partial (and leaking) result.
77       *
78       * @throws IOException if the control message did not carry exactly one well-formed {@code SCM_RIGHTS} fd.
79       */
80      static int readScmRights(ByteBuffer cmsghdr, int cmsgHdrDataOffset, boolean truncated) throws IOException {
81          int cmsghdrPosition = cmsghdr.position();
82          long cmsgLen = Native.SIZEOF_SIZE_T == 4
83                  ? cmsghdr.getInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN) & 0xFFFFFFFFL
84                  : cmsghdr.getLong(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEN);
85          int cmsgLevel = cmsghdr.getInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_LEVEL);
86          int cmsgType = cmsghdr.getInt(cmsghdrPosition + Native.CMSG_OFFSETOF_CMSG_TYPE);
87  
88          if (cmsgLevel != Native.SOL_SOCKET || cmsgType != Native.SCM_RIGHTS) {
89              throw new IOException(
90                      "Received unexpected cmsg (level: " + cmsgLevel + ", type: " + cmsgType + ')');
91          }
92  
93          // MSG_CTRUNC only means that some of the control data was discarded, the kernel still reports the fds it did
94          // deliver via cmsg_len. So always compute nfds from cmsg_len to be able to close whatever was delivered.
95          int nfds = numFds(cmsgLen, Native.CMSG_LEN_FOR_FD, Native.SIZEOF_INT);
96          if (truncated || nfds != 1) {
97              IOException error = new IOException(truncated
98                      ? "Received a truncated SCM_RIGHTS cmsg (cmsg_len: " + cmsgLen + ')'
99                      : nfds < 0
100                         ? "Received a malformed SCM_RIGHTS cmsg (cmsg_len: " + cmsgLen + ')'
101                         : "Received " + nfds + " file descriptors in a single SCM_RIGHTS cmsg, expected 1");
102             // Close every fd that was actually delivered so nothing leaks, even though we reject the message.
103             int toClose = numFdsToClose(nfds, truncated);
104             for (int i = 0; i < toClose; i++) {
105                 closeQuietly(cmsghdr.getInt(cmsghdrPosition + cmsgHdrDataOffset + i * Native.SIZEOF_INT), error);
106             }
107             throw error;
108         }
109         return cmsghdr.getInt(cmsghdrPosition + cmsgHdrDataOffset);
110     }
111 
112     /**
113      * Computes how many file descriptors a {@code SCM_RIGHTS} cmsg of the given {@code cmsgLen} carries, given
114      * {@code cmsgLenForFd}, the platform's {@code CMSG_LEN(sizeof(int))} (the length of a cmsg holding exactly
115      * one fd), and {@code sizeofInt}, the platform's {@code sizeof(int)}. Returns {@code -1} if {@code cmsgLen}
116      * is smaller than one fd or not a whole number of fds (i.e. truncated or malformed).
117      * <p>
118      * Pure function of primitives (no native/JNI dependency) so it can be unit tested in isolation.
119      */
120     static int numFds(long cmsgLen, int cmsgLenForFd, int sizeofInt) {
121         long dataLen = cmsgLen - (cmsgLenForFd - sizeofInt);
122         if (dataLen < sizeofInt || dataLen % sizeofInt != 0) {
123             return -1;
124         }
125         return (int) (dataLen / sizeofInt);
126     }
127 
128     /**
129      * Returns the number of delivered fds that must be closed because the message is rejected: all of them if the
130      * message was truncated (and at least one fd was delivered) or if more than one fd was delivered.
131      * <p>
132      * Pure function of primitives so it can be unit tested in isolation.
133      */
134     static int numFdsToClose(int nfds, boolean truncated) {
135         return nfds > 0 && (truncated || nfds > 1) ? nfds : 0;
136     }
137 
138     private static void closeQuietly(int fd, Throwable error) {
139         if (fd < 0) {
140             return;
141         }
142         int res = Native.close(fd);
143         if (res < 0) {
144             error.addSuppressed(Errors.newIOException("close", res));
145         }
146     }
147 }