// SPDX-License-Identifier: GPL-2.0 #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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/. */ 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