* [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access
@ 2026-10-30 13:04 Tao Cui
2026-10-30 13:04 ` [PATCH 1/2] nsfs: require ptrace read access for pid translation ioctls Tao Cui
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Tao Cui @ 2026-10-30 13:04 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, linux-kernel, Tycho Andersen,
Alexander Mikhalitsyn, Andrei Vagin, cui.tao, Tao Cui
From: Tao Cui <cuitao@kylinos.cn>
The four NS_GET_{PID,TGID}_{FROM,IN}_PIDNS ioctls on nsfs descriptors
only check that the fd refers to a PID namespace; they never check
whether the caller may inspect the task being looked up. An nsfs fd
is freely transferable (SCM_RIGHTS), so an unprivileged process that
receives one - the way container runtimes and LXCFS hand namespace
fds to less privileged helpers - can use these ioctls as a task
enumeration oracle across pid namespaces, bypassing the ptrace-gated
/proc files. With hidepid=2 on the host, a measured run of this
scenario resolved every live container pid (the init and its
children) to its host pid.
Patch 1 checks ptrace_may_access(PTRACE_MODE_READ_FSCREDS) on the
looked-up task. The errno (-EACCES) and the check itself mirror
proc_ns_get_link()/proc_ns_readlink() (fs/proc/namespaces.c) and
PIDFD_GET_*_NAMESPACE (fs/pidfs.c). The legitimate holder - the task
itself, or a privileged runtime daemon such as LXCFS - is
unaffected. The check is per-call on the target task, not at fd
installation: checking at open time would authorize every current and
future task of that namespace at once, which is exactly what must not
be granted.
Two residual behaviours are worth stating explicitly: ESRCH is still
returned before the permission check, so pid existence remains a
1-bit signal - the same residual pidfs and /proc carry today; and
with hidepid=0, translating a same-uid non-dumpable task now
fails, aligning nsfs with ptrace-gated /proc files.
Patch 2 adds a selftest with four checks: root translation of the
container init (lxcfs path), full denial for an unprivileged
receiver of the fd on all four ioctls in both translation
directions, a root-side re-check, and same-task translation.
Test 2 is expected to fail on trees without patch 1.
Tao Cui (2):
nsfs: require ptrace read access for pid translation ioctls
selftests: namespaces: test nsfs pid-translation access gate
fs/nsfs.c | 14 +
tools/testing/selftests/namespaces/.gitignore | 1 +
tools/testing/selftests/namespaces/Makefile | 1 +
.../namespaces/nsfs_pid_translation_test.c | 399 ++++++++++++++++++
4 files changed, 415 insertions(+)
create mode 100644 tools/testing/selftests/namespaces/nsfs_pid_translation_test.c
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/2] nsfs: require ptrace read access for pid translation ioctls
2026-10-30 13:04 [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Tao Cui
@ 2026-10-30 13:04 ` Tao Cui
2026-10-30 13:04 ` [PATCH 2/2] selftests/namespaces: test nsfs pid-translation access gate Tao Cui
2026-10-30 16:25 ` [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Christian Brauner
2 siblings, 0 replies; 4+ messages in thread
From: Tao Cui @ 2026-10-30 13:04 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, linux-kernel, Tycho Andersen,
Alexander Mikhalitsyn, Andrei Vagin, cui.tao, Tao Cui
From: Tao Cui <cuitao@kylinos.cn>
The four NS_GET_{PID,TGID}_{FROM,IN}_PIDNS ioctls translate a pid
between the caller's pid namespace and the namespace referred to by
the open nsfs file without checking that the caller may ptrace the
task being looked up. Nsfs fds travel freely over SCM_RIGHTS, so
once such an fd reaches a process on a hidepid=2 host it can
enumerate and resolve the pids of tasks hidepid means to hide.
Mirror the ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS) check
the PIDFD_GET_*_NAMESPACE ioctls have had since
commit 5b08bd408534 ("pidfs: allow retrieval of namespace file
descriptors"): perform the check inside the existing RCU read-side
critical section and return -EACCES when the caller lacks ptrace
access. Legitimate holders such as LXCFS are unaffected.
Fixes: ca567df74a28 ("nsfs: add pid translation ioctls")
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
fs/nsfs.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/fs/nsfs.c b/fs/nsfs.c
index 8179d69da038..9ff30bd876cf 100644
--- a/fs/nsfs.c
+++ b/fs/nsfs.c
@@ -9,6 +9,7 @@
#include <linux/ktime.h>
#include <linux/seq_file.h>
#include <linux/pid_namespace.h>
+#include <linux/ptrace.h>
#include <linux/user_namespace.h>
#include <linux/nsfs.h>
#include <linux/uaccess.h>
@@ -268,6 +269,19 @@ static long ns_ioctl(struct file *filp, unsigned int ioctl,
if (!tsk)
return ret;
+ /*
+ * The returned ids reveal whether @tsk is alive and which
+ * pid it has in @pid_ns, so require the same read access
+ * to @tsk that ptrace-gated /proc/<pid> files need. @tsk is
+ * only pinned by RCU, but calling ptrace_may_access()
+ * here is safe by the same pattern as validate_nsset()
+ * (kernel/nsproxy.c); ptrace_may_access() runs under
+ * the target's task_lock. The PIDFD_GET_*_NAMESPACE ioctls
+ * in fs/pidfs.c carry the same check.
+ */
+ if (!ptrace_may_access(tsk, PTRACE_MODE_READ_FSCREDS))
+ return -EACCES;
+
switch (ioctl) {
case NS_GET_PID_FROM_PIDNS:
ret = task_pid_vnr(tsk);
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 2/2] selftests/namespaces: test nsfs pid-translation access gate
2026-10-30 13:04 [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Tao Cui
2026-10-30 13:04 ` [PATCH 1/2] nsfs: require ptrace read access for pid translation ioctls Tao Cui
@ 2026-10-30 13:04 ` Tao Cui
2026-10-30 16:25 ` [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Christian Brauner
2 siblings, 0 replies; 4+ messages in thread
From: Tao Cui @ 2026-10-30 13:04 UTC (permalink / raw)
To: Christian Brauner
Cc: linux-fsdevel, linux-kernel, Tycho Andersen,
Alexander Mikhalitsyn, Andrei Vagin, cui.tao, Tao Cui
From: Tao Cui <cuitao@kylinos.cn>
Add a selftest for the nsfs pid translation ioctls: a privileged
holder of a pidns fd must still be able to translate, an
unprivileged receiver of the fd must get -EACCES on all four ioctls
in both translation directions, and same-task translation must keep
working.
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
tools/testing/selftests/namespaces/.gitignore | 1 +
tools/testing/selftests/namespaces/Makefile | 1 +
.../namespaces/nsfs_pid_translation_test.c | 399 ++++++++++++++++++
3 files changed, 401 insertions(+)
create mode 100644 tools/testing/selftests/namespaces/nsfs_pid_translation_test.c
diff --git a/tools/testing/selftests/namespaces/.gitignore b/tools/testing/selftests/namespaces/.gitignore
index 995b082357fa..5c522d2f06a6 100644
--- a/tools/testing/selftests/namespaces/.gitignore
+++ b/tools/testing/selftests/namespaces/.gitignore
@@ -11,3 +11,4 @@ stress_test
listns_pagination_bug
regression_pidfd_setns_test
uid_map_range_test
+nsfs_pid_translation_test
diff --git a/tools/testing/selftests/namespaces/Makefile b/tools/testing/selftests/namespaces/Makefile
index 9b22d12708e1..8333aa694ad6 100644
--- a/tools/testing/selftests/namespaces/Makefile
+++ b/tools/testing/selftests/namespaces/Makefile
@@ -3,6 +3,7 @@ CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := nsid_test \
+ nsfs_pid_translation_test \
file_handle_test \
init_ino_test \
ns_active_ref_test \
diff --git a/tools/testing/selftests/namespaces/nsfs_pid_translation_test.c b/tools/testing/selftests/namespaces/nsfs_pid_translation_test.c
new file mode 100644
index 000000000000..e3a19318be5c
--- /dev/null
+++ b/tools/testing/selftests/namespaces/nsfs_pid_translation_test.c
@@ -0,0 +1,399 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test that the nsfs pid translation ioctls enforce ptrace read
+ * access on the task whose pid they translate: a "container" task
+ * tree in a new pid namespace passes its pidns fd over SCM_RIGHTS to
+ * an unprivileged attacker, who must not be able to use it as a pid
+ * oracle, while a privileged holder of the same fd still can.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <unistd.h>
+#include <linux/nsfs.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "../kselftest.h"
+
+/* fall back when the build host's uapi headers predate the ioctls */
+#ifndef NS_GET_PID_FROM_PIDNS
+#define NS_GET_PID_FROM_PIDNS _IOR(NSIO, 0x6, int)
+#endif
+#ifndef NS_GET_TGID_FROM_PIDNS
+#define NS_GET_TGID_FROM_PIDNS _IOR(NSIO, 0x7, int)
+#endif
+#ifndef NS_GET_PID_IN_PIDNS
+#define NS_GET_PID_IN_PIDNS _IOR(NSIO, 0x8, int)
+#endif
+#ifndef NS_GET_TGID_IN_PIDNS
+#define NS_GET_TGID_IN_PIDNS _IOR(NSIO, 0x9, int)
+#endif
+
+#define ATTACKER_UID 65534
+#define PROBE_PIDS 8
+
+#define ATTACK_PASS 0
+#define ATTACK_LEAK 1 /* ioctl succeeded for an unseen task */
+#define ATTACK_ERRNO 2 /* rejected but errno != EACCES */
+#define ATTACK_SETUP 3 /* could not drop privileges */
+
+/* container-side setup exit codes, reported through the container fork */
+#define CONT_EXIT_UNSHARE 10
+#define CONT_EXIT_FORK 11
+#define CONT_EXIT_HANDSHAKE 12
+
+/* watchdog: a hang kills the test instead of blocking kselftest */
+#define TEST_TIMEOUT 60
+
+static void send_fd(int sock, int fd, int payload)
+{
+ struct msghdr msg = {};
+ struct iovec iov = { &payload, sizeof(payload) };
+ union {
+ struct cmsghdr cm;
+ char buf[CMSG_SPACE(sizeof(int))];
+ } u;
+ struct cmsghdr *cm;
+
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf;
+ msg.msg_controllen = sizeof(u.buf);
+ cm = CMSG_FIRSTHDR(&msg);
+ cm->cmsg_level = SOL_SOCKET;
+ cm->cmsg_type = SCM_RIGHTS;
+ cm->cmsg_len = CMSG_LEN(sizeof(fd));
+ memcpy(CMSG_DATA(cm), &fd, sizeof(fd));
+ if (sendmsg(sock, &msg, 0) < 0)
+ perror("sendmsg");
+}
+
+static int recv_fd(int sock, int *payload)
+{
+ int fd = -1;
+ struct msghdr msg = {};
+ struct iovec iov = { payload, sizeof(*payload) };
+ union {
+ struct cmsghdr cm;
+ char buf[CMSG_SPACE(sizeof(int))];
+ } u;
+ struct cmsghdr *cm;
+
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf;
+ msg.msg_controllen = sizeof(u.buf);
+ if (recvmsg(sock, &msg, 0) < 0)
+ return -1;
+ cm = CMSG_FIRSTHDR(&msg);
+ if (!cm)
+ return -1;
+ memcpy(&fd, CMSG_DATA(cm), sizeof(fd));
+ return fd;
+}
+
+/*
+ * Child: drop all privileges, receive the container pidns fd and try
+ * to use it as a translation oracle for container pids 1..PROBE_PIDS
+ * and, in the reverse direction, our own pid and the container init's
+ * initial-namespace pid. EPERM is tolerated defensively alongside
+ * EACCES.
+ */
+static int attacker(int sock)
+{
+ int fd;
+ int i;
+ int leaked = 0;
+ int bad_errno = 0;
+ int init_host_pid;
+ long ret;
+
+ if (setresgid(ATTACKER_UID, ATTACKER_UID, ATTACKER_UID) ||
+ setresuid(ATTACKER_UID, ATTACKER_UID, ATTACKER_UID))
+ return ATTACK_SETUP;
+
+ fd = recv_fd(sock, &init_host_pid);
+ if (fd < 0)
+ return ATTACK_SETUP;
+
+ for (i = 1; i <= PROBE_PIDS; i++) {
+ ret = ioctl(fd, NS_GET_PID_FROM_PIDNS, i);
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM &&
+ errno != ESRCH)
+ bad_errno++;
+
+ ret = ioctl(fd, NS_GET_TGID_FROM_PIDNS, i);
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM &&
+ errno != ESRCH)
+ bad_errno++;
+ }
+
+ /* reverse probe 1: translating ourselves - any result is a leak */
+ ret = ioctl(fd, NS_GET_PID_IN_PIDNS, getpid());
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM && errno != ESRCH)
+ bad_errno++;
+
+ ret = ioctl(fd, NS_GET_TGID_IN_PIDNS, getpid());
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM && errno != ESRCH)
+ bad_errno++;
+
+ /* reverse probe 2: the container init addressed by its
+ * initial-namespace pid - a gate pasted into only one branch still
+ * translates here and must be caught as a leak
+ */
+ ret = ioctl(fd, NS_GET_PID_IN_PIDNS, init_host_pid);
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM && errno != ESRCH)
+ bad_errno++;
+
+ ret = ioctl(fd, NS_GET_TGID_IN_PIDNS, init_host_pid);
+ if (ret >= 1)
+ leaked++;
+ else if (ret < 0 && errno != EACCES && errno != EPERM && errno != ESRCH)
+ bad_errno++;
+
+ close(fd);
+
+ if (leaked)
+ return ATTACK_LEAK;
+ if (bad_errno)
+ return ATTACK_ERRNO;
+ return ATTACK_PASS;
+}
+
+int main(void)
+{
+ int syncsock[2];
+ int sk[2];
+ int nsfd = -1;
+ int st;
+ pid_t cont = -1;
+ pid_t att;
+ char path[64];
+ const char *bail_msg = NULL;
+
+ ksft_print_header();
+ ksft_set_plan(4);
+
+ if (geteuid())
+ ksft_exit_skip("test requires root, run with -C or as root\n");
+
+ alarm(TEST_TIMEOUT);
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, syncsock)) {
+ ksft_print_msg("socketpair(sync) failed: %s\n",
+ strerror(errno));
+ bail_msg = "socketpair(sync) failed";
+ goto out;
+ }
+
+ /* "container": task tree in a new pidns; stays alive */
+ cont = fork();
+ if (cont < 0) {
+ ksft_print_msg("fork(container) failed: %s\n",
+ strerror(errno));
+ bail_msg = "fork(container) failed";
+ goto out;
+ }
+ if (cont == 0) {
+ pid_t inner;
+
+ /* die with the test: no orphaned pause() children */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+ close(syncsock[0]);
+ if (unshare(CLONE_NEWPID)) {
+ perror("unshare(CLONE_NEWPID)");
+ _exit(CONT_EXIT_UNSHARE);
+ }
+ inner = fork(); /* container init */
+ if (inner < 0)
+ _exit(CONT_EXIT_FORK);
+ if (inner == 0) {
+ int j;
+
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ /* two more tasks so the 1..8 probe range
+ * covers several live tasks plus free slots
+ */
+ for (j = 0; j < 2; j++) {
+ pid_t sub = fork();
+
+ if (sub < 0)
+ break;
+ if (sub == 0) {
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ pause();
+ }
+ }
+ pause();
+ }
+ {
+ /* initial-namespace pid of the container init,
+ * sent as a native pid_t
+ */
+ if (write(syncsock[1], &inner, sizeof(inner)) !=
+ (ssize_t)sizeof(inner))
+ _exit(CONT_EXIT_HANDSHAKE);
+ }
+ pause();
+ _exit(0);
+ }
+ close(syncsock[1]);
+ {
+ if (read(syncsock[0], &cont, sizeof(cont)) != (ssize_t)sizeof(cont)) {
+ /* a rejected unshare(CLONE_NEWPID) (no
+ * CAP_SYS_ADMIN, common in CI) is an environment
+ * limitation, not a test failure
+ */
+ int cst;
+
+ kill(cont, SIGKILL);
+ waitpid(cont, &cst, 0);
+ if (WIFEXITED(cst) &&
+ WEXITSTATUS(cst) == CONT_EXIT_UNSHARE)
+ ksft_exit_skip("unshare(CLONE_NEWPID) not permitted\n");
+ ksft_print_msg("container setup failed: exit %d\n",
+ WIFEXITED(cst) ? WEXITSTATUS(cst) : -1);
+ bail_msg = "container setup failed";
+ goto out;
+ }
+ }
+ close(syncsock[0]);
+
+ /* cont is the container init: its /proc/<pid>/ns/pid is the
+ * container pidns (the unshare() caller stays in the parent ns)
+ */
+ snprintf(path, sizeof(path), "/proc/%d/ns/pid", cont);
+ nsfd = open(path, O_RDONLY);
+ if (nsfd < 0) {
+ ksft_print_msg("open(%s) failed: %s\n", path, strerror(errno));
+ bail_msg = "open(container pidns) failed";
+ goto out;
+ }
+
+ /* 1. privileged holder of the fd can still translate pid 1 */
+ {
+ long ret = ioctl(nsfd, NS_GET_PID_FROM_PIDNS, 1);
+ long tgid = ioctl(nsfd, NS_GET_TGID_FROM_PIDNS, 1);
+
+ if (ret >= 1 && tgid >= 1)
+ ksft_test_result_pass("root translation of container init\n");
+ else
+ ksft_test_result_fail("root translation of init failed: %ld %ld %d\n",
+ ret, tgid, errno);
+ }
+
+ /* 2. unprivileged receiver of the fd is denied everywhere */
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk)) {
+ ksft_print_msg("socketpair(attacker) failed: %s\n",
+ strerror(errno));
+ bail_msg = "socketpair(attacker) failed";
+ goto out;
+ }
+
+ att = fork();
+ if (att < 0) {
+ ksft_print_msg("fork(attacker) failed: %s\n",
+ strerror(errno));
+ bail_msg = "fork(attacker) failed";
+ goto out;
+ }
+ if (att == 0) {
+ int rc;
+
+ close(sk[0]);
+ close(nsfd);
+ rc = attacker(sk[1]);
+ close(sk[1]);
+ _exit(rc);
+ }
+ close(sk[1]);
+ /* cont = container init's host pid, passed for the IN_PIDNS
+ * probes on a task the attacker must not see
+ */
+ send_fd(sk[0], nsfd, cont);
+ close(sk[0]);
+ waitpid(att, &st, 0);
+
+ switch (WIFEXITED(st) ? WEXITSTATUS(st) : -1) {
+ case ATTACK_PASS:
+ ksft_test_result_pass("unprivileged translation denied with EACCES\n");
+ break;
+ case ATTACK_LEAK:
+ ksft_test_result_fail("unprivileged translation succeeded (info leak)\n");
+ break;
+ case ATTACK_ERRNO:
+ ksft_test_result_fail("translation denied with unexpected errno\n");
+ break;
+ case ATTACK_SETUP:
+ default:
+ ksft_test_result_skip("attacker setup failed (no uid %d?)\n",
+ ATTACKER_UID);
+ break;
+ }
+
+ /* root-side re-check: rules out a false "denied" PASS when the
+ * container init died early during the attacker run
+ */
+ {
+ long ret = ioctl(nsfd, NS_GET_PID_FROM_PIDNS, 1);
+
+ if (ret >= 1)
+ ksft_test_result_pass("container init alive after attacker run\n");
+ else
+ ksft_test_result_fail("container init gone: %ld %d\n",
+ ret, errno);
+ }
+
+ close(nsfd);
+ nsfd = -1;
+ kill(cont, SIGKILL);
+ waitpid(cont, &st, 0);
+
+ /* 4. same-task query still works after the gate (sanity) */
+ {
+ int self_ns = open("/proc/self/ns/pid", O_RDONLY);
+ long ret;
+
+ if (self_ns < 0) {
+ ksft_test_result_skip("open(/proc/self/ns/pid) failed\n");
+ } else {
+ ret = ioctl(self_ns, NS_GET_PID_FROM_PIDNS, getpid());
+ if (ret == getpid())
+ ksft_test_result_pass("self translation still allowed\n");
+ else
+ ksft_test_result_fail("self translation: ret=%ld errno=%d (%s)\n",
+ ret, errno, strerror(errno));
+ close(self_ns);
+ }
+ }
+
+ ksft_finished();
+ return 0;
+
+out:
+ if (nsfd >= 0)
+ close(nsfd);
+ if (cont > 0)
+ kill(cont, SIGKILL);
+ ksft_exit_fail_msg("%s\n", bail_msg ? bail_msg : "unexpected failure");
+}
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access
2026-10-30 13:04 [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Tao Cui
2026-10-30 13:04 ` [PATCH 1/2] nsfs: require ptrace read access for pid translation ioctls Tao Cui
2026-10-30 13:04 ` [PATCH 2/2] selftests/namespaces: test nsfs pid-translation access gate Tao Cui
@ 2026-10-30 16:25 ` Christian Brauner
2 siblings, 0 replies; 4+ messages in thread
From: Christian Brauner @ 2026-10-30 16:25 UTC (permalink / raw)
To: Tao Cui
Cc: linux-fsdevel, linux-kernel, Tycho Andersen,
Alexander Mikhalitsyn, Andrei Vagin, Tao Cui
On Fri, Oct 30, 2026 at 09:04:20PM +0800, Tao Cui wrote:
> The four NS_GET_{PID,TGID}_{FROM,IN}_PIDNS ioctls on nsfs descriptors
> only check that the fd refers to a PID namespace; they never check
> whether the caller may inspect the task being looked up. An nsfs fd
> is freely transferable (SCM_RIGHTS), so an unprivileged process that
> receives one - the way container runtimes and LXCFS hand namespace
> fds to less privileged helpers - can use these ioctls as a task
> enumeration oracle across pid namespaces, bypassing the ptrace-gated
> /proc files.
No. These ioctls translate pid numbers between the caller's pid
namespace and the one the fd refers to. NS_GET_PID_FROM_PIDNS does
find_task_by_pid_ns() and then task_pid_vnr(), so a task the caller
can't see from its own pid namespace already comes back as -ESRCH.
And NS_GET_PID_IN_PIDNS starts from find_task_by_vpid(), i.e. from tasks
the caller can see anyway. IOW, they already follow the same rule that
PIDFD_GET_INFO enforces.
And pid numbers aren't hidden from unprivileged tasks to begin with.
pidfd_open() has no permission check, /proc/self/fdinfo/<pidfd> prints
NSpid for every level below the caller's pid namespace, PIDFD_GET_INFO
returns pid, tgid and ppid without a ptrace check and so does
/proc/<pid>/status with hidepid=0. hidepid hides /proc/<pid>, it never
hid pid existence or pid numbers.
> Two residual behaviours are worth stating explicitly: ESRCH is still
> returned before the permission check, so pid existence remains a
> 1-bit signal - the same residual pidfs and /proc carry today; and
That's not what the patch does. With an ancestor or sibling pid
namespace fd a task outside the caller's hierarchy now fails with
-EACCES while a missing pid fails with -ESRCH. That's a new existence
oracle. for example, for host pids from inside a container, that doesn't
exist today. And it breaks same-uid translation of non-dumpable tasks
and any LSM-confined holder that gets ptrace read denied, which includes
the LXCFS case you say is unaffected.
The selftest isn't correct either.
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-30 16:26 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 13:04 [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Tao Cui
2026-10-30 13:04 ` [PATCH 1/2] nsfs: require ptrace read access for pid translation ioctls Tao Cui
2026-10-30 13:04 ` [PATCH 2/2] selftests/namespaces: test nsfs pid-translation access gate Tao Cui
2026-10-30 16:25 ` [PATCH 0/2] nsfs: gate pid-translation ioctls behind ptrace read access Christian Brauner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®