| // 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 |