From fd0349aadbb99252931e237df15f72e43e649254 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:31 -0500 Subject: [PATCH 01/10] pid: add helpers to operate on a struct pid array We're about to extend af_unix sockets and coredump code with the ability to hand out thread-specific pidfds. Add a few simple helpers that allow to operate on multiple struct pids up to PIDTYPE_MAX with automatic bounds checking. Reviewed-by: Alexander Mikhalitsyn Reviewed-by: Kuniyuki Iwashima Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-1-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- fs/pidfs.c | 16 ++++++++ include/linux/pid.h | 53 +++++++++++++++++++++++++++ include/linux/pid_types.h | 8 ++++ include/linux/pidfs.h | 5 +++ include/linux/sched/signal.h | 18 +++++++++ tools/lib/python/kdoc/xforms_lists.py | 1 + 6 files changed, 101 insertions(+) diff --git a/fs/pidfs.c b/fs/pidfs.c index a6a643f15d08..586af2e5811c 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -1070,6 +1070,22 @@ int pidfs_register_pid_gfp(struct pid *pid, gfp_t gfp) return 0; } +/* Register the pids up to pid type @last of @pids in pidfs. */ +int __pidfs_register_pids(struct pid *const *pids, enum pid_type last) +{ + if (WARN_ON_ONCE(last >= PIDTYPE_MAX)) + return -EINVAL; + + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) { + int ret = pidfs_register_pid(pids[type]); + + if (unlikely(ret)) + return ret; + } + + return 0; +} + static struct dentry *pidfs_stash_dentry(struct dentry **stashed, struct dentry *dentry) { diff --git a/include/linux/pid.h b/include/linux/pid.h index ddaef0bbc8ba..87635d0cc1f7 100644 --- a/include/linux/pid.h +++ b/include/linux/pid.h @@ -2,6 +2,9 @@ #ifndef _LINUX_PID_H #define _LINUX_PID_H +#include +#include +#include #include #include #include @@ -92,6 +95,56 @@ static inline struct pid *get_pid(struct pid *pid) } extern void put_pid(struct pid *pid); + +/* + * Helpers for arrays of struct pid indexed by pid type declared with + * DECLARE_PIDS(). The array covers PIDTYPE_PID up to the pid type it + * was declared with and the helpers take that bound from the array. + */ +static inline void __get_pids(struct pid **dst, struct pid *const *src, + enum pid_type last) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) + dst[type] = get_pid(src[type]); +} + +static inline void __put_pids(struct pid **pids, enum pid_type last) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) { + put_pid(pids[type]); + pids[type] = NULL; + } +} + +static inline void __swap_pids(struct pid **a, struct pid **b, + enum pid_type last) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) + swap(a[type], b[type]); +} + +static inline bool __pids_equal(struct pid *const *a, struct pid *const *b, + enum pid_type last) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) + if (a[type] != b[type]) + return false; + return true; +} + +/* The last pid type an array declared with DECLARE_PIDS() covers. */ +#define pids_last(pids) \ + ((enum pid_type)(ARRAY_SIZE(pids) - 1 + \ + BUILD_BUG_ON_ZERO(ARRAY_SIZE(pids) > PIDTYPE_MAX))) + +#define __pids_last2(a, b) \ + (pids_last(a) + BUILD_BUG_ON_ZERO(ARRAY_SIZE(a) != ARRAY_SIZE(b))) + +#define get_pids(dst, src) __get_pids(dst, src, __pids_last2(dst, src)) +#define put_pids(pids) __put_pids(pids, pids_last(pids)) +#define swap_pids(a, b) __swap_pids(a, b, __pids_last2(a, b)) +#define pids_equal(a, b) __pids_equal(a, b, __pids_last2(a, b)) + extern struct task_struct *pid_task(struct pid *pid, enum pid_type); static inline bool pid_has_task(struct pid *pid, enum pid_type type) { diff --git a/include/linux/pid_types.h b/include/linux/pid_types.h index c2aee1d91dcf..3302690a2a28 100644 --- a/include/linux/pid_types.h +++ b/include/linux/pid_types.h @@ -2,6 +2,8 @@ #ifndef _LINUX_PID_TYPES_H #define _LINUX_PID_TYPES_H +#include + enum pid_type { PIDTYPE_PID, PIDTYPE_TGID, @@ -10,6 +12,12 @@ enum pid_type { PIDTYPE_MAX, }; +struct pid; + +/* An array of struct pid indexed by pid type, PIDTYPE_PID up to @last. */ +#define DECLARE_PIDS(name, last) \ + struct pid *name[(last) + 1 + BUILD_BUG_ON_ZERO((last) >= PIDTYPE_MAX)] + struct pid_namespace; extern struct pid_namespace init_pid_ns; diff --git a/include/linux/pidfs.h b/include/linux/pidfs.h index 0abf7da9ab23..3c1e82f1a369 100644 --- a/include/linux/pidfs.h +++ b/include/linux/pidfs.h @@ -3,6 +3,7 @@ #define _LINUX_PID_FS_H #include +#include struct coredump_params; @@ -32,5 +33,9 @@ static inline int pidfs_register_pid(struct pid *pid) } void pidfs_free_pid(struct pid *pid); +int __pidfs_register_pids(struct pid *const *pids, enum pid_type last); + +/* Register the pids of an array declared with DECLARE_PIDS(). */ +#define pidfs_register_pids(pids) __pidfs_register_pids(pids, pids_last(pids)) #endif /* _LINUX_PID_FS_H */ diff --git a/include/linux/sched/signal.h b/include/linux/sched/signal.h index d45a5476b97d..3149cdb976c6 100644 --- a/include/linux/sched/signal.h +++ b/include/linux/sched/signal.h @@ -674,6 +674,24 @@ struct pid *task_pid_type(struct task_struct *task, enum pid_type type) return pid; } +/* Fill @pids with the pid types of @task up to @last, without references. */ +static inline void __task_pids(struct pid **pids, enum pid_type last, + struct task_struct *task) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) + pids[type] = task_pid_type(task, type); +} + +static inline void __get_task_pids(struct pid **pids, enum pid_type last, + struct task_struct *task) +{ + for (enum pid_type type = PIDTYPE_PID; type <= last; type++) + pids[type] = get_pid(task_pid_type(task, type)); +} + +#define task_pids(pids, task) __task_pids(pids, pids_last(pids), task) +#define get_task_pids(pids, task) __get_task_pids(pids, pids_last(pids), task) + static inline struct pid *task_tgid(struct task_struct *task) { return task->signal->pids[PIDTYPE_TGID]; diff --git a/tools/lib/python/kdoc/xforms_lists.py b/tools/lib/python/kdoc/xforms_lists.py index e3dda2fe8a53..e792785a15ec 100644 --- a/tools/lib/python/kdoc/xforms_lists.py +++ b/tools/lib/python/kdoc/xforms_lists.py @@ -45,6 +45,7 @@ class CTransforms: (CMatch("DECLARE_HASHTABLE"), r"unsigned long \1[1 << ((\2) - 1)]"), (CMatch("DECLARE_KFIFO"), r"\2 *\1"), (CMatch("DECLARE_KFIFO_PTR"), r"\2 *\1"), + (CMatch("DECLARE_PIDS"), r"struct pid *\1[(\2) + 1]"), (CMatch("(?:__)?DECLARE_FLEX_ARRAY"), r"\1 \2[]"), (CMatch("DEFINE_DMA_UNMAP_ADDR"), r"dma_addr_t \1"), (CMatch("DEFINE_DMA_UNMAP_LEN"), r"__u32 \1"), From c48ae5bcfc356a8472132ad7fbdd5df83a34e8ac Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:32 -0500 Subject: [PATCH 02/10] af_unix: record the pid of the sending thread Currently only the struct pid of the thread-group leader is recorded. The identity of the actual thread that sent the message or is connected to a given socket cannot be retrieved. Add the plumbing to make it possible to retrieve a pidfd for the sender. Nothing uses the thread-specific struct pid yet. No functional changes. Reviewed-by: Alexander Mikhalitsyn Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-2-350183b6e02a@amutable.com Reviewed-by: Kuniyuki Iwashima Signed-off-by: Christian Brauner (Amutable) --- include/net/scm.h | 10 +++++----- net/core/scm.c | 21 +++++++++++++++++---- net/unix/af_unix.c | 21 +++++++++++---------- net/unix/af_unix.h | 3 ++- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/include/net/scm.h b/include/net/scm.h index 86ae6bc109ec..b68a143f42de 100644 --- a/include/net/scm.h +++ b/include/net/scm.h @@ -42,7 +42,7 @@ struct scm_fp_list { }; struct scm_cookie { - struct pid *pid; /* Skb credentials */ + DECLARE_PIDS(pid, PIDTYPE_TGID); /* Skb credentials by pid type */ struct scm_fp_list *fp; /* Passed files */ struct scm_creds creds; /* Skb credentials */ #ifdef CONFIG_SECURITY_NETWORK @@ -69,7 +69,6 @@ static __inline__ void unix_get_peersec_dgram(struct socket *sock, struct scm_co static __inline__ void scm_set_cred(struct scm_cookie *scm, struct pid *pid, kuid_t uid, kgid_t gid) { - scm->pid = get_pid(pid); scm->creds.pid = pid_vnr(pid); scm->creds.uid = uid; scm->creds.gid = gid; @@ -77,8 +76,7 @@ static __inline__ void scm_set_cred(struct scm_cookie *scm, static __inline__ void scm_destroy_cred(struct scm_cookie *scm) { - put_pid(scm->pid); - scm->pid = NULL; + put_pids(scm->pid); } static __inline__ void scm_destroy(struct scm_cookie *scm) @@ -94,8 +92,10 @@ static __inline__ int scm_send(struct socket *sock, struct msghdr *msg, memset(scm, 0, sizeof(*scm)); scm->creds.uid = INVALID_UID; scm->creds.gid = INVALID_GID; - if (forcecreds) + if (forcecreds) { + scm->pid[PIDTYPE_TGID] = get_pid(task_tgid(current)); scm_set_cred(scm, task_tgid(current), current_uid(), current_gid()); + } unix_get_peersec_dgram(sock, scm); if (msg->msg_controllen <= 0) return 0; diff --git a/net/core/scm.c b/net/core/scm.c index f0d44ecdb11f..9b9e119c353a 100644 --- a/net/core/scm.c +++ b/net/core/scm.c @@ -149,6 +149,7 @@ EXPORT_SYMBOL(__scm_destroy); static inline int scm_replace_pid(struct scm_cookie *scm, struct pid *pid) { + struct pid *thread_pid; int err; /* drop all previous references */ @@ -158,7 +159,18 @@ static inline int scm_replace_pid(struct scm_cookie *scm, struct pid *pid) if (unlikely(err)) return err; - scm->pid = pid; + /* A sender naming its own thread-group sends from the current thread. */ + if (pid == task_tgid(current)) + thread_pid = task_pid(current); + else + thread_pid = pid; + + err = pidfs_register_pid(thread_pid); + if (unlikely(err)) + return err; + + scm->pid[PIDTYPE_TGID] = pid; + scm->pid[PIDTYPE_PID] = get_pid(thread_pid); scm->creds.pid = pid_vnr(pid); return 0; } @@ -207,7 +219,8 @@ int __scm_send(struct socket *sock, struct msghdr *msg, struct scm_cookie *p) if (err) goto error; - if (!p->pid || pid_vnr(p->pid) != creds.pid) { + if (!p->pid[PIDTYPE_TGID] || + pid_vnr(p->pid[PIDTYPE_TGID]) != creds.pid) { struct pid *pid; err = -ESRCH; pid = find_get_pid(creds.pid); @@ -504,10 +517,10 @@ static void scm_pidfd_recv(struct msghdr *msg, struct scm_cookie *scm) return; } - if (!scm->pid) + if (!scm->pid[PIDTYPE_TGID]) return; - pidfd = pidfd_prepare(scm->pid, PIDFD_STALE, &pidfd_file); + pidfd = pidfd_prepare(scm->pid[PIDTYPE_TGID], PIDFD_STALE, &pidfd_file); if (put_cmsg(msg, SOL_SOCKET, SCM_PIDFD, sizeof(int), &pidfd)) { if (pidfd_file) { diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index 42cffeafc8c1..64e720bd289b 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -1973,7 +1973,7 @@ static void unix_destruct_scm(struct sk_buff *skb) { struct scm_cookie scm = {}; - swap(scm.pid, UNIXCB(skb).pid); + swap_pids(scm.pid, UNIXCB(skb).pid); if (UNIXCB(skb).fp) unix_detach_fds(&scm, skb); @@ -1991,7 +1991,7 @@ static int unix_scm_to_skb(struct scm_cookie *scm, struct sk_buff *skb, bool sen { int err = 0; - UNIXCB(skb).pid = get_pid(scm->pid); + get_pids(UNIXCB(skb).pid, scm->pid); UNIXCB(skb).uid = scm->creds.uid; UNIXCB(skb).gid = scm->creds.gid; UNIXCB(skb).fp = NULL; @@ -2005,7 +2005,8 @@ static int unix_scm_to_skb(struct scm_cookie *scm, struct sk_buff *skb, bool sen static void unix_skb_to_scm(struct sk_buff *skb, struct scm_cookie *scm) { - scm_set_cred(scm, UNIXCB(skb).pid, UNIXCB(skb).uid, UNIXCB(skb).gid); + get_pids(scm->pid, UNIXCB(skb).pid); + scm_set_cred(scm, UNIXCB(skb).pid[PIDTYPE_TGID], UNIXCB(skb).uid, UNIXCB(skb).gid); unix_set_secdata(scm, skb); } @@ -2025,20 +2026,20 @@ static void unix_skb_to_scm(struct sk_buff *skb, struct scm_cookie *scm) static int unix_maybe_add_creds(struct sk_buff *skb, const struct sock *sk, const struct sock *other) { - if (UNIXCB(skb).pid) + if (UNIXCB(skb).pid[PIDTYPE_TGID]) return 0; if (unix_may_passcred(sk) || unix_may_passcred(other) || !other->sk_socket) { - struct pid *pid; int err; - pid = task_tgid(current); - err = pidfs_register_pid(pid); - if (unlikely(err)) + get_task_pids(UNIXCB(skb).pid, current); + err = pidfs_register_pids(UNIXCB(skb).pid); + if (unlikely(err)) { + put_pids(UNIXCB(skb).pid); return err; + } - UNIXCB(skb).pid = get_pid(pid); current_uid_gid(&UNIXCB(skb).uid, &UNIXCB(skb).gid); } @@ -2048,7 +2049,7 @@ static int unix_maybe_add_creds(struct sk_buff *skb, const struct sock *sk, static bool unix_skb_scm_eq(struct sk_buff *skb, struct scm_cookie *scm) { - return UNIXCB(skb).pid == scm->pid && + return UNIXCB(skb).pid[PIDTYPE_TGID] == scm->pid[PIDTYPE_TGID] && uid_eq(UNIXCB(skb).uid, scm->creds.uid) && gid_eq(UNIXCB(skb).gid, scm->creds.gid) && unix_secdata_eq(scm, skb); diff --git a/net/unix/af_unix.h b/net/unix/af_unix.h index 8119dbeef3a3..402742895acc 100644 --- a/net/unix/af_unix.h +++ b/net/unix/af_unix.h @@ -2,6 +2,7 @@ #ifndef __AF_UNIX_H #define __AF_UNIX_H +#include #include #define UNIX_HASH_MOD (256 - 1) @@ -11,7 +12,7 @@ struct sock *unix_peer_get(struct sock *sk); struct unix_skb_parms { - struct pid *pid; /* skb credentials */ + DECLARE_PIDS(pid, PIDTYPE_TGID); /* skb credentials by pid type */ kuid_t uid; kgid_t gid; struct scm_fp_list *fp; /* Passed files */ From be3a5e06b962ed056afb2be933817242a6ce32a5 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:33 -0500 Subject: [PATCH 03/10] net: add SO_PASSPIDFD_THREAD to get a thread-specific SCM_PIDFD Currently, SCM_PIDFD carries a pidfd for the thread-group leader. A broker or the coredump server cannot learn the identity of the specific thread that sent a given message. Now that both struct pids are recorded a receiver can ask for the specific identity it needs. So add SO_PASSPIDFD_THREAD as a sibling of SO_PASSPIDFD. SO_PASSPIDFD_THREAD delivers an SCM_PIDFD_THREAD, i.e. scoped to the specific thread sending. This is completely independent of SO_PASSPIDFD, meaning you can use both options and get both cmsgs. On SOCK_STREAM sockets recvmsg() only stops merging data at a thread boundary when the receiver asked for a thread pidfd. For SO_PASSCRED and SO_PASSPIDFD receivers all threads of one process remain a single writer. Co-developed-by: Andrew Halaney Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-3-350183b6e02a@amutable.com Reviewed-by: Kuniyuki Iwashima Signed-off-by: Christian Brauner (Amutable) --- arch/alpha/include/uapi/asm/socket.h | 2 ++ arch/mips/include/uapi/asm/socket.h | 2 ++ arch/parisc/include/uapi/asm/socket.h | 2 ++ arch/sparc/include/uapi/asm/socket.h | 2 ++ include/linux/socket.h | 9 +++++---- include/net/sock.h | 4 +++- include/uapi/asm-generic/socket.h | 2 ++ net/core/scm.c | 23 +++++++++++++++++------ net/core/sock.c | 14 ++++++++++++++ net/unix/af_unix.c | 12 +++++++++--- 10 files changed, 58 insertions(+), 14 deletions(-) diff --git a/arch/alpha/include/uapi/asm/socket.h b/arch/alpha/include/uapi/asm/socket.h index 946a5fad2691..bb3d534826bb 100644 --- a/arch/alpha/include/uapi/asm/socket.h +++ b/arch/alpha/include/uapi/asm/socket.h @@ -157,6 +157,8 @@ #define SO_RIGHTS_NOTRUNC 85 +#define SO_PASSPIDFD_THREAD 86 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/mips/include/uapi/asm/socket.h b/arch/mips/include/uapi/asm/socket.h index f1641dde135f..269badcaa086 100644 --- a/arch/mips/include/uapi/asm/socket.h +++ b/arch/mips/include/uapi/asm/socket.h @@ -168,6 +168,8 @@ #define SO_RIGHTS_NOTRUNC 85 +#define SO_PASSPIDFD_THREAD 86 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/parisc/include/uapi/asm/socket.h b/arch/parisc/include/uapi/asm/socket.h index f3a3815c7dc2..313aee10a52c 100644 --- a/arch/parisc/include/uapi/asm/socket.h +++ b/arch/parisc/include/uapi/asm/socket.h @@ -149,6 +149,8 @@ #define SO_RIGHTS_NOTRUNC 0x4053 +#define SO_PASSPIDFD_THREAD 0x4054 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/sparc/include/uapi/asm/socket.h b/arch/sparc/include/uapi/asm/socket.h index 7907f3b1f0ee..bd3e69bcce7a 100644 --- a/arch/sparc/include/uapi/asm/socket.h +++ b/arch/sparc/include/uapi/asm/socket.h @@ -150,6 +150,8 @@ #define SO_RIGHTS_NOTRUNC 0x005e +#define SO_PASSPIDFD_THREAD 0x005f + #if !defined(__KERNEL__) diff --git a/include/linux/socket.h b/include/linux/socket.h index 5a5eb1250103..b17fdb7d38a7 100644 --- a/include/linux/socket.h +++ b/include/linux/socket.h @@ -189,10 +189,11 @@ static inline size_t msg_data_left(const struct msghdr *msg) /* "Socket"-level control message types: */ -#define SCM_RIGHTS 0x01 /* rw: access rights (array of int) */ -#define SCM_CREDENTIALS 0x02 /* rw: struct ucred */ -#define SCM_SECURITY 0x03 /* rw: security label */ -#define SCM_PIDFD 0x04 /* ro: pidfd (int) */ +#define SCM_RIGHTS 0x01 /* rw: access rights (array of int) */ +#define SCM_CREDENTIALS 0x02 /* rw: struct ucred */ +#define SCM_SECURITY 0x03 /* rw: security label */ +#define SCM_PIDFD 0x04 /* ro: pidfd (int) */ +#define SCM_PIDFD_THREAD 0x05 /* ro: thread pidfd (int) */ struct ucred { __u32 pid; diff --git a/include/net/sock.h b/include/net/sock.h index 60ea55dc1885..2c4f754b498b 100644 --- a/include/net/sock.h +++ b/include/net/sock.h @@ -356,6 +356,7 @@ struct sk_filter; * @sk_scm_security: flagged by SO_PASSSEC to recv SCM_SECURITY * @sk_scm_pidfd: flagged by SO_PASSPIDFD to recv SCM_PIDFD * @sk_scm_rights: flagged by SO_PASSRIGHTS to recv SCM_RIGHTS + * @sk_scm_pidfd_thread: flagged by SO_PASSPIDFD_THREAD to recv a thread SCM_PIDFD * @sk_scm_unused: unused flags for scm_recv() * @ns_tracker: tracker for netns reference * @sk_user_frags: xarray of pages the user is holding a reference on. @@ -562,7 +563,8 @@ struct sock { sk_scm_security : 1, sk_scm_pidfd : 1, sk_scm_rights : 1, - sk_scm_unused : 4; + sk_scm_pidfd_thread : 1, + sk_scm_unused : 3; }; }; u8 sk_clockid; diff --git a/include/uapi/asm-generic/socket.h b/include/uapi/asm-generic/socket.h index 84ea7b92936e..d1e5c6de146d 100644 --- a/include/uapi/asm-generic/socket.h +++ b/include/uapi/asm-generic/socket.h @@ -152,6 +152,8 @@ #define SO_RIGHTS_NOTRUNC 85 +#define SO_PASSPIDFD_THREAD 86 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 || (defined(__x86_64__) && defined(__ILP32__)) diff --git a/net/core/scm.c b/net/core/scm.c index 9b9e119c353a..00025579d53e 100644 --- a/net/core/scm.c +++ b/net/core/scm.c @@ -499,11 +499,16 @@ static bool scm_has_secdata(struct sock *sk) } #endif -static void scm_pidfd_recv(struct msghdr *msg, struct scm_cookie *scm) +static void scm_pidfd_recv(struct msghdr *msg, struct scm_cookie *scm, + enum pid_type type, int cmsg_type) { + unsigned int flags = PIDFD_STALE; struct file *pidfd_file = NULL; + struct pid *pid; int len, pidfd; + pid = scm->pid[type]; + /* put_cmsg() doesn't return an error if CMSG is truncated, * that's why we need to opencode these checks here. */ @@ -517,12 +522,15 @@ static void scm_pidfd_recv(struct msghdr *msg, struct scm_cookie *scm) return; } - if (!scm->pid[PIDTYPE_TGID]) + if (!pid) return; - pidfd = pidfd_prepare(scm->pid[PIDTYPE_TGID], PIDFD_STALE, &pidfd_file); + if (type == PIDTYPE_PID) + flags |= PIDFD_THREAD; - if (put_cmsg(msg, SOL_SOCKET, SCM_PIDFD, sizeof(int), &pidfd)) { + pidfd = pidfd_prepare(pid, flags, &pidfd_file); + + if (put_cmsg(msg, SOL_SOCKET, cmsg_type, sizeof(int), &pidfd)) { if (pidfd_file) { put_unused_fd(pidfd); fput(pidfd_file); @@ -540,7 +548,7 @@ static bool __scm_recv_common(struct sock *sk, struct msghdr *msg, { if (!msg->msg_control) { if (sk->sk_scm_credentials || sk->sk_scm_pidfd || - scm->fp || scm_has_secdata(sk)) + sk->sk_scm_pidfd_thread || scm->fp || scm_has_secdata(sk)) msg->msg_flags |= MSG_CTRUNC; scm_destroy(scm); @@ -587,7 +595,10 @@ void scm_recv_unix(struct socket *sock, struct msghdr *msg, } if (sock->sk->sk_scm_pidfd) - scm_pidfd_recv(msg, scm); + scm_pidfd_recv(msg, scm, PIDTYPE_TGID, SCM_PIDFD); + + if (sock->sk->sk_scm_pidfd_thread) + scm_pidfd_recv(msg, scm, PIDTYPE_PID, SCM_PIDFD_THREAD); scm_destroy_cred(scm); } diff --git a/net/core/sock.c b/net/core/sock.c index e8551df8330f..c6f33fcbea43 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -1578,6 +1578,13 @@ int sk_setsockopt(struct sock *sk, int level, int optname, ret = -EOPNOTSUPP; break; + case SO_PASSPIDFD_THREAD: + if (sk_is_unix(sk)) + sk->sk_scm_pidfd_thread = valbool; + else + ret = -EOPNOTSUPP; + break; + case SO_PASSRIGHTS: if (sk_is_unix(sk)) sk->sk_scm_rights = valbool; @@ -1893,6 +1900,13 @@ int sk_getsockopt(struct sock *sk, int level, int optname, v.val = sk->sk_scm_pidfd; break; + case SO_PASSPIDFD_THREAD: + if (!sk_is_unix(sk)) + return -EOPNOTSUPP; + + v.val = sk->sk_scm_pidfd_thread; + break; + case SO_PASSRIGHTS: if (!sk_is_unix(sk)) return -EOPNOTSUPP; diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index 64e720bd289b..e44afb059abf 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -803,7 +803,8 @@ static void copy_peercred(struct sock *sk, struct sock *peersk) static bool unix_may_passcred(const struct sock *sk) { - return sk->sk_scm_credentials || sk->sk_scm_pidfd; + return sk->sk_scm_credentials || sk->sk_scm_pidfd || + sk->sk_scm_pidfd_thread; } static int unix_listen(struct socket *sock, int backlog) @@ -2046,9 +2047,14 @@ static int unix_maybe_add_creds(struct sk_buff *skb, const struct sock *sk, return 0; } -static bool unix_skb_scm_eq(struct sk_buff *skb, +static bool unix_skb_scm_eq(const struct sock *sk, struct sk_buff *skb, struct scm_cookie *scm) { + /* Only a thread pidfd receiver can tell threads of one process apart. */ + if (sk->sk_scm_pidfd_thread && + UNIXCB(skb).pid[PIDTYPE_PID] != scm->pid[PIDTYPE_PID]) + return false; + return UNIXCB(skb).pid[PIDTYPE_TGID] == scm->pid[PIDTYPE_TGID] && uid_eq(UNIXCB(skb).uid, scm->creds.uid) && gid_eq(UNIXCB(skb).gid, scm->creds.gid) && @@ -3031,7 +3037,7 @@ static int unix_stream_read_generic(struct unix_stream_read_state *state, if (check_creds) { /* Never glue messages from different writers */ - if (!unix_skb_scm_eq(skb, &scm)) + if (!unix_skb_scm_eq(sk, skb, &scm)) break; } else if (unix_may_passcred(sk)) { /* Copy credentials */ From 4e2446c83f384fb535379ef4d0f52151e8079d67 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:34 -0500 Subject: [PATCH 04/10] selftests/net: SO_PASSPIDFD_THREAD Extend the af_unix scm_pidfd selftest with SO_PASSPIDFD_THREAD tests. Co-developed-by: Andrew Halaney Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-4-350183b6e02a@amutable.com Reviewed-by: Kuniyuki Iwashima Signed-off-by: Christian Brauner (Amutable) --- tools/testing/selftests/net/af_unix/Makefile | 2 + .../testing/selftests/net/af_unix/scm_pidfd.c | 505 ++++++++++++++++++ 2 files changed, 507 insertions(+) diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile index a66f10fb0c23..45b841758f1b 100644 --- a/tools/testing/selftests/net/af_unix/Makefile +++ b/tools/testing/selftests/net/af_unix/Makefile @@ -23,6 +23,8 @@ TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o include ../../lib.mk include ../bpf.mk +$(OUTPUT)/scm_pidfd: CFLAGS += -pthread + $(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ) $(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include $(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz diff --git a/tools/testing/selftests/net/af_unix/scm_pidfd.c b/tools/testing/selftests/net/af_unix/scm_pidfd.c index 2c18b92a2603..d73ae9d27a48 100644 --- a/tools/testing/selftests/net/af_unix/scm_pidfd.c +++ b/tools/testing/selftests/net/af_unix/scm_pidfd.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,14 @@ #define SCM_PIDFD 0x04 #endif +#ifndef SCM_PIDFD_THREAD +#define SCM_PIDFD_THREAD 0x05 +#endif + +#ifndef SO_PASSPIDFD_THREAD +#define SO_PASSPIDFD_THREAD 86 +#endif + #define CHILD_EXIT_CODE_OK 123 static void child_die() @@ -553,4 +562,500 @@ TEST_F(scm_pidfd, test) close(pfd); } +struct thread_ids { + pid_t pid; + pid_t tid; +}; + +#define MAX_WRITERS 2 + +/* Used by writers to signal they've written, so we can sequence multiple writers */ +static int seq_pipe[2]; + +static void *send_ids_thread(void *arg) +{ + int fd = *(int *)arg; + struct thread_ids ids = { + .pid = getpid(), + .tid = gettid(), + }; + char sync; + + if (send(fd, &ids, sizeof(ids), 0) != sizeof(ids)) + return (void *)1; + + /* let the client start the next writer, so the queue order is known */ + if (write(seq_pipe[1], "1", 1) != 1) + return (void *)1; + + /* stay alive until the receiver has looked at our pidfd */ + if (read(fd, &sync, 1) != 1) + return (void *)1; + + return NULL; +} + +static void *send_ids_creds_thread(void *arg) +{ + int fd = *(int *)arg; + struct thread_ids ids = { + .pid = getpid(), + .tid = gettid(), + }; + struct ucred ucred = { + .pid = getpid(), + .uid = getuid(), + .gid = getgid(), + }; + char control[CMSG_SPACE(sizeof(ucred))] = { 0 }; + struct iovec iov; + struct msghdr msg = { 0 }; + struct cmsghdr *cmsg; + char sync; + + iov.iov_base = &ids; + iov.iov_len = sizeof(ids); + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_CREDENTIALS; + cmsg->cmsg_len = CMSG_LEN(sizeof(ucred)); + memcpy(CMSG_DATA(cmsg), &ucred, sizeof(ucred)); + + if (sendmsg(fd, &msg, 0) != sizeof(ids)) + return (void *)1; + + /* signal we've written our info */ + if (write(seq_pipe[1], "1", 1) != 1) + return (void *)1; + + /* stay alive until the receiver has looked at our pidfd */ + if (read(fd, &sync, 1) != 1) + return (void *)1; + + return NULL; +} + +/* + * Runs @nwriters copies of @sender, each in its own non-leader thread, one + * at a time so the queue order is known, then tells the parent on @ackfd + * that everything is queued. + */ +static void thread_client(int fd, int syncfd, int ackfd, + void *(*sender)(void *), int nwriters) +{ + pthread_t writers[MAX_WRITERS]; + void *ret; + char sync; + int i; + + /* wait until the receiver enabled the options it wants */ + if (read(syncfd, &sync, 1) != 1) + child_die(); + + for (i = 0; i < nwriters; i++) { + if (pthread_create(&writers[i], NULL, sender, &fd)) + child_die(); + + if (read(seq_pipe[0], &sync, 1) != 1) + child_die(); + } + + if (write(ackfd, "1", 1) != 1) + child_die(); + + for (i = 0; i < nwriters; i++) + if (pthread_join(writers[i], &ret) || ret) + child_die(); + + exit(0); +} + +struct pidfd_msg { + ssize_t len; + struct thread_ids ids; + struct pidfd_info tgid_info; + struct pidfd_info thread_info; + int tgid_flags; + int thread_flags; + bool have_tgid; + bool have_thread; +}; + +static int get_pidfd_info(int pidfd, struct pidfd_info *info) +{ + info->mask = PIDFD_INFO_PID; + if (ioctl(pidfd, PIDFD_GET_INFO, info)) { + log_err("ioctl(PIDFD_GET_INFO)"); + return -1; + } + + return 0; +} + +/* One recvmsg(), reporting how many bytes came back and what the pidfd + * cmsgs that came with them say. + */ +static int read_pidfd_msg(int fd, struct pidfd_msg *out) +{ + char control[CMSG_SPACE(sizeof(int)) * 2] = { 0 }; + int tgid_pidfd = -1, thread_pidfd = -1; + struct thread_ids buf[2] = { 0 }; + struct msghdr msg = { 0 }; + struct cmsghdr *cmsg; + struct iovec iov; + + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + out->len = recvmsg(fd, &msg, 0); + if (out->len < 0) { + log_err("recvmsg"); + return -1; + } + + if (msg.msg_flags & MSG_CTRUNC) { + log_err("recvmsg: control truncated"); + return -1; + } + + out->ids = buf[0]; + + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; + cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) + continue; + + if (cmsg->cmsg_type == SCM_PIDFD) + memcpy(&tgid_pidfd, CMSG_DATA(cmsg), sizeof(tgid_pidfd)); + else if (cmsg->cmsg_type == SCM_PIDFD_THREAD) + memcpy(&thread_pidfd, CMSG_DATA(cmsg), sizeof(thread_pidfd)); + } + + out->have_tgid = tgid_pidfd >= 0; + out->have_thread = thread_pidfd >= 0; + + if (out->have_tgid) { + if (get_pidfd_info(tgid_pidfd, &out->tgid_info)) + return -1; + out->tgid_flags = fcntl(tgid_pidfd, F_GETFL); + close(tgid_pidfd); + } + + if (out->have_thread) { + if (get_pidfd_info(thread_pidfd, &out->thread_info)) + return -1; + out->thread_flags = fcntl(thread_pidfd, F_GETFL); + close(thread_pidfd); + } + + return 0; +} + +/* + * Runs @nwriters threads of one child process against a SOCK_STREAM pair, + * all of them queued before the parent reads. SO_PASSPIDFD is set when + * @want_tgid is given, SO_PASSPIDFD_THREAD when @want_thread is. @nread + * reads are returned in @out. + */ +static int pidfd_flow(void *(*sender)(void *), int nwriters, bool want_tgid, + bool want_thread, struct pidfd_msg *out, int nread) +{ + int child_status = 0; + int syncpipe[2]; + int ackpipe[2]; + int sk[2]; + int on = 1; + char sync; + pid_t child; + int i; + + if (nwriters > MAX_WRITERS) + return -1; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk)) + return -1; + if (pipe(syncpipe) || pipe(ackpipe) || pipe(seq_pipe)) + return -1; + + child = fork(); + if (child < 0) + return -1; + + if (child == 0) { + close(sk[0]); + close(syncpipe[1]); + close(ackpipe[0]); + thread_client(sk[1], syncpipe[0], ackpipe[1], sender, nwriters); + } + close(sk[1]); + close(syncpipe[0]); + close(ackpipe[1]); + close(seq_pipe[0]); + close(seq_pipe[1]); + + if (want_tgid && + setsockopt(sk[0], SOL_SOCKET, SO_PASSPIDFD, &on, sizeof(on))) { + log_err("Failed to set SO_PASSPIDFD"); + return -1; + } + + if (want_thread && + setsockopt(sk[0], SOL_SOCKET, SO_PASSPIDFD_THREAD, &on, sizeof(on))) { + log_err("Failed to set SO_PASSPIDFD_THREAD"); + return -1; + } + + /* let the child know the options are set, it can write now */ + if (write(syncpipe[1], "1", 1) != 1) + return -1; + close(syncpipe[1]); + + /* wait until every writer has queued its message */ + if (read(ackpipe[0], &sync, 1) != 1) + return -1; + close(ackpipe[0]); + + for (i = 0; i < nread; i++) + if (read_pidfd_msg(sk[0], &out[i])) + return -1; + + /* release the writers */ + for (i = 0; i < nwriters; i++) + if (write(sk[0], "x", 1) != 1) + return -1; + close(sk[0]); + + waitpid(child, &child_status, 0); + if (!WIFEXITED(child_status) || WEXITSTATUS(child_status)) + return -1; + + return 0; +} + +static int sockopt_set(int fd, int optname, int val) +{ + return setsockopt(fd, SOL_SOCKET, optname, &val, sizeof(val)); +} + +static int sockopt_get(int fd, int optname) +{ + socklen_t len = sizeof(int); + int val = -1; + + if (getsockopt(fd, SOL_SOCKET, optname, &val, &len)) + return -1; + + return val; +} + +TEST(scm_pidfd_setsockopt_values) +{ + int sk[2]; + + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sk)); + + /* Verify that the options are truly set independently */ + ASSERT_EQ(0, sockopt_set(sk[0], SO_PASSPIDFD_THREAD, 1)); + ASSERT_EQ(1, sockopt_get(sk[0], SO_PASSPIDFD_THREAD)); + ASSERT_EQ(0, sockopt_get(sk[0], SO_PASSPIDFD)); + + ASSERT_EQ(0, sockopt_set(sk[0], SO_PASSPIDFD, 1)); + ASSERT_EQ(1, sockopt_get(sk[0], SO_PASSPIDFD)); + ASSERT_EQ(1, sockopt_get(sk[0], SO_PASSPIDFD_THREAD)); + + /* Verify that the options are truly cleared independently */ + ASSERT_EQ(0, sockopt_set(sk[0], SO_PASSPIDFD, 0)); + ASSERT_EQ(0, sockopt_get(sk[0], SO_PASSPIDFD)); + ASSERT_EQ(1, sockopt_get(sk[0], SO_PASSPIDFD_THREAD)); + + ASSERT_EQ(0, sockopt_set(sk[0], SO_PASSPIDFD_THREAD, 0)); + ASSERT_EQ(0, sockopt_get(sk[0], SO_PASSPIDFD_THREAD)); + ASSERT_EQ(0, sockopt_get(sk[0], SO_PASSPIDFD)); + + close(sk[0]); + close(sk[1]); +} + +/* A receiver that only asked about the process cannot tell the two + * threads apart, so their writes are glued into one read. + */ +TEST(scm_pidfd_stream_glues_threads) +{ + struct pidfd_msg msg[1] = { 0 }; + + ASSERT_EQ(0, pidfd_flow(send_ids_thread, 2, true, false, msg, 1)); + EXPECT_EQ(sizeof(struct thread_ids) * 2, msg[0].len); + + EXPECT_TRUE(msg[0].have_tgid); + EXPECT_FALSE(msg[0].have_thread); + EXPECT_EQ(msg[0].ids.pid, msg[0].tgid_info.pid); + EXPECT_EQ(msg[0].ids.pid, msg[0].tgid_info.tgid); +} + +/* A receiver asking for both thread and process pidfd should get a unique + * msg for each thread writing + */ +TEST(scm_pidfd_thread_stream_splits_on_threads) +{ + struct pidfd_msg msg[2] = { 0 }; + int i; + + ASSERT_EQ(0, pidfd_flow(send_ids_thread, 2, true, true, msg, 2)); + + for (i = 0; i < 2; i++) { + EXPECT_EQ(sizeof(struct thread_ids), msg[i].len); + EXPECT_TRUE(msg[i].have_tgid); + EXPECT_TRUE(msg[i].have_thread); + + /* Make sure the info makes sense for the pidfd type */ + EXPECT_EQ(msg[i].ids.pid, msg[i].tgid_info.pid); + EXPECT_EQ(msg[i].ids.pid, msg[i].tgid_info.tgid); + EXPECT_EQ(msg[i].ids.tid, msg[i].thread_info.pid); + EXPECT_EQ(msg[i].ids.pid, msg[i].thread_info.tgid); + EXPECT_NE(msg[i].ids.pid, msg[i].ids.tid); + } + + /* Make sure we really got unique threads per message */ + EXPECT_NE(msg[0].ids.tid, msg[1].ids.tid); + EXPECT_EQ(msg[0].ids.pid, msg[1].ids.pid); + EXPECT_EQ(msg[0].tgid_info.pid, msg[1].tgid_info.pid); + EXPECT_NE(msg[0].thread_info.pid, msg[1].thread_info.pid); +} + +/* Sends from the thread-group leader */ +static int leader_flow(bool want_thread, struct pidfd_msg *out) +{ + struct thread_ids ids = { + .pid = getpid(), + .tid = gettid(), + }; + int sk[2]; + int on = 1; + int ret; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk)) + return -1; + + if (setsockopt(sk[0], SOL_SOCKET, SO_PASSPIDFD, &on, sizeof(on))) { + log_err("Failed to set SO_PASSPIDFD"); + return -1; + } + + if (want_thread && + setsockopt(sk[0], SOL_SOCKET, SO_PASSPIDFD_THREAD, &on, sizeof(on))) { + log_err("Failed to set SO_PASSPIDFD_THREAD"); + return -1; + } + + if (send(sk[1], &ids, sizeof(ids), 0) != sizeof(ids)) { + log_err("send"); + return -1; + } + + ret = read_pidfd_msg(sk[0], out); + + close(sk[0]); + close(sk[1]); + + return ret; +} + +TEST(scm_pidfd_leader_sender) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(getpid(), gettid()); + + ASSERT_EQ(0, leader_flow(false, &msg)); + ASSERT_TRUE(msg.have_tgid); + EXPECT_FALSE(msg.have_thread); + + EXPECT_EQ(getpid(), msg.tgid_info.pid); + EXPECT_EQ(getpid(), msg.tgid_info.tgid); + EXPECT_FALSE(msg.tgid_flags & O_EXCL); +} + +TEST(scm_pidfd_thread_leader_sender) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(getpid(), gettid()); + + ASSERT_EQ(0, leader_flow(true, &msg)); + ASSERT_TRUE(msg.have_tgid); + ASSERT_TRUE(msg.have_thread); + + EXPECT_EQ(getpid(), msg.tgid_info.pid); + EXPECT_EQ(getpid(), msg.tgid_info.tgid); + EXPECT_EQ(getpid(), msg.thread_info.pid); + EXPECT_EQ(getpid(), msg.thread_info.tgid); + + EXPECT_FALSE(msg.tgid_flags & O_EXCL); + EXPECT_TRUE(msg.thread_flags & O_EXCL); +} + +TEST(scm_pidfd_thread_and_group) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(0, pidfd_flow(send_ids_thread, 1, true, true, &msg, 1)); + ASSERT_NE(msg.ids.pid, msg.ids.tid); + ASSERT_TRUE(msg.have_tgid); + ASSERT_TRUE(msg.have_thread); + + EXPECT_EQ(msg.ids.pid, msg.tgid_info.pid); + EXPECT_EQ(msg.ids.pid, msg.tgid_info.tgid); + + EXPECT_EQ(msg.ids.tid, msg.thread_info.pid); + EXPECT_EQ(msg.ids.pid, msg.thread_info.tgid); +} + +TEST(scm_pidfd_thread) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(0, pidfd_flow(send_ids_thread, 1, false, true, &msg, 1)); + ASSERT_NE(msg.ids.pid, msg.ids.tid); + ASSERT_TRUE(msg.have_thread); + EXPECT_FALSE(msg.have_tgid); + EXPECT_EQ(msg.ids.tid, msg.thread_info.pid); + EXPECT_EQ(msg.ids.pid, msg.thread_info.tgid); +} + +TEST(scm_pidfd_thread_group) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(0, pidfd_flow(send_ids_thread, 1, true, false, &msg, 1)); + ASSERT_NE(msg.ids.pid, msg.ids.tid); + ASSERT_TRUE(msg.have_tgid); + EXPECT_FALSE(msg.have_thread); + EXPECT_EQ(msg.ids.pid, msg.tgid_info.pid); + EXPECT_EQ(msg.ids.pid, msg.tgid_info.tgid); +} + +TEST(scm_pidfd_thread_creds) +{ + struct pidfd_msg msg = { 0 }; + + ASSERT_EQ(0, pidfd_flow(send_ids_creds_thread, 1, true, true, &msg, 1)); + ASSERT_NE(msg.ids.pid, msg.ids.tid); + ASSERT_TRUE(msg.have_tgid); + ASSERT_TRUE(msg.have_thread); + EXPECT_EQ(msg.ids.pid, msg.tgid_info.pid); + EXPECT_EQ(msg.ids.pid, msg.tgid_info.tgid); + EXPECT_EQ(msg.ids.tid, msg.thread_info.pid); + EXPECT_EQ(msg.ids.pid, msg.thread_info.tgid); +} + TEST_HARNESS_MAIN From 95d9b7369665a7de15b42bf6924d193ccb952151 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:35 -0500 Subject: [PATCH 05/10] net: turn sk_peer_pid into an array indexed by pid type Currently only the struct pid of the thread-group leader is recorded for a socket's peer. To make room for the struct pid of the thread that called connect(), listen() or socketpair() turn sk_peer_pid into an array indexed by pid type. All users, including bluetooth and the coredump socket, keep using the PIDTYPE_TGID slot. Nothing fills the PIDTYPE_PID slot yet. No functional changes. Reviewed-by: Alexander Mikhalitsyn Reviewed-by: Kuniyuki Iwashima Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-5-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- fs/coredump.c | 2 +- include/net/sock.h | 4 ++-- include/trace/events/landlock.h | 2 +- net/bluetooth/af_bluetooth.c | 6 +++--- net/bluetooth/hci_sock.c | 8 ++++---- net/bluetooth/l2cap_sock.c | 2 +- net/core/sock.c | 9 +++++---- net/unix/af_unix.c | 14 +++++++------- 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/fs/coredump.c b/fs/coredump.c index 6114839f5178..9b267d3c0ed7 100644 --- a/fs/coredump.c +++ b/fs/coredump.c @@ -722,7 +722,7 @@ static bool coredump_sock_connect(struct core_name *cn, struct coredump_params * } /* ... and validate that @sk_peer_pid matches @cprm.pid. */ - if (WARN_ON_ONCE(unix_peer(socket->sk)->sk_peer_pid != cprm->pid)) + if (WARN_ON_ONCE(unix_peer(socket->sk)->sk_peer_pid[PIDTYPE_TGID] != cprm->pid)) return false; cprm->limit = RLIM_INFINITY; diff --git a/include/net/sock.h b/include/net/sock.h index 2c4f754b498b..77dfb170d3c8 100644 --- a/include/net/sock.h +++ b/include/net/sock.h @@ -301,7 +301,7 @@ struct sk_filter; * @sk_type: socket type (%SOCK_STREAM, etc) * @sk_protocol: which protocol this socket belongs in this network family * @sk_peer_lock: lock protecting @sk_peer_pid and @sk_peer_cred - * @sk_peer_pid: &struct pid for this socket's peer + * @sk_peer_pid: &struct pid for this socket's peer, by pid type * @sk_peer_cred: %SO_PEERCRED setting * @sk_rcvlowat: %SO_RCVLOWAT setting * @sk_rcvtimeo: %SO_RCVTIMEO setting @@ -546,7 +546,7 @@ struct sock { u64 sk_ino; spinlock_t sk_peer_lock; int sk_bind_phc; - struct pid *sk_peer_pid; + DECLARE_PIDS(sk_peer_pid, PIDTYPE_TGID); const struct cred *sk_peer_cred; ktime_t sk_stamp; diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index 3a43638c9bc2..9e172ea22d95 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -1037,7 +1037,7 @@ TRACE_EVENT(landlock_deny_scope_abstract_unix_socket, * updates. The peer socket keeps a reference to sk_peer_pid * through pid_nr(); sun_path is the reliable identifier. */ - peer_pid = READ_ONCE(peer->sk_peer_pid); + peer_pid = READ_ONCE(peer->sk_peer_pid[PIDTYPE_TGID]); __entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0; __assign_str(sun_path); ), diff --git a/net/bluetooth/af_bluetooth.c b/net/bluetooth/af_bluetooth.c index 411d66f24393..7758e9ea3848 100644 --- a/net/bluetooth/af_bluetooth.c +++ b/net/bluetooth/af_bluetooth.c @@ -161,7 +161,7 @@ struct sock *bt_sock_alloc(struct net *net, struct socket *sock, /* Init peer information so it can be properly monitored */ if (!kern) { spin_lock(&sk->sk_peer_lock); - sk->sk_peer_pid = get_pid(task_tgid(current)); + sk->sk_peer_pid[PIDTYPE_TGID] = get_pid(task_tgid(current)); sk->sk_peer_cred = get_current_cred(); spin_unlock(&sk->sk_peer_lock); } @@ -235,9 +235,9 @@ void bt_accept_enqueue(struct sock *parent, struct sock *sk, bool bh) * socket is allocated by the kernel. */ spin_lock(&sk->sk_peer_lock); - old_pid = sk->sk_peer_pid; + old_pid = sk->sk_peer_pid[PIDTYPE_TGID]; old_cred = sk->sk_peer_cred; - sk->sk_peer_pid = get_pid(parent->sk_peer_pid); + sk->sk_peer_pid[PIDTYPE_TGID] = get_pid(parent->sk_peer_pid[PIDTYPE_TGID]); sk->sk_peer_cred = get_cred(parent->sk_peer_cred); spin_unlock(&sk->sk_peer_lock); diff --git a/net/bluetooth/hci_sock.c b/net/bluetooth/hci_sock.c index 81068b585764..ec45b531e387 100644 --- a/net/bluetooth/hci_sock.c +++ b/net/bluetooth/hci_sock.c @@ -284,21 +284,21 @@ static void hci_sock_copy_creds(struct sock *sk, struct sk_buff *skb) creds = &bt_cb(skb)->creds; /* Check if peer credentials is set */ - if (!sk->sk_peer_pid) { + if (!sk->sk_peer_pid[PIDTYPE_TGID]) { /* Check if parent peer credentials is set */ - if (bt_sk(sk)->parent && bt_sk(sk)->parent->sk_peer_pid) + if (bt_sk(sk)->parent && bt_sk(sk)->parent->sk_peer_pid[PIDTYPE_TGID]) sk = bt_sk(sk)->parent; else return; } /* Check if scm_creds already set */ - if (creds->pid == pid_vnr(sk->sk_peer_pid)) + if (creds->pid == pid_vnr(sk->sk_peer_pid[PIDTYPE_TGID])) return; memset(creds, 0, sizeof(*creds)); - creds->pid = pid_vnr(sk->sk_peer_pid); + creds->pid = pid_vnr(sk->sk_peer_pid[PIDTYPE_TGID]); if (sk->sk_peer_cred) { creds->uid = sk->sk_peer_cred->uid; creds->gid = sk->sk_peer_cred->gid; diff --git a/net/bluetooth/l2cap_sock.c b/net/bluetooth/l2cap_sock.c index 1194c37e466f..872d8fb31b6f 100644 --- a/net/bluetooth/l2cap_sock.c +++ b/net/bluetooth/l2cap_sock.c @@ -1890,7 +1890,7 @@ static struct pid *l2cap_sock_get_peer_pid_cb(struct l2cap_chan *chan) { struct sock *sk = chan->data; - return sk->sk_peer_pid; + return sk->sk_peer_pid[PIDTYPE_TGID]; } static void l2cap_sock_suspend_cb(struct l2cap_chan *chan) diff --git a/net/core/sock.c b/net/core/sock.c index c6f33fcbea43..f8436e494138 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -1921,7 +1921,8 @@ int sk_getsockopt(struct sock *sk, int level, int optname, len = sizeof(peercred); spin_lock(&sk->sk_peer_lock); - cred_to_ucred(sk->sk_peer_pid, sk->sk_peer_cred, &peercred); + cred_to_ucred(sk->sk_peer_pid[PIDTYPE_TGID], sk->sk_peer_cred, + &peercred); spin_unlock(&sk->sk_peer_lock); if (copy_to_sockptr(optval, &peercred, len)) @@ -1940,7 +1941,7 @@ int sk_getsockopt(struct sock *sk, int level, int optname, len = sizeof(pidfd); spin_lock(&sk->sk_peer_lock); - peer_pid = get_pid(sk->sk_peer_pid); + peer_pid = get_pid(sk->sk_peer_pid[PIDTYPE_TGID]); spin_unlock(&sk->sk_peer_lock); if (!peer_pid) @@ -2394,7 +2395,7 @@ static void __sk_destruct(struct rcu_head *head) /* We do not need to acquire sk->sk_peer_lock, we are the last user. */ put_cred(sk->sk_peer_cred); - put_pid(sk->sk_peer_pid); + put_pids(sk->sk_peer_pid); if (likely(sk->sk_net_refcnt)) { put_net_track(net, &sk->ns_tracker); @@ -3799,7 +3800,7 @@ void sock_init_data_uid(struct socket *sock, struct sock *sk, kuid_t uid) sk->sk_frag.offset = 0; sk->sk_peek_off = -1; - sk->sk_peer_pid = NULL; + memset(sk->sk_peer_pid, 0, sizeof(sk->sk_peer_pid)); sk->sk_peer_cred = NULL; spin_lock_init(&sk->sk_peer_lock); diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index e44afb059abf..4e571c5a0e16 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -737,7 +737,7 @@ static void unix_release_sock(struct sock *sk, int embrion) } struct unix_peercred { - struct pid *peer_pid; + DECLARE_PIDS(peer_pid, PIDTYPE_TGID); const struct cred *peer_cred; }; @@ -749,7 +749,7 @@ static inline int prepare_peercred(struct unix_peercred *peercred) pid = task_tgid(current); err = pidfs_register_pid(pid); if (likely(!err)) { - peercred->peer_pid = get_pid(pid); + peercred->peer_pid[PIDTYPE_TGID] = get_pid(pid); peercred->peer_cred = get_current_cred(); } return err; @@ -762,7 +762,7 @@ static void drop_peercred(struct unix_peercred *peercred) might_sleep(); - swap(peercred->peer_pid, pid); + swap(peercred->peer_pid[PIDTYPE_TGID], pid); swap(peercred->peer_cred, cred); put_pid(pid); @@ -772,7 +772,7 @@ static void drop_peercred(struct unix_peercred *peercred) static inline void init_peercred(struct sock *sk, const struct unix_peercred *peercred) { - sk->sk_peer_pid = peercred->peer_pid; + sk->sk_peer_pid[PIDTYPE_TGID] = peercred->peer_pid[PIDTYPE_TGID]; sk->sk_peer_cred = peercred->peer_cred; } @@ -782,12 +782,12 @@ static void update_peercred(struct sock *sk, struct unix_peercred *peercred) struct pid *old_pid; spin_lock(&sk->sk_peer_lock); - old_pid = sk->sk_peer_pid; + old_pid = sk->sk_peer_pid[PIDTYPE_TGID]; old_cred = sk->sk_peer_cred; init_peercred(sk, peercred); spin_unlock(&sk->sk_peer_lock); - peercred->peer_pid = old_pid; + peercred->peer_pid[PIDTYPE_TGID] = old_pid; peercred->peer_cred = old_cred; } @@ -796,7 +796,7 @@ static void copy_peercred(struct sock *sk, struct sock *peersk) lockdep_assert_held(&unix_sk(peersk)->lock); spin_lock(&sk->sk_peer_lock); - sk->sk_peer_pid = get_pid(peersk->sk_peer_pid); + sk->sk_peer_pid[PIDTYPE_TGID] = get_pid(peersk->sk_peer_pid[PIDTYPE_TGID]); sk->sk_peer_cred = get_cred(peersk->sk_peer_cred); spin_unlock(&sk->sk_peer_lock); } From 1592d89361e9bff94bb34106d523d0e64853183d Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:36 -0500 Subject: [PATCH 06/10] af_unix: record the pid of the connecting thread Currently only the struct pid of the thread-group leader is recorded. The identity of the actual thread that connected to a given socket cannot be retrieved. Add the plumbing to make it possible to retrieve a pidfd for the connecting thread. Nothing uses the thread-specific struct pid yet. No functional changes. Reviewed-by: Alexander Mikhalitsyn Reviewed-by: Kuniyuki Iwashima Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-6-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- net/unix/af_unix.c | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index 4e571c5a0e16..b7d62e61d9c4 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -743,51 +743,47 @@ struct unix_peercred { static inline int prepare_peercred(struct unix_peercred *peercred) { - struct pid *pid; int err; - pid = task_tgid(current); - err = pidfs_register_pid(pid); - if (likely(!err)) { - peercred->peer_pid[PIDTYPE_TGID] = get_pid(pid); - peercred->peer_cred = get_current_cred(); + get_task_pids(peercred->peer_pid, current); + err = pidfs_register_pids(peercred->peer_pid); + if (unlikely(err)) { + put_pids(peercred->peer_pid); + return err; } - return err; + + peercred->peer_cred = get_current_cred(); + return 0; } static void drop_peercred(struct unix_peercred *peercred) { const struct cred *cred = NULL; - struct pid *pid = NULL; might_sleep(); - swap(peercred->peer_pid[PIDTYPE_TGID], pid); + put_pids(peercred->peer_pid); swap(peercred->peer_cred, cred); - - put_pid(pid); put_cred(cred); } static inline void init_peercred(struct sock *sk, const struct unix_peercred *peercred) { - sk->sk_peer_pid[PIDTYPE_TGID] = peercred->peer_pid[PIDTYPE_TGID]; + memcpy(sk->sk_peer_pid, peercred->peer_pid, sizeof(sk->sk_peer_pid)); sk->sk_peer_cred = peercred->peer_cred; } static void update_peercred(struct sock *sk, struct unix_peercred *peercred) { const struct cred *old_cred; - struct pid *old_pid; spin_lock(&sk->sk_peer_lock); - old_pid = sk->sk_peer_pid[PIDTYPE_TGID]; + swap_pids(sk->sk_peer_pid, peercred->peer_pid); old_cred = sk->sk_peer_cred; - init_peercred(sk, peercred); + sk->sk_peer_cred = peercred->peer_cred; spin_unlock(&sk->sk_peer_lock); - peercred->peer_pid[PIDTYPE_TGID] = old_pid; peercred->peer_cred = old_cred; } @@ -796,7 +792,7 @@ static void copy_peercred(struct sock *sk, struct sock *peersk) lockdep_assert_held(&unix_sk(peersk)->lock); spin_lock(&sk->sk_peer_lock); - sk->sk_peer_pid[PIDTYPE_TGID] = get_pid(peersk->sk_peer_pid[PIDTYPE_TGID]); + get_pids(sk->sk_peer_pid, peersk->sk_peer_pid); sk->sk_peer_cred = get_cred(peersk->sk_peer_cred); spin_unlock(&sk->sk_peer_lock); } From e10899b2a098f496af6316a38a107c859b47c2a4 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:37 -0500 Subject: [PATCH 07/10] net: add SO_PEERPIDFD_THREAD to get a thread-specific pidfd SO_PEERPIDFD hands out a pidfd for the thread-group that called connect() or socketpair(). Enable workloads such as the coredump server or a broker to get a pidfd of the specific thread that connected to the socket. Reviewed-by: Alexander Mikhalitsyn Reviewed-by: Kuniyuki Iwashima Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-7-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- arch/alpha/include/uapi/asm/socket.h | 2 + arch/mips/include/uapi/asm/socket.h | 2 + arch/parisc/include/uapi/asm/socket.h | 2 + arch/sparc/include/uapi/asm/socket.h | 2 + include/uapi/asm-generic/socket.h | 2 + net/core/sock.c | 87 +++++++++++++++------------ net/unix/af_unix.c | 1 + 7 files changed, 61 insertions(+), 37 deletions(-) diff --git a/arch/alpha/include/uapi/asm/socket.h b/arch/alpha/include/uapi/asm/socket.h index bb3d534826bb..5d3524c26b2b 100644 --- a/arch/alpha/include/uapi/asm/socket.h +++ b/arch/alpha/include/uapi/asm/socket.h @@ -159,6 +159,8 @@ #define SO_PASSPIDFD_THREAD 86 +#define SO_PEERPIDFD_THREAD 87 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/mips/include/uapi/asm/socket.h b/arch/mips/include/uapi/asm/socket.h index 269badcaa086..245a43f52fb1 100644 --- a/arch/mips/include/uapi/asm/socket.h +++ b/arch/mips/include/uapi/asm/socket.h @@ -170,6 +170,8 @@ #define SO_PASSPIDFD_THREAD 86 +#define SO_PEERPIDFD_THREAD 87 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/parisc/include/uapi/asm/socket.h b/arch/parisc/include/uapi/asm/socket.h index 313aee10a52c..f23710e1c671 100644 --- a/arch/parisc/include/uapi/asm/socket.h +++ b/arch/parisc/include/uapi/asm/socket.h @@ -151,6 +151,8 @@ #define SO_PASSPIDFD_THREAD 0x4054 +#define SO_PEERPIDFD_THREAD 0x4055 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 diff --git a/arch/sparc/include/uapi/asm/socket.h b/arch/sparc/include/uapi/asm/socket.h index bd3e69bcce7a..b35b25bdefc2 100644 --- a/arch/sparc/include/uapi/asm/socket.h +++ b/arch/sparc/include/uapi/asm/socket.h @@ -152,6 +152,8 @@ #define SO_PASSPIDFD_THREAD 0x005f +#define SO_PEERPIDFD_THREAD 0x0060 + #if !defined(__KERNEL__) diff --git a/include/uapi/asm-generic/socket.h b/include/uapi/asm-generic/socket.h index d1e5c6de146d..56fed7ab27ab 100644 --- a/include/uapi/asm-generic/socket.h +++ b/include/uapi/asm-generic/socket.h @@ -154,6 +154,8 @@ #define SO_PASSPIDFD_THREAD 86 +#define SO_PEERPIDFD_THREAD 87 + #if !defined(__KERNEL__) #if __BITS_PER_LONG == 64 || (defined(__x86_64__) && defined(__ILP32__)) diff --git a/net/core/sock.c b/net/core/sock.c index f8436e494138..3c25d66ba31a 100644 --- a/net/core/sock.c +++ b/net/core/sock.c @@ -1736,6 +1736,50 @@ static int groups_to_user(sockptr_t dst, const struct group_info *src) return 0; } +/* Hand out a pidfd for @type of the socket's peer via SO_PEERPIDFD*. */ +static int sk_getsockopt_peerpidfd(struct sock *sk, enum pid_type type, + sockptr_t optval, sockptr_t optlen, int len) +{ + struct file *pidfd_file = NULL; + unsigned int flags = 0; + struct pid *peer_pid; + int pidfd; + + if (len > sizeof(pidfd)) + len = sizeof(pidfd); + + spin_lock(&sk->sk_peer_lock); + peer_pid = get_pid(sk->sk_peer_pid[type]); + spin_unlock(&sk->sk_peer_lock); + + if (!peer_pid) + return -ENODATA; + + /* The use of PIDFD_STALE requires stashing of struct pid on pidfs + * with pidfs_register_pid() and only AF_UNIX is prepared for this. + */ + if (sk_is_unix(sk)) + flags |= PIDFD_STALE; + if (type == PIDTYPE_PID) + flags |= PIDFD_THREAD; + + pidfd = pidfd_prepare(peer_pid, flags, &pidfd_file); + put_pid(peer_pid); + if (pidfd < 0) + return pidfd; + + if (copy_to_sockptr(optval, &pidfd, len) || + copy_to_sockptr(optlen, &len, sizeof(int))) { + put_unused_fd(pidfd); + fput(pidfd_file); + + return -EFAULT; + } + + fd_install(pidfd, pidfd_file); + return 0; +} + int sk_getsockopt(struct sock *sk, int level, int optname, sockptr_t optval, sockptr_t optlen) { @@ -1931,45 +1975,14 @@ int sk_getsockopt(struct sock *sk, int level, int optname, } case SO_PEERPIDFD: - { - struct pid *peer_pid; - struct file *pidfd_file = NULL; - unsigned int flags = 0; - int pidfd; + return sk_getsockopt_peerpidfd(sk, PIDTYPE_TGID, optval, optlen, len); - if (len > sizeof(pidfd)) - len = sizeof(pidfd); + case SO_PEERPIDFD_THREAD: + /* Only AF_UNIX records the peer's connecting thread. */ + if (!sk_is_unix(sk)) + return -EOPNOTSUPP; - spin_lock(&sk->sk_peer_lock); - peer_pid = get_pid(sk->sk_peer_pid[PIDTYPE_TGID]); - spin_unlock(&sk->sk_peer_lock); - - if (!peer_pid) - return -ENODATA; - - /* The use of PIDFD_STALE requires stashing of struct pid - * on pidfs with pidfs_register_pid() and only AF_UNIX - * were prepared for this. - */ - if (sk->sk_family == AF_UNIX) - flags = PIDFD_STALE; - - pidfd = pidfd_prepare(peer_pid, flags, &pidfd_file); - put_pid(peer_pid); - if (pidfd < 0) - return pidfd; - - if (copy_to_sockptr(optval, &pidfd, len) || - copy_to_sockptr(optlen, &len, sizeof(int))) { - put_unused_fd(pidfd); - fput(pidfd_file); - - return -EFAULT; - } - - fd_install(pidfd, pidfd_file); - return 0; - } + return sk_getsockopt_peerpidfd(sk, PIDTYPE_PID, optval, optlen, len); case SO_PEERGROUPS: { diff --git a/net/unix/af_unix.c b/net/unix/af_unix.c index b7d62e61d9c4..a78e8ef3e0b0 100644 --- a/net/unix/af_unix.c +++ b/net/unix/af_unix.c @@ -1057,6 +1057,7 @@ static bool unix_bpf_bypass_getsockopt(int level, int optname) if (level == SOL_SOCKET) { switch (optname) { case SO_PEERPIDFD: + case SO_PEERPIDFD_THREAD: return true; default: return false; From 02e658dffe501640079ad78c8fdd7eb6e1a5e4dc Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:38 -0500 Subject: [PATCH 08/10] selftests/net: SO_PEERPIDFD_THREAD Add tests for SO_PEERPIDFD_THREAD. Reviewed-by: Alexander Mikhalitsyn Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-8-350183b6e02a@amutable.com Reviewed-by: Kuniyuki Iwashima Signed-off-by: Christian Brauner (Amutable) --- .../testing/selftests/net/af_unix/scm_pidfd.c | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tools/testing/selftests/net/af_unix/scm_pidfd.c b/tools/testing/selftests/net/af_unix/scm_pidfd.c index d73ae9d27a48..cbe53fab44fc 100644 --- a/tools/testing/selftests/net/af_unix/scm_pidfd.c +++ b/tools/testing/selftests/net/af_unix/scm_pidfd.c @@ -36,6 +36,10 @@ #define SO_PASSPIDFD_THREAD 86 #endif +#ifndef SO_PEERPIDFD_THREAD +#define SO_PEERPIDFD_THREAD 87 +#endif + #define CHILD_EXIT_CODE_OK 123 static void child_die() @@ -1058,4 +1062,97 @@ TEST(scm_pidfd_thread_creds) EXPECT_EQ(msg.ids.pid, msg.thread_info.tgid); } +static void *peer_connect_thread(void *arg) +{ + struct sock_addr *sa = arg; + struct thread_ids ids = { + .pid = getpid(), + .tid = gettid(), + }; + int fd; + char sync; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return (void *)1; + + if (connect(fd, (struct sockaddr *)&sa->listen_addr, sa->addrlen)) + return (void *)1; + + if (send(fd, &ids, sizeof(ids), 0) != sizeof(ids)) + return (void *)1; + + /* stay alive until the server has looked at our pidfd */ + if (read(fd, &sync, 1) != 1) + return (void *)1; + + close(fd); + return NULL; +} + +static int peer_pidfd_info(int fd, int optname, struct pidfd_info *info) +{ + int pidfd; + socklen_t len = sizeof(pidfd); + + if (getsockopt(fd, SOL_SOCKET, optname, &pidfd, &len)) { + log_err("getsockopt(SO_PEERPIDFD*)"); + return -1; + } + + info->mask = PIDFD_INFO_PID; + if (ioctl(pidfd, PIDFD_GET_INFO, info)) { + log_err("ioctl(PIDFD_GET_INFO)"); + close(pidfd); + return -1; + } + + close(pidfd); + return 0; +} + +/* SO_PEERPIDFD_THREAD returns a pidfd for the peer's connecting thread. */ +TEST(so_peerpidfd_thread) +{ + struct sock_addr sa; + struct thread_ids ids; + struct pidfd_info info; + pthread_t thread; + void *tret; + int server, cfd; + + server = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, server); + + fill_sockaddr(&sa, true); + ASSERT_EQ(0, bind(server, (struct sockaddr *)&sa.listen_addr, sa.addrlen)); + ASSERT_EQ(0, listen(server, 1)); + + ASSERT_EQ(0, pthread_create(&thread, NULL, peer_connect_thread, &sa)); + + cfd = accept(server, NULL, NULL); + ASSERT_LE(0, cfd); + + ASSERT_EQ(sizeof(ids), recv(cfd, &ids, sizeof(ids), MSG_WAITALL)); + ASSERT_NE(ids.pid, ids.tid); + + /* SO_PEERPIDFD refers to the peer's thread-group. */ + ASSERT_EQ(0, peer_pidfd_info(cfd, SO_PEERPIDFD, &info)); + EXPECT_EQ(ids.pid, info.pid); + EXPECT_EQ(ids.pid, info.tgid); + + /* SO_PEERPIDFD_THREAD refers to the connecting thread. */ + ASSERT_EQ(0, peer_pidfd_info(cfd, SO_PEERPIDFD_THREAD, &info)); + EXPECT_EQ(ids.tid, info.pid); + EXPECT_EQ(ids.pid, info.tgid); + + /* release the connecting thread */ + ASSERT_EQ(1, write(cfd, "x", 1)); + ASSERT_EQ(0, pthread_join(thread, &tret)); + ASSERT_EQ(NULL, tret); + + close(cfd); + close(server); +} + TEST_HARNESS_MAIN From 3209535cd39e492074d5519ecf5721605bea3a26 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:39 -0500 Subject: [PATCH 09/10] pidfs: record the coredump on the dumping thread's pid too If a thread-group coredumps only the thread-group leader pidfd will return coredump information. A pidfd for the thread that took the fatal signal cannot be used to retrieve it. Record both the thread-group leader and the specific thread that took the signal and register both in pidfs. Mark both the thread-group leader and the specific thread with the coredump information so retrieval works for both pidfds. Now that both SO_PEERPIDFD and SO_PEERPIDFD_THREAD are available it's easy to get the coredump information for the specific thread. Reviewed-by: Alexander Mikhalitsyn Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-9-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- fs/coredump.c | 22 +++++++++++++--------- fs/pidfs.c | 11 +++++++++-- include/linux/coredump.h | 4 +++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/fs/coredump.c b/fs/coredump.c index 9b267d3c0ed7..870ad720b215 100644 --- a/fs/coredump.c +++ b/fs/coredump.c @@ -454,7 +454,7 @@ static bool coredump_parse(struct core_name *cn, struct coredump_params *cprm, * leader we know that the thread-group leader * cannot be reaped until @current has exited. */ - cprm->pid = task_tgid(current); + task_pids(cprm->pid, current); err = cn_printf(cn, "%d", COREDUMP_PIDFD_NUMBER); break; } @@ -626,13 +626,17 @@ static int umh_coredump_setup(struct subprocess_info *info, struct cred *new) struct coredump_params *cp = (struct coredump_params *)info->data; int err; - if (cp->pid) { + if (cp->pid[PIDTYPE_TGID]) { struct file *pidfs_file __free(fput) = NULL; - pidfs_file = pidfs_alloc_file(cp->pid, 0); + pidfs_file = pidfs_alloc_file(cp->pid[PIDTYPE_TGID], 0); if (IS_ERR(pidfs_file)) return PTR_ERR(pidfs_file); + err = pidfs_register_pids(cp->pid); + if (err) + return err; + pidfs_coredump(cp); /* @@ -695,12 +699,12 @@ static bool coredump_sock_connect(struct core_name *cn, struct coredump_params * return false; /* - * Set the thread-group leader pid which is used for the peer - * credentials during connect() below. Then immediately register - * it in pidfs... + * Set the pids of the dumping thread and its thread-group leader + * which are used for the peer credentials during connect() below. + * Then immediately register them in pidfs... */ - cprm->pid = task_tgid(current); - retval = pidfs_register_pid(cprm->pid); + task_pids(cprm->pid, current); + retval = pidfs_register_pids(cprm->pid); if (retval) return false; @@ -722,7 +726,7 @@ static bool coredump_sock_connect(struct core_name *cn, struct coredump_params * } /* ... and validate that @sk_peer_pid matches @cprm.pid. */ - if (WARN_ON_ONCE(unix_peer(socket->sk)->sk_peer_pid[PIDTYPE_TGID] != cprm->pid)) + if (WARN_ON_ONCE(!pids_equal(unix_peer(socket->sk)->sk_peer_pid, cprm->pid))) return false; cprm->limit = RLIM_INFINITY; diff --git a/fs/pidfs.c b/fs/pidfs.c index 586af2e5811c..29299b2c7ca7 100644 --- a/fs/pidfs.c +++ b/fs/pidfs.c @@ -793,9 +793,9 @@ void pidfs_exit(struct task_struct *tsk) } #ifdef CONFIG_COREDUMP -void pidfs_coredump(const struct coredump_params *cprm) +static void pidfs_coredump_pid(struct pid *pid, + const struct coredump_params *cprm) { - struct pid *pid = cprm->pid; struct pidfs_attr *attr; attr = READ_ONCE(pid->attr); @@ -814,6 +814,13 @@ void pidfs_coredump(const struct coredump_params *cprm) smp_wmb(); set_bit(PIDFS_ATTR_BIT_COREDUMP, &attr->attr_mask); } + +void pidfs_coredump(const struct coredump_params *cprm) +{ + /* The dumping thread's pidfd reports the coredump as well. */ + for (enum pid_type type = PIDTYPE_PID; type <= pids_last(cprm->pid); type++) + pidfs_coredump_pid(cprm->pid[type], cprm); +} #endif static struct vfsmount *pidfs_mnt __ro_after_init; diff --git a/include/linux/coredump.h b/include/linux/coredump.h index 7b38ee2e7913..0bbb7de6a402 100644 --- a/include/linux/coredump.h +++ b/include/linux/coredump.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -32,7 +33,8 @@ struct coredump_params { int vma_count; size_t vma_data_size; struct core_vma_metadata *vma_meta; - struct pid *pid; + /* Dumping thread and its thread-group leader by pid type. */ + DECLARE_PIDS(pid, PIDTYPE_TGID); }; extern unsigned int core_file_note_size_limit; From 1f47b275d5d5aca297ef66a438f160a904789699 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 5 Oct 2026 10:09:40 -0500 Subject: [PATCH 10/10] selftests/coredump: check the dumping thread's pidfd Crash from a non-leader thread and verify that the pidfd from SO_PEERPIDFD_THREAD on the coredump socket refers to that thread and reports the coredump like the thread-group leader's pidfd does. Reviewed-by: Alexander Mikhalitsyn Signed-off-by: Andrew Halaney Link: https://patch.msgid.link/20261005-work-unix-passpidfd-v4-10-350183b6e02a@amutable.com Signed-off-by: Christian Brauner (Amutable) --- .../selftests/coredump/coredump_socket_test.c | 175 ++++++++++++++++++ .../selftests/coredump/coredump_test.h | 2 + .../coredump/coredump_test_helpers.c | 49 +++++ 3 files changed, 226 insertions(+) diff --git a/tools/testing/selftests/coredump/coredump_socket_test.c b/tools/testing/selftests/coredump/coredump_socket_test.c index 422728f632ca..ec73bb690bbc 100644 --- a/tools/testing/selftests/coredump/coredump_socket_test.c +++ b/tools/testing/selftests/coredump/coredump_socket_test.c @@ -592,6 +592,181 @@ TEST_F(coredump, socket_coredump_signal_sigsegv) wait_and_check_coredump_server(pid_coredump_server, _metadata, self); } +static bool check_coredump_info(const struct pidfd_info *info, const char *what) +{ + if (!(info->mask & PIDFD_INFO_COREDUMP)) { + fprintf(stderr, "%s: PIDFD_INFO_COREDUMP not set in mask\n", what); + return false; + } + + if (!(info->coredump_mask & PIDFD_COREDUMPED)) { + fprintf(stderr, "%s: PIDFD_COREDUMPED not set in coredump_mask\n", what); + return false; + } + + if (!(info->mask & PIDFD_INFO_COREDUMP_SIGNAL) || info->coredump_signal != SIGSEGV) { + fprintf(stderr, "%s: coredump_signal=%d, expected SIGSEGV=%d\n", + what, info->coredump_signal, SIGSEGV); + return false; + } + + if (!(info->mask & PIDFD_INFO_COREDUMP_CODE) || info->coredump_code != SEGV_MAPERR) { + fprintf(stderr, "%s: coredump_code=%d, expected SEGV_MAPERR=%d\n", + what, info->coredump_code, SEGV_MAPERR); + return false; + } + + return true; +} + +/* + * Test: PIDFD_INFO_COREDUMP on the dumping thread's pidfd + * + * Crash from a non-leader thread and verify that the pidfd from + * SO_PEERPIDFD_THREAD refers to that thread and reports the coredump + * like the thread-group leader's pidfd from SO_PEERPIDFD does. + */ +TEST_F(coredump, socket_coredump_thread) +{ + int pidfd, ret, status; + pid_t pid, pid_coredump_server; + struct pidfd_info info = {}; + int ipc_sockets[2]; + char c; + + ASSERT_TRUE(set_core_pattern("@/tmp/coredump.socket")); + + ret = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc_sockets); + ASSERT_EQ(ret, 0); + + pid_coredump_server = fork(); + ASSERT_GE(pid_coredump_server, 0); + if (pid_coredump_server == 0) { + int fd_server = -1, fd_coredump = -1, fd_peer_pidfd = -1; + int fd_thread_pidfd = -1, fd_core_file = -1; + struct pidfd_info thread_info = {}; + int exit_code = EXIT_FAILURE; + + close(ipc_sockets[0]); + + fd_server = create_and_listen_unix_socket("/tmp/coredump.socket"); + if (fd_server < 0) { + fprintf(stderr, "socket_coredump_thread: listen socket failed: %m\n"); + goto out; + } + + if (write_nointr(ipc_sockets[1], "1", 1) < 0) { + fprintf(stderr, "socket_coredump_thread: ipc write failed: %m\n"); + goto out; + } + + close(ipc_sockets[1]); + + fd_coredump = accept4(fd_server, NULL, NULL, SOCK_CLOEXEC); + if (fd_coredump < 0) { + fprintf(stderr, "socket_coredump_thread: accept4 failed: %m\n"); + goto out; + } + + fd_peer_pidfd = get_peer_pidfd(fd_coredump); + if (fd_peer_pidfd < 0) { + fprintf(stderr, "socket_coredump_thread: get_peer_pidfd failed\n"); + goto out; + } + + fd_thread_pidfd = get_peer_pidfd_thread(fd_coredump); + if (fd_thread_pidfd < 0) { + fprintf(stderr, "socket_coredump_thread: get_peer_pidfd_thread failed\n"); + goto out; + } + + if (!get_pidfd_info(fd_peer_pidfd, &info) || + !get_pidfd_info(fd_thread_pidfd, &thread_info)) { + fprintf(stderr, "socket_coredump_thread: get_pidfd_info failed\n"); + goto out; + } + + /* The peer is the thread-group leader, the dumping thread is not. */ + if (info.pid != info.tgid || thread_info.tgid != info.tgid || + thread_info.pid == thread_info.tgid) { + fprintf(stderr, "socket_coredump_thread: unexpected ids %d/%d and %d/%d\n", + info.pid, info.tgid, thread_info.pid, thread_info.tgid); + goto out; + } + + if (!check_coredump_info(&info, "SO_PEERPIDFD") || + !check_coredump_info(&thread_info, "SO_PEERPIDFD_THREAD")) + goto out; + + fd_core_file = open_coredump_tmpfile(self->fd_tmpfs_detached); + if (fd_core_file < 0) { + fprintf(stderr, "socket_coredump_thread: core tmpfile failed: %m\n"); + goto out; + } + + for (;;) { + char buffer[4096]; + ssize_t bytes_read, bytes_write; + + bytes_read = read(fd_coredump, buffer, sizeof(buffer)); + if (bytes_read < 0) { + fprintf(stderr, "socket_coredump_thread: core read failed: %m\n"); + goto out; + } + + if (bytes_read == 0) + break; + + bytes_write = write(fd_core_file, buffer, bytes_read); + if (bytes_read != bytes_write) { + fprintf(stderr, "socket_coredump_thread: core write %zd/%zd: %m\n", + bytes_read, bytes_write); + goto out; + } + } + + exit_code = EXIT_SUCCESS; + fprintf(stderr, "socket_coredump_thread: completed successfully\n"); +out: + if (fd_core_file >= 0) + close(fd_core_file); + if (fd_thread_pidfd >= 0) + close(fd_thread_pidfd); + if (fd_peer_pidfd >= 0) + close(fd_peer_pidfd); + if (fd_coredump >= 0) + close(fd_coredump); + if (fd_server >= 0) + close(fd_server); + _exit(exit_code); + } + self->pid_coredump_server = pid_coredump_server; + + EXPECT_EQ(close(ipc_sockets[1]), 0); + ASSERT_EQ(read_nointr(ipc_sockets[0], &c, 1), 1); + EXPECT_EQ(close(ipc_sockets[0]), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + crashing_child_thread(); + + pidfd = sys_pidfd_open(pid, 0); + ASSERT_GE(pidfd, 0); + + waitpid(pid, &status, 0); + ASSERT_TRUE(WIFSIGNALED(status)); + ASSERT_EQ(WTERMSIG(status), SIGSEGV); + ASSERT_TRUE(WCOREDUMP(status)); + + ASSERT_TRUE(get_pidfd_info(pidfd, &info)); + ASSERT_TRUE(!!(info.mask & PIDFD_INFO_COREDUMP)); + ASSERT_TRUE(!!(info.coredump_mask & PIDFD_COREDUMPED)); + ASSERT_EQ(info.coredump_signal, SIGSEGV); + + wait_and_check_coredump_server(pid_coredump_server, _metadata, self); +} + /* * Test: PIDFD_INFO_COREDUMP_SIGNAL via simple socket coredump with SIGABRT * diff --git a/tools/testing/selftests/coredump/coredump_test.h b/tools/testing/selftests/coredump/coredump_test.h index ed47f01fa53c..4212656e31f0 100644 --- a/tools/testing/selftests/coredump/coredump_test.h +++ b/tools/testing/selftests/coredump/coredump_test.h @@ -27,10 +27,12 @@ FIXTURE(coredump) /* Shared helper function declarations */ void *do_nothing(void *arg); void crashing_child(void); +void crashing_child_thread(void); int create_detached_tmpfs(void); int create_and_listen_unix_socket(const char *path); bool set_core_pattern(const char *pattern); int get_peer_pidfd(int fd); +int get_peer_pidfd_thread(int fd); bool get_pidfd_info(int fd_peer_pidfd, struct pidfd_info *info); /* Inline helper that uses harness types */ diff --git a/tools/testing/selftests/coredump/coredump_test_helpers.c b/tools/testing/selftests/coredump/coredump_test_helpers.c index 2a20faf9cb0a..36306069f62e 100644 --- a/tools/testing/selftests/coredump/coredump_test_helpers.c +++ b/tools/testing/selftests/coredump/coredump_test_helpers.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,10 @@ struct _fixture_coredump_data { #define NUM_THREAD_SPAWN 128 +#ifndef SO_PEERPIDFD_THREAD +#define SO_PEERPIDFD_THREAD 87 +#endif + void *do_nothing(void *arg) { (void)arg; @@ -59,6 +64,36 @@ void crashing_child(void) i = *(volatile int *)NULL; } +static void *crashing_thread(void *arg) +{ + int *p; + + (void)arg; + + /* crash on purpose with SEGV_MAPERR */ + p = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p == MAP_FAILED) + return NULL; + munmap(p, PAGE_SIZE); + *p = 0; + + return NULL; +} + +void crashing_child_thread(void) +{ + pthread_t thread; + int i; + + for (i = 0; i < NUM_THREAD_SPAWN; ++i) + pthread_create(&thread, NULL, do_nothing, NULL); + + /* crash from a non-leader thread */ + pthread_create(&thread, NULL, crashing_thread, NULL); + pause(); +} + int create_detached_tmpfs(void) { int fd_context, fd_tmpfs; @@ -138,6 +173,20 @@ int get_peer_pidfd(int fd) return fd_peer_pidfd; } +int get_peer_pidfd_thread(int fd) +{ + int fd_peer_pidfd; + socklen_t fd_peer_pidfd_len = sizeof(fd_peer_pidfd); + int ret = getsockopt(fd, SOL_SOCKET, SO_PEERPIDFD_THREAD, &fd_peer_pidfd, + &fd_peer_pidfd_len); + if (ret < 0) { + fprintf(stderr, "%s: getsockopt(SO_PEERPIDFD_THREAD) failed: %m\n", __func__); + return -1; + } + fprintf(stderr, "%s: successfully retrieved pidfd %d\n", __func__, fd_peer_pidfd); + return fd_peer_pidfd; +} + bool get_pidfd_info(int fd_peer_pidfd, struct pidfd_info *info) { int ret;