| // SPDX-License-Identifier: GPL-2.0 | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <limits.h> | |
| #include <sched.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/socket.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <linux/if.h> | |
| #include <linux/sockios.h> | |
| #include <linux/nsfs.h> | |
| #include <arpa/inet.h> | |
| #include "../kselftest_harness.h" | |
| #include "../filesystems/utils.h" | |
| #include "wrappers.h" | |
| #ifndef SIOCGSKNS | |
| #define SIOCGSKNS 0x894C | |
| #endif | |
| #ifndef FD_NSFS_ROOT | |
| #define FD_NSFS_ROOT -10003 | |
| #endif | |
| #ifndef FILEID_NSFS | |
| #define FILEID_NSFS 0xf1 | |
| #endif | |
| /* | |
| * Test basic SIOCGSKNS functionality. | |
| * Create a socket and verify SIOCGSKNS returns the correct network namespace. | |
| */ | |
| TEST(siocgskns_basic) | |
| { | |
| int sock_fd, netns_fd, current_netns_fd; | |
| struct stat st1, st2; | |
| /* Create a TCP socket */ | |
| sock_fd = socket(AF_INET, SOCK_STREAM, 0); | |
| ASSERT_GE(sock_fd, 0); | |
| /* Use SIOCGSKNS to get network namespace */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| /* Get current network namespace */ | |
| current_netns_fd = open("/proc/self/ns/net", O_RDONLY); | |
| ASSERT_GE(current_netns_fd, 0); | |
| /* Verify they match */ | |
| ASSERT_EQ(fstat(netns_fd, &st1), 0); | |
| ASSERT_EQ(fstat(current_netns_fd, &st2), 0); | |
| ASSERT_EQ(st1.st_ino, st2.st_ino); | |
| close(sock_fd); | |
| close(netns_fd); | |
| close(current_netns_fd); | |
| } | |
| /* | |
| * Test that socket file descriptors keep network namespaces active. | |
| * Create a network namespace, create a socket in it, then exit the namespace. | |
| * The namespace should remain active while the socket FD is held. | |
| */ | |
| TEST(siocgskns_keeps_netns_active) | |
| { | |
| int sock_fd, netns_fd, test_fd; | |
| int ipc_sockets[2]; | |
| pid_t pid; | |
| int status; | |
| struct stat st; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| /* Child: create new netns and socket */ | |
| close(ipc_sockets[0]); | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| TH_LOG("unshare(CLONE_NEWNET) failed: %s", strerror(errno)); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Create a socket in the new network namespace */ | |
| sock_fd = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (sock_fd < 0) { | |
| TH_LOG("socket() failed: %s", strerror(errno)); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Send socket FD to parent via SCM_RIGHTS */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(0); | |
| } | |
| /* Parent: receive socket FD */ | |
| close(ipc_sockets[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| ASSERT_EQ(n, 1); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| ASSERT_NE(cmsg, NULL); | |
| ASSERT_EQ(cmsg->cmsg_type, SCM_RIGHTS); | |
| memcpy(&sock_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for child to exit */ | |
| waitpid(pid, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| ASSERT_EQ(WEXITSTATUS(status), 0); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| ASSERT_EQ(fstat(netns_fd, &st), 0); | |
| /* | |
| * Namespace should still be active because socket FD keeps it alive. | |
| * Try to access it via /proc/self/fd/<fd>. | |
| */ | |
| char path[64]; | |
| snprintf(path, sizeof(path), "/proc/self/fd/%d", netns_fd); | |
| test_fd = open(path, O_RDONLY); | |
| ASSERT_GE(test_fd, 0); | |
| close(test_fd); | |
| close(netns_fd); | |
| /* Close socket - namespace should become inactive */ | |
| close(sock_fd); | |
| /* Try SIOCGSKNS again - should fail since socket is closed */ | |
| ASSERT_LT(ioctl(sock_fd, SIOCGSKNS), 0); | |
| } | |
| /* | |
| * Test SIOCGSKNS with different socket types (TCP, UDP, RAW). | |
| */ | |
| TEST(siocgskns_socket_types) | |
| { | |
| int sock_tcp, sock_udp, sock_raw; | |
| int netns_tcp, netns_udp, netns_raw; | |
| struct stat st_tcp, st_udp, st_raw; | |
| /* TCP socket */ | |
| sock_tcp = socket(AF_INET, SOCK_STREAM, 0); | |
| ASSERT_GE(sock_tcp, 0); | |
| /* UDP socket */ | |
| sock_udp = socket(AF_INET, SOCK_DGRAM, 0); | |
| ASSERT_GE(sock_udp, 0); | |
| /* RAW socket (may require privileges) */ | |
| sock_raw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); | |
| if (sock_raw < 0 && (errno == EPERM || errno == EACCES)) { | |
| sock_raw = -1; /* Skip raw socket test */ | |
| } | |
| /* Test SIOCGSKNS on TCP */ | |
| netns_tcp = ioctl(sock_tcp, SIOCGSKNS); | |
| if (netns_tcp < 0) { | |
| close(sock_tcp); | |
| close(sock_udp); | |
| if (sock_raw >= 0) close(sock_raw); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_tcp, 0); | |
| } | |
| /* Test SIOCGSKNS on UDP */ | |
| netns_udp = ioctl(sock_udp, SIOCGSKNS); | |
| ASSERT_GE(netns_udp, 0); | |
| /* Test SIOCGSKNS on RAW (if available) */ | |
| if (sock_raw >= 0) { | |
| netns_raw = ioctl(sock_raw, SIOCGSKNS); | |
| ASSERT_GE(netns_raw, 0); | |
| } | |
| /* Verify all return the same network namespace */ | |
| ASSERT_EQ(fstat(netns_tcp, &st_tcp), 0); | |
| ASSERT_EQ(fstat(netns_udp, &st_udp), 0); | |
| ASSERT_EQ(st_tcp.st_ino, st_udp.st_ino); | |
| if (sock_raw >= 0) { | |
| ASSERT_EQ(fstat(netns_raw, &st_raw), 0); | |
| ASSERT_EQ(st_tcp.st_ino, st_raw.st_ino); | |
| close(netns_raw); | |
| close(sock_raw); | |
| } | |
| close(netns_tcp); | |
| close(netns_udp); | |
| close(sock_tcp); | |
| close(sock_udp); | |
| } | |
| /* | |
| * Test SIOCGSKNS across setns. | |
| * Create a socket in netns A, switch to netns B, verify SIOCGSKNS still | |
| * returns netns A. | |
| */ | |
| TEST(siocgskns_across_setns) | |
| { | |
| int sock_fd, netns_a_fd, netns_b_fd, result_fd; | |
| struct stat st_a; | |
| /* Get current netns (A) */ | |
| netns_a_fd = open("/proc/self/ns/net", O_RDONLY); | |
| ASSERT_GE(netns_a_fd, 0); | |
| ASSERT_EQ(fstat(netns_a_fd, &st_a), 0); | |
| /* Create socket in netns A */ | |
| sock_fd = socket(AF_INET, SOCK_STREAM, 0); | |
| ASSERT_GE(sock_fd, 0); | |
| /* Create new netns (B) */ | |
| ASSERT_EQ(unshare(CLONE_NEWNET), 0); | |
| netns_b_fd = open("/proc/self/ns/net", O_RDONLY); | |
| ASSERT_GE(netns_b_fd, 0); | |
| /* Get netns from socket created in A */ | |
| result_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (result_fd < 0) { | |
| close(sock_fd); | |
| setns(netns_a_fd, CLONE_NEWNET); | |
| close(netns_a_fd); | |
| close(netns_b_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(result_fd, 0); | |
| } | |
| /* Verify it still points to netns A */ | |
| struct stat st_result_stat; | |
| ASSERT_EQ(fstat(result_fd, &st_result_stat), 0); | |
| ASSERT_EQ(st_a.st_ino, st_result_stat.st_ino); | |
| close(result_fd); | |
| close(sock_fd); | |
| close(netns_b_fd); | |
| /* Restore original netns */ | |
| ASSERT_EQ(setns(netns_a_fd, CLONE_NEWNET), 0); | |
| close(netns_a_fd); | |
| } | |
| /* | |
| * Test SIOCGSKNS fails on non-socket file descriptors. | |
| */ | |
| TEST(siocgskns_non_socket) | |
| { | |
| int fd; | |
| int pipefd[2]; | |
| /* Test on regular file */ | |
| fd = open("/dev/null", O_RDONLY); | |
| ASSERT_GE(fd, 0); | |
| ASSERT_LT(ioctl(fd, SIOCGSKNS), 0); | |
| ASSERT_TRUE(errno == ENOTTY || errno == EINVAL); | |
| close(fd); | |
| /* Test on pipe */ | |
| ASSERT_EQ(pipe(pipefd), 0); | |
| ASSERT_LT(ioctl(pipefd[0], SIOCGSKNS), 0); | |
| ASSERT_TRUE(errno == ENOTTY || errno == EINVAL); | |
| close(pipefd[0]); | |
| close(pipefd[1]); | |
| } | |
| /* | |
| * Test multiple sockets keep the same network namespace active. | |
| * Create multiple sockets, verify closing some doesn't affect others. | |
| */ | |
| TEST(siocgskns_multiple_sockets) | |
| { | |
| int socks[5]; | |
| int netns_fds[5]; | |
| int i; | |
| struct stat st; | |
| ino_t netns_ino; | |
| /* Create new network namespace */ | |
| ASSERT_EQ(unshare(CLONE_NEWNET), 0); | |
| /* Create multiple sockets */ | |
| for (i = 0; i < 5; i++) { | |
| socks[i] = socket(AF_INET, SOCK_STREAM, 0); | |
| ASSERT_GE(socks[i], 0); | |
| } | |
| /* Get netns from all sockets */ | |
| for (i = 0; i < 5; i++) { | |
| netns_fds[i] = ioctl(socks[i], SIOCGSKNS); | |
| if (netns_fds[i] < 0) { | |
| int j; | |
| for (j = 0; j <= i; j++) { | |
| close(socks[j]); | |
| if (j < i && netns_fds[j] >= 0) | |
| close(netns_fds[j]); | |
| } | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fds[i], 0); | |
| } | |
| } | |
| /* Verify all point to same netns */ | |
| ASSERT_EQ(fstat(netns_fds[0], &st), 0); | |
| netns_ino = st.st_ino; | |
| for (i = 1; i < 5; i++) { | |
| ASSERT_EQ(fstat(netns_fds[i], &st), 0); | |
| ASSERT_EQ(st.st_ino, netns_ino); | |
| } | |
| /* Close some sockets */ | |
| for (i = 0; i < 3; i++) { | |
| close(socks[i]); | |
| } | |
| /* Remaining netns FDs should still be valid */ | |
| for (i = 3; i < 5; i++) { | |
| char path[64]; | |
| snprintf(path, sizeof(path), "/proc/self/fd/%d", netns_fds[i]); | |
| int test_fd = open(path, O_RDONLY); | |
| ASSERT_GE(test_fd, 0); | |
| close(test_fd); | |
| } | |
| /* Cleanup */ | |
| for (i = 0; i < 5; i++) { | |
| if (i >= 3) | |
| close(socks[i]); | |
| close(netns_fds[i]); | |
| } | |
| } | |
| /* | |
| * Test socket keeps netns active after creating process exits. | |
| * Verify that as long as the socket FD exists, the namespace remains active. | |
| */ | |
| TEST(siocgskns_netns_lifecycle) | |
| { | |
| int sock_fd, netns_fd; | |
| int ipc_sockets[2]; | |
| int syncpipe[2]; | |
| pid_t pid; | |
| int status; | |
| char sync_byte; | |
| struct stat st; | |
| ino_t netns_ino; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| ASSERT_EQ(pipe(syncpipe), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| /* Child */ | |
| close(ipc_sockets[0]); | |
| close(syncpipe[1]); | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| close(ipc_sockets[1]); | |
| close(syncpipe[0]); | |
| exit(1); | |
| } | |
| sock_fd = socket(AF_INET, SOCK_STREAM, 0); | |
| if (sock_fd < 0) { | |
| close(ipc_sockets[1]); | |
| close(syncpipe[0]); | |
| exit(1); | |
| } | |
| /* Send socket to parent */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| close(syncpipe[0]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| /* Wait for parent signal */ | |
| read(syncpipe[0], &sync_byte, 1); | |
| close(syncpipe[0]); | |
| exit(0); | |
| } | |
| /* Parent */ | |
| close(ipc_sockets[1]); | |
| close(syncpipe[0]); | |
| /* Receive socket FD */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| ASSERT_EQ(n, 1); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| ASSERT_NE(cmsg, NULL); | |
| memcpy(&sock_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Get netns from socket while child is alive */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| sync_byte = 'G'; | |
| write(syncpipe[1], &sync_byte, 1); | |
| close(syncpipe[1]); | |
| close(sock_fd); | |
| waitpid(pid, NULL, 0); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| ASSERT_EQ(fstat(netns_fd, &st), 0); | |
| netns_ino = st.st_ino; | |
| /* Signal child to exit */ | |
| sync_byte = 'G'; | |
| write(syncpipe[1], &sync_byte, 1); | |
| close(syncpipe[1]); | |
| waitpid(pid, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| /* | |
| * Socket FD should still keep namespace active even after | |
| * the creating process exited. | |
| */ | |
| int test_fd = ioctl(sock_fd, SIOCGSKNS); | |
| ASSERT_GE(test_fd, 0); | |
| struct stat st_test; | |
| ASSERT_EQ(fstat(test_fd, &st_test), 0); | |
| ASSERT_EQ(st_test.st_ino, netns_ino); | |
| close(test_fd); | |
| close(netns_fd); | |
| /* Close socket - namespace should become inactive */ | |
| close(sock_fd); | |
| } | |
| /* | |
| * Test IPv6 sockets also work with SIOCGSKNS. | |
| */ | |
| TEST(siocgskns_ipv6) | |
| { | |
| int sock_fd, netns_fd, current_netns_fd; | |
| struct stat st1, st2; | |
| /* Create an IPv6 TCP socket */ | |
| sock_fd = socket(AF_INET6, SOCK_STREAM, 0); | |
| ASSERT_GE(sock_fd, 0); | |
| /* Use SIOCGSKNS */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| /* Verify it matches current namespace */ | |
| current_netns_fd = open("/proc/self/ns/net", O_RDONLY); | |
| ASSERT_GE(current_netns_fd, 0); | |
| ASSERT_EQ(fstat(netns_fd, &st1), 0); | |
| ASSERT_EQ(fstat(current_netns_fd, &st2), 0); | |
| ASSERT_EQ(st1.st_ino, st2.st_ino); | |
| close(sock_fd); | |
| close(netns_fd); | |
| close(current_netns_fd); | |
| } | |
| /* | |
| * Test that socket-kept netns appears in listns() output. | |
| * Verify that a network namespace kept alive by a socket FD appears in | |
| * listns() output even after the creating process exits, and that it | |
| * disappears when the socket is closed. | |
| */ | |
| TEST(siocgskns_listns_visibility) | |
| { | |
| int sock_fd, netns_fd, owner_fd; | |
| int ipc_sockets[2]; | |
| pid_t pid; | |
| int status; | |
| __u64 netns_id, owner_id; | |
| struct ns_id_req req = { | |
| .size = sizeof(req), | |
| .spare = 0, | |
| .ns_id = 0, | |
| .ns_type = CLONE_NEWNET, | |
| .spare2 = 0, | |
| .user_ns_id = 0, | |
| }; | |
| __u64 ns_ids[256]; | |
| int ret, i; | |
| bool found_netns = false; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| /* Child: create new netns and socket */ | |
| close(ipc_sockets[0]); | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| sock_fd = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (sock_fd < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Send socket FD to parent via SCM_RIGHTS */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(0); | |
| } | |
| /* Parent: receive socket FD */ | |
| close(ipc_sockets[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| ASSERT_EQ(n, 1); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| ASSERT_NE(cmsg, NULL); | |
| memcpy(&sock_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for child to exit */ | |
| waitpid(pid, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| ASSERT_EQ(WEXITSTATUS(status), 0); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| /* Get namespace ID */ | |
| ret = ioctl(netns_fd, NS_GET_ID, &netns_id); | |
| if (ret < 0) { | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_ID not supported"); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| /* Get owner user namespace */ | |
| owner_fd = ioctl(netns_fd, NS_GET_USERNS); | |
| if (owner_fd < 0) { | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_USERNS not supported"); | |
| ASSERT_GE(owner_fd, 0); | |
| } | |
| /* Get owner namespace ID */ | |
| ret = ioctl(owner_fd, NS_GET_ID, &owner_id); | |
| if (ret < 0) { | |
| close(owner_fd); | |
| close(sock_fd); | |
| close(netns_fd); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| close(owner_fd); | |
| /* Namespace should appear in listns() output */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| if (ret < 0) { | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOSYS) | |
| SKIP(return, "listns() not supported"); | |
| TH_LOG("listns failed: %s", strerror(errno)); | |
| ASSERT_GE(ret, 0); | |
| } | |
| /* Search for our network namespace in the list */ | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) { | |
| found_netns = true; | |
| break; | |
| } | |
| } | |
| ASSERT_TRUE(found_netns); | |
| TH_LOG("Found netns %llu in listns() output (kept alive by socket)", netns_id); | |
| /* Now verify with owner filtering */ | |
| req.user_ns_id = owner_id; | |
| found_netns = false; | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| ASSERT_GE(ret, 0); | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) { | |
| found_netns = true; | |
| break; | |
| } | |
| } | |
| ASSERT_TRUE(found_netns); | |
| TH_LOG("Found netns %llu owned by userns %llu", netns_id, owner_id); | |
| /* Close socket - namespace should become inactive and disappear from listns() */ | |
| close(sock_fd); | |
| close(netns_fd); | |
| /* Verify it's no longer in listns() output */ | |
| req.user_ns_id = 0; | |
| found_netns = false; | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| ASSERT_GE(ret, 0); | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) { | |
| found_netns = true; | |
| break; | |
| } | |
| } | |
| ASSERT_FALSE(found_netns); | |
| TH_LOG("Netns %llu correctly disappeared from listns() after socket closed", netns_id); | |
| } | |
| /* | |
| * Test that socket-kept netns can be reopened via file handle. | |
| * Verify that a network namespace kept alive by a socket FD can be | |
| * reopened using file handles even after the creating process exits. | |
| */ | |
| TEST(siocgskns_file_handle) | |
| { | |
| int sock_fd, netns_fd, reopened_fd; | |
| int ipc_sockets[2]; | |
| pid_t pid; | |
| int status; | |
| struct stat st1, st2; | |
| ino_t netns_ino; | |
| __u64 netns_id; | |
| struct file_handle *handle; | |
| struct nsfs_file_handle *nsfs_fh; | |
| int ret; | |
| /* Allocate file_handle structure for nsfs */ | |
| handle = malloc(sizeof(struct file_handle) + sizeof(struct nsfs_file_handle)); | |
| ASSERT_NE(handle, NULL); | |
| handle->handle_bytes = sizeof(struct nsfs_file_handle); | |
| handle->handle_type = FILEID_NSFS; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| /* Child: create new netns and socket */ | |
| close(ipc_sockets[0]); | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| sock_fd = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (sock_fd < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Send socket FD to parent via SCM_RIGHTS */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(0); | |
| } | |
| /* Parent: receive socket FD */ | |
| close(ipc_sockets[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| ASSERT_EQ(n, 1); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| ASSERT_NE(cmsg, NULL); | |
| memcpy(&sock_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for child to exit */ | |
| waitpid(pid, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| ASSERT_EQ(WEXITSTATUS(status), 0); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| ASSERT_EQ(fstat(netns_fd, &st1), 0); | |
| netns_ino = st1.st_ino; | |
| /* Get namespace ID */ | |
| ret = ioctl(netns_fd, NS_GET_ID, &netns_id); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_ID not supported"); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| /* Construct file handle from namespace ID */ | |
| nsfs_fh = (struct nsfs_file_handle *)handle->f_handle; | |
| nsfs_fh->ns_id = netns_id; | |
| nsfs_fh->ns_type = 0; /* Type field not needed for reopening */ | |
| nsfs_fh->ns_inum = 0; /* Inum field not needed for reopening */ | |
| TH_LOG("Constructed file handle for netns %lu (id=%llu)", netns_ino, netns_id); | |
| /* Reopen namespace using file handle (while socket still keeps it alive) */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == EOPNOTSUPP || errno == ENOSYS || errno == EBADF) | |
| SKIP(return, "open_by_handle_at with FD_NSFS_ROOT not supported"); | |
| TH_LOG("open_by_handle_at failed: %s", strerror(errno)); | |
| ASSERT_GE(reopened_fd, 0); | |
| } | |
| /* Verify it's the same namespace */ | |
| ASSERT_EQ(fstat(reopened_fd, &st2), 0); | |
| ASSERT_EQ(st1.st_ino, st2.st_ino); | |
| ASSERT_EQ(st1.st_dev, st2.st_dev); | |
| TH_LOG("Successfully reopened netns %lu via file handle", netns_ino); | |
| close(reopened_fd); | |
| /* Close the netns FD */ | |
| close(netns_fd); | |
| /* Try to reopen via file handle - should fail since namespace is now inactive */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| ASSERT_LT(reopened_fd, 0); | |
| TH_LOG("Correctly failed to reopen inactive netns: %s", strerror(errno)); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| /* Reopen namespace using file handle (while socket still keeps it alive) */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == EOPNOTSUPP || errno == ENOSYS || errno == EBADF) | |
| SKIP(return, "open_by_handle_at with FD_NSFS_ROOT not supported"); | |
| TH_LOG("open_by_handle_at failed: %s", strerror(errno)); | |
| ASSERT_GE(reopened_fd, 0); | |
| } | |
| /* Verify it's the same namespace */ | |
| ASSERT_EQ(fstat(reopened_fd, &st2), 0); | |
| ASSERT_EQ(st1.st_ino, st2.st_ino); | |
| ASSERT_EQ(st1.st_dev, st2.st_dev); | |
| TH_LOG("Successfully reopened netns %lu via file handle", netns_ino); | |
| /* Close socket - namespace should become inactive */ | |
| close(sock_fd); | |
| free(handle); | |
| } | |
| /* | |
| * Test combined listns() and file handle operations with socket-kept netns. | |
| * Create a netns, keep it alive with a socket, verify it appears in listns(), | |
| * then reopen it via file handle obtained from listns() entry. | |
| */ | |
| TEST(siocgskns_listns_and_file_handle) | |
| { | |
| int sock_fd, netns_fd, userns_fd, reopened_fd; | |
| int ipc_sockets[2]; | |
| pid_t pid; | |
| int status; | |
| struct stat st; | |
| ino_t netns_ino; | |
| __u64 netns_id, userns_id; | |
| struct ns_id_req req = { | |
| .size = sizeof(req), | |
| .spare = 0, | |
| .ns_id = 0, | |
| .ns_type = CLONE_NEWNET | CLONE_NEWUSER, | |
| .spare2 = 0, | |
| .user_ns_id = 0, | |
| }; | |
| __u64 ns_ids[256]; | |
| int ret, i; | |
| bool found_netns = false, found_userns = false; | |
| struct file_handle *handle; | |
| struct nsfs_file_handle *nsfs_fh; | |
| /* Allocate file_handle structure for nsfs */ | |
| handle = malloc(sizeof(struct file_handle) + sizeof(struct nsfs_file_handle)); | |
| ASSERT_NE(handle, NULL); | |
| handle->handle_bytes = sizeof(struct nsfs_file_handle); | |
| handle->handle_type = FILEID_NSFS; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| pid = fork(); | |
| ASSERT_GE(pid, 0); | |
| if (pid == 0) { | |
| /* Child: create new userns and netns with socket */ | |
| close(ipc_sockets[0]); | |
| if (setup_userns() < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| sock_fd = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (sock_fd < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Send socket FD to parent via SCM_RIGHTS */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_sockets[1]); | |
| exit(0); | |
| } | |
| /* Parent: receive socket FD */ | |
| close(ipc_sockets[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| ASSERT_EQ(n, 1); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| ASSERT_NE(cmsg, NULL); | |
| memcpy(&sock_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for child to exit */ | |
| waitpid(pid, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| ASSERT_EQ(WEXITSTATUS(status), 0); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| ASSERT_EQ(fstat(netns_fd, &st), 0); | |
| netns_ino = st.st_ino; | |
| /* Get namespace ID */ | |
| ret = ioctl(netns_fd, NS_GET_ID, &netns_id); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_ID not supported"); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| /* Get owner user namespace */ | |
| userns_fd = ioctl(netns_fd, NS_GET_USERNS); | |
| if (userns_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_USERNS not supported"); | |
| ASSERT_GE(userns_fd, 0); | |
| } | |
| /* Get owner namespace ID */ | |
| ret = ioctl(userns_fd, NS_GET_ID, &userns_id); | |
| if (ret < 0) { | |
| close(userns_fd); | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| close(userns_fd); | |
| TH_LOG("Testing netns %lu (id=%llu) owned by userns id=%llu", netns_ino, netns_id, userns_id); | |
| /* Verify namespace appears in listns() */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOSYS) | |
| SKIP(return, "listns() not supported"); | |
| TH_LOG("listns failed: %s", strerror(errno)); | |
| ASSERT_GE(ret, 0); | |
| } | |
| found_netns = false; | |
| found_userns = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) | |
| found_netns = true; | |
| if (ns_ids[i] == userns_id) | |
| found_userns = true; | |
| } | |
| ASSERT_TRUE(found_netns); | |
| ASSERT_TRUE(found_userns); | |
| TH_LOG("Found netns %llu in listns() output", netns_id); | |
| /* Construct file handle from namespace ID */ | |
| nsfs_fh = (struct nsfs_file_handle *)handle->f_handle; | |
| nsfs_fh->ns_id = netns_id; | |
| nsfs_fh->ns_type = 0; | |
| nsfs_fh->ns_inum = 0; | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == EOPNOTSUPP || errno == ENOSYS || errno == EBADF) | |
| SKIP(return, "open_by_handle_at with FD_NSFS_ROOT not supported"); | |
| TH_LOG("open_by_handle_at failed: %s", strerror(errno)); | |
| ASSERT_GE(reopened_fd, 0); | |
| } | |
| struct stat reopened_st; | |
| ASSERT_EQ(fstat(reopened_fd, &reopened_st), 0); | |
| ASSERT_EQ(reopened_st.st_ino, netns_ino); | |
| TH_LOG("Successfully reopened netns %lu via file handle (socket-kept)", netns_ino); | |
| close(reopened_fd); | |
| close(netns_fd); | |
| /* Try to reopen via file handle - should fail since namespace is now inactive */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| ASSERT_LT(reopened_fd, 0); | |
| TH_LOG("Correctly failed to reopen inactive netns: %s", strerror(errno)); | |
| /* Get network namespace from socket */ | |
| netns_fd = ioctl(sock_fd, SIOCGSKNS); | |
| if (netns_fd < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_fd, 0); | |
| } | |
| /* Verify namespace appears in listns() */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOSYS) | |
| SKIP(return, "listns() not supported"); | |
| TH_LOG("listns failed: %s", strerror(errno)); | |
| ASSERT_GE(ret, 0); | |
| } | |
| found_netns = false; | |
| found_userns = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) | |
| found_netns = true; | |
| if (ns_ids[i] == userns_id) | |
| found_userns = true; | |
| } | |
| ASSERT_TRUE(found_netns); | |
| ASSERT_TRUE(found_userns); | |
| TH_LOG("Found netns %llu in listns() output", netns_id); | |
| close(netns_fd); | |
| /* Verify namespace appears in listns() */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_fd); | |
| close(netns_fd); | |
| if (errno == ENOSYS) | |
| SKIP(return, "listns() not supported"); | |
| TH_LOG("listns failed: %s", strerror(errno)); | |
| ASSERT_GE(ret, 0); | |
| } | |
| found_netns = false; | |
| found_userns = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_id) | |
| found_netns = true; | |
| if (ns_ids[i] == userns_id) | |
| found_userns = true; | |
| } | |
| ASSERT_FALSE(found_netns); | |
| ASSERT_FALSE(found_userns); | |
| TH_LOG("Netns %llu correctly disappeared from listns() after socket closed", netns_id); | |
| close(sock_fd); | |
| free(handle); | |
| } | |
| /* | |
| * Test multi-level namespace resurrection across three user namespace levels. | |
| * | |
| * This test creates a complex namespace hierarchy with three levels of user | |
| * namespaces and a network namespace at the deepest level. It verifies that | |
| * the resurrection semantics work correctly when SIOCGSKNS is called on a | |
| * socket from an inactive namespace tree, and that listns() and | |
| * open_by_handle_at() correctly respect visibility rules. | |
| * | |
| * Hierarchy after child processes exit (all with 0 active refcount): | |
| * | |
| * net_L3A (0) <- Level 3 network namespace | |
| * | | |
| * + | |
| * userns_L3 (0) <- Level 3 user namespace | |
| * | | |
| * + | |
| * userns_L2 (0) <- Level 2 user namespace | |
| * | | |
| * + | |
| * userns_L1 (0) <- Level 1 user namespace | |
| * | | |
| * x | |
| * init_user_ns | |
| * | |
| * The test verifies: | |
| * 1. SIOCGSKNS on a socket from inactive net_L3A resurrects the entire chain | |
| * 2. After resurrection, all namespaces are visible in listns() | |
| * 3. Resurrected namespaces can be reopened via file handles | |
| * 4. Closing the netns FD cascades down: the entire ownership chain | |
| * (userns_L3 -> userns_L2 -> userns_L1) becomes inactive again | |
| * 5. Inactive namespaces disappear from listns() and cannot be reopened | |
| * 6. Calling SIOCGSKNS again on the same socket resurrects the tree again | |
| * 7. After second resurrection, namespaces are visible and can be reopened | |
| */ | |
| TEST(siocgskns_multilevel_resurrection) | |
| { | |
| int ipc_sockets[2]; | |
| pid_t pid_l1, pid_l2, pid_l3; | |
| int status; | |
| /* Namespace file descriptors to be received from child */ | |
| int sock_L3A_fd = -1; | |
| int netns_L3A_fd = -1; | |
| __u64 netns_L3A_id; | |
| __u64 userns_L1_id, userns_L2_id, userns_L3_id; | |
| /* For listns() and file handle testing */ | |
| struct ns_id_req req = { | |
| .size = sizeof(req), | |
| .spare = 0, | |
| .ns_id = 0, | |
| .ns_type = CLONE_NEWNET | CLONE_NEWUSER, | |
| .spare2 = 0, | |
| .user_ns_id = 0, | |
| }; | |
| __u64 ns_ids[256]; | |
| int ret, i; | |
| struct file_handle *handle; | |
| struct nsfs_file_handle *nsfs_fh; | |
| int reopened_fd; | |
| /* Allocate file handle for testing */ | |
| handle = malloc(sizeof(struct file_handle) + sizeof(struct nsfs_file_handle)); | |
| ASSERT_NE(handle, NULL); | |
| handle->handle_bytes = sizeof(struct nsfs_file_handle); | |
| handle->handle_type = FILEID_NSFS; | |
| EXPECT_EQ(socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets), 0); | |
| /* | |
| * Fork level 1 child that creates userns_L1 | |
| */ | |
| pid_l1 = fork(); | |
| ASSERT_GE(pid_l1, 0); | |
| if (pid_l1 == 0) { | |
| /* Level 1 child */ | |
| int ipc_L2[2]; | |
| close(ipc_sockets[0]); | |
| /* Create userns_L1 */ | |
| if (setup_userns() < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* Create socketpair for communicating with L2 child */ | |
| if (socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_L2) < 0) { | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| /* | |
| * Fork level 2 child that creates userns_L2 | |
| */ | |
| pid_l2 = fork(); | |
| if (pid_l2 < 0) { | |
| close(ipc_sockets[1]); | |
| close(ipc_L2[0]); | |
| close(ipc_L2[1]); | |
| exit(1); | |
| } | |
| if (pid_l2 == 0) { | |
| /* Level 2 child */ | |
| int ipc_L3[2]; | |
| close(ipc_L2[0]); | |
| /* Create userns_L2 (nested inside userns_L1) */ | |
| if (setup_userns() < 0) { | |
| close(ipc_L2[1]); | |
| exit(1); | |
| } | |
| /* Create socketpair for communicating with L3 child */ | |
| if (socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_L3) < 0) { | |
| close(ipc_L2[1]); | |
| exit(1); | |
| } | |
| /* | |
| * Fork level 3 child that creates userns_L3 and network namespaces | |
| */ | |
| pid_l3 = fork(); | |
| if (pid_l3 < 0) { | |
| close(ipc_L2[1]); | |
| close(ipc_L3[0]); | |
| close(ipc_L3[1]); | |
| exit(1); | |
| } | |
| if (pid_l3 == 0) { | |
| /* Level 3 child - the deepest level */ | |
| int sock_fd; | |
| close(ipc_L3[0]); | |
| /* Create userns_L3 (nested inside userns_L2) */ | |
| if (setup_userns() < 0) { | |
| close(ipc_L3[1]); | |
| exit(1); | |
| } | |
| /* Create network namespace at level 3 */ | |
| if (unshare(CLONE_NEWNET) < 0) { | |
| close(ipc_L3[1]); | |
| exit(1); | |
| } | |
| /* Create socket in net_L3A */ | |
| sock_fd = socket(AF_INET, SOCK_DGRAM, 0); | |
| if (sock_fd < 0) { | |
| close(ipc_L3[1]); | |
| exit(1); | |
| } | |
| /* Send socket FD to L2 parent */ | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1] = {'X'}; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &sock_fd, sizeof(int)); | |
| if (sendmsg(ipc_L3[1], &msg, 0) < 0) { | |
| close(sock_fd); | |
| close(ipc_L3[1]); | |
| exit(1); | |
| } | |
| close(sock_fd); | |
| close(ipc_L3[1]); | |
| exit(0); | |
| } | |
| /* Level 2 child - receive from L3 and forward to L1 */ | |
| close(ipc_L3[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| int received_fd; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_L3[0], &msg, 0); | |
| close(ipc_L3[0]); | |
| if (n != 1) { | |
| close(ipc_L2[1]); | |
| waitpid(pid_l3, NULL, 0); | |
| exit(1); | |
| } | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| if (!cmsg) { | |
| close(ipc_L2[1]); | |
| waitpid(pid_l3, NULL, 0); | |
| exit(1); | |
| } | |
| memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for L3 child */ | |
| waitpid(pid_l3, NULL, 0); | |
| /* Forward the socket FD to L1 parent */ | |
| memset(&msg, 0, sizeof(msg)); | |
| buf[0] = 'Y'; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &received_fd, sizeof(int)); | |
| if (sendmsg(ipc_L2[1], &msg, 0) < 0) { | |
| close(received_fd); | |
| close(ipc_L2[1]); | |
| exit(1); | |
| } | |
| close(received_fd); | |
| close(ipc_L2[1]); | |
| exit(0); | |
| } | |
| /* Level 1 child - receive from L2 and forward to parent */ | |
| close(ipc_L2[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| int received_fd; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_L2[0], &msg, 0); | |
| close(ipc_L2[0]); | |
| if (n != 1) { | |
| close(ipc_sockets[1]); | |
| waitpid(pid_l2, NULL, 0); | |
| exit(1); | |
| } | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| if (!cmsg) { | |
| close(ipc_sockets[1]); | |
| waitpid(pid_l2, NULL, 0); | |
| exit(1); | |
| } | |
| memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for L2 child */ | |
| waitpid(pid_l2, NULL, 0); | |
| /* Forward the socket FD to parent */ | |
| memset(&msg, 0, sizeof(msg)); | |
| buf[0] = 'Z'; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| cmsg = CMSG_FIRSTHDR(&msg); | |
| cmsg->cmsg_level = SOL_SOCKET; | |
| cmsg->cmsg_type = SCM_RIGHTS; | |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); | |
| memcpy(CMSG_DATA(cmsg), &received_fd, sizeof(int)); | |
| if (sendmsg(ipc_sockets[1], &msg, 0) < 0) { | |
| close(received_fd); | |
| close(ipc_sockets[1]); | |
| exit(1); | |
| } | |
| close(received_fd); | |
| close(ipc_sockets[1]); | |
| exit(0); | |
| } | |
| /* Parent - receive the socket from the deepest level */ | |
| close(ipc_sockets[1]); | |
| struct msghdr msg = {0}; | |
| struct iovec iov = {0}; | |
| char buf[1]; | |
| char cmsg_buf[CMSG_SPACE(sizeof(int))]; | |
| iov.iov_base = buf; | |
| iov.iov_len = 1; | |
| msg.msg_iov = &iov; | |
| msg.msg_iovlen = 1; | |
| msg.msg_control = cmsg_buf; | |
| msg.msg_controllen = sizeof(cmsg_buf); | |
| ssize_t n = recvmsg(ipc_sockets[0], &msg, 0); | |
| close(ipc_sockets[0]); | |
| if (n != 1) { | |
| free(handle); | |
| waitpid(pid_l1, NULL, 0); | |
| SKIP(return, "Failed to receive socket from child"); | |
| } | |
| struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); | |
| if (!cmsg) { | |
| free(handle); | |
| waitpid(pid_l1, NULL, 0); | |
| SKIP(return, "Failed to receive socket from child"); | |
| } | |
| memcpy(&sock_L3A_fd, CMSG_DATA(cmsg), sizeof(int)); | |
| /* Wait for L1 child */ | |
| waitpid(pid_l1, &status, 0); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| ASSERT_EQ(WEXITSTATUS(status), 0); | |
| /* | |
| * At this point, all child processes have exited. The socket itself | |
| * doesn't keep the namespace active - we need to call SIOCGSKNS which | |
| * will resurrect the entire namespace tree by taking active references. | |
| */ | |
| /* Get network namespace from socket - this resurrects the tree */ | |
| netns_L3A_fd = ioctl(sock_L3A_fd, SIOCGSKNS); | |
| if (netns_L3A_fd < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "SIOCGSKNS not supported"); | |
| ASSERT_GE(netns_L3A_fd, 0); | |
| } | |
| /* Get namespace ID for net_L3A */ | |
| ret = ioctl(netns_L3A_fd, NS_GET_ID, &netns_L3A_id); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_ID not supported"); | |
| ASSERT_EQ(ret, 0); | |
| } | |
| /* Get owner user namespace chain: userns_L3 -> userns_L2 -> userns_L1 */ | |
| int userns_L3_fd = ioctl(netns_L3A_fd, NS_GET_USERNS); | |
| if (userns_L3_fd < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| if (errno == ENOTTY || errno == EINVAL) | |
| SKIP(return, "NS_GET_USERNS not supported"); | |
| ASSERT_GE(userns_L3_fd, 0); | |
| } | |
| ret = ioctl(userns_L3_fd, NS_GET_ID, &userns_L3_id); | |
| ASSERT_EQ(ret, 0); | |
| int userns_L2_fd = ioctl(userns_L3_fd, NS_GET_USERNS); | |
| ASSERT_GE(userns_L2_fd, 0); | |
| ret = ioctl(userns_L2_fd, NS_GET_ID, &userns_L2_id); | |
| ASSERT_EQ(ret, 0); | |
| int userns_L1_fd = ioctl(userns_L2_fd, NS_GET_USERNS); | |
| ASSERT_GE(userns_L1_fd, 0); | |
| ret = ioctl(userns_L1_fd, NS_GET_ID, &userns_L1_id); | |
| ASSERT_EQ(ret, 0); | |
| close(userns_L1_fd); | |
| close(userns_L2_fd); | |
| close(userns_L3_fd); | |
| TH_LOG("Multi-level hierarchy: net_L3A (id=%llu) -> userns_L3 (id=%llu) -> userns_L2 (id=%llu) -> userns_L1 (id=%llu)", | |
| netns_L3A_id, userns_L3_id, userns_L2_id, userns_L1_id); | |
| /* | |
| * Test 1: Verify net_L3A is visible in listns() after resurrection. | |
| * The entire ownership chain should be resurrected and visible. | |
| */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| if (ret < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| if (errno == ENOSYS) | |
| SKIP(return, "listns() not supported"); | |
| ASSERT_GE(ret, 0); | |
| } | |
| bool found_netns_L3A = false; | |
| bool found_userns_L1 = false; | |
| bool found_userns_L2 = false; | |
| bool found_userns_L3 = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_L3A_id) | |
| found_netns_L3A = true; | |
| if (ns_ids[i] == userns_L1_id) | |
| found_userns_L1 = true; | |
| if (ns_ids[i] == userns_L2_id) | |
| found_userns_L2 = true; | |
| if (ns_ids[i] == userns_L3_id) | |
| found_userns_L3 = true; | |
| } | |
| ASSERT_TRUE(found_netns_L3A); | |
| ASSERT_TRUE(found_userns_L1); | |
| ASSERT_TRUE(found_userns_L2); | |
| ASSERT_TRUE(found_userns_L3); | |
| TH_LOG("Resurrection verified: all namespaces in hierarchy visible in listns()"); | |
| /* | |
| * Test 2: Verify net_L3A can be reopened via file handle. | |
| */ | |
| nsfs_fh = (struct nsfs_file_handle *)handle->f_handle; | |
| nsfs_fh->ns_id = netns_L3A_id; | |
| nsfs_fh->ns_type = 0; | |
| nsfs_fh->ns_inum = 0; | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| if (errno == EOPNOTSUPP || errno == ENOSYS || errno == EBADF) | |
| SKIP(return, "open_by_handle_at with FD_NSFS_ROOT not supported"); | |
| TH_LOG("open_by_handle_at failed: %s", strerror(errno)); | |
| ASSERT_GE(reopened_fd, 0); | |
| } | |
| close(reopened_fd); | |
| TH_LOG("File handle test passed: net_L3A can be reopened"); | |
| /* | |
| * Test 3: Verify that when we close the netns FD (dropping the last | |
| * active reference), the entire tree becomes inactive and disappears | |
| * from listns(). The cascade goes: net_L3A drops -> userns_L3 drops -> | |
| * userns_L2 drops -> userns_L1 drops. | |
| */ | |
| close(netns_L3A_fd); | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| ASSERT_GE(ret, 0); | |
| found_netns_L3A = false; | |
| found_userns_L1 = false; | |
| found_userns_L2 = false; | |
| found_userns_L3 = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_L3A_id) | |
| found_netns_L3A = true; | |
| if (ns_ids[i] == userns_L1_id) | |
| found_userns_L1 = true; | |
| if (ns_ids[i] == userns_L2_id) | |
| found_userns_L2 = true; | |
| if (ns_ids[i] == userns_L3_id) | |
| found_userns_L3 = true; | |
| } | |
| ASSERT_FALSE(found_netns_L3A); | |
| ASSERT_FALSE(found_userns_L1); | |
| ASSERT_FALSE(found_userns_L2); | |
| ASSERT_FALSE(found_userns_L3); | |
| TH_LOG("Cascade test passed: all namespaces disappeared after netns FD closed"); | |
| /* | |
| * Test 4: Verify file handle no longer works for inactive namespace. | |
| */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd >= 0) { | |
| close(reopened_fd); | |
| free(handle); | |
| ASSERT_TRUE(false); /* Should have failed */ | |
| } | |
| TH_LOG("Inactive namespace correctly cannot be reopened via file handle"); | |
| /* | |
| * Test 5: Verify that calling SIOCGSKNS again resurrects the tree again. | |
| * The socket is still valid, so we can call SIOCGSKNS on it to resurrect | |
| * the namespace tree once more. | |
| */ | |
| netns_L3A_fd = ioctl(sock_L3A_fd, SIOCGSKNS); | |
| ASSERT_GE(netns_L3A_fd, 0); | |
| TH_LOG("Called SIOCGSKNS again to resurrect the namespace tree"); | |
| /* Verify the namespace tree is resurrected and visible in listns() */ | |
| ret = sys_listns(&req, ns_ids, ARRAY_SIZE(ns_ids), 0); | |
| ASSERT_GE(ret, 0); | |
| found_netns_L3A = false; | |
| found_userns_L1 = false; | |
| found_userns_L2 = false; | |
| found_userns_L3 = false; | |
| for (i = 0; i < ret; i++) { | |
| if (ns_ids[i] == netns_L3A_id) | |
| found_netns_L3A = true; | |
| if (ns_ids[i] == userns_L1_id) | |
| found_userns_L1 = true; | |
| if (ns_ids[i] == userns_L2_id) | |
| found_userns_L2 = true; | |
| if (ns_ids[i] == userns_L3_id) | |
| found_userns_L3 = true; | |
| } | |
| ASSERT_TRUE(found_netns_L3A); | |
| ASSERT_TRUE(found_userns_L1); | |
| ASSERT_TRUE(found_userns_L2); | |
| ASSERT_TRUE(found_userns_L3); | |
| TH_LOG("Second resurrection verified: all namespaces in hierarchy visible in listns() again"); | |
| /* Verify we can reopen via file handle again */ | |
| reopened_fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY); | |
| if (reopened_fd < 0) { | |
| free(handle); | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| TH_LOG("open_by_handle_at failed after second resurrection: %s", strerror(errno)); | |
| ASSERT_GE(reopened_fd, 0); | |
| } | |
| close(reopened_fd); | |
| TH_LOG("File handle test passed: net_L3A can be reopened after second resurrection"); | |
| /* Final cleanup */ | |
| close(sock_L3A_fd); | |
| close(netns_L3A_fd); | |
| free(handle); | |
| } | |
| TEST_HARNESS_MAIN |