fix: port 0 tracking not working for threads (#41051)
In the port 0 tracking logic. The seccomp filter provides the tid instead of the pid. But the pidfd_open call by default requires the first parameter to be a pid. This causes bind calls in threads not tracked by the tracker. Which further causes the local host forwarding to not work for consomme. This PR applies the PIDFD_THREAD flag introduced in kernel 6.9 to the pidfd_open call. So, it works with non group leader threads as well. Please note: PIDFD_THREAD is defined in place as musl does not provide <sys/pidfd.h> and including <linux/pidfd.h> introduces conflict with musl's header files. The test is not using a perl script since the perl in the test distro is lacking threading support.
Feng Wang committed
Aug 5, 2026 at 10:55 UTC
55d457846d5e5e5d3b82f7c2305480a2aa3893e7
9 files changed
+191
-8
src/linux/init/GnsPortTracker.cpp
+14
-1
@@ -15,6 +15,12 @@
15
#include "lxinitshared.h"
16
#include "seccomp_defs.h"
17
18
+// TODO: Include <sys/pidfd.h> and remove this once musl provides it.
19
+// <linux/pidfd.h> cannot be used because it conflicts with musl's <fcntl.h>.
20
+#ifndef PIDFD_THREAD
21
+#define PIDFD_THREAD O_EXCL
22
+#endif
23
+
24
constexpr size_t c_bind_timeout_seconds = 60;
25
constexpr auto c_sock_diag_refresh_delay = std::chrono::milliseconds(500);
26
constexpr auto c_sock_diag_poll_timeout = std::chrono::milliseconds(10);
@@ -623,7 +629,14 @@ wil::unique_fd GnsPortTracker::DuplicateSocketFd(pid_t Pid, int SocketFd)
629
// Duplicate the socket fd from the target process into our address space.
630
// We cannot use open("/proc/pid/fd/N") for sockets because the symlink target
631
// (socket:[inode]) is not a valid filesystem path. Use pidfd_getfd() instead.
626
- wil::unique_fd pidFd(static_cast<int>(syscall(SYS_pidfd_open, Pid, 0u)));
632
+ // PIDFD_THREAD requires kernel >= 6.9. Fallback to process only if not supported.
633
+ int pidFdResult = static_cast<int>(syscall(SYS_pidfd_open, Pid, PIDFD_THREAD));
634
+ if (pidFdResult < 0 && errno == EINVAL)
635
+ {
636
+ pidFdResult = static_cast<int>(syscall(SYS_pidfd_open, Pid, 0u));
637
+ }
638
+
639
+ wil::unique_fd pidFd(pidFdResult);
640
if (!pidFd)
641
{
642
GNS_LOG_INFO("Port-0 bind: pidfd_open failed for pid {} (errno {})", Pid, errno);
test/linux/unit_tests/Makefile
+1
@@ -51,6 +51,7 @@ UNIT_TEST_OBJECTS=\
51
sched.o \
52
sem.o \
53
shm.o \
54
+ socket.o \
55
socket_nonblock.o \
56
splice.o \
57
sysfs.o \
test/linux/unit_tests/socket.c
+102
-3
@@ -19,6 +19,7 @@ Abstract:
19
#include <sys/types.h>
20
#include <sys/socket.h>
21
#include <sys/un.h>
22
+#include <pthread.h>
23
#include <netinet/in.h>
24
#include <netdb.h>
25
#include "lxtcommon.h"
@@ -71,6 +72,10 @@ int SocketServerDgram(PLXT_ARGS Args);
72
73
int SocketServerUnix(PLXT_ARGS Args);
74
75
+int SocketServerPortZeroFromThread(PLXT_ARGS Args);
76
+
77
+void* SocketServerPortZeroFromThreadWorker(void* Context);
78
+
79
//
80
// Global constants.
81
//
@@ -99,7 +104,8 @@ static const LXT_VARIATION g_LxtServerVariations[] = {
104
{"Socket Server - AF_UNIX", SocketServerUnix},
105
{"Socket Server - accept multiple Ipv6", SocketServerAcceptMultipleIpv6},
106
{"Socket Server - accept (MSG_WAITALL)", SocketServerAcceptWithFlags},
102
- {"Socket Server - SOCK_DGRAM", SocketServerDgram}};
107
+ {"Socket Server - SOCK_DGRAM", SocketServerDgram},
108
+ {"Socket Server - port zero bind from thread", SocketServerPortZeroFromThread}};
109
110
//
111
// Function definitions.
@@ -118,7 +124,7 @@ long long GetTickCount(void)
124
return Now.tv_sec * 1000 + Now.tv_nsec / 1000000;
125
}
126
121
-int main(int Argc, char* Argv[])
127
+int SocketTestEntry(int Argc, char* Argv[])
128
129
/*++
130
--*/
@@ -1109,4 +1115,97 @@ int SocketServerUnix(PLXT_ARGS Args)
1115
{
1116
1117
return SocketServerAccept(1, AF_UNIX, SOCK_SEQPACKET, 0);
1112
-}
\ No newline at end of file
1118
+}
1119
+
1120
+int SocketServerPortZeroFromThread(PLXT_ARGS Args)
1121
+{
1122
+ int Error;
1123
+ int Result = LXT_RESULT_FAILURE;
1124
+ int ThreadError = 0;
1125
+ pthread_t Thread;
1126
+
1127
+ Error = pthread_create(&Thread, NULL, SocketServerPortZeroFromThreadWorker, &ThreadError);
1128
+ if (Error != 0)
1129
+ {
1130
+ LxtLogError("pthread_create - %s", strerror(Error));
1131
+ goto ErrorExit;
1132
+ }
1133
+
1134
+ Error = pthread_join(Thread, NULL);
1135
+ if (Error != 0)
1136
+ {
1137
+ LxtLogError("pthread_join - %s", strerror(Error));
1138
+ goto ErrorExit;
1139
+ }
1140
+
1141
+ if (ThreadError != 0)
1142
+ {
1143
+ LxtLogError("Threaded port-zero server failed - %s", strerror(ThreadError));
1144
+ goto ErrorExit;
1145
+ }
1146
+
1147
+ Result = LXT_RESULT_SUCCESS;
1148
+
1149
+ErrorExit:
1150
+ return Result;
1151
+}
1152
+
1153
+void* SocketServerPortZeroFromThreadWorker(void* Context)
1154
+{
1155
+ int* ThreadError = Context;
1156
+ int AcceptedSocket = -1;
1157
+ struct sockaddr_in ServerAddress = {0};
1158
+ socklen_t ServerAddressLength = sizeof(ServerAddress);
1159
+ int ServerSocket = -1;
1160
+ int Error = 0;
1161
+
1162
+ ServerSocket = socket(AF_INET, SOCK_STREAM, 0);
1163
+ if (ServerSocket < 0)
1164
+ {
1165
+ Error = errno;
1166
+ goto ErrorExit;
1167
+ }
1168
+
1169
+ ServerAddress.sin_family = AF_INET;
1170
+ ServerAddress.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1171
+ if (bind(ServerSocket, (struct sockaddr*)&ServerAddress, sizeof(ServerAddress)) < 0)
1172
+ {
1173
+ Error = errno;
1174
+ goto ErrorExit;
1175
+ }
1176
+
1177
+ if (getsockname(ServerSocket, (struct sockaddr*)&ServerAddress, &ServerAddressLength) < 0)
1178
+ {
1179
+ Error = errno;
1180
+ goto ErrorExit;
1181
+ }
1182
+
1183
+ if (listen(ServerSocket, 1) < 0)
1184
+ {
1185
+ Error = errno;
1186
+ goto ErrorExit;
1187
+ }
1188
+
1189
+ printf("PORT_ZERO_THREAD_LISTENER_PORT=%u\n", ntohs(ServerAddress.sin_port));
1190
+ fflush(stdout);
1191
+
1192
+ AcceptedSocket = accept(ServerSocket, NULL, NULL);
1193
+ if (AcceptedSocket < 0)
1194
+ {
1195
+ Error = errno;
1196
+ }
1197
+
1198
+ErrorExit:
1199
+ *ThreadError = Error;
1200
+ if (AcceptedSocket >= 0)
1201
+ {
1202
+ close(AcceptedSocket);
1203
+ }
1204
+
1205
+ if (ServerSocket >= 0)
1206
+ {
1207
+ close(ServerSocket);
1208
+ }
1209
+
1210
+ return NULL;
1211
+}
test/linux/unit_tests/unittests.c
+1
@@ -55,6 +55,7 @@ static const LXT_TEST LxtTests[] = {
55
#endif
56
{"sem", false, SemTestEntry},
57
{"shm", false, ShmTestEntry},
58
+ {"socket", false, SocketTestEntry},
59
{"socket_nonblock", false, SocketNonblockTestEntry},
60
{"splice", false, SpliceTestEntry},
61
{"sysfs", false, SysfsTestEntry},
test/linux/unit_tests/unittests.h
+3
@@ -56,6 +56,7 @@ Abstract:
56
#define SELECT_TESTNAME "select"
57
#define SEM_TESTNAME "sem"
58
#define SHM_TESTNAME "shm"
59
+#define SOCKET_TESTNAME "socket"
60
#define SOCKET_NONBLOCK_TESTNAME "socket_nonblock"
61
#define SPLICE_TESTNAME "splice"
62
#define SYSFS_TESTNAME "sysfs"
@@ -154,6 +155,8 @@ int SemTestEntry(int Argc, char* Argv[]);
155
156
int ShmTestEntry(int Argc, char* Argv[]);
157
158
+int SocketTestEntry(int Argc, char* Argv[]);
159
+
160
int SocketNonblockTestEntry(int Argc, char* Argv[]);
161
162
int SpliceTestEntry(int Argc, char* Argv[]);
test/windows/Common.h
+1
@@ -37,6 +37,7 @@ using namespace std::chrono_literals;
37
#define LXSS_DISTRO_NAME_TEST_L WIDEN(LXSS_DISTRO_NAME_TEST)
38
39
#define LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE L"-u root -e rm /etc/wsl.conf"
40
+#define LXSST_TESTS_INSTALL_COMMAND_LINE L"/bin/bash -c 'cd /data/test; ./build_tests.sh'"
41
42
//
43
// Test method declaration macros that tag tests with TAEF metadata for version-based selection.
test/windows/DrvFsTests.cpp
-2
@@ -43,8 +43,6 @@ Abstract:
43
44
#define LXSST_DRVFS_METADATA_TEST_MODE (5)
45
46
-#define LXSST_TESTS_INSTALL_COMMAND_LINE L"/bin/bash -c 'cd /data/test; ./build_tests.sh'"
47
-
46
#define LXSST_METADATA_EA_NAME_LENGTH (RTL_NUMBER_OF(LX_FILE_METADATA_UID_EA_NAME) - 1)
47
48
#define LX_DRVFS_DISABLE_NONE (0)
test/windows/NetworkTests.cpp
+69
@@ -2294,6 +2294,65 @@ class NetworkTests
2294
std::chrono::minutes(2)));
2295
}
2296
2297
+ static void VerifyPortZeroBindFromThreadIsTracked()
2298
+ {
2299
+ auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true);
2300
+ // LXT uses one bit per variation; the threaded port-zero server is the sixth server variation.
2301
+ constexpr unsigned long long c_portZeroThreadVariationMask = 1ull << 5;
2302
+ const auto commandLine = std::format(L"/data/test/wsl_unit_tests socket -s -v {}", c_portZeroThreadVariationMask);
2303
+ auto cmd = LxssGenerateWslCommandLine(commandLine.data());
2304
+ unique_kill_process serverProcess(LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get()));
2305
+ stdOutWrite.reset();
2306
+
2307
+ constexpr std::string_view portMarker = "PORT_ZERO_THREAD_LISTENER_PORT=";
2308
+ std::string output(512, '\0');
2309
+ DWORD writeOffset = 0;
2310
+ uint16_t assignedPort = 0;
2311
+ while (assignedPort == 0)
2312
+ {
2313
+ if (writeOffset == output.size())
2314
+ {
2315
+ output.resize(output.size() * 2);
2316
+ }
2317
+
2318
+ DWORD bytesRead = 0;
2319
+ VERIFY_IS_TRUE(ReadFile(
2320
+ stdOutRead.get(), output.data() + writeOffset, static_cast<DWORD>(output.size() - writeOffset), &bytesRead, nullptr));
2321
+ VERIFY_ARE_NOT_EQUAL(bytesRead, 0u);
2322
+ writeOffset += bytesRead;
2323
+
2324
+ const std::string_view outputView(output.data(), writeOffset);
2325
+ const auto markerPosition = outputView.find(portMarker);
2326
+ if (markerPosition == std::string_view::npos)
2327
+ {
2328
+ continue;
2329
+ }
2330
+
2331
+ const auto portBegin = markerPosition + portMarker.size();
2332
+ const auto portEnd = outputView.find_first_not_of("0123456789", portBegin);
2333
+ if (portEnd == std::string_view::npos)
2334
+ {
2335
+ continue;
2336
+ }
2337
+
2338
+ assignedPort = static_cast<uint16_t>(std::stoi(std::string(outputView.substr(portBegin, portEnd - portBegin))));
2339
+ }
2340
+
2341
+ LogInfo("Threaded guest listener assigned port %u", assignedPort);
2342
+ const auto connectToGuest = [&]() {
2343
+ wil::unique_socket clientSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
2344
+ THROW_LAST_ERROR_IF(!clientSocket);
2345
+
2346
+ SOCKADDR_IN address{};
2347
+ address.sin_family = AF_INET;
2348
+ address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2349
+ address.sin_port = htons(assignedPort);
2350
+ THROW_LAST_ERROR_IF(connect(clientSocket.get(), reinterpret_cast<SOCKADDR*>(&address), sizeof(address)) == SOCKET_ERROR);
2351
+ };
2352
+
2353
+ VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(connectToGuest, std::chrono::seconds(1), std::chrono::seconds(30)));
2354
+ }
2355
+
2356
static void VerifyPortZeroRebindSucceeds()
2357
{
2358
// Verify that bind(0) -> close -> immediate rebind on the same port succeeds.
@@ -3880,6 +3939,9 @@ class MirroredTests
3939
{
3940
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
3941
3942
+ // Build the Linux unit tests used by the port tracking tests.
3943
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), (DWORD)0);
3944
+
3945
if (LxsstuVmMode())
3946
{
3947
m_config.emplace(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
@@ -4295,6 +4357,8 @@ class MirroredTests
4357
// this range, so even after the guest releases the port the host still cannot bind
4358
// it — the range-level reservation remains, making release unverifiable.
4359
NetworkTests::VerifyPortZeroBindIsTracked(false);
4360
+
4361
+ NetworkTests::VerifyPortZeroBindFromThreadIsTracked();
4362
}
4363
4364
WSL2_TEST_METHOD(ListenWithoutBindIsTracked)
@@ -5181,6 +5245,9 @@ class ConsommeTests
5245
{
5246
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
5247
5248
+ // Build the Linux unit tests used by the port tracking tests.
5249
+ VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), (DWORD)0);
5250
+
5251
if (LxsstuVmMode())
5252
{
5253
m_config.emplace(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Consomme}));
@@ -5344,6 +5411,8 @@ class ConsommeTests
5411
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Consomme}));
5412
5413
NetworkTests::VerifyPortZeroBindIsTracked();
5414
+
5415
+ NetworkTests::VerifyPortZeroBindFromThreadIsTracked();
5416
}
5417
5418
WSL2_TEST_METHOD(ListenWithoutBindIsTracked)
test/windows/UnitTests.cpp
-2
@@ -45,8 +45,6 @@ Abstract:
45
#define LXSST_FSTAB_SETUP_COMMAND_LINE L"/bin/bash -c 'echo C:\\\\ /mnt/c drvfs metadata 0 0 >> /etc/fstab'"
46
#define LXSST_FSTAB_CLEANUP_COMMAND_LINE L"/bin/bash -c \"cp /etc/fstab.bak /etc/fstab\""
47
48
-#define LXSST_TESTS_INSTALL_COMMAND_LINE L"/bin/bash -c 'cd /data/test; ./build_tests.sh'"
49
-
48
#define LXSST_IMPORT_DISTRO_TEST_DIR L"C:\\importtest\\"
49
50
#define LXSST_UID_ROOT 0