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 }