linux/tools/testing/selftests/namespaces/ns_active_ref_test.c

829 lines
21 KiB
C

// 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 <linux/nsfs.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../kselftest_harness.h"
#include "../filesystems/utils.h"
#ifndef FD_NSFS_ROOT
#define FD_NSFS_ROOT -10003 /* Root of the nsfs filesystem */
#endif
#ifndef FILEID_NSFS
#define FILEID_NSFS 0xf1
#endif
/*
* Test that initial namespaces can be reopened via file handle.
* Initial namespaces should have active ref count of 1 from boot.
*/
TEST(init_ns_always_active)
{
struct file_handle *handle;
int mount_id;
int ret;
int fd1, fd2;
struct stat st1, st2;
handle = malloc(sizeof(*handle) + MAX_HANDLE_SZ);
ASSERT_NE(handle, NULL);
/* Open initial network namespace */
fd1 = open("/proc/1/ns/net", O_RDONLY);
ASSERT_GE(fd1, 0);
/* Get file handle for initial namespace */
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(fd1, "", handle, &mount_id, AT_EMPTY_PATH);
if (ret < 0 && errno == EOPNOTSUPP) {
SKIP(free(handle); close(fd1);
return, "nsfs doesn't support file handles");
}
ASSERT_EQ(ret, 0);
/* Close the namespace fd */
close(fd1);
/* Try to reopen via file handle - should succeed since init ns is always active */
fd2 = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
if (fd2 < 0 && (errno == EINVAL || errno == EOPNOTSUPP)) {
SKIP(free(handle);
return, "open_by_handle_at with FD_NSFS_ROOT not supported");
}
ASSERT_GE(fd2, 0);
/* Verify we opened the same namespace */
fd1 = open("/proc/1/ns/net", O_RDONLY);
ASSERT_GE(fd1, 0);
ASSERT_EQ(fstat(fd1, &st1), 0);
ASSERT_EQ(fstat(fd2, &st2), 0);
ASSERT_EQ(st1.st_ino, st2.st_ino);
close(fd1);
close(fd2);
free(handle);
}
/*
* Test namespace lifecycle: create a namespace in a child process,
* get a file handle while it's active, then try to reopen after
* the process exits (namespace becomes inactive).
*/
TEST(ns_inactive_after_exit)
{
struct file_handle *handle;
int mount_id;
int ret;
int fd;
int pipefd[2];
pid_t pid;
int status;
char buf[sizeof(*handle) + MAX_HANDLE_SZ];
/* Create pipe for passing file handle from child */
ASSERT_EQ(pipe(pipefd), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Child process */
close(pipefd[0]);
/* Create new network namespace */
ret = unshare(CLONE_NEWNET);
if (ret < 0) {
close(pipefd[1]);
exit(1);
}
/* Open our new namespace */
fd = open("/proc/self/ns/net", O_RDONLY);
if (fd < 0) {
close(pipefd[1]);
exit(1);
}
/* Get file handle for the namespace */
handle = (struct file_handle *)buf;
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(fd, "", handle, &mount_id, AT_EMPTY_PATH);
close(fd);
if (ret < 0) {
close(pipefd[1]);
exit(1);
}
/* Send handle to parent */
write(pipefd[1], buf, sizeof(*handle) + handle->handle_bytes);
close(pipefd[1]);
/* Exit - namespace should become inactive */
exit(0);
}
/* Parent process */
close(pipefd[1]);
/* Read file handle from child */
ret = read(pipefd[0], buf, sizeof(buf));
close(pipefd[0]);
/* Wait for child to exit */
waitpid(pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 0);
ASSERT_GT(ret, 0);
handle = (struct file_handle *)buf;
/* Try to reopen namespace - should fail with ENOENT since it's inactive */
fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_LT(fd, 0);
/* Should fail with ENOENT (namespace inactive) or ESTALE */
ASSERT_TRUE(errno == ENOENT || errno == ESTALE);
}
/*
* Test that a namespace remains active while a process is using it,
* even after the creating process exits.
*/
TEST(ns_active_with_multiple_processes)
{
struct file_handle *handle;
int mount_id;
int ret;
int fd;
int pipefd[2];
int syncpipe[2];
pid_t pid1, pid2;
int status;
char buf[sizeof(*handle) + MAX_HANDLE_SZ];
char sync_byte;
/* Create pipes for communication */
ASSERT_EQ(pipe(pipefd), 0);
ASSERT_EQ(pipe(syncpipe), 0);
pid1 = fork();
ASSERT_GE(pid1, 0);
if (pid1 == 0) {
/* First child - creates namespace */
close(pipefd[0]);
close(syncpipe[1]);
/* Create new network namespace */
ret = unshare(CLONE_NEWNET);
if (ret < 0) {
close(pipefd[1]);
close(syncpipe[0]);
exit(1);
}
/* Open and get handle */
fd = open("/proc/self/ns/net", O_RDONLY);
if (fd < 0) {
close(pipefd[1]);
close(syncpipe[0]);
exit(1);
}
handle = (struct file_handle *)buf;
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(fd, "", handle, &mount_id, AT_EMPTY_PATH);
close(fd);
if (ret < 0) {
close(pipefd[1]);
close(syncpipe[0]);
exit(1);
}
/* Send handle to parent */
write(pipefd[1], buf, sizeof(*handle) + handle->handle_bytes);
close(pipefd[1]);
/* Wait for signal before exiting */
read(syncpipe[0], &sync_byte, 1);
close(syncpipe[0]);
exit(0);
}
/* Parent reads handle */
close(pipefd[1]);
ret = read(pipefd[0], buf, sizeof(buf));
close(pipefd[0]);
ASSERT_GT(ret, 0);
handle = (struct file_handle *)buf;
/* Create second child that will keep namespace active */
pid2 = fork();
ASSERT_GE(pid2, 0);
if (pid2 == 0) {
/* Second child - reopens the namespace */
close(syncpipe[0]);
close(syncpipe[1]);
/* Open the namespace via handle */
fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
if (fd < 0) {
exit(1);
}
/* Join the namespace */
ret = setns(fd, CLONE_NEWNET);
close(fd);
if (ret < 0) {
exit(1);
}
/* Sleep to keep namespace active */
sleep(1);
exit(0);
}
/* Let second child enter the namespace */
usleep(100000); /* 100ms */
/* Signal first child to exit */
close(syncpipe[0]);
sync_byte = 'X';
write(syncpipe[1], &sync_byte, 1);
close(syncpipe[1]);
/* Wait for first child */
waitpid(pid1, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
/* Namespace should still be active because second child is using it */
fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_GE(fd, 0);
close(fd);
/* Wait for second child */
waitpid(pid2, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
}
/*
* Test user namespace active ref tracking via credential lifecycle
*/
TEST(userns_active_ref_lifecycle)
{
struct file_handle *handle;
int mount_id;
int ret;
int fd;
int pipefd[2];
pid_t pid;
int status;
char buf[sizeof(*handle) + MAX_HANDLE_SZ];
ASSERT_EQ(pipe(pipefd), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Child process */
close(pipefd[0]);
/* Create new user namespace */
ret = unshare(CLONE_NEWUSER);
if (ret < 0) {
close(pipefd[1]);
exit(1);
}
/* Set up uid/gid mappings */
int uid_map_fd = open("/proc/self/uid_map", O_WRONLY);
int gid_map_fd = open("/proc/self/gid_map", O_WRONLY);
int setgroups_fd = open("/proc/self/setgroups", O_WRONLY);
if (uid_map_fd >= 0 && gid_map_fd >= 0 && setgroups_fd >= 0) {
write(setgroups_fd, "deny", 4);
close(setgroups_fd);
char mapping[64];
snprintf(mapping, sizeof(mapping), "0 %d 1", getuid());
write(uid_map_fd, mapping, strlen(mapping));
close(uid_map_fd);
snprintf(mapping, sizeof(mapping), "0 %d 1", getgid());
write(gid_map_fd, mapping, strlen(mapping));
close(gid_map_fd);
}
/* Get file handle */
fd = open("/proc/self/ns/user", O_RDONLY);
if (fd < 0) {
close(pipefd[1]);
exit(1);
}
handle = (struct file_handle *)buf;
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(fd, "", handle, &mount_id, AT_EMPTY_PATH);
close(fd);
if (ret < 0) {
close(pipefd[1]);
exit(1);
}
/* Send handle to parent */
write(pipefd[1], buf, sizeof(*handle) + handle->handle_bytes);
close(pipefd[1]);
exit(0);
}
/* Parent */
close(pipefd[1]);
ret = read(pipefd[0], buf, sizeof(buf));
close(pipefd[0]);
waitpid(pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 0);
ASSERT_GT(ret, 0);
handle = (struct file_handle *)buf;
/* Namespace should be inactive after all tasks exit */
fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_LT(fd, 0);
ASSERT_TRUE(errno == ENOENT || errno == ESTALE);
}
/*
* Test PID namespace active ref tracking
*/
TEST(pidns_active_ref_lifecycle)
{
struct file_handle *handle;
int mount_id;
int ret;
int fd;
int pipefd[2];
pid_t pid;
int status;
char buf[sizeof(*handle) + MAX_HANDLE_SZ];
ASSERT_EQ(pipe(pipefd), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Child process */
close(pipefd[0]);
/* Create new PID namespace */
ret = unshare(CLONE_NEWPID);
if (ret < 0) {
close(pipefd[1]);
exit(1);
}
/* Fork to actually enter the PID namespace */
pid_t child = fork();
if (child < 0) {
close(pipefd[1]);
exit(1);
}
if (child == 0) {
/* Grandchild - in new PID namespace */
fd = open("/proc/self/ns/pid", O_RDONLY);
if (fd < 0) {
exit(1);
}
handle = (struct file_handle *)buf;
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(fd, "", handle, &mount_id, AT_EMPTY_PATH);
close(fd);
if (ret < 0) {
exit(1);
}
/* Send handle to grandparent */
write(pipefd[1], buf, sizeof(*handle) + handle->handle_bytes);
close(pipefd[1]);
exit(0);
}
/* Wait for grandchild */
waitpid(child, NULL, 0);
exit(0);
}
/* Parent */
close(pipefd[1]);
ret = read(pipefd[0], buf, sizeof(buf));
close(pipefd[0]);
waitpid(pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 0);
ASSERT_GT(ret, 0);
handle = (struct file_handle *)buf;
/* Namespace should be inactive after all processes exit */
fd = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_LT(fd, 0);
ASSERT_TRUE(errno == ENOENT || errno == ESTALE);
}
/*
* Test that an open file descriptor keeps a namespace active.
* Even after the creating process exits, the namespace should remain
* active as long as an fd is held open.
*/
TEST(ns_fd_keeps_active)
{
struct file_handle *handle;
int mount_id;
int ret;
int nsfd;
int pipe_child_ready[2];
int pipe_parent_ready[2];
pid_t pid;
int status;
char buf[sizeof(*handle) + MAX_HANDLE_SZ];
char sync_byte;
char proc_path[64];
ASSERT_EQ(pipe(pipe_child_ready), 0);
ASSERT_EQ(pipe(pipe_parent_ready), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Child process */
close(pipe_child_ready[0]);
close(pipe_parent_ready[1]);
TH_LOG("Child: creating new network namespace");
/* Create new network namespace */
ret = unshare(CLONE_NEWNET);
if (ret < 0) {
TH_LOG("Child: unshare(CLONE_NEWNET) failed: %s", strerror(errno));
close(pipe_child_ready[1]);
close(pipe_parent_ready[0]);
exit(1);
}
TH_LOG("Child: network namespace created successfully");
/* Get file handle for the namespace */
nsfd = open("/proc/self/ns/net", O_RDONLY);
if (nsfd < 0) {
TH_LOG("Child: failed to open /proc/self/ns/net: %s", strerror(errno));
close(pipe_child_ready[1]);
close(pipe_parent_ready[0]);
exit(1);
}
TH_LOG("Child: opened namespace fd %d", nsfd);
handle = (struct file_handle *)buf;
handle->handle_bytes = MAX_HANDLE_SZ;
ret = name_to_handle_at(nsfd, "", handle, &mount_id, AT_EMPTY_PATH);
close(nsfd);
if (ret < 0) {
TH_LOG("Child: name_to_handle_at failed: %s", strerror(errno));
close(pipe_child_ready[1]);
close(pipe_parent_ready[0]);
exit(1);
}
TH_LOG("Child: got file handle (bytes=%u)", handle->handle_bytes);
/* Send file handle to parent */
ret = write(pipe_child_ready[1], buf, sizeof(*handle) + handle->handle_bytes);
TH_LOG("Child: sent %d bytes of file handle to parent", ret);
close(pipe_child_ready[1]);
/* Wait for parent to open the fd */
TH_LOG("Child: waiting for parent to open fd");
ret = read(pipe_parent_ready[0], &sync_byte, 1);
close(pipe_parent_ready[0]);
TH_LOG("Child: parent signaled (read %d bytes), exiting now", ret);
/* Exit - namespace should stay active because parent holds fd */
exit(0);
}
/* Parent process */
close(pipe_child_ready[1]);
close(pipe_parent_ready[0]);
TH_LOG("Parent: reading file handle from child");
/* Read file handle from child */
ret = read(pipe_child_ready[0], buf, sizeof(buf));
close(pipe_child_ready[0]);
ASSERT_GT(ret, 0);
handle = (struct file_handle *)buf;
TH_LOG("Parent: received %d bytes, handle size=%u", ret, handle->handle_bytes);
/* Open the child's namespace while it's still alive */
snprintf(proc_path, sizeof(proc_path), "/proc/%d/ns/net", pid);
TH_LOG("Parent: opening child's namespace at %s", proc_path);
nsfd = open(proc_path, O_RDONLY);
if (nsfd < 0) {
TH_LOG("Parent: failed to open %s: %s", proc_path, strerror(errno));
close(pipe_parent_ready[1]);
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
SKIP(return, "Failed to open child's namespace");
}
TH_LOG("Parent: opened child's namespace, got fd %d", nsfd);
/* Signal child that we have the fd */
sync_byte = 'G';
write(pipe_parent_ready[1], &sync_byte, 1);
close(pipe_parent_ready[1]);
TH_LOG("Parent: signaled child that we have the fd");
/* Wait for child to exit */
waitpid(pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 0);
TH_LOG("Child exited, parent holds fd %d to namespace", nsfd);
/*
* Namespace should still be ACTIVE because we hold an fd.
* We should be able to reopen it via file handle.
*/
TH_LOG("Attempting to reopen namespace via file handle (should succeed - fd held)");
int fd2 = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_GE(fd2, 0);
TH_LOG("Successfully reopened namespace via file handle, got fd %d", fd2);
/* Verify it's the same namespace */
struct stat st1, st2;
ASSERT_EQ(fstat(nsfd, &st1), 0);
ASSERT_EQ(fstat(fd2, &st2), 0);
TH_LOG("Namespace inodes: nsfd=%lu, fd2=%lu", st1.st_ino, st2.st_ino);
ASSERT_EQ(st1.st_ino, st2.st_ino);
close(fd2);
/* Now close the fd - namespace should become inactive */
TH_LOG("Closing fd %d - namespace should become inactive", nsfd);
close(nsfd);
/* Now reopening should fail - namespace is inactive */
TH_LOG("Attempting to reopen namespace via file handle (should fail - inactive)");
fd2 = open_by_handle_at(FD_NSFS_ROOT, handle, O_RDONLY);
ASSERT_LT(fd2, 0);
/* Should fail with ENOENT (inactive) or ESTALE (gone) */
TH_LOG("Reopen failed as expected: %s (errno=%d)", strerror(errno), errno);
ASSERT_TRUE(errno == ENOENT || errno == ESTALE);
}
/*
* Test hierarchical active reference propagation.
* When a child namespace is active, its owning user namespace should also
* be active automatically due to hierarchical active reference propagation.
* This ensures parents are always reachable when children are active.
*/
TEST(ns_parent_always_reachable)
{
struct file_handle *parent_handle, *child_handle;
int ret;
int child_nsfd;
int pipefd[2];
pid_t pid;
int status;
__u64 parent_id, child_id;
char parent_buf[sizeof(*parent_handle) + MAX_HANDLE_SZ];
char child_buf[sizeof(*child_handle) + MAX_HANDLE_SZ];
ASSERT_EQ(pipe(pipefd), 0);
pid = fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
/* Child process */
close(pipefd[0]);
TH_LOG("Child: creating parent user namespace and setting up mappings");
/* Create parent user namespace with mappings */
ret = setup_userns();
if (ret < 0) {
TH_LOG("Child: setup_userns() for parent failed: %s", strerror(errno));
close(pipefd[1]);
exit(1);
}
TH_LOG("Child: parent user namespace created, now uid=%d gid=%d", getuid(), getgid());
/* Get namespace ID for parent user namespace */
int parent_fd = open("/proc/self/ns/user", O_RDONLY);
if (parent_fd < 0) {
TH_LOG("Child: failed to open parent /proc/self/ns/user: %s", strerror(errno));
close(pipefd[1]);
exit(1);
}
TH_LOG("Child: opened parent userns fd %d", parent_fd);
if (ioctl(parent_fd, NS_GET_ID, &parent_id) < 0) {
TH_LOG("Child: NS_GET_ID for parent failed: %s", strerror(errno));
close(parent_fd);
close(pipefd[1]);
exit(1);
}
close(parent_fd);
TH_LOG("Child: got parent namespace ID %llu", (unsigned long long)parent_id);
/* Create child user namespace within parent */
TH_LOG("Child: creating nested child user namespace");
ret = setup_userns();
if (ret < 0) {
TH_LOG("Child: setup_userns() for child failed: %s", strerror(errno));
close(pipefd[1]);
exit(1);
}
TH_LOG("Child: nested child user namespace created, uid=%d gid=%d", getuid(), getgid());
/* Get namespace ID for child user namespace */
int child_fd = open("/proc/self/ns/user", O_RDONLY);
if (child_fd < 0) {
TH_LOG("Child: failed to open child /proc/self/ns/user: %s", strerror(errno));
close(pipefd[1]);
exit(1);
}
TH_LOG("Child: opened child userns fd %d", child_fd);
if (ioctl(child_fd, NS_GET_ID, &child_id) < 0) {
TH_LOG("Child: NS_GET_ID for child failed: %s", strerror(errno));
close(child_fd);
close(pipefd[1]);
exit(1);
}
close(child_fd);
TH_LOG("Child: got child namespace ID %llu", (unsigned long long)child_id);
/* Send both namespace IDs to parent */
TH_LOG("Child: sending both namespace IDs to parent");
write(pipefd[1], &parent_id, sizeof(parent_id));
write(pipefd[1], &child_id, sizeof(child_id));
close(pipefd[1]);
TH_LOG("Child: exiting - parent userns should become inactive");
/* Exit - parent user namespace should become inactive */
exit(0);
}
/* Parent process */
close(pipefd[1]);
TH_LOG("Parent: reading both namespace IDs from child");
/* Read both namespace IDs - fixed size, no parsing needed */
ret = read(pipefd[0], &parent_id, sizeof(parent_id));
if (ret != sizeof(parent_id)) {
close(pipefd[0]);
waitpid(pid, NULL, 0);
SKIP(return, "Failed to read parent namespace ID from child");
}
ret = read(pipefd[0], &child_id, sizeof(child_id));
close(pipefd[0]);
if (ret != sizeof(child_id)) {
waitpid(pid, NULL, 0);
SKIP(return, "Failed to read child namespace ID from child");
}
TH_LOG("Parent: received parent_id=%llu, child_id=%llu",
(unsigned long long)parent_id, (unsigned long long)child_id);
/* Construct file handles from namespace IDs */
parent_handle = (struct file_handle *)parent_buf;
parent_handle->handle_bytes = sizeof(struct nsfs_file_handle);
parent_handle->handle_type = FILEID_NSFS;
struct nsfs_file_handle *parent_fh = (struct nsfs_file_handle *)parent_handle->f_handle;
parent_fh->ns_id = parent_id;
parent_fh->ns_type = 0;
parent_fh->ns_inum = 0;
child_handle = (struct file_handle *)child_buf;
child_handle->handle_bytes = sizeof(struct nsfs_file_handle);
child_handle->handle_type = FILEID_NSFS;
struct nsfs_file_handle *child_fh = (struct nsfs_file_handle *)child_handle->f_handle;
child_fh->ns_id = child_id;
child_fh->ns_type = 0;
child_fh->ns_inum = 0;
TH_LOG("Parent: opening child namespace BEFORE child exits");
/* Open child namespace while child is still alive to keep it active */
child_nsfd = open_by_handle_at(FD_NSFS_ROOT, child_handle, O_RDONLY);
if (child_nsfd < 0) {
TH_LOG("Failed to open child namespace: %s (errno=%d)", strerror(errno), errno);
waitpid(pid, NULL, 0);
SKIP(return, "Failed to open child namespace");
}
TH_LOG("Opened child namespace fd %d", child_nsfd);
/* Now wait for child to exit */
TH_LOG("Parent: waiting for child to exit");
waitpid(pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
ASSERT_EQ(WEXITSTATUS(status), 0);
TH_LOG("Child process exited, parent holds fd to child namespace");
/*
* With hierarchical active reference propagation:
* Since the child namespace is active (parent process holds fd),
* the parent user namespace should ALSO be active automatically.
* This is because when we took an active reference on the child,
* it propagated up to the owning user namespace.
*/
TH_LOG("Attempting to reopen parent namespace (should SUCCEED - hierarchical propagation)");
int parent_fd = open_by_handle_at(FD_NSFS_ROOT, parent_handle, O_RDONLY);
ASSERT_GE(parent_fd, 0);
TH_LOG("SUCCESS: Parent namespace is active (fd=%d) due to active child", parent_fd);
/* Verify we can also get parent via NS_GET_USERNS */
TH_LOG("Verifying NS_GET_USERNS also works");
int parent_fd2 = ioctl(child_nsfd, NS_GET_USERNS);
if (parent_fd2 < 0) {
close(parent_fd);
close(child_nsfd);
TH_LOG("NS_GET_USERNS failed: %s (errno=%d)", strerror(errno), errno);
SKIP(return, "NS_GET_USERNS not supported or failed");
}
TH_LOG("NS_GET_USERNS succeeded, got parent fd %d", parent_fd2);
/* Verify both methods give us the same namespace */
struct stat st1, st2;
ASSERT_EQ(fstat(parent_fd, &st1), 0);
ASSERT_EQ(fstat(parent_fd2, &st2), 0);
TH_LOG("Parent namespace inodes: parent_fd=%lu, parent_fd2=%lu", st1.st_ino, st2.st_ino);
ASSERT_EQ(st1.st_ino, st2.st_ino);
/*
* Close child fd - parent should remain active because we still
* hold direct references to it (parent_fd and parent_fd2).
*/
TH_LOG("Closing child fd - parent should remain active (direct refs held)");
close(child_nsfd);
/* Parent should still be openable */
TH_LOG("Verifying parent still active via file handle");
int parent_fd3 = open_by_handle_at(FD_NSFS_ROOT, parent_handle, O_RDONLY);
ASSERT_GE(parent_fd3, 0);
close(parent_fd3);
TH_LOG("Closing all fds to parent namespace");
close(parent_fd);
close(parent_fd2);
/* Both should now be inactive */
TH_LOG("Attempting to reopen parent (should fail - inactive, no refs)");
parent_fd = open_by_handle_at(FD_NSFS_ROOT, parent_handle, O_RDONLY);
ASSERT_LT(parent_fd, 0);
TH_LOG("Parent inactive as expected: %s (errno=%d)", strerror(errno), errno);
ASSERT_TRUE(errno == ENOENT || errno == ESTALE);
}
TEST_HARNESS_MAIN